Skip to main content

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            "eq" | "equalizer" => {
198                let low = get_f32_param(&params_map, "low", 1.0);
199                let mid = get_f32_param(&params_map, "mid", 1.0);
200                let high = get_f32_param(&params_map, "high", 1.0);
201                Some(Box::new(super::processors::EqProcessor::new(
202                    low, mid, high,
203                )))
204            }
205            "lowpass" | "lpf" => {
206                let cutoff = get_f32_param(&params_map, "cutoff", 5000.0);
207                let resonance = get_f32_param(&params_map, "resonance", 0.1);
208                Some(Box::new(super::processors::LowpassProcessor::new(
209                    cutoff, resonance,
210                )))
211            }
212            "highpass" | "hpf" => {
213                let cutoff = get_f32_param(&params_map, "cutoff", 200.0);
214                let resonance = get_f32_param(&params_map, "resonance", 0.1);
215                Some(Box::new(super::processors::HighpassProcessor::new(
216                    cutoff, resonance,
217                )))
218            }
219            "bandpass" | "bpf" => {
220                let cutoff = get_f32_param(&params_map, "cutoff", 1000.0);
221                let resonance = get_f32_param(&params_map, "resonance", 0.2);
222                Some(Box::new(super::processors::BandpassProcessor::new(
223                    cutoff, resonance,
224                )))
225            }
226            "distortion" | "dist" => {
227                let amount = get_f32_param(&params_map, "amount", 0.5);
228                let mix = get_f32_param(&params_map, "mix", 0.5);
229                Some(Box::new(super::processors::DistortionProcessor::new(
230                    amount, mix,
231                )))
232            }
233            "speed" => {
234                // support both {'speed': 2.0} and {'value': 2.0} normalized forms
235                let speed = get_f32_param(
236                    &params_map,
237                    "speed",
238                    get_f32_param(&params_map, "value", 1.0),
239                );
240
241                Some(Box::new(super::processors::SpeedProcessor::new(speed)))
242            }
243            "lfo" => {
244                // parse LFO params: rate, depth, waveform, target, phase
245                // rate may be a number or string like "1/8"
246                let bpm = get_f32_param(&params_map, "bpm", 120.0);
247                let depth = get_f32_param(&params_map, "depth", 0.5).clamp(0.0, 1.0);
248                let phase = get_f32_param(&params_map, "phase", 0.0).fract();
249
250                // waveform
251                let waveform = params_map
252                    .get("waveform")
253                    .and_then(|v| match v {
254                        Value::String(s) | Value::Identifier(s) => Some(LfoWaveform::from_str(s)),
255                        _ => None,
256                    })
257                    .unwrap_or(LfoWaveform::Sine);
258
259                // rate
260                let rate_value_opt = params_map.get("rate");
261                let rate = match rate_value_opt {
262                    Some(Value::String(s)) | Some(Value::Identifier(s)) => LfoRate::from_value(s),
263                    Some(Value::Number(n)) => LfoRate::Hz(*n),
264                    _ => LfoRate::Hz(1.0),
265                };
266
267                // target
268                let target = params_map
269                    .get("target")
270                    .and_then(|v| match v {
271                        Value::String(s) | Value::Identifier(s) => LfoTarget::from_str(s),
272                        _ => None,
273                    })
274                    .unwrap_or(LfoTarget::Volume);
275
276                let params = LfoParams {
277                    rate,
278                    depth,
279                    waveform,
280                    target,
281                    phase,
282                };
283                // parse optional ranges
284                let semitones = get_f32_param(&params_map, "semitones", 2.0);
285                let base_cutoff = get_f32_param(&params_map, "cutoff", 1000.0);
286                let cutoff_range = get_f32_param(&params_map, "cutoff_range", 1000.0);
287                Some(Box::new(super::processors::LfoProcessor::new(
288                    params,
289                    bpm,
290                    semitones,
291                    base_cutoff,
292                    cutoff_range,
293                )))
294            }
295            "reverse" => {
296                let reverse = get_bool_param(
297                    &params_map,
298                    "reverse",
299                    get_bool_param(&params_map, "value", true),
300                );
301                Some(Box::new(super::processors::ReverseProcessor::new(reverse)))
302            }
303            _ => Some(base_processor),
304        }
305    } else {
306        Some(base_processor)
307    }
308}
309
310/// Helper to extract f32 parameter from map
311fn get_f32_param(map: &HashMap<String, Value>, key: &str, default: f32) -> f32 {
312    map.get(key)
313        .and_then(|v| match v {
314            Value::Number(n) => Some(*n),
315            Value::String(s) => s.parse::<f32>().ok(),
316            _ => None,
317        })
318        .unwrap_or(default)
319}
320
321/// Helper to extract bool parameter from map
322fn get_bool_param(map: &HashMap<String, Value>, key: &str, default: bool) -> bool {
323    map.get(key)
324        .and_then(|v| match v {
325            Value::Boolean(b) => Some(*b),
326            Value::Number(n) => Some(*n != 0.0),
327            Value::String(s) => s.parse::<bool>().ok(),
328            _ => None,
329        })
330        .unwrap_or(default)
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336
337    #[test]
338    fn test_effect_chain_creation() {
339        let chain = EffectChain::new(true); // Synth context
340        assert_eq!(chain.len(), 0);
341        assert!(chain.is_empty());
342
343        // Test available effects in synth context
344        let synth_effects = chain.available_effects();
345        assert!(synth_effects.contains(&"reverb"));
346        assert!(!synth_effects.contains(&"speed")); // Trigger-only effect
347    }
348
349    #[test]
350    fn test_effect_chain_trigger_context() {
351        let mut chain = EffectChain::new(false); // Trigger context
352
353        // Test trigger-specific effects
354        assert!(chain.is_effect_available("speed"));
355        assert!(chain.is_effect_available("reverse"));
356
357        // Test adding trigger-specific effect
358        assert!(chain.add_effect("speed", None));
359        assert_eq!(chain.len(), 1);
360    }
361
362    #[test]
363    fn test_effect_parameter_parsing() {
364        let mut chain = EffectChain::new(true);
365
366        // Test adding effect with parameters
367        let mut params = HashMap::new();
368        params.insert("depth".to_string(), Value::Number(0.8));
369        params.insert("rate".to_string(), Value::Number(1.0));
370        params.insert("mix".to_string(), Value::Number(0.6));
371
372        assert!(chain.add_effect("chorus", Some(Value::Map(params))));
373        assert_eq!(chain.len(), 1);
374    }
375
376    #[test]
377    fn test_filter_effects_accept_parameter_maps() {
378        let mut params = HashMap::new();
379        params.insert("cutoff".to_string(), Value::Number(900.0));
380        params.insert("resonance".to_string(), Value::Number(0.2));
381
382        let effects = vec![Value::Map({
383            let mut effect = HashMap::new();
384            effect.insert("type".to_string(), Value::String("lowpass".to_string()));
385            effect.extend(params);
386            effect
387        })];
388
389        let chain = build_effect_chain(&effects, true);
390        assert_eq!(chain.len(), 1);
391    }
392
393    #[test]
394    fn test_eq_effect_accepts_three_band_map() {
395        let effects = vec![Value::Map({
396            let mut effect = HashMap::new();
397            effect.insert("type".to_string(), Value::String("eq".to_string()));
398            effect.insert("low".to_string(), Value::Number(1.2));
399            effect.insert("mid".to_string(), Value::Number(0.8));
400            effect.insert("high".to_string(), Value::Number(1.1));
401            effect
402        })];
403
404        let chain = build_effect_chain(&effects, true);
405        assert_eq!(chain.len(), 1);
406    }
407
408    #[test]
409    fn test_effect_context_restrictions() {
410        let mut synth_chain = EffectChain::new(true);
411        let mut trigger_chain = EffectChain::new(false);
412
413        // Speed effect should only work in trigger context
414        assert!(!synth_chain.add_effect("speed", None));
415        assert!(trigger_chain.add_effect("speed", None));
416
417        // Reverb effect should work in both contexts
418        assert!(synth_chain.add_effect("reverb", None));
419        assert!(trigger_chain.add_effect("reverb", None));
420    }
421}