Skip to main content

mittens_engine/engine/ecs/system/
audio_graph_compiler.rs

1use 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    /// PCM-backed playable source. The RT thread pulls samples from
33    /// `SynthRtState::clip_assets` keyed by the source's component_ffi.
34    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                // First one wins (deterministic due to sorting).
127                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                // Exclude mix metadata nodes from the effect branch list.
139                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}