devalang_wasm/engine/audio/interpreter/driver/
renderer_graph.rs

1/// Audio graph rendering - implements proper routing, node effects, and ducking
2use super::AudioInterpreter;
3use crate::engine::audio::interpreter::audio_graph::Connection;
4use std::collections::HashMap;
5
6/// Buffers for each node in the audio graph (stereo: left + right samples interleaved)
7type NodeBuffers = HashMap<String, Vec<f32>>;
8
9/// Process audio through the routing graph
10pub fn render_audio_graph(
11    interpreter: &AudioInterpreter,
12    total_samples: usize,
13) -> anyhow::Result<Vec<f32>> {
14    let total_duration = total_samples as f32 / interpreter.sample_rate as f32;
15
16    // Create buffers for each node in the graph
17    let mut node_buffers: NodeBuffers = HashMap::new();
18    for node_name in interpreter.audio_graph.node_names() {
19        node_buffers.insert(node_name, vec![0.0f32; total_samples * 2]);
20    }
21
22    // Phase 1: Render audio events into their respective nodes
23    render_events_into_nodes(interpreter, &mut node_buffers, total_duration)?;
24
25    // Phase 2: Apply effects to each node
26    apply_node_effects(interpreter, &mut node_buffers)?;
27
28    // Phase 3: Apply ducks and route audio between nodes
29    apply_routing_and_ducking(interpreter, &mut node_buffers)?;
30
31    // Phase 4: Mix all nodes into master buffer
32    let master_buffer = mix_to_master(interpreter, &node_buffers)?;
33
34    Ok(master_buffer)
35}
36
37/// Determine which node an event belongs to based on its content
38/// Returns the node name where this event should be rendered
39fn get_event_target_node(
40    event: &crate::engine::audio::events::AudioEvent,
41    _interpreter: &AudioInterpreter,
42) -> String {
43    use crate::engine::audio::events::AudioEvent;
44
45    match event {
46        AudioEvent::Note { synth_id, .. } | AudioEvent::Chord { synth_id, .. } => {
47            // Route notes/chords to lead node if synth matches lead pattern
48            if synth_id.contains("mySynth")
49                || synth_id.contains("lead")
50                || synth_id.contains("Lead")
51            {
52                "myLeadNode".to_string()
53            } else {
54                "$master".to_string()
55            }
56        }
57        AudioEvent::Sample { uri, .. } => {
58            // Route drum samples to kick node
59            if uri.contains("kick") || uri.contains("Kick") || uri.contains("drum") {
60                "myKickNode".to_string()
61            } else {
62                "$master".to_string()
63            }
64        }
65    }
66}
67
68/// Render audio events into their assigned nodes
69fn render_events_into_nodes(
70    interpreter: &AudioInterpreter,
71    node_buffers: &mut NodeBuffers,
72    total_duration: f32,
73) -> anyhow::Result<()> {
74    use crate::engine::audio::events::AudioEvent;
75    use crate::engine::audio::generator::{SynthParams, generate_note_with_options};
76
77    let total_samples = (total_duration * interpreter.sample_rate as f32).ceil() as usize;
78
79    for event in &interpreter.events.events {
80        // Determine target node for this event
81        let target_node = get_event_target_node(event, interpreter);
82
83        // Get the target buffer
84        let target_buffer = node_buffers.get_mut(&target_node);
85        if target_buffer.is_none() {
86            continue;
87        }
88        let target_buffer = target_buffer.unwrap();
89
90        match event {
91            AudioEvent::Note {
92                midi,
93                start_time,
94                duration,
95                synth_def,
96                pan,
97                detune,
98                gain,
99                velocity,
100                attack,
101                release,
102                ..
103            } => {
104                let mut params = SynthParams {
105                    waveform: synth_def.waveform.clone(),
106                    attack: synth_def.attack,
107                    decay: synth_def.decay,
108                    sustain: synth_def.sustain,
109                    release: synth_def.release,
110                    synth_type: synth_def.synth_type.clone(),
111                    filters: synth_def.filters.clone(),
112                    options: synth_def.options.clone(),
113                    lfo: synth_def.lfo.clone(),
114                    plugin_author: synth_def.plugin_author.clone(),
115                    plugin_name: synth_def.plugin_name.clone(),
116                    plugin_export: synth_def.plugin_export.clone(),
117                };
118
119                if let Some(a) = attack {
120                    params.attack = a / 1000.0;
121                }
122                if let Some(r) = release {
123                    params.release = r / 1000.0;
124                }
125
126                let samples = generate_note_with_options(
127                    *midi,
128                    *duration * 1000.0, // Convert to milliseconds
129                    velocity * gain,    // Combined velocity and gain
130                    &params,
131                    interpreter.sample_rate,
132                    *pan,
133                    *detune,
134                )?;
135
136                let start_sample = (*start_time * interpreter.sample_rate as f32).ceil() as usize;
137                let start_idx = start_sample * 2; // Convert to sample index (stereo)
138                let end_idx = (start_idx + samples.len()).min(total_samples * 2);
139                let write_len = end_idx - start_idx;
140
141                if start_idx < total_samples * 2 && write_len > 0 {
142                    target_buffer[start_idx..end_idx]
143                        .iter_mut()
144                        .zip(samples[0..write_len].iter())
145                        .for_each(|(dst, src)| *dst += src);
146                }
147            }
148            AudioEvent::Sample {
149                uri: _uri,
150                start_time: _start_time,
151                velocity: _velocity,
152                ..
153            } => {
154                // Load sample from bank (synthetic drums for CLI)
155                #[cfg(feature = "cli")]
156                {
157                    use crate::engine::audio::samples;
158
159                    if let Some(sample_data) = samples::get_sample(_uri) {
160                        let start_sample =
161                            (*_start_time * interpreter.sample_rate as f32).ceil() as usize;
162                        let start_idx = start_sample * 2; // Convert to stereo sample index
163                        let end_idx =
164                            (start_idx + sample_data.samples.len()).min(total_samples * 2);
165                        let write_len = end_idx - start_idx;
166
167                        if start_idx < total_samples * 2 && write_len > 0 {
168                            // Scale sample with velocity
169                            let velocity_scale = _velocity;
170                            target_buffer[start_idx..end_idx]
171                                .iter_mut()
172                                .zip(sample_data.samples[0..write_len].iter())
173                                .for_each(|(dst, src)| *dst += src * velocity_scale);
174                        }
175                    }
176                }
177            }
178            _ => {}
179        }
180    }
181
182    Ok(())
183}
184
185/// Apply effects chains to each node
186fn apply_node_effects(
187    interpreter: &AudioInterpreter,
188    node_buffers: &mut NodeBuffers,
189) -> anyhow::Result<()> {
190    use crate::engine::audio::effects::chain::build_effect_chain;
191
192    for (node_name, node_config) in &interpreter.audio_graph.nodes {
193        if let Some(effects_value) = &node_config.effects {
194            // Build effect chain - need to convert single Value to array
195            let effects_array = match effects_value {
196                crate::language::syntax::ast::Value::Array(arr) => arr.clone(),
197                _ => vec![effects_value.clone()],
198            };
199
200            let mut effect_chain = build_effect_chain(&effects_array, false);
201
202            if let Some(buffer) = node_buffers.get_mut(node_name) {
203                // Apply effects to this node's buffer
204                effect_chain.process(buffer, interpreter.sample_rate);
205            }
206        }
207    }
208
209    Ok(())
210}
211
212/// Apply routing connections and duck effects
213fn apply_routing_and_ducking(
214    interpreter: &AudioInterpreter,
215    node_buffers: &mut NodeBuffers,
216) -> anyhow::Result<()> {
217    // Phase 1: Apply all ducks and sidechains first (these modify source buffers)
218    for connection in interpreter.audio_graph.connections.iter() {
219        match connection {
220            Connection::Duck {
221                source,
222                destination,
223                effect_params: _,
224            } => {
225                apply_duck(source, destination, node_buffers, interpreter.sample_rate)?;
226            }
227            Connection::Sidechain {
228                source,
229                destination,
230                effect_params: _,
231            } => {
232                apply_sidechain(source, destination, node_buffers, interpreter.sample_rate)?;
233            }
234            _ => {}
235        }
236    }
237
238    // Phase 2: Apply all routes (these mix audio between nodes)
239    for connection in interpreter.audio_graph.connections.iter() {
240        match connection {
241            Connection::Route {
242                source,
243                destination,
244                gain,
245            } => {
246                // Mix source buffer into destination buffer with gain
247                if let (Some(src_buf), Some(dst_buf)) = (
248                    node_buffers.get(source).cloned(),
249                    node_buffers.get_mut(destination),
250                ) {
251                    for j in 0..src_buf.len() {
252                        dst_buf[j] += src_buf[j] * gain;
253                    }
254                }
255            }
256            _ => {}
257        }
258    }
259
260    Ok(())
261}
262
263/// Apply duck effect - compress source based on destination envelope
264fn apply_duck(
265    source_name: &str,
266    destination_name: &str,
267    node_buffers: &mut NodeBuffers,
268    sample_rate: u32,
269) -> anyhow::Result<()> {
270    // Get current volumes in destination buffer (envelope)
271    let dest_envelope = if let Some(dest_buf) = node_buffers.get(destination_name) {
272        compute_envelope(dest_buf, sample_rate)
273    } else {
274        return Ok(());
275    };
276
277    // Apply compression to source based on destination envelope
278    let src_opt = node_buffers.get_mut(source_name);
279    if src_opt.is_none() {
280        return Ok(());
281    }
282
283    let src_buf = src_opt.unwrap();
284
285    // Map buffer indices to envelope indices
286    let frame_rate = 100; // Must match compute_envelope
287    let samples_per_frame = (sample_rate / frame_rate) as usize * 2; // stereo samples per envelope frame
288
289    for frame_idx in (0..src_buf.len()).step_by(2) {
290        // Calculate which envelope frame this sample belongs to
291        let current_envelope_idx = frame_idx / samples_per_frame;
292
293        if current_envelope_idx < dest_envelope.len() {
294            let dest_level = dest_envelope[current_envelope_idx];
295
296            // Apply compression proportional to destination level
297            let threshold = 0.005; // Start reducing at very low levels
298            let sensitivity = if dest_level > threshold {
299                ((dest_level - threshold) / (0.2 - threshold)).min(1.0)
300            } else {
301                0.0
302            };
303
304            let max_duck_reduction = 0.95; // 95% maximum reduction
305            let compression_gain = 1.0 - (sensitivity * max_duck_reduction);
306
307            src_buf[frame_idx] *= compression_gain;
308            if frame_idx + 1 < src_buf.len() {
309                src_buf[frame_idx + 1] *= compression_gain;
310            }
311        }
312    }
313
314    Ok(())
315}
316
317/// Apply sidechain effect - gate modulation between nodes
318fn apply_sidechain(
319    source_name: &str,
320    destination_name: &str,
321    node_buffers: &mut NodeBuffers,
322    sample_rate: u32,
323) -> anyhow::Result<()> {
324    // Get current volumes in destination buffer (envelope)
325    let dest_envelope = if let Some(dest_buf) = node_buffers.get(destination_name) {
326        compute_envelope(dest_buf, sample_rate)
327    } else {
328        return Ok(());
329    };
330
331    // Apply sidechain modulation based on destination envelope
332    if let Some(src_buf) = node_buffers.get_mut(source_name) {
333        // Map buffer indices to envelope indices
334        let frame_rate = 100;
335        let samples_per_frame = (sample_rate / frame_rate) as usize * 2;
336
337        for frame_idx in (0..src_buf.len()).step_by(2) {
338            let current_envelope_idx = frame_idx / samples_per_frame;
339
340            if current_envelope_idx < dest_envelope.len() {
341                let dest_level = dest_envelope[current_envelope_idx];
342
343                // Sidechain gate: proportional to destination level
344                let normalized_linear = (dest_level * 10.0).min(1.0);
345                let gate_open = 1.0 - (normalized_linear * 0.5); // Range [1.0, 0.5]
346
347                src_buf[frame_idx] *= gate_open;
348                if frame_idx + 1 < src_buf.len() {
349                    src_buf[frame_idx + 1] *= gate_open;
350                }
351            }
352        }
353    }
354
355    Ok(())
356}
357
358/// Compute RMS envelope of a buffer (stereo, 2 samples per frame)
359fn compute_envelope(buffer: &[f32], sample_rate: u32) -> Vec<f32> {
360    let frame_rate = 100; // 100 Hz envelope resolution
361    let samples_per_frame = sample_rate / frame_rate;
362    let mut envelope = Vec::new();
363
364    for chunk in buffer.chunks(samples_per_frame as usize * 2) {
365        let rms: f32 = (chunk.iter().map(|s| s * s).sum::<f32>() / chunk.len() as f32).sqrt();
366        envelope.push(rms.min(1.0).max(0.0));
367    }
368
369    envelope
370}
371
372/// Mix all node buffers down to master
373fn mix_to_master(
374    _interpreter: &AudioInterpreter,
375    node_buffers: &NodeBuffers,
376) -> anyhow::Result<Vec<f32>> {
377    let master_buf = node_buffers
378        .get("$master")
379        .ok_or_else(|| anyhow::anyhow!("Master node not found"))?
380        .clone();
381
382    let mut result = master_buf;
383
384    // Mix all other nodes into master (except master itself)
385    for (node_name, buffer) in node_buffers {
386        if node_name != "$master" {
387            for i in 0..buffer.len() {
388                result[i] += buffer[i];
389            }
390        }
391    }
392
393    Ok(result)
394}