1use mittens_engine::{engine, utils};
2
3#[path = "example_util/mod.rs"]
4mod example_util;
5
6fn main() {
7 mittens_engine::example_support::ensure_model_assets();
8 utils::logger::init();
9
10 let audio_enabled = std::env::var("CAT_AUDIO_EXAMPLE_DISABLE_AUDIO")
14 .ok()
15 .as_deref()
16 != Some("1");
17
18 let audio_output_enabled = std::env::var("CAT_AUDIO_EXAMPLE_AUDIO_OUTPUT_OFF")
21 .ok()
22 .as_deref()
23 != Some("1");
24
25 println!("[audio-graph-example] start");
26
27 let world = engine::ecs::World::default();
28 let mut universe = engine::Universe::new(world);
29
30 println!("[audio-graph-example] universe created");
31
32 let clear = universe
34 .world
35 .add_component(engine::ecs::component::BackgroundColorComponent::new());
36 let clear_c = universe
37 .world
38 .add_component(engine::ecs::component::ColorComponent::rgba(
39 0.07, 0.07, 0.07, 1.0,
40 ));
41 let _ = universe.world.add_child(clear, clear_c);
42 universe.add(clear);
43
44 let ambient = universe
48 .world
49 .add_component(engine::ecs::component::AmbientLightComponent::rgb(
50 0.075, 0.075, 0.075,
51 ));
52 universe.add(ambient);
53
54 let input = universe
56 .world
57 .add_component(engine::ecs::component::InputComponent::new().with_speed(2.0));
58 let rig_transform = universe.world.add_component(
59 engine::ecs::component::TransformComponent::new().with_position(2.0, 0.0, 7.0),
60 );
61 let input_mode = universe.world.add_component(
62 engine::ecs::component::InputTransformModeComponent::forward_z().with_roll_axis_y(),
63 );
64 let camera3d = universe.world.add_component(
65 engine::ecs::component::Camera3DComponent::new()
66 .with_far(250.0)
67 .with_fov(70.0),
68 );
69 let _ = universe.attach(input, input_mode);
70 let _ = universe.attach(input, rig_transform);
71 let _ = universe.attach(rig_transform, camera3d);
72
73 example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
75 universe.add(input);
76
77 let light_tx = universe.world.add_component(
80 engine::ecs::component::TransformComponent::new().with_position(0.2, 0.7, 1.0),
81 );
82 let light = universe.world.add_component(
83 engine::ecs::component::DirectionalLightComponent::new()
84 .with_color(1.0, 1.0, 1.0)
85 .with_intensity(0.35),
86 );
87 let _ = universe.attach(light_tx, light);
88 universe.add(light_tx);
89
90 let bg_root = universe.world.add_component(
94 engine::ecs::component::BackgroundComponent::new().with_occlusion_and_lighting(),
95 );
96 universe.add(bg_root);
97 let mut cloud_params = example_util::CloudRingParams::default();
98 cloud_params.cloud_count = 7;
99 cloud_params.radius = 22.0;
100 cloud_params.center_y = 6.0;
103 cloud_params.puffs_per_cloud = 26;
104 cloud_params.angle_jitter = 0.0;
105 cloud_params.high_y_probability = 0.0;
106 cloud_params.high_y_multiplier = 1.0;
107 cloud_params.seed = 0xA0_D1_0C_01u32;
108 example_util::spawn_cloud_ring(&mut universe, bg_root, cloud_params);
109
110 if audio_enabled {
112 let clock = universe
113 .world
114 .add_component(engine::ecs::component::ClockComponent::new().with_bpm(128.0));
115 universe.add(clock);
116 }
117
118 if audio_enabled {
119 println!("[audio-graph-example] clock added");
120 } else {
121 println!("[audio-graph-example] audio disabled; skipping clock");
122 }
123
124 let audio_out = if audio_enabled {
126 let audio_out_comp = if audio_output_enabled {
127 engine::ecs::component::AudioOutputComponent::new()
128 } else {
129 engine::ecs::component::AudioOutputComponent::off()
130 };
131 let audio_out = universe.world.add_component(audio_out_comp);
132 universe.add(audio_out);
133 Some(audio_out)
134 } else {
135 None
136 };
137
138 match (audio_enabled, audio_output_enabled) {
139 (false, _) => println!("[audio-graph-example] audio disabled; skipping audio output"),
140 (true, true) => println!("[audio-graph-example] audio output added (CPAL on)"),
141 (true, false) => println!("[audio-graph-example] audio output added (CPAL off)"),
142 }
143
144 let osc_a_comp =
146 if audio_enabled {
147 let osc_a = engine::ecs::component::AudioOscillator::square()
148 .with_frequency(110.0)
149 .with_amplitude(0.12)
150 .with_enabled(false);
151 let osc_a_comp = universe.world.add_component(
152 engine::ecs::component::AudioOscillatorComponent::single(osc_a),
153 );
154 if let Some(audio_out) = audio_out {
155 let _ = universe.attach(audio_out, osc_a_comp);
156 }
157 Some(osc_a_comp)
158 } else {
159 None
160 };
161
162 if audio_enabled {
163 println!("[audio-graph-example] track A created");
164 } else {
165 println!("[audio-graph-example] audio disabled; skipping track A");
166 }
167
168 let mut bp_a_comp: Option<engine::ecs::ComponentId> = None;
174 if let Some(osc_a_comp) = osc_a_comp {
175 let gain_a = universe
176 .world
177 .add_component(engine::ecs::component::AudioGainComponent::new(3.2));
178 let bp_a = universe.world.add_component(
179 engine::ecs::component::AudioBandPassFilterComponent::new(120.0, 3.0, 0.40),
180 );
181 bp_a_comp = Some(bp_a);
182 let lim_a =
183 universe
184 .world
185 .add_component(engine::ecs::component::AudioLimiterComponent::new(
186 4.0, 80.0, 0.90,
187 ));
188
189 let _ = universe.attach(osc_a_comp, gain_a);
190 let _ = universe.attach(gain_a, bp_a);
191 let _ = universe.attach(bp_a, lim_a);
192 }
193
194 if audio_enabled {
195 println!("[audio-graph-example] track A effects attached");
196 }
197
198 fn spawn_text(
200 universe: &mut engine::Universe,
201 pos: (f32, f32, f32),
202 scale: f32,
203 wrap_cols: usize,
204 text: &str,
205 ) {
206 let tx = universe.world.add_component(
207 engine::ecs::component::TransformComponent::new()
208 .with_position(pos.0, pos.1, pos.2)
209 .with_scale(scale, scale, 1.0),
210 );
211 let t =
212 universe
213 .world
214 .add_component(engine::ecs::component::TextComponent::with_word_wrap(
215 text, wrap_cols,
216 ));
217 let _ = universe.attach(tx, t);
218
219 let filtering = universe
221 .world
222 .add_component(engine::ecs::component::TextureFilteringComponent::nearest());
223 let _ = universe.attach(t, filtering);
224
225 let emissive = universe
227 .world
228 .add_component(engine::ecs::component::EmissiveComponent::on());
229 let _ = universe.attach(t, emissive);
230
231 universe.add(tx);
232 }
233
234 fn spawn_emissive_cube(
235 universe: &mut engine::Universe,
236 parent: engine::ecs::ComponentId,
237 pos: (f32, f32, f32),
238 scale: f32,
239 rgba: [f32; 4],
240 ) {
241 let tx = universe.world.add_component(
242 engine::ecs::component::TransformComponent::new()
243 .with_position(pos.0, pos.1, pos.2)
244 .with_scale(scale, scale, scale),
245 );
246 let r = universe
247 .world
248 .add_component(engine::ecs::component::RenderableComponent::cube());
249 let c = universe
250 .world
251 .add_component(engine::ecs::component::ColorComponent::rgba(
252 rgba[0], rgba[1], rgba[2], rgba[3],
253 ));
254 let e = universe
255 .world
256 .add_component(engine::ecs::component::EmissiveComponent::on());
257 let _ = universe.attach(parent, tx);
258 let _ = universe.attach(tx, r);
259 let _ = universe.attach(r, c);
260 let _ = universe.attach(r, e);
261 }
262
263 fn spawn_op_cube(
264 universe: &mut engine::Universe,
265 parent: engine::ecs::ComponentId,
266 pos: (f32, f32, f32),
267 scale: f32,
268 base_rgba: [f32; 4],
269 ) -> engine::ecs::ComponentId {
270 let tx = universe.world.add_component(
271 engine::ecs::component::TransformComponent::new()
272 .with_position(pos.0, pos.1, pos.2)
273 .with_scale(scale, scale, scale),
274 );
275 let r = universe
276 .world
277 .add_component(engine::ecs::component::RenderableComponent::cube());
278 let c = universe
279 .world
280 .add_component(engine::ecs::component::ColorComponent::rgba(
281 base_rgba[0],
282 base_rgba[1],
283 base_rgba[2],
284 base_rgba[3],
285 ));
286 let e = universe
287 .world
288 .add_component(engine::ecs::component::EmissiveComponent::on());
289
290 let _ = universe.attach(parent, tx);
291 let _ = universe.attach(tx, r);
292 let _ = universe.attach(r, c);
293 let _ = universe.attach(r, e);
294
295 tx
296 }
297
298 let lane_x = -2.6_f32;
300 let lane_title_z = -0.4_f32;
301 let lane_cfg_z = -0.4_f32;
302 let lane_pat_z = -1.3_f32;
303 let lane_graph_z = -1.15_f32;
304
305 let lane_labels_x = lane_x + 1.6_f32;
308
309 let track_a_y = 0.9_f32;
310
311 spawn_text(
312 &mut universe,
313 (lane_x, track_a_y + 0.55, lane_title_z),
314 0.09,
315 42,
316 "Track A: AudioOscillator::square()",
317 );
318 spawn_text(
319 &mut universe,
320 (lane_x, track_a_y + 0.25, lane_cfg_z),
321 0.07,
322 48,
323 "oscillators=1\nfrequency_hz=110.0\namplitude=0.12\nenabled=false\nlookahead=0.10s",
324 );
325
326 let viz_root = universe.world.add_component(
327 engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 0.0),
328 );
329 universe.add(viz_root);
330
331 println!("[audio-graph-example] viz root added");
332
333 spawn_emissive_cube(
335 &mut universe,
336 viz_root,
337 (lane_x - 0.28, track_a_y + 0.55, lane_title_z),
338 0.16,
339 [0.85, 0.40, 1.00, 1.0],
340 );
341
342 let track_a_pattern_root = universe.world.add_component(
344 engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 0.0),
345 );
346 let _ = universe.attach(viz_root, track_a_pattern_root);
347
348 spawn_text(
350 &mut universe,
351 (lane_labels_x, track_a_y, lane_title_z),
352 0.06,
353 42,
354 "pattern",
355 );
356 spawn_text(
357 &mut universe,
358 (lane_labels_x, track_a_y - 0.40, lane_title_z),
359 0.06,
360 42,
361 "graph",
362 );
363
364 use engine::ecs::system::audio_graph_compiler::{
366 AudioGraphCompiler, AudioGraphNode, AudioGraphNodeKind,
367 };
368
369 fn effect_grey_for_depth(depth: usize) -> f32 {
370 let base = 0.55;
372 let step = 0.13;
373 let d = depth.saturating_sub(1) as f32;
374 (base + step * d).min(0.92)
375 }
376
377 fn node_rgba(node: &AudioGraphNode, depth: usize) -> [f32; 4] {
378 match node.kind {
379 AudioGraphNodeKind::OscillatorSource { .. } => [1.0, 0.78, 0.22, 1.0],
380 _ => {
381 let g = effect_grey_for_depth(depth);
382 [g, g, g, 1.0]
383 }
384 }
385 }
386
387 fn node_label(node: &AudioGraphNode) -> String {
388 match &node.kind {
389 AudioGraphNodeKind::OscillatorSource { voices } => {
390 format!("OscillatorSource voices={voices}")
391 }
392 AudioGraphNodeKind::Gain { gain } => {
393 format!("Gain gain={gain:.3}")
394 }
395 AudioGraphNodeKind::LowPass {
396 cutoff_hz,
397 resonance,
398 } => {
399 format!("LowPass cutoff={cutoff_hz:.1}Hz res={resonance:.3}")
400 }
401 AudioGraphNodeKind::BandPass {
402 center_hz,
403 bandwidth_octaves,
404 resonance,
405 } => {
406 format!(
407 "BandPass center={center_hz:.1}Hz bw={bandwidth_octaves:.3}oct res={resonance:.3}"
408 )
409 }
410 AudioGraphNodeKind::HighPass {
411 cutoff_hz,
412 resonance,
413 } => {
414 format!("HighPass cutoff={cutoff_hz:.1}Hz res={resonance:.3}")
415 }
416 AudioGraphNodeKind::Limiter {
417 attack_ms,
418 release_ms,
419 threshold,
420 } => {
421 format!("Limiter atk={attack_ms:.1}ms rel={release_ms:.1}ms thr={threshold:.3}")
422 }
423 AudioGraphNodeKind::ClipSource => "ClipSource".to_string(),
424 }
425 }
426
427 fn compute_layout(
428 node: &AudioGraphNode,
429 depth: usize,
430 x_cursor: &mut i32,
431 out: &mut std::collections::HashMap<*const AudioGraphNode, (i32, usize)>,
432 ) -> i32 {
433 let _ = x_cursor;
437 for ch in node.children.iter() {
438 let _ = compute_layout(ch, depth + 1, x_cursor, out);
439 }
440
441 let my_x = 0;
442 out.insert(node as *const AudioGraphNode, (my_x, depth));
443 my_x
444 }
445
446 fn spawn_graph_tree(
447 universe: &mut engine::Universe,
448 parent: engine::ecs::ComponentId,
449 node: &AudioGraphNode,
450 bp_component: Option<engine::ecs::ComponentId>,
451 bp_label_out: &mut Option<engine::ecs::ComponentId>,
452 origin: (f32, f32, f32),
453 layout: &std::collections::HashMap<*const AudioGraphNode, (i32, usize)>,
454 dx: f32,
455 dy: f32,
456 cube_scale: f32,
457 sibling_index: usize,
458 sibling_count: usize,
459 ) {
460 let Some((x_unit, depth)) = layout.get(&(node as *const AudioGraphNode)).copied() else {
461 return;
462 };
463
464 let x = origin.0 + (x_unit as f32) * dx;
465 let y = origin.1 - (depth as f32) * dy;
466
467 let dz_sibling = 0.18;
470 let z = origin.2 - (sibling_index as f32) * dz_sibling;
471
472 let z_text = z + 0.03;
474
475 let rgba = node_rgba(node, depth);
476 let cube_tx = spawn_op_cube(universe, parent, (x, y, z), cube_scale, rgba);
477 let _ = cube_tx;
478
479 let label = node_label(node);
481 let tx = universe.world.add_component(
482 engine::ecs::component::TransformComponent::new()
483 .with_position(x + 0.14, y + 0.02, z_text)
484 .with_scale(0.06, 0.06, 1.0),
485 );
486 let t =
487 universe
488 .world
489 .add_component(engine::ecs::component::TextComponent::with_word_wrap(
490 &label, 25,
491 ));
492 let _ = universe.attach(parent, tx);
493 let _ = universe.attach(tx, t);
494
495 if bp_component == Some(node.component) {
496 *bp_label_out = Some(t);
497 }
498
499 let filtering = universe
500 .world
501 .add_component(engine::ecs::component::TextureFilteringComponent::nearest());
502 let _ = universe.attach(t, filtering);
503
504 let emissive = universe
505 .world
506 .add_component(engine::ecs::component::EmissiveComponent::on());
507 let _ = universe.attach(t, emissive);
508
509 universe.add(tx);
510
511 if node.children.len() > 1 {
513 let mut weights: Vec<f32> = Vec::with_capacity(node.children.len());
514 for i in 0..node.children.len() {
515 let w = node
516 .mix
517 .as_ref()
518 .map(|m| m.weights.get(i).copied().unwrap_or(1.0))
519 .unwrap_or(1.0);
520 weights.push(w);
521 }
522 let mix_label = if node.mix.is_some() {
523 format!("Mix weights={weights:?}")
524 } else {
525 format!("Mix <implicit> w={weights:?}")
526 };
527 let mix_tx = universe.world.add_component(
528 engine::ecs::component::TransformComponent::new()
529 .with_position(x + 0.14, y - 0.11, z_text)
530 .with_scale(0.05, 0.05, 1.0),
531 );
532 let mix_t = universe.world.add_component(
533 engine::ecs::component::TextComponent::with_word_wrap(&mix_label, 25),
534 );
535 let _ = universe.attach(parent, mix_tx);
536 let _ = universe.attach(mix_tx, mix_t);
537
538 let filtering = universe
539 .world
540 .add_component(engine::ecs::component::TextureFilteringComponent::nearest());
541 let _ = universe.attach(mix_t, filtering);
542
543 let emissive = universe
544 .world
545 .add_component(engine::ecs::component::EmissiveComponent::on());
546 let _ = universe.attach(mix_t, emissive);
547
548 universe.add(mix_tx);
549 }
550
551 let child_count = node.children.len().max(1);
552 for (i, ch) in node.children.iter().enumerate() {
553 let _ = sibling_count;
556 spawn_graph_tree(
557 universe,
558 parent,
559 ch,
560 bp_component,
561 bp_label_out,
562 origin,
563 layout,
564 dx,
565 dy,
566 cube_scale,
567 i,
568 child_count,
569 );
570 }
571 }
572
573 let compiled_a = if audio_enabled {
575 println!("[audio-graph-example] compiling graph...");
576 let Some(osc_a_comp) = osc_a_comp else {
577 panic!("audio_enabled but track A was not created");
578 };
579 let compiled_a =
580 AudioGraphCompiler::compile(&universe.world, osc_a_comp).expect("compile A");
581 println!("[audio-graph-example] graph compiled");
582 Some(compiled_a)
583 } else {
584 println!("[audio-graph-example] audio disabled; skipping graph compilation");
585 None
586 };
587
588 let mut bp_graph_label_text: Option<engine::ecs::ComponentId> = None;
590 if let Some(compiled_a) = &compiled_a {
591 let mut x_cursor = 0;
592 let mut layout: std::collections::HashMap<*const AudioGraphNode, (i32, usize)> =
593 std::collections::HashMap::new();
594 let _root_x = compute_layout(&compiled_a.root, 0, &mut x_cursor, &mut layout);
595
596 let origin = (0.0, track_a_y - 0.40, lane_graph_z);
599
600 spawn_graph_tree(
601 &mut universe,
602 viz_root,
603 &compiled_a.root,
604 bp_a_comp,
605 &mut bp_graph_label_text,
606 origin,
607 &layout,
608 0.45,
609 0.28,
610 0.08,
611 0,
612 1,
613 );
614 } else {
615 spawn_text(
616 &mut universe,
617 (0.0, track_a_y - 0.40, lane_graph_z),
618 0.06,
619 60,
620 "(audio disabled)",
621 );
622 }
623
624 let anim = universe
626 .world
627 .add_component(engine::ecs::component::AnimationComponent::new());
628
629 let dur_a = 0.85_f32;
630 let beat_spacing = 0.38_f32;
631
632 let a_dark = [0.18, 0.08, 0.22, 1.0];
633 let a_bright = [0.85, 0.40, 1.00, 1.0];
634
635 for i in 0..16 {
636 let kf_beat = i as f64;
637 let kf = universe
638 .world
639 .add_component(engine::ecs::component::KeyframeComponent::new(kf_beat));
640
641 let _ = universe.attach(anim, kf);
642
643 if audio_enabled {
646 let Some(osc_a_comp) = osc_a_comp else {
647 panic!("audio_enabled but track A was not created");
648 };
649
650 let note_a = engine::ecs::component::MusicNote::c(1, dur_a).with_velocity(0.80);
651
652 let act_a = universe
653 .world
654 .add_component(engine::ecs::component::ActionComponent::new(
655 engine::ecs::IntentValue::AudioSchedulePlay {
656 component_ids: vec![osc_a_comp],
657 beat_offset: 0.0,
658 beat_context: None,
659 note: Some(note_a),
660 gain: None,
661 rate: None,
662 duration: None,
663 },
664 ));
665
666 let _ = universe.attach(kf, act_a);
667 }
668
669 if audio_enabled {
672 if let Some(bp_a_comp) = bp_a_comp {
673 let t = (i as f32) / 15.0;
674 let center_hz = 10.0 + t * (1000.0 - 10.0);
675
676 let bp_center =
677 universe
678 .world
679 .add_component(engine::ecs::component::ActionComponent::new(
680 engine::ecs::IntentValue::AudioBandPassSetCenterHz {
681 component_ids: vec![bp_a_comp],
682 center_hz,
683 },
684 ));
685 let _ = universe.attach(kf, bp_center);
686
687 if let Some(text_id) = bp_graph_label_text {
689 let label =
690 universe
691 .world
692 .add_component(engine::ecs::component::ActionComponent::new(
693 engine::ecs::IntentValue::SetText {
694 component_ids: vec![text_id],
695 text: format!("BandPass center={center_hz:.1}Hz"),
696 },
697 ));
698 let _ = universe.attach(kf, label);
699 }
700 }
701 }
702
703 let reset_a = universe
705 .world
706 .add_component(engine::ecs::component::ActionComponent::new(
707 engine::ecs::IntentValue::SetColor {
708 component_ids: vec![track_a_pattern_root],
709 rgba: a_dark,
710 },
711 ));
712 let _ = universe.attach(kf, reset_a);
713
714 let x = (kf_beat as f32) * beat_spacing;
716 let cube_a = spawn_op_cube(
717 &mut universe,
718 track_a_pattern_root,
719 (x, track_a_y, lane_pat_z),
720 0.10,
721 a_dark,
722 );
723
724 let bright_a = universe
725 .world
726 .add_component(engine::ecs::component::ActionComponent::new(
727 engine::ecs::IntentValue::SetColor {
728 component_ids: vec![cube_a],
729 rgba: a_bright,
730 },
731 ));
732 let _ = universe.attach(kf, bright_a);
733 }
734 universe.add(anim);
735
736 println!("[audio-graph-example] animation created");
737
738 println!("[audio-graph-example] processing commands...");
740 universe.systems.process_commands(
741 &mut universe.world,
742 &mut universe.visuals,
743 &mut universe.render_assets,
744 &mut universe.command_queue,
745 );
746
747 println!("[audio-graph-example] commands processed; launching window");
748
749 engine::Windowing::run_app(universe).expect("Windowing failed");
750}