Skip to main content

AudioOutputComponent

Struct AudioOutputComponent 

Source
pub struct AudioOutputComponent {
    pub enabled: bool,
    /* private fields */
}

Fields§

§enabled: bool

Placeholder: when present, AudioSystem will try to start the default output stream.

Implementations§

Source§

impl AudioOutputComponent

Source

pub fn new() -> Self

Examples found in repository?
examples/animation-example.rs (line 69)
6fn main() {
7    mittens_engine::example_support::ensure_model_assets();
8    utils::logger::init();
9
10    let world = engine::ecs::World::default();
11    let mut universe = engine::Universe::new(world);
12
13    // Minimal scene with a camera so the window opens.
14    let clear = universe
15        .world
16        .add_component(engine::ecs::component::BackgroundColorComponent::new());
17    let clear_c = universe
18        .world
19        .add_component(engine::ecs::component::ColorComponent::rgba(
20            0.08, 0.08, 0.08, 1.0,
21        ));
22    let _ = universe.world.add_child(clear, clear_c);
23    universe.add(clear);
24
25    // Input-driven camera rig.
26    let input = universe
27        .world
28        .add_component(engine::ecs::component::InputComponent::new().with_speed(2.0));
29    let rig_transform = universe.world.add_component(
30        engine::ecs::component::TransformComponent::new().with_position(2.0, 0.0, 7.0),
31    );
32    let input_mode = universe.world.add_component(
33        engine::ecs::component::InputTransformModeComponent::forward_z().with_roll_axis_y(),
34    );
35    let camera3d = universe.world.add_component(
36        engine::ecs::component::Camera3DComponent::new()
37            .with_far(250.0)
38            .with_fov(70.0),
39    );
40    let _ = universe.attach(input, input_mode);
41    let _ = universe.attach(input, rig_transform);
42    let _ = universe.attach(rig_transform, camera3d);
43
44    // Topology: I { T { C3D } } — add a small camera-attached controls hint.
45    example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
46    universe.add(input);
47
48    // Light so we can see non-emissive materials (even though our cubes are emissive).
49    let light_tx = universe.world.add_component(
50        engine::ecs::component::TransformComponent::new().with_position(0.0, 2.5, 4.0),
51    );
52    let light = universe.world.add_component(
53        engine::ecs::component::PointLightComponent::new()
54            .with_distance(25.0)
55            .with_color(1.0, 1.0, 1.0),
56    );
57    let _ = universe.attach(light_tx, light);
58    universe.add(light_tx);
59
60    // ClockComponent sets global tempo.
61    let clock = universe
62        .world
63        .add_component(engine::ecs::component::ClockComponent::new().with_bpm(128.0));
64    universe.add(clock);
65
66    // Audio output + oscillators driven by scheduled actions.
67    let audio_out = universe
68        .world
69        .add_component(engine::ecs::component::AudioOutputComponent::new());
70    universe.add(audio_out);
71
72    // Noise oscillator: short white-noise hits.
73    let osc_noise = engine::ecs::component::AudioOscillator::noise()
74        .with_frequency(0.0)
75        .with_amplitude(0.06)
76        .with_enabled(false);
77    let osc_noise_comp =
78        universe
79            .world
80            .add_component(engine::ecs::component::AudioOscillatorComponent::single(
81                osc_noise,
82            ));
83    let _ = universe.attach(audio_out, osc_noise_comp);
84
85    // Drum oscillator: retriggers (phase + sweep) every enable.
86    let osc_drum = engine::ecs::component::AudioOscillator::drum()
87        // A low-ish pitch scale; actual pitch is set by scheduled notes.
88        .with_frequency(32.0)
89        .with_amplitude(0.40)
90        .with_enabled(false);
91    let osc_drum_comp =
92        universe
93            .world
94            .add_component(engine::ecs::component::AudioOscillatorComponent::single(
95                osc_drum,
96            ));
97
98    let _ = universe.attach(audio_out, osc_drum_comp);
99
100    // --- Visual layout helpers ---
101    fn spawn_text(universe: &mut engine::Universe, pos: (f32, f32, f32), scale: f32, text: &str) {
102        let tx = universe.world.add_component(
103            engine::ecs::component::TransformComponent::new()
104                .with_position(pos.0, pos.1, pos.2)
105                .with_scale(scale, scale, 1.0),
106        );
107        let t =
108            universe
109                .world
110                .add_component(engine::ecs::component::TextComponent::with_word_wrap(
111                    text, 38,
112                ));
113        let _ = universe.attach(tx, t);
114        universe.add(tx);
115    }
116
117    fn spawn_emissive_cube(
118        universe: &mut engine::Universe,
119        parent: engine::ecs::ComponentId,
120        pos: (f32, f32, f32),
121        scale: f32,
122        rgba: [f32; 4],
123    ) {
124        let tx = universe.world.add_component(
125            engine::ecs::component::TransformComponent::new()
126                .with_position(pos.0, pos.1, pos.2)
127                .with_scale(scale, scale, scale),
128        );
129        let r = universe
130            .world
131            .add_component(engine::ecs::component::RenderableComponent::cube());
132        let c = universe
133            .world
134            .add_component(engine::ecs::component::ColorComponent::rgba(
135                rgba[0], rgba[1], rgba[2], rgba[3],
136            ));
137        let e = universe
138            .world
139            .add_component(engine::ecs::component::EmissiveComponent::on());
140        let _ = universe.attach(parent, tx);
141        let _ = universe.attach(tx, r);
142        let _ = universe.attach(r, c);
143        let _ = universe.attach(r, e);
144    }
145
146    fn spawn_op_cube(
147        universe: &mut engine::Universe,
148        parent: engine::ecs::ComponentId,
149        pos: (f32, f32, f32),
150        scale: f32,
151        base_rgba: [f32; 4],
152    ) -> engine::ecs::ComponentId {
153        let tx = universe.world.add_component(
154            engine::ecs::component::TransformComponent::new()
155                .with_position(pos.0, pos.1, pos.2)
156                .with_scale(scale, scale, scale),
157        );
158        let r = universe
159            .world
160            .add_component(engine::ecs::component::RenderableComponent::cube());
161        let c = universe
162            .world
163            .add_component(engine::ecs::component::ColorComponent::rgba(
164                base_rgba[0],
165                base_rgba[1],
166                base_rgba[2],
167                base_rgba[3],
168            ));
169        let e = universe
170            .world
171            .add_component(engine::ecs::component::EmissiveComponent::on());
172
173        let _ = universe.attach(parent, tx);
174        let _ = universe.attach(tx, r);
175        let _ = universe.attach(r, c);
176        let _ = universe.attach(r, e);
177
178        tx
179    }
180
181    // --- HUD / lanes ---
182    let lane_x = -2.6_f32;
183    let lane_title_z = -0.4_f32;
184    let lane_cfg_z = -0.4_f32;
185    let _lane_cube_z = -0.8_f32;
186    let drum_y = 0.9_f32;
187    let noise_y = -0.4_f32;
188
189    spawn_text(
190        &mut universe,
191        (lane_x, drum_y + 0.55, lane_title_z),
192        0.09,
193        "Audio Source A: Drum (kick)",
194    );
195    spawn_text(
196        &mut universe,
197        (lane_x, drum_y + 0.25, lane_cfg_z),
198        0.07,
199        "type=Drum\namp=0.40\nbase_freq=32Hz\nkick: C0 dur=0.12 vel=0.90\nlookahead=0.10s",
200    );
201
202    spawn_text(
203        &mut universe,
204        (lane_x, noise_y + 0.55, lane_title_z),
205        0.09,
206        "Audio Source B: Noise",
207    );
208    spawn_text(
209        &mut universe,
210        (lane_x, noise_y + 0.25, lane_cfg_z),
211        0.07,
212        "type=Noise\namp=0.06\nnoise: C9 dur=0.06 vel=0.25\noffset=+0.5 beats\nlookahead=0.10s",
213    );
214
215    // Representative cubes for the two audio sources.
216    let viz_root = universe.world.add_component(
217        engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 0.0),
218    );
219    universe.add(viz_root);
220    spawn_emissive_cube(
221        &mut universe,
222        viz_root,
223        (lane_x - 0.28, drum_y + 0.55, lane_title_z),
224        0.16,
225        [1.00, 0.90, 0.10, 1.0],
226    );
227    spawn_emissive_cube(
228        &mut universe,
229        viz_root,
230        (lane_x - 0.28, noise_y + 0.55, lane_title_z),
231        0.16,
232        [1.00, 1.00, 1.00, 1.0],
233    );
234
235    // Timeline roots so we can "reset all" via a single set_color action.
236    let kick_timeline_root = universe.world.add_component(
237        engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 0.0),
238    );
239    let noise_timeline_root = universe.world.add_component(
240        engine::ecs::component::TransformComponent::new().with_position(0.0, 0.0, 0.0),
241    );
242    let _ = universe.attach(viz_root, kick_timeline_root);
243    let _ = universe.attach(viz_root, noise_timeline_root);
244
245    // Animation with 16 keyframes: each keyframe schedules a kick at offset 0.0,
246    // and a noise hit at offset 0.5.
247    let anim = universe
248        .world
249        .add_component(engine::ecs::component::AnimationComponent::new());
250
251    let kick_dur = 0.12_f32;
252    let noise_dur = 0.06_f32;
253
254    // Requested palette:
255    // - kick: dark yellow -> bright yellow
256    // - noise: grey -> white
257    let kick_dark = [0.35, 0.28, 0.02, 1.0];
258    let kick_bright = [1.00, 0.90, 0.10, 1.0];
259    let noise_dark = [0.35, 0.35, 0.35, 1.0];
260    let noise_bright = [1.00, 1.00, 1.00, 1.0];
261
262    let beat_spacing = 0.38_f32;
263    for i in 0..16 {
264        let kf_beat = i as f64;
265        let kf = universe
266            .world
267            .add_component(engine::ecs::component::KeyframeComponent::new(kf_beat));
268
269        let kick = universe
270            .world
271            .add_component(engine::ecs::component::ActionComponent::new(
272                engine::ecs::IntentValue::AudioSchedulePlay {
273                    component_ids: vec![osc_drum_comp],
274                    beat_offset: 0.0,
275                    beat_context: None,
276                    note: Some(
277                        engine::ecs::component::MusicNote::c(0, kick_dur).with_velocity(0.9),
278                    ),
279                    gain: None,
280                    rate: None,
281                    duration: None,
282                },
283            ));
284
285        let noise = universe
286            .world
287            .add_component(engine::ecs::component::ActionComponent::new(
288                engine::ecs::IntentValue::AudioSchedulePlay {
289                    component_ids: vec![osc_noise_comp],
290                    beat_offset: 0.5,
291                    beat_context: None,
292                    note: Some(
293                        engine::ecs::component::MusicNote::c(9, noise_dur).with_velocity(0.25),
294                    ),
295                    gain: None,
296                    rate: None,
297                    duration: None,
298                },
299            ));
300
301        let _ = universe.attach(anim, kf);
302        let _ = universe.attach(kf, kick);
303        let _ = universe.attach(kf, noise);
304
305        // Each keyframe drives visualization purely via normal actions:
306        // 1) reset all timeline cubes to their base colors
307        // 2) brighten the current beat's cubes
308        let reset_kick_lane =
309            universe
310                .world
311                .add_component(engine::ecs::component::ActionComponent::new(
312                    engine::ecs::IntentValue::SetColor {
313                        component_ids: vec![kick_timeline_root],
314                        rgba: kick_dark,
315                    },
316                ));
317        let reset_noise_lane =
318            universe
319                .world
320                .add_component(engine::ecs::component::ActionComponent::new(
321                    engine::ecs::IntentValue::SetColor {
322                        component_ids: vec![noise_timeline_root],
323                        rgba: noise_dark,
324                    },
325                ));
326        let _ = universe.attach(kf, reset_kick_lane);
327        let _ = universe.attach(kf, reset_noise_lane);
328
329        // Timeline cubes: one cube per scheduled audio operation.
330        // Kick (offset 0.0) on the drum lane.
331        let kick_x = (kf_beat as f32) * beat_spacing;
332        let kick_cube = spawn_op_cube(
333            &mut universe,
334            kick_timeline_root,
335            (kick_x, drum_y, -1.3),
336            0.10,
337            kick_dark,
338        );
339
340        // Noise (offset 0.5) on the noise lane.
341        let noise_x = ((kf_beat + 0.5) as f32) * beat_spacing;
342        let noise_cube = spawn_op_cube(
343            &mut universe,
344            noise_timeline_root,
345            (noise_x, noise_y, -1.3),
346            0.10,
347            noise_dark,
348        );
349
350        let brighten_kick =
351            universe
352                .world
353                .add_component(engine::ecs::component::ActionComponent::new(
354                    engine::ecs::IntentValue::SetColor {
355                        component_ids: vec![kick_cube],
356                        rgba: kick_bright,
357                    },
358                ));
359        let brighten_noise =
360            universe
361                .world
362                .add_component(engine::ecs::component::ActionComponent::new(
363                    engine::ecs::IntentValue::SetColor {
364                        component_ids: vec![noise_cube],
365                        rgba: noise_bright,
366                    },
367                ));
368        let _ = universe.attach(kf, brighten_kick);
369        let _ = universe.attach(kf, brighten_noise);
370    }
371
372    universe.add(anim);
373
374    universe.systems.process_commands(
375        &mut universe.world,
376        &mut universe.visuals,
377        &mut universe.render_assets,
378        &mut universe.command_queue,
379    );
380
381    engine::Windowing::run_app(universe).expect("Windowing failed");
382}
More examples
Hide additional examples
examples/audio-graph-example.rs (line 127)
6fn main() {
7    mittens_engine::example_support::ensure_model_assets();
8    utils::logger::init();
9
10    // Debug toggle: allow isolating stack overflows to audio vs. non-audio init.
11    // Set `CAT_AUDIO_EXAMPLE_DISABLE_AUDIO=1` to skip creating audio components and
12    // skip scheduling audio notes. The UI/text/cube visualization will still spawn.
13    let audio_enabled = std::env::var("CAT_AUDIO_EXAMPLE_DISABLE_AUDIO")
14        .ok()
15        .as_deref()
16        != Some("1");
17
18    // If set, we still build audio graph components but we don't start the CPAL output stream.
19    // This helps distinguish "audio graph / scheduling" issues from "CPAL backend" issues.
20    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    // Minimal scene with a camera so the window opens (copied from animation-example).
33    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    // Ambient light so unlit areas aren't pitch black.
45    // Keep it dark to match the background clear color.
46    // (User request) 2.5x brighter.
47    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    // Input-driven camera rig.
55    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    // Topology: I { T { C3D } } — add a small camera-attached controls hint.
74    example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
75    universe.add(input);
76
77    // Directional light (sun-ish). Note: the renderer interprets the node's world position
78    // as a direction vector (see DirectionalLightComponent docs).
79    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    // --- Background clouds (occluded + lit) ---
91    // Mirrors the vtuber example: use a BackgroundComponent stage so the cloud volume
92    // self-occludes and is lit, but renders as background.
93    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    // Move the ring up by ~one cloud height. The cloud generator uses a ~4.0 unit
101    // vertical spread for puff offsets, so +4.0 is a good "one height" bump.
102    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    // ClockComponent sets global tempo.
111    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    // Audio output + 2 oscillator sources.
125    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    // Track A: square lead.
145    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    // Effect tree A (single chain):
169    // osc_a
170    //   Gain
171    //     BandPass
172    //       Limiter
173    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    // --- Visual layout helpers (copied/adapted from animation-example) ---
199    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        // TextSystem looks for an immediate TextureFilteringComponent child.
220        let filtering = universe
221            .world
222            .add_component(engine::ecs::component::TextureFilteringComponent::nearest());
223        let _ = universe.attach(t, filtering);
224
225        // TextSystem also supports styling from immediate Emissive children.
226        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    // --- HUD / lane (single track) ---
299    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    // Content for pattern/chain/graph starts at x=0; keep section labels aligned there.
306    // This also keeps them from overlapping the config block (which is anchored at lane_x).
307    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    // Small identifier cubes next to titles.
334    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    // Pattern roots so we can reset via one SetColor action.
343    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    // Labels for pattern/chain/graph sections.
349    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    // --- Processing chain + graph visualization (compiled graph → cubes + labels) ---
365    use engine::ecs::system::audio_graph_compiler::{
366        AudioGraphCompiler, AudioGraphNode, AudioGraphNodeKind,
367    };
368
369    fn effect_grey_for_depth(depth: usize) -> f32 {
370        // depth=1 => medium grey; deeper => lighter, capped.
371        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        // Depth-only layout:
434        // - keep children directly below their parent on X
435        // - use Z sibling offsets (in spawn_graph_tree) to show branching
436        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        // Push siblings “behind” each other along Z to reduce overlap between
468        // a sibling's cube and another node's label.
469        let dz_sibling = 0.18;
470        let z = origin.2 - (sibling_index as f32) * dz_sibling;
471
472        // Keep text slightly in front of its cube.
473        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        // Label next to the cube.
480        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        // Mix/branch label if branching.
512        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            // Each node's children form a sibling group; offset them in Z.
554            // For the root node, sibling_index is 0.
555            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    // Compile + display chain and graph for the track.
574    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    // Full compiled graph visualization (tree layout + mix labels).
589    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        // With depth-only X layout, we keep the tree centered at x=0.
597        // Pull the graph up now that we don't show the separate chain view.
598        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    // Animation with 16 keyframes drives scheduled notes + pattern highlights.
625    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        // Scheduled audio note (sample-accurate via lookahead).
644        // Pulse every beat.
645        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        // Keyframed band-pass center.
670        // This is an immediate parameter update applied RT-side (no graph rebuild).
671        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                // Update the BandPass node label in the graph visualization.
688                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        // Visualization reset per keyframe.
704        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        // Pattern cube per step for each track.
715        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    // Keep window open.
739    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}
Source

pub fn off() -> Self

Examples found in repository?
examples/audio-graph-example.rs (line 129)
6fn main() {
7    mittens_engine::example_support::ensure_model_assets();
8    utils::logger::init();
9
10    // Debug toggle: allow isolating stack overflows to audio vs. non-audio init.
11    // Set `CAT_AUDIO_EXAMPLE_DISABLE_AUDIO=1` to skip creating audio components and
12    // skip scheduling audio notes. The UI/text/cube visualization will still spawn.
13    let audio_enabled = std::env::var("CAT_AUDIO_EXAMPLE_DISABLE_AUDIO")
14        .ok()
15        .as_deref()
16        != Some("1");
17
18    // If set, we still build audio graph components but we don't start the CPAL output stream.
19    // This helps distinguish "audio graph / scheduling" issues from "CPAL backend" issues.
20    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    // Minimal scene with a camera so the window opens (copied from animation-example).
33    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    // Ambient light so unlit areas aren't pitch black.
45    // Keep it dark to match the background clear color.
46    // (User request) 2.5x brighter.
47    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    // Input-driven camera rig.
55    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    // Topology: I { T { C3D } } — add a small camera-attached controls hint.
74    example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
75    universe.add(input);
76
77    // Directional light (sun-ish). Note: the renderer interprets the node's world position
78    // as a direction vector (see DirectionalLightComponent docs).
79    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    // --- Background clouds (occluded + lit) ---
91    // Mirrors the vtuber example: use a BackgroundComponent stage so the cloud volume
92    // self-occludes and is lit, but renders as background.
93    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    // Move the ring up by ~one cloud height. The cloud generator uses a ~4.0 unit
101    // vertical spread for puff offsets, so +4.0 is a good "one height" bump.
102    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    // ClockComponent sets global tempo.
111    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    // Audio output + 2 oscillator sources.
125    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    // Track A: square lead.
145    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    // Effect tree A (single chain):
169    // osc_a
170    //   Gain
171    //     BandPass
172    //       Limiter
173    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    // --- Visual layout helpers (copied/adapted from animation-example) ---
199    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        // TextSystem looks for an immediate TextureFilteringComponent child.
220        let filtering = universe
221            .world
222            .add_component(engine::ecs::component::TextureFilteringComponent::nearest());
223        let _ = universe.attach(t, filtering);
224
225        // TextSystem also supports styling from immediate Emissive children.
226        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    // --- HUD / lane (single track) ---
299    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    // Content for pattern/chain/graph starts at x=0; keep section labels aligned there.
306    // This also keeps them from overlapping the config block (which is anchored at lane_x).
307    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    // Small identifier cubes next to titles.
334    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    // Pattern roots so we can reset via one SetColor action.
343    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    // Labels for pattern/chain/graph sections.
349    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    // --- Processing chain + graph visualization (compiled graph → cubes + labels) ---
365    use engine::ecs::system::audio_graph_compiler::{
366        AudioGraphCompiler, AudioGraphNode, AudioGraphNodeKind,
367    };
368
369    fn effect_grey_for_depth(depth: usize) -> f32 {
370        // depth=1 => medium grey; deeper => lighter, capped.
371        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        // Depth-only layout:
434        // - keep children directly below their parent on X
435        // - use Z sibling offsets (in spawn_graph_tree) to show branching
436        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        // Push siblings “behind” each other along Z to reduce overlap between
468        // a sibling's cube and another node's label.
469        let dz_sibling = 0.18;
470        let z = origin.2 - (sibling_index as f32) * dz_sibling;
471
472        // Keep text slightly in front of its cube.
473        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        // Label next to the cube.
480        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        // Mix/branch label if branching.
512        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            // Each node's children form a sibling group; offset them in Z.
554            // For the root node, sibling_index is 0.
555            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    // Compile + display chain and graph for the track.
574    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    // Full compiled graph visualization (tree layout + mix labels).
589    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        // With depth-only X layout, we keep the tree centered at x=0.
597        // Pull the graph up now that we don't show the separate chain view.
598        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    // Animation with 16 keyframes drives scheduled notes + pattern highlights.
625    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        // Scheduled audio note (sample-accurate via lookahead).
644        // Pulse every beat.
645        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        // Keyframed band-pass center.
670        // This is an immediate parameter update applied RT-side (no graph rebuild).
671        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                // Update the BandPass node label in the graph visualization.
688                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        // Visualization reset per keyframe.
704        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        // Pattern cube per step for each track.
715        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    // Keep window open.
739    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}
Source

pub fn id(&self) -> Option<ComponentId>

Trait Implementations§

Source§

impl Clone for AudioOutputComponent

Source§

fn clone(&self) -> AudioOutputComponent

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Component for AudioOutputComponent

Source§

fn set_id(&mut self, component: ComponentId)

Source§

fn name(&self) -> &'static str

Short debug/type name for this component kind (e.g. “transform”, “camera”).
Source§

fn init(&mut self, emit: &mut dyn SignalEmitter, component: ComponentId)

Called when component is added to the World
Source§

fn as_any(&self) -> &dyn Any

Source§

fn as_any_mut(&mut self) -> &mut dyn Any

Source§

fn encode(&self) -> HashMap<String, Value>

Encode component data to a HashMap for serialization. Read more
Source§

fn decode(&mut self, data: &HashMap<String, Value>) -> Result<(), String>

Decode component data from a HashMap after deserialization. Read more
Source§

fn cleanup(&mut self, _emit: &mut dyn SignalEmitter, _component: ComponentId)

Called when component is removed from the World.
Source§

fn to_mms_ast(&self, _world: &World) -> ComponentExpression

Encode this component as an MMS Component Expression AST node. Read more
Source§

impl Copy for AudioOutputComponent

Source§

impl Debug for AudioOutputComponent

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for AudioOutputComponent

Source§

fn default() -> Self

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> Downcast for T
where T: Any,

Source§

fn into_any(self: Box<T>) -> Box<dyn Any>

Convert Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.
Source§

fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>

Convert Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be further downcast into Rc<ConcreteType> where ConcreteType implements Trait.
Source§

fn as_any(&self) -> &(dyn Any + 'static)

Convert &Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &Any’s vtable from &Trait’s.
Source§

fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)

Convert &mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &mut Any’s vtable from &mut Trait’s.
Source§

impl<T> DowncastSync for T
where T: Any + Send + Sync,

Source§

fn into_any_arc(self: Arc<T>) -> Arc<dyn Any + Sync + Send>

Convert Arc<Trait> (where Trait: Downcast) to Arc<Any>. Arc<Any> can then be further downcast into Arc<ConcreteType> where ConcreteType implements Trait.
Source§

impl<S, T> Duplex<S> for T
where T: FromSample<S> + ToSample<S>,

Source§

impl<T> DynClone for T
where T: Clone,

Source§

fn __clone_box(&self, _: Private) -> *mut ()

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<S> FromSample<S> for S

Source§

fn from_sample_(s: S) -> S

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<F, T> IntoSample<T> for F
where T: FromSample<F>,

Source§

fn into_sample(self) -> T

Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> ToSample<U> for T
where U: FromSample<T>,

Source§

fn to_sample_(self) -> U

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more