Skip to main content

devalang_wasm/engine/audio/graph/
mod.rs

1// Audio Graph (v0.2.0)
2// Explicit representation of audio routing and node connections
3
4use crate::engine::audio::nodes::AudioNode;
5use crate::language::syntax::ast::Value;
6use anyhow::{Result, anyhow};
7use std::collections::HashMap;
8
9/// Sidechain/ducking mode
10#[derive(Debug, Clone)]
11pub struct SidechainParam {
12    pub mode: SidechainMode,
13    pub amount: f32,
14}
15
16/// Types of sidechain ducking
17#[derive(Debug, Clone, PartialEq)]
18pub enum SidechainMode {
19    /// Amplitude-based ducking (simple volume reduction)
20    Amplitude,
21    /// Compressor-based ducking (more musical)
22    Compressor,
23}
24
25/// Type of connection represented by a route.
26#[derive(Debug, Clone, PartialEq)]
27pub enum AudioRouteKind {
28    /// Standard mix route.
29    Mix,
30    /// Duck destination based on source activity.
31    Duck,
32    /// Sidechain source based on destination activity.
33    Sidechain,
34}
35
36/// Audio route: explicit connection between nodes
37#[derive(Debug, Clone)]
38pub struct AudioRoute {
39    /// Source node ID
40    pub from: String,
41    /// Destination node ID
42    pub to: String,
43    /// Volume scaling (0.0 to 1.0+)
44    pub volume: f32,
45    /// Pan (-1.0 to 1.0: left to right)
46    pub pan: f32,
47    /// Route behavior
48    pub kind: AudioRouteKind,
49    /// Optional routing/effect parameters from the DSL
50    pub effect_params: Option<Value>,
51    /// Optional sidechain/ducking
52    pub sidechain: Option<SidechainParam>,
53}
54
55impl AudioRoute {
56    /// Create new route from source to destination
57    pub fn new(from: impl Into<String>, to: impl Into<String>) -> Self {
58        Self {
59            from: from.into(),
60            to: to.into(),
61            volume: 1.0,
62            pan: 0.0,
63            kind: AudioRouteKind::Mix,
64            effect_params: None,
65            sidechain: None,
66        }
67    }
68
69    /// Create a ducking route.
70    pub fn duck(from: impl Into<String>, to: impl Into<String>, params: Value) -> Self {
71        Self {
72            kind: AudioRouteKind::Duck,
73            effect_params: Some(params),
74            ..Self::new(from, to)
75        }
76    }
77
78    /// Create a sidechain route.
79    pub fn sidechain(from: impl Into<String>, to: impl Into<String>, params: Value) -> Self {
80        Self {
81            kind: AudioRouteKind::Sidechain,
82            effect_params: Some(params),
83            ..Self::new(from, to)
84        }
85    }
86
87    /// Set route volume
88    pub fn with_volume(mut self, vol: f32) -> Self {
89        self.volume = vol;
90        self
91    }
92
93    /// Set route pan
94    pub fn with_pan(mut self, pan: f32) -> Self {
95        self.pan = pan.clamp(-1.0, 1.0);
96        self
97    }
98
99    /// Add sidechain ducking
100    pub fn with_sidechain(mut self, mode: SidechainMode, amount: f32) -> Self {
101        self.sidechain = Some(SidechainParam { mode, amount });
102        self
103    }
104}
105
106/// Audio Graph: collection of nodes and routes
107#[derive(Debug, Clone)]
108pub struct AudioGraph {
109    /// All nodes by ID
110    pub nodes: HashMap<String, AudioNode>,
111    /// All routes (connections)
112    pub routes: Vec<AudioRoute>,
113    /// Master output node ID (always present)
114    pub master: String,
115}
116
117pub fn canonicalize_node_id(name: &str) -> String {
118    if name == "$master" || name == "master" {
119        "master".to_string()
120    } else {
121        name.to_string()
122    }
123}
124
125impl AudioGraph {
126    /// Create new empty graph with master output
127    pub fn new() -> Self {
128        let mut nodes = HashMap::new();
129        let master_node = AudioNode::new_master();
130        nodes.insert("master".to_string(), master_node);
131
132        Self {
133            nodes,
134            routes: Vec::new(),
135            master: "master".to_string(),
136        }
137    }
138
139    /// Add node to graph
140    pub fn add_node(&mut self, node: AudioNode) -> Result<()> {
141        if canonicalize_node_id(&node.id) == self.master {
142            return Err(anyhow!("Cannot add node with reserved ID 'master'"));
143        }
144        if self.nodes.contains_key(&node.id) {
145            return Err(anyhow!("Node with ID '{}' already exists", node.id));
146        }
147        self.nodes.insert(node.id.clone(), node);
148        Ok(())
149    }
150
151    /// Add route to graph (validates source and destination exist)
152    pub fn add_route(&mut self, route: AudioRoute) -> Result<()> {
153        if !self.nodes.contains_key(&route.from) {
154            return Err(anyhow!("Source node '{}' does not exist", route.from));
155        }
156        if !self.nodes.contains_key(&route.to) {
157            return Err(anyhow!("Destination node '{}' does not exist", route.to));
158        }
159        self.routes.push(route);
160        Ok(())
161    }
162
163    /// Add or update a bus node. This is used for routing declarations, where
164    /// repeated declarations may add effects to an existing bus.
165    pub fn upsert_bus(
166        &mut self,
167        id: impl Into<String>,
168        alias: Option<String>,
169        effects: Option<Value>,
170    ) -> Result<()> {
171        let id = canonicalize_node_id(&id.into());
172        if id == self.master {
173            if let Some(effects) = effects {
174                if let Some(master) = self.nodes.get_mut(&self.master) {
175                    master.effects = normalize_effect_value(effects)
176                        .into_iter()
177                        .map(effect_to_node_effect)
178                        .collect();
179                }
180            }
181            return Ok(());
182        }
183
184        if !self.nodes.contains_key(&id) {
185            let mut node = AudioNode::new_bus(&id);
186            node.name = alias.clone();
187            self.nodes.insert(id.clone(), node);
188        }
189
190        if let Some(node) = self.nodes.get_mut(&id) {
191            if alias.is_some() {
192                node.name = alias;
193            }
194            if let Some(effects) = effects {
195                node.effects = normalize_effect_value(effects)
196                    .into_iter()
197                    .map(effect_to_node_effect)
198                    .collect();
199            }
200        }
201
202        Ok(())
203    }
204
205    /// Build the canonical graph from the interpreter routing setup.
206    pub fn from_routing_setup(
207        routing_setup: &crate::engine::audio::interpreter::driver::RoutingSetup,
208    ) -> Self {
209        let mut graph = AudioGraph::new();
210
211        for (name, node_config) in &routing_setup.nodes {
212            let _ = graph.upsert_bus(name, node_config.alias.clone(), node_config.effects.clone());
213        }
214
215        for route in &routing_setup.routes {
216            let source = canonicalize_node_id(&route.source);
217            let destination = canonicalize_node_id(&route.destination);
218            let _ = graph.upsert_bus(source.clone(), None, None);
219            let _ = graph.upsert_bus(destination.clone(), None, None);
220
221            let gain = route
222                .effects
223                .as_ref()
224                .and_then(extract_route_gain)
225                .unwrap_or(1.0);
226
227            let _ = graph.add_route(AudioRoute::new(source, destination).with_volume(gain));
228        }
229
230        for duck in &routing_setup.ducks {
231            let source = canonicalize_node_id(&duck.source);
232            let destination = canonicalize_node_id(&duck.destination);
233            let _ = graph.upsert_bus(source.clone(), None, None);
234            let _ = graph.upsert_bus(destination.clone(), None, None);
235            let _ = graph.add_route(AudioRoute::duck(source, destination, duck.effect.clone()));
236        }
237
238        for sidechain in &routing_setup.sidechains {
239            let source = canonicalize_node_id(&sidechain.source);
240            let destination = canonicalize_node_id(&sidechain.destination);
241            let _ = graph.upsert_bus(source.clone(), None, None);
242            let _ = graph.upsert_bus(destination.clone(), None, None);
243            let _ = graph.add_route(AudioRoute::sidechain(
244                source,
245                destination,
246                sidechain.effect.clone(),
247            ));
248        }
249
250        graph.optimized()
251    }
252
253    /// Get node by ID
254    pub fn get_node(&self, id: &str) -> Option<&AudioNode> {
255        self.nodes.get(id)
256    }
257
258    /// Get mutable node by ID
259    pub fn get_node_mut(&mut self, id: &str) -> Option<&mut AudioNode> {
260        self.nodes.get_mut(id)
261    }
262
263    /// Get all routes from a node
264    pub fn get_routes_from(&self, node_id: &str) -> Vec<&AudioRoute> {
265        self.routes.iter().filter(|r| r.from == node_id).collect()
266    }
267
268    /// Get all routes to a node
269    pub fn get_routes_to(&self, node_id: &str) -> Vec<&AudioRoute> {
270        self.routes.iter().filter(|r| r.to == node_id).collect()
271    }
272
273    /// Count total nodes
274    pub fn node_count(&self) -> usize {
275        self.nodes.len()
276    }
277
278    /// Count total routes
279    pub fn route_count(&self) -> usize {
280        self.routes.len()
281    }
282
283    /// Validate graph consistency
284    pub fn validate(&self) -> Result<()> {
285        // Check master exists
286        if !self.nodes.contains_key(&self.master) {
287            return Err(anyhow!("Master node '{}' does not exist", self.master));
288        }
289
290        // Check all routes reference valid nodes
291        for route in &self.routes {
292            if !self.nodes.contains_key(&route.from) {
293                return Err(anyhow!("Route source '{}' does not exist", route.from));
294            }
295            if !self.nodes.contains_key(&route.to) {
296                return Err(anyhow!("Route destination '{}' does not exist", route.to));
297            }
298        }
299
300        Ok(())
301    }
302
303    /// Get all node names.
304    pub fn node_names(&self) -> Vec<String> {
305        self.nodes.keys().cloned().collect()
306    }
307
308    /// Check if a node exists.
309    pub fn has_node(&self, node_id: &str) -> bool {
310        self.nodes.contains_key(&canonicalize_node_id(node_id))
311    }
312
313    /// Return an optimized copy with duplicate mix routes merged and node
314    /// effects normalized.
315    pub fn optimized(&self) -> Self {
316        let mut optimized = self.clone();
317        optimized.merge_duplicate_mix_routes();
318        optimized.normalize_node_effects();
319        optimized
320    }
321
322    fn merge_duplicate_mix_routes(&mut self) {
323        let mut mix_routes: HashMap<(String, String), AudioRoute> = HashMap::new();
324        let mut routes = Vec::new();
325
326        for route in self.routes.drain(..) {
327            if route.kind == AudioRouteKind::Mix {
328                let key = (route.from.clone(), route.to.clone());
329                mix_routes
330                    .entry(key)
331                    .and_modify(|existing| existing.volume += route.volume)
332                    .or_insert(route);
333            } else {
334                routes.push(route);
335            }
336        }
337
338        routes.extend(mix_routes.into_values().filter(|route| route.volume != 0.0));
339        self.routes = routes;
340    }
341
342    fn normalize_node_effects(&mut self) {
343        for node in self.nodes.values_mut() {
344            if node.effects.is_empty() {
345                continue;
346            }
347
348            let effects = Value::Array(
349                node.effects
350                    .iter()
351                    .map(|(_, params)| params.clone())
352                    .collect(),
353            );
354            node.effects = normalize_effect_value(effects)
355                .into_iter()
356                .map(effect_to_node_effect)
357                .collect();
358        }
359    }
360}
361
362impl Default for AudioGraph {
363    fn default() -> Self {
364        Self::new()
365    }
366}
367
368fn extract_route_gain(effects: &Value) -> Option<f32> {
369    if let Value::Map(effects_map) = effects {
370        if let Some(Value::Map(volume_map)) = effects_map.get("volume") {
371            if let Some(Value::Number(gain)) = volume_map.get("gain") {
372                return Some(*gain);
373            }
374        }
375        if let Some(Value::Number(gain)) = effects_map.get("volume") {
376            return Some(*gain);
377        }
378    }
379    None
380}
381
382fn effect_to_node_effect(effect: Value) -> (String, Value) {
383    let name = effect_name(&effect).unwrap_or_else(|| "effect".to_string());
384    (name, effect)
385}
386
387fn normalize_effect_value(effects: Value) -> Vec<Value> {
388    let flattened = flatten_effects(effects);
389    let mut normalized = Vec::new();
390
391    for effect in flattened {
392        if let Some(effect_key) = effect_name(&effect) {
393            if let Some(last) = normalized.last_mut() {
394                if let Some(last_key) = effect_name(last) {
395                    if last_key == effect_key {
396                        merge_effect_items(last, &effect);
397                        continue;
398                    }
399                }
400            }
401        }
402
403        normalized.push(effect);
404    }
405
406    normalized
407}
408
409fn flatten_effects(effects: Value) -> Vec<Value> {
410    match effects {
411        Value::Array(arr) => arr.into_iter().flat_map(flatten_effects).collect(),
412        other => vec![other],
413    }
414}
415
416fn effect_name(value: &Value) -> Option<String> {
417    match value {
418        Value::String(s) | Value::Identifier(s) => Some(s.clone()),
419        Value::Map(map) => {
420            if let Some(effect_type) = map.get("type").or_else(|| map.get("effect")) {
421                match effect_type {
422                    Value::String(s) | Value::Identifier(s) => Some(s.clone()),
423                    _ => None,
424                }
425            } else if map.len() == 1 {
426                map.keys().next().cloned()
427            } else {
428                None
429            }
430        }
431        _ => None,
432    }
433}
434
435fn merge_effect_items(target: &mut Value, source: &Value) {
436    if let Value::Map(target_map) = target {
437        if let Value::Map(source_map) = source {
438            for (key, value) in source_map {
439                target_map.insert(key.clone(), value.clone());
440            }
441            return;
442        }
443    }
444
445    if matches!(target, Value::String(_) | Value::Identifier(_)) {
446        if let Value::Map(_) = source {
447            *target = source.clone();
448        }
449    }
450}
451
452#[cfg(test)]
453mod tests {
454    use super::*;
455
456    #[test]
457    fn test_new_graph() {
458        let graph = AudioGraph::new();
459        assert_eq!(graph.node_count(), 1); // Master
460        assert!(graph.nodes.contains_key("master"));
461    }
462
463    #[test]
464    fn test_add_node() {
465        let mut graph = AudioGraph::new();
466        let node = AudioNode::new_synth("synth_1");
467        graph.add_node(node).unwrap();
468        assert_eq!(graph.node_count(), 2); // Master + synth_1
469    }
470
471    #[test]
472    fn test_add_duplicate_node() {
473        let mut graph = AudioGraph::new();
474        let node = AudioNode::new_synth("syn");
475        graph.add_node(node.clone()).unwrap();
476        let result = graph.add_node(node);
477        assert!(result.is_err());
478    }
479
480    #[test]
481    fn test_add_route() {
482        let mut graph = AudioGraph::new();
483        let node = AudioNode::new_synth("syn");
484        graph.add_node(node).unwrap();
485
486        let route = AudioRoute::new("syn", "master");
487        graph.add_route(route).unwrap();
488        assert_eq!(graph.route_count(), 1);
489    }
490
491    #[test]
492    fn test_route_from_nonexistent_node() {
493        let mut graph = AudioGraph::new();
494        let route = AudioRoute::new("nonexistent", "master");
495        assert!(graph.add_route(route).is_err());
496    }
497
498    #[test]
499    fn test_route_to_nonexistent_node() {
500        let mut graph = AudioGraph::new();
501        let route = AudioRoute::new("master", "nonexistent");
502        assert!(graph.add_route(route).is_err());
503    }
504
505    #[test]
506    fn test_get_routes_from() {
507        let mut graph = AudioGraph::new();
508        let syn1 = AudioNode::new_synth("syn1");
509        let syn2 = AudioNode::new_synth("syn2");
510        graph.add_node(syn1).unwrap();
511        graph.add_node(syn2).unwrap();
512
513        graph.add_route(AudioRoute::new("syn1", "master")).unwrap();
514        graph.add_route(AudioRoute::new("syn1", "syn2")).unwrap();
515        graph.add_route(AudioRoute::new("syn2", "master")).unwrap();
516
517        let from_syn1 = graph.get_routes_from("syn1");
518        assert_eq!(from_syn1.len(), 2);
519    }
520
521    #[test]
522    fn test_route_with_volume_and_pan() {
523        let route = AudioRoute::new("syn", "master")
524            .with_volume(0.5)
525            .with_pan(-0.3);
526        assert_eq!(route.volume, 0.5);
527        assert_eq!(route.pan, -0.3);
528    }
529
530    #[test]
531    fn test_canonicalize_master_aliases() {
532        assert_eq!(canonicalize_node_id("$master"), "master");
533        assert_eq!(canonicalize_node_id("master"), "master");
534        assert_eq!(canonicalize_node_id("drums"), "drums");
535    }
536
537    #[test]
538    fn test_from_routing_setup_creates_bus_routes_and_effects() {
539        let mut setup = crate::engine::audio::interpreter::driver::RoutingSetup::default();
540        setup.nodes.insert(
541            "drums".to_string(),
542            crate::engine::audio::interpreter::driver::RoutingNodeConfig {
543                name: "drums".to_string(),
544                alias: Some("kickPattern".to_string()),
545                effects: Some(Value::Array(vec![
546                    Value::String("reverb".to_string()),
547                    Value::String("reverb".to_string()),
548                ])),
549            },
550        );
551        setup
552            .routes
553            .push(crate::engine::audio::interpreter::driver::RouteConfig {
554                source: "drums".to_string(),
555                destination: "$master".to_string(),
556                effects: Some(Value::Map({
557                    let mut volume = HashMap::new();
558                    volume.insert("gain".to_string(), Value::Number(0.75));
559                    let mut map = HashMap::new();
560                    map.insert("volume".to_string(), Value::Map(volume));
561                    map
562                })),
563            });
564        setup
565            .ducks
566            .push(crate::engine::audio::interpreter::driver::DuckConfig {
567                source: "kick".to_string(),
568                destination: "drums".to_string(),
569                effect: Value::String("compressor".to_string()),
570            });
571        setup
572            .sidechains
573            .push(crate::engine::audio::interpreter::driver::SidechainConfig {
574                source: "drums".to_string(),
575                destination: "pad".to_string(),
576                effect: Value::String("gate".to_string()),
577            });
578
579        let graph = AudioGraph::from_routing_setup(&setup);
580
581        assert!(graph.has_node("master"));
582        assert!(graph.has_node("drums"));
583        assert!(graph.has_node("kick"));
584        assert!(graph.has_node("pad"));
585        assert_eq!(graph.nodes["drums"].name, Some("kickPattern".to_string()));
586        assert_eq!(graph.nodes["drums"].effects.len(), 1);
587        assert!(graph.routes.iter().any(|route| {
588            route.kind == AudioRouteKind::Mix
589                && route.from == "drums"
590                && route.to == "master"
591                && (route.volume - 0.75).abs() < f32::EPSILON
592        }));
593        assert!(
594            graph
595                .routes
596                .iter()
597                .any(|route| route.kind == AudioRouteKind::Duck)
598        );
599        assert!(
600            graph
601                .routes
602                .iter()
603                .any(|route| route.kind == AudioRouteKind::Sidechain)
604        );
605    }
606}