devalang_wasm/engine/audio/effects/
chain.rs

1/// Effect chain module - sequential processing of multiple effects
2use super::registry::{CloneableEffect, EffectRegistry};
3use crate::engine::audio::lfo::{LfoParams, LfoRate, LfoTarget, LfoWaveform};
4use crate::language::syntax::ast::Value;
5use std::collections::HashMap;
6
7/// Effect chain - processes audio through multiple effects in sequence
8#[derive(Debug)]
9pub struct EffectChain {
10    effects: Vec<Box<dyn CloneableEffect>>,
11    registry: EffectRegistry,
12    synth_context: bool,
13}
14
15impl EffectChain {
16    /// Create a new effect chain with context
17    pub fn new(synth_context: bool) -> Self {
18        Self {
19            effects: Vec::new(),
20            registry: EffectRegistry::new(),
21            synth_context,
22        }
23    }
24
25    /// Add an effect to the chain if available in current context
26    pub fn add_effect(&mut self, name: &str, params: Option<Value>) -> bool {
27        if !self.registry.is_effect_available(name, self.synth_context) {
28            return false;
29        }
30
31        if let Some(processor) =
32            build_effect_processor(&self.registry, name, params, self.synth_context)
33        {
34            self.effects.push(processor);
35            true
36        } else {
37            false
38        }
39    }
40
41    /// Process audio samples through all effects in the chain
42    pub fn process(&mut self, samples: &mut [f32], sample_rate: u32) {
43        for effect in &mut self.effects {
44            effect.process(samples, sample_rate);
45        }
46    }
47
48    /// Reset all effects in the chain
49    pub fn reset(&mut self) {
50        for effect in &mut self.effects {
51            effect.reset();
52        }
53    }
54
55    /// Get number of effects in the chain
56    pub fn len(&self) -> usize {
57        self.effects.len()
58    }
59
60    /// Check if chain is empty
61    pub fn is_empty(&self) -> bool {
62        self.effects.is_empty()
63    }
64
65    /// Get all available effects for the current context
66    pub fn available_effects(&self) -> Vec<&'static str> {
67        self.registry.list_available_effects(self.synth_context)
68    }
69
70    /// Check if an effect is available in current context
71    pub fn is_effect_available(&self, name: &str) -> bool {
72        self.registry.is_effect_available(name, self.synth_context)
73    }
74}
75
76impl Default for EffectChain {
77    fn default() -> Self {
78        Self::new(true) // Default to synth context
79    }
80}
81
82/// Build an effect chain from a Value::Array of effect definitions
83pub fn build_effect_chain(effects_array: &[Value], synth_context: bool) -> EffectChain {
84    let mut chain = EffectChain::new(synth_context);
85
86    for effect_value in effects_array {
87        match effect_value {
88            Value::Map(map) => {
89                // Get effect type
90                if let Some(effect_type) = map.get("type").or_else(|| map.get("effect")) {
91                    if let Value::String(s) | Value::Identifier(s) = effect_type {
92                        chain.add_effect(s, Some(Value::Map(map.clone())));
93                    }
94                } else {
95                    // Fallback: map may be of the form { "reverb": { ... } } (no explicit "type" key)
96                    // or { "reverb": value } - in that case, expand each key as an effect entry.
97                    for (k, v) in map.iter() {
98                        match v {
99                            Value::Map(sm) => {
100                                chain.add_effect(k, Some(Value::Map(sm.clone())));
101                            }
102                            _ => {
103                                // non-map value, wrap as { "value": v }
104                                let mut sub = std::collections::HashMap::new();
105                                sub.insert("value".to_string(), v.clone());
106                                chain.add_effect(k, Some(Value::Map(sub.clone())));
107                            }
108                        }
109                    }
110                }
111            }
112            Value::String(s) | Value::Identifier(s) => {
113                chain.add_effect(s, None);
114            }
115            _ => {}
116        }
117    }
118
119    chain
120}
121
122/// Build a single effect processor from name and optional parameters
123fn build_effect_processor(
124    registry: &EffectRegistry,
125    name: &str,
126    params: Option<Value>,
127    synth_context: bool,
128) -> Option<Box<dyn CloneableEffect>> {
129    let base_processor = registry.get_effect(name, synth_context)?;
130
131    if let Some(Value::Map(params_map)) = params {
132        match name {
133            "chorus" => {
134                let depth = get_f32_param(&params_map, "depth", 0.7);
135                let rate = get_f32_param(&params_map, "rate", 0.5);
136                let mix = get_f32_param(&params_map, "mix", 0.5);
137                Some(Box::new(super::processors::ChorusProcessor::new(
138                    depth, rate, mix,
139                )))
140            }
141            "flanger" => {
142                let depth = get_f32_param(&params_map, "depth", 0.7);
143                let rate = get_f32_param(&params_map, "rate", 0.5);
144                let feedback = get_f32_param(&params_map, "feedback", 0.5);
145                let mix = get_f32_param(&params_map, "mix", 0.5);
146                Some(Box::new(super::processors::FlangerProcessor::new(
147                    depth, rate, feedback, mix,
148                )))
149            }
150            "phaser" => {
151                let stages = get_f32_param(&params_map, "stages", 4.0) as usize;
152                let rate = get_f32_param(&params_map, "rate", 0.5);
153                let depth = get_f32_param(&params_map, "depth", 0.7);
154                let feedback = get_f32_param(&params_map, "feedback", 0.5);
155                let mix = get_f32_param(&params_map, "mix", 0.5);
156                Some(Box::new(super::processors::PhaserProcessor::new(
157                    stages, rate, depth, feedback, mix,
158                )))
159            }
160            "compressor" => {
161                let threshold = get_f32_param(&params_map, "threshold", -20.0);
162                let ratio = get_f32_param(&params_map, "ratio", 4.0);
163                let attack = get_f32_param(&params_map, "attack", 0.005);
164                let release = get_f32_param(&params_map, "release", 0.1);
165                Some(Box::new(super::processors::CompressorProcessor::new(
166                    threshold, ratio, attack, release,
167                )))
168            }
169            "drive" => {
170                let amount = get_f32_param(&params_map, "amount", 0.7);
171                let mix = get_f32_param(&params_map, "mix", 0.5);
172                let tone = get_f32_param(&params_map, "tone", 0.5);
173                let color = get_f32_param(&params_map, "color", 0.5);
174
175                Some(Box::new(super::processors::DriveProcessor::new(
176                    amount, tone, color, mix,
177                )))
178            }
179            "reverb" => {
180                let size = get_f32_param(&params_map, "size", 0.5);
181                let damping = get_f32_param(&params_map, "damping", 0.5);
182                let decay = get_f32_param(&params_map, "decay", 0.5);
183                let mix = get_f32_param(&params_map, "mix", 0.3);
184
185                Some(Box::new(super::processors::ReverbProcessor::new(
186                    size, damping, decay, mix,
187                )))
188            }
189            "delay" => {
190                let time = get_f32_param(&params_map, "time", 250.0);
191                let feedback = get_f32_param(&params_map, "feedback", 0.4);
192                let mix = get_f32_param(&params_map, "mix", 0.3);
193                Some(Box::new(super::processors::DelayProcessor::new(
194                    time, feedback, mix,
195                )))
196            }
197            "speed" => {
198                // support both {'speed': 2.0} and {'value': 2.0} normalized forms
199                let speed = get_f32_param(
200                    &params_map,
201                    "speed",
202                    get_f32_param(&params_map, "value", 1.0),
203                );
204
205                Some(Box::new(super::processors::SpeedProcessor::new(speed)))
206            }
207            "lfo" => {
208                // parse LFO params: rate, depth, waveform, target, phase
209                // rate may be a number or string like "1/8"
210                let bpm = get_f32_param(&params_map, "bpm", 120.0);
211                let depth = get_f32_param(&params_map, "depth", 0.5).clamp(0.0, 1.0);
212                let phase = get_f32_param(&params_map, "phase", 0.0).fract();
213
214                // waveform
215                let waveform = params_map
216                    .get("waveform")
217                    .and_then(|v| match v {
218                        Value::String(s) | Value::Identifier(s) => Some(LfoWaveform::from_str(s)),
219                        _ => None,
220                    })
221                    .unwrap_or(LfoWaveform::Sine);
222
223                // rate
224                let rate_value_opt = params_map.get("rate");
225                let rate = match rate_value_opt {
226                    Some(Value::String(s)) | Some(Value::Identifier(s)) => LfoRate::from_value(s),
227                    Some(Value::Number(n)) => LfoRate::Hz(*n),
228                    _ => LfoRate::Hz(1.0),
229                };
230
231                // target
232                let target = params_map
233                    .get("target")
234                    .and_then(|v| match v {
235                        Value::String(s) | Value::Identifier(s) => LfoTarget::from_str(s),
236                        _ => None,
237                    })
238                    .unwrap_or(LfoTarget::Volume);
239
240                let params = LfoParams {
241                    rate,
242                    depth,
243                    waveform,
244                    target,
245                    phase,
246                };
247                // parse optional ranges
248                let semitones = get_f32_param(&params_map, "semitones", 2.0);
249                let base_cutoff = get_f32_param(&params_map, "cutoff", 1000.0);
250                let cutoff_range = get_f32_param(&params_map, "cutoff_range", 1000.0);
251                Some(Box::new(super::processors::LfoProcessor::new(
252                    params,
253                    bpm,
254                    semitones,
255                    base_cutoff,
256                    cutoff_range,
257                )))
258            }
259            "reverse" => {
260                let reverse = get_bool_param(
261                    &params_map,
262                    "reverse",
263                    get_bool_param(&params_map, "value", true),
264                );
265                Some(Box::new(super::processors::ReverseProcessor::new(reverse)))
266            }
267            _ => Some(base_processor),
268        }
269    } else {
270        Some(base_processor)
271    }
272}
273
274/// Helper to extract f32 parameter from map
275fn get_f32_param(map: &HashMap<String, Value>, key: &str, default: f32) -> f32 {
276    map.get(key)
277        .and_then(|v| match v {
278            Value::Number(n) => Some(*n),
279            Value::String(s) => s.parse::<f32>().ok(),
280            _ => None,
281        })
282        .unwrap_or(default)
283}
284
285/// Helper to extract bool parameter from map
286fn get_bool_param(map: &HashMap<String, Value>, key: &str, default: bool) -> bool {
287    map.get(key)
288        .and_then(|v| match v {
289            Value::Boolean(b) => Some(*b),
290            Value::Number(n) => Some(*n != 0.0),
291            Value::String(s) => s.parse::<bool>().ok(),
292            _ => None,
293        })
294        .unwrap_or(default)
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300
301    #[test]
302    fn test_effect_chain_creation() {
303        let chain = EffectChain::new(true); // Synth context
304        assert_eq!(chain.len(), 0);
305        assert!(chain.is_empty());
306
307        // Test available effects in synth context
308        let synth_effects = chain.available_effects();
309        assert!(synth_effects.contains(&"reverb"));
310        assert!(!synth_effects.contains(&"speed")); // Trigger-only effect
311    }
312
313    #[test]
314    fn test_effect_chain_trigger_context() {
315        let mut chain = EffectChain::new(false); // Trigger context
316
317        // Test trigger-specific effects
318        assert!(chain.is_effect_available("speed"));
319        assert!(chain.is_effect_available("reverse"));
320
321        // Test adding trigger-specific effect
322        assert!(chain.add_effect("speed", None));
323        assert_eq!(chain.len(), 1);
324    }
325
326    #[test]
327    fn test_effect_parameter_parsing() {
328        let mut chain = EffectChain::new(true);
329
330        // Test adding effect with parameters
331        let mut params = HashMap::new();
332        params.insert("depth".to_string(), Value::Number(0.8));
333        params.insert("rate".to_string(), Value::Number(1.0));
334        params.insert("mix".to_string(), Value::Number(0.6));
335
336        assert!(chain.add_effect("chorus", Some(Value::Map(params))));
337        assert_eq!(chain.len(), 1);
338    }
339
340    #[test]
341    fn test_effect_context_restrictions() {
342        let mut synth_chain = EffectChain::new(true);
343        let mut trigger_chain = EffectChain::new(false);
344
345        // Speed effect should only work in trigger context
346        assert!(!synth_chain.add_effect("speed", None));
347        assert!(trigger_chain.add_effect("speed", None));
348
349        // Reverb effect should work in both contexts
350        assert!(synth_chain.add_effect("reverb", None));
351        assert!(trigger_chain.add_effect("reverb", None));
352    }
353}