pub struct AudioOutputComponent {
pub enabled: bool,
/* private fields */
}Fields§
§enabled: boolPlaceholder: when present, AudioSystem will try to start the default output stream.
Implementations§
Source§impl AudioOutputComponent
impl AudioOutputComponent
Sourcepub fn new() -> Self
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
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}Sourcepub fn off() -> Self
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}pub fn id(&self) -> Option<ComponentId>
Trait Implementations§
Source§impl Clone for AudioOutputComponent
impl Clone for AudioOutputComponent
Source§fn clone(&self) -> AudioOutputComponent
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)
fn clone_from(&mut self, source: &Self)
Performs copy-assignment from
source. Read moreSource§impl Component for AudioOutputComponent
impl Component for AudioOutputComponent
fn set_id(&mut self, component: ComponentId)
Source§fn name(&self) -> &'static str
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)
fn init(&mut self, emit: &mut dyn SignalEmitter, component: ComponentId)
Called when component is added to the World
fn as_any(&self) -> &dyn Any
fn as_any_mut(&mut self) -> &mut dyn Any
Source§fn encode(&self) -> HashMap<String, Value>
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>
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)
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
fn to_mms_ast(&self, _world: &World) -> ComponentExpression
Encode this component as an MMS Component Expression AST node. Read more
impl Copy for AudioOutputComponent
Source§impl Debug for AudioOutputComponent
impl Debug for AudioOutputComponent
Auto Trait Implementations§
impl Freeze for AudioOutputComponent
impl RefUnwindSafe for AudioOutputComponent
impl Send for AudioOutputComponent
impl Sync for AudioOutputComponent
impl Unpin for AudioOutputComponent
impl UnsafeUnpin for AudioOutputComponent
impl UnwindSafe for AudioOutputComponent
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Mutably borrows from an owned value. Read more
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
Source§fn into_any(self: Box<T>) -> Box<dyn Any>
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>
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)
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)
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.