mittens_engine/engine/ecs/system/
audio_graph_compiler.rs1use crate::engine::ecs::component::{
2 AudioBandPassFilterComponent, AudioClipComponent, AudioGainComponent,
3 AudioHighPassFilterComponent, AudioLimiterComponent, AudioLowPassFilterComponent,
4 AudioMixComponent, AudioOscillatorComponent,
5};
6use crate::engine::ecs::{ComponentId, World};
7
8#[derive(Debug, Clone)]
9pub struct CompiledAudioGraph {
10 pub root: AudioGraphNode,
11}
12
13#[derive(Debug, Clone)]
14pub struct AudioGraphNode {
15 pub component: ComponentId,
16 pub kind: AudioGraphNodeKind,
17 pub mix: Option<AudioMixSpec>,
18 pub children: Vec<AudioGraphNode>,
19}
20
21#[derive(Debug, Clone)]
22pub struct AudioMixSpec {
23 pub component: ComponentId,
24 pub weights: Vec<f32>,
25}
26
27#[derive(Debug, Clone)]
28pub enum AudioGraphNodeKind {
29 OscillatorSource {
30 voices: usize,
31 },
32 ClipSource,
35 Gain {
36 gain: f32,
37 },
38 LowPass {
39 cutoff_hz: f32,
40 resonance: f32,
41 },
42 BandPass {
43 center_hz: f32,
44 bandwidth_octaves: f32,
45 resonance: f32,
46 },
47 HighPass {
48 cutoff_hz: f32,
49 resonance: f32,
50 },
51 Limiter {
52 attack_ms: f32,
53 release_ms: f32,
54 threshold: f32,
55 },
56}
57
58#[derive(Debug)]
59pub enum CompileAudioGraphError {
60 NotAnAudioSource(ComponentId),
61}
62
63impl std::fmt::Display for CompileAudioGraphError {
64 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65 match self {
66 CompileAudioGraphError::NotAnAudioSource(cid) => {
67 write!(f, "component {cid:?} is not an AudioSource")
68 }
69 }
70 }
71}
72
73impl std::error::Error for CompileAudioGraphError {}
74
75pub struct AudioGraphCompiler;
76
77impl AudioGraphCompiler {
78 pub fn compile(
79 world: &World,
80 source_root: ComponentId,
81 ) -> Result<CompiledAudioGraph, CompileAudioGraphError> {
82 if let Some(src) = world.get_component_by_id_as::<AudioOscillatorComponent>(source_root) {
83 let kind = AudioGraphNodeKind::OscillatorSource {
84 voices: src.oscillators.len(),
85 };
86 let (mix, children) = Self::compile_effect_children(world, source_root);
87 return Ok(CompiledAudioGraph {
88 root: AudioGraphNode {
89 component: source_root,
90 kind,
91 mix,
92 children,
93 },
94 });
95 }
96
97 if world
98 .get_component_by_id_as::<AudioClipComponent>(source_root)
99 .is_some()
100 {
101 let kind = AudioGraphNodeKind::ClipSource;
102 let (mix, children) = Self::compile_effect_children(world, source_root);
103 return Ok(CompiledAudioGraph {
104 root: AudioGraphNode {
105 component: source_root,
106 kind,
107 mix,
108 children,
109 },
110 });
111 }
112
113 Err(CompileAudioGraphError::NotAnAudioSource(source_root))
114 }
115
116 fn compile_effect_children(
117 world: &World,
118 parent: ComponentId,
119 ) -> (Option<AudioMixSpec>, Vec<AudioGraphNode>) {
120 let mut child_ids: Vec<ComponentId> = world.children_of(parent).iter().copied().collect();
121 child_ids.sort();
122
123 let mut mix: Option<AudioMixSpec> = None;
124 for &cid in &child_ids {
125 if let Some(m) = world.get_component_by_id_as::<AudioMixComponent>(cid) {
126 mix = Some(AudioMixSpec {
128 component: cid,
129 weights: m.weights.clone(),
130 });
131 break;
132 }
133 }
134
135 let effect_children: Vec<AudioGraphNode> = child_ids
136 .into_iter()
137 .filter(|&cid| {
138 world
140 .get_component_by_id_as::<AudioMixComponent>(cid)
141 .is_none()
142 })
143 .filter(|&cid| Self::effect_kind(world, cid).is_some())
144 .filter_map(|cid| Self::compile_effect_node(world, cid))
145 .collect();
146
147 (mix, effect_children)
148 }
149
150 fn compile_effect_node(world: &World, effect_cid: ComponentId) -> Option<AudioGraphNode> {
151 let kind = Self::effect_kind(world, effect_cid)?;
152 let (mix, children) = Self::compile_effect_children(world, effect_cid);
153 Some(AudioGraphNode {
154 component: effect_cid,
155 kind,
156 mix,
157 children,
158 })
159 }
160
161 fn effect_kind(world: &World, cid: ComponentId) -> Option<AudioGraphNodeKind> {
162 if let Some(c) = world.get_component_by_id_as::<AudioGainComponent>(cid) {
163 return Some(AudioGraphNodeKind::Gain { gain: c.gain });
164 }
165 if let Some(c) = world.get_component_by_id_as::<AudioLowPassFilterComponent>(cid) {
166 return Some(AudioGraphNodeKind::LowPass {
167 cutoff_hz: c.cutoff_hz,
168 resonance: c.resonance,
169 });
170 }
171 if let Some(c) = world.get_component_by_id_as::<AudioBandPassFilterComponent>(cid) {
172 return Some(AudioGraphNodeKind::BandPass {
173 center_hz: c.center_hz,
174 bandwidth_octaves: c.bandwidth_octaves,
175 resonance: c.resonance,
176 });
177 }
178 if let Some(c) = world.get_component_by_id_as::<AudioHighPassFilterComponent>(cid) {
179 return Some(AudioGraphNodeKind::HighPass {
180 cutoff_hz: c.cutoff_hz,
181 resonance: c.resonance,
182 });
183 }
184 if let Some(c) = world.get_component_by_id_as::<AudioLimiterComponent>(cid) {
185 return Some(AudioGraphNodeKind::Limiter {
186 attack_ms: c.attack_ms,
187 release_ms: c.release_ms,
188 threshold: c.threshold,
189 });
190 }
191 None
192 }
193}
194
195impl CompiledAudioGraph {
196 pub fn pretty(&self) -> String {
197 let mut out = String::new();
198 self.root.pretty_into(&mut out, 0);
199 out
200 }
201}
202
203impl AudioGraphNode {
204 fn pretty_into(&self, out: &mut String, indent: usize) {
205 let pad = " ".repeat(indent);
206 let line = match &self.kind {
207 AudioGraphNodeKind::OscillatorSource { voices } => {
208 format!(
209 "{pad}- AudioOscillatorComponent {{ oscillators: <len={voices}> }} (component={:?})\n",
210 self.component
211 )
212 }
213 AudioGraphNodeKind::ClipSource => {
214 format!(
215 "{pad}- AudioClipComponent (component={:?})\n",
216 self.component
217 )
218 }
219 AudioGraphNodeKind::Gain { gain } => {
220 format!(
221 "{pad}- AudioGainComponent {{ gain: {gain:.3} }} (component={:?})\n",
222 self.component
223 )
224 }
225 AudioGraphNodeKind::LowPass {
226 cutoff_hz,
227 resonance,
228 } => {
229 format!(
230 "{pad}- AudioLowPassFilterComponent {{ cutoff_hz: {cutoff_hz:.1}, resonance: {resonance:.3} }} (component={:?})\n",
231 self.component
232 )
233 }
234 AudioGraphNodeKind::BandPass {
235 center_hz,
236 bandwidth_octaves,
237 resonance,
238 } => {
239 format!(
240 "{pad}- AudioBandPassFilterComponent {{ center_hz: {center_hz:.1}, bandwidth_octaves: {bandwidth_octaves:.3}, resonance: {resonance:.3} }} (component={:?})\n",
241 self.component
242 )
243 }
244 AudioGraphNodeKind::HighPass {
245 cutoff_hz,
246 resonance,
247 } => {
248 format!(
249 "{pad}- AudioHighPassFilterComponent {{ cutoff_hz: {cutoff_hz:.1}, resonance: {resonance:.3} }} (component={:?})\n",
250 self.component
251 )
252 }
253 AudioGraphNodeKind::Limiter {
254 attack_ms,
255 release_ms,
256 threshold,
257 } => format!(
258 "{pad}- AudioLimiterComponent {{ attack_ms: {attack_ms:.1}, release_ms: {release_ms:.1}, threshold: {threshold:.3} }} (component={:?})\n",
259 self.component
260 ),
261 };
262 out.push_str(&line);
263
264 if self.children.len() > 1 {
265 let mut weights: Vec<f32> = Vec::with_capacity(self.children.len());
266 for i in 0..self.children.len() {
267 let w = self
268 .mix
269 .as_ref()
270 .map(|m| m.weights.get(i).copied().unwrap_or(1.0))
271 .unwrap_or(1.0);
272 weights.push(w);
273 }
274
275 if let Some(m) = &self.mix {
276 out.push_str(&format!(
277 "{pad} mix: AudioMixComponent {{ weights: {:?} }} (component={:?})\n",
278 weights, m.component
279 ));
280 } else {
281 out.push_str(&format!(
282 "{pad} mix: <implicit> (weights: {:?})\n",
283 weights
284 ));
285 }
286 }
287
288 for (i, ch) in self.children.iter().enumerate() {
289 if self.children.len() > 1 {
290 out.push_str(&format!("{pad} [branch {i}]\n"));
291 }
292 ch.pretty_into(out, indent + 2);
293 }
294 }
295}