devalang_wasm/engine/audio/graph/
mod.rs1use crate::engine::audio::nodes::AudioNode;
5use crate::language::syntax::ast::Value;
6use anyhow::{Result, anyhow};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone)]
11pub struct SidechainParam {
12 pub mode: SidechainMode,
13 pub amount: f32,
14}
15
16#[derive(Debug, Clone, PartialEq)]
18pub enum SidechainMode {
19 Amplitude,
21 Compressor,
23}
24
25#[derive(Debug, Clone, PartialEq)]
27pub enum AudioRouteKind {
28 Mix,
30 Duck,
32 Sidechain,
34}
35
36#[derive(Debug, Clone)]
38pub struct AudioRoute {
39 pub from: String,
41 pub to: String,
43 pub volume: f32,
45 pub pan: f32,
47 pub kind: AudioRouteKind,
49 pub effect_params: Option<Value>,
51 pub sidechain: Option<SidechainParam>,
53}
54
55impl AudioRoute {
56 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 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 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 pub fn with_volume(mut self, vol: f32) -> Self {
89 self.volume = vol;
90 self
91 }
92
93 pub fn with_pan(mut self, pan: f32) -> Self {
95 self.pan = pan.clamp(-1.0, 1.0);
96 self
97 }
98
99 pub fn with_sidechain(mut self, mode: SidechainMode, amount: f32) -> Self {
101 self.sidechain = Some(SidechainParam { mode, amount });
102 self
103 }
104}
105
106#[derive(Debug, Clone)]
108pub struct AudioGraph {
109 pub nodes: HashMap<String, AudioNode>,
111 pub routes: Vec<AudioRoute>,
113 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 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 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 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 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 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 pub fn get_node(&self, id: &str) -> Option<&AudioNode> {
255 self.nodes.get(id)
256 }
257
258 pub fn get_node_mut(&mut self, id: &str) -> Option<&mut AudioNode> {
260 self.nodes.get_mut(id)
261 }
262
263 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 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 pub fn node_count(&self) -> usize {
275 self.nodes.len()
276 }
277
278 pub fn route_count(&self) -> usize {
280 self.routes.len()
281 }
282
283 pub fn validate(&self) -> Result<()> {
285 if !self.nodes.contains_key(&self.master) {
287 return Err(anyhow!("Master node '{}' does not exist", self.master));
288 }
289
290 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 pub fn node_names(&self) -> Vec<String> {
305 self.nodes.keys().cloned().collect()
306 }
307
308 pub fn has_node(&self, node_id: &str) -> bool {
310 self.nodes.contains_key(&canonicalize_node_id(node_id))
311 }
312
313 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); 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); }
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}