Skip to main content

dirtydata_runtime/nodes/
legacy.rs

1use dirtydata_core::types::ConfigSnapshot;
2use dirtydata_host::PluginHost;
3use rand::prelude::*;
4use rand_pcg::Pcg32;
5use std::sync::Arc;
6use std::collections::VecDeque;
7
8use super::base::*;
9
10// ──────────────────────────────────────────────
11// §1 — Sources
12// ──────────────────────────────────────────────
13
14pub struct OscillatorNode {
15    phase: f32,
16    freq_smooth: Option<SmoothedValue>,
17}
18
19impl OscillatorNode {
20    pub fn new() -> Self {
21        Self { phase: 0.0, freq_smooth: None }
22    }
23}
24
25impl DspNode for OscillatorNode {
26    fn process(&mut self, _inputs: &[f32], outputs: &mut [[f32; 2]], config: &ConfigSnapshot, ctx: &ProcessContext) {
27        let freq_target = config.get("frequency").and_then(|v| v.as_float()).unwrap_or(440.0) as f32;
28        let wave_type = config.get("waveform").and_then(|v| v.as_string());
29        
30        let smooth = self.freq_smooth.get_or_insert_with(|| SmoothedValue::new(freq_target, ctx.sample_rate, 10.0));
31        let freq = smooth.next();
32        let phase_inc = freq / ctx.sample_rate;
33        
34        let val = match wave_type.map(|s| s.as_str()).unwrap_or("sine") {
35            "sine" => (self.phase * 2.0 * std::f32::consts::PI).sin(),
36            "saw" => (self.phase * 2.0) - 1.0,
37            "square" => if self.phase < 0.5 { 1.0 } else { -1.0 },
38            "triangle" => {
39                let v = self.phase * 4.0;
40                if v < 1.0 { v - 0.0 }
41                else if v < 3.0 { 2.0 - v }
42                else { v - 4.0 }
43            }
44            _ => (self.phase * 2.0 * std::f32::consts::PI).sin(),
45        };
46
47        outputs[0][0] = val;
48        outputs[0][1] = val;
49
50        self.phase = (self.phase + phase_inc) % 1.0;
51    }
52
53    fn update_parameter(&mut self, param: &str, value: f32) {
54        if param == "frequency" {
55            if let Some(s) = &mut self.freq_smooth {
56                s.set_target(value);
57            }
58        }
59    }
60
61    fn extract_state(&self) -> NodeState {
62        NodeState::from_json(serde_json::json!({ "phase": self.phase }))
63    }
64
65    fn inject_state(&mut self, state: &NodeState) {
66        if let Some(val) = state.to_json::<serde_json::Value>() {
67            if let Some(phase) = val.get("phase").and_then(|v| v.as_f64()) {
68                self.phase = phase as f32;
69            }
70        }
71    }
72}
73
74pub struct NoiseNode {
75    rng: Pcg32,
76}
77
78impl NoiseNode {
79    pub fn new(seed: u64) -> Self {
80        Self { rng: Pcg32::seed_from_u64(seed) }
81    }
82}
83
84impl DspNode for NoiseNode {
85    fn process(&mut self, _inputs: &[f32], outputs: &mut [[f32; 2]], _config: &ConfigSnapshot, _ctx: &ProcessContext) {
86        let val: f32 = self.rng.gen_range(-1.0..1.0);
87        outputs[0][0] = val;
88        outputs[0][1] = val;
89    }
90}
91
92pub struct AssetReaderNode {
93    data: Arc<Vec<f32>>,
94    cursor: usize,
95}
96
97impl AssetReaderNode {
98    pub fn new(data: Arc<Vec<f32>>) -> Self {
99        Self { data, cursor: 0 }
100    }
101}
102
103impl DspNode for AssetReaderNode {
104    fn process(&mut self, _inputs: &[f32], outputs: &mut [[f32; 2]], _config: &ConfigSnapshot, _ctx: &ProcessContext) {
105        if self.cursor + 1 < self.data.len() {
106            outputs[0][0] = self.data[self.cursor];
107            outputs[0][1] = self.data[self.cursor + 1];
108            self.cursor += 2;
109        } else {
110            outputs[0][0] = 0.0;
111            outputs[0][1] = 0.0;
112        }
113    }
114}
115
116// ──────────────────────────────────────────────
117// §2 — Processors
118// ──────────────────────────────────────────────
119
120pub struct GainNode {
121    gain_smooth: Option<SmoothedValue>,
122}
123
124impl GainNode {
125    pub fn new() -> Self {
126        Self { gain_smooth: None }
127    }
128}
129
130impl DspNode for GainNode {
131    fn process(&mut self, inputs: &[f32], outputs: &mut [[f32; 2]], config: &ConfigSnapshot, ctx: &ProcessContext) {
132        let gain_db_target = config.get("gain_db").and_then(|v| v.as_float()).unwrap_or(0.0) as f32;
133        
134        let smooth = self.gain_smooth.get_or_insert_with(|| SmoothedValue::new(gain_db_target, ctx.sample_rate, 10.0));
135        let gain_db = smooth.next();
136        let linear = 10.0_f32.powf(gain_db / 20.0);
137        
138        if inputs.len() >= 2 {
139            outputs[0][0] = inputs[0] * linear;
140            outputs[0][1] = inputs[1] * linear;
141        }
142    }
143
144    fn update_parameter(&mut self, param: &str, value: f32) {
145        if param == "gain_db" {
146            if let Some(s) = &mut self.gain_smooth {
147                s.set_target(value);
148            }
149        }
150    }
151}
152
153impl BiquadFilterNode {
154    pub fn new() -> Self {
155        Self { z1: [0.0, 0.0], z2: [0.0, 0.0], freq_smooth: None }
156    }
157}
158
159pub struct BiquadFilterNode {
160    z1: [f32; 2],
161    z2: [f32; 2],
162    freq_smooth: Option<SmoothedValue>,
163}
164
165impl DspNode for BiquadFilterNode {
166    fn process(&mut self, inputs: &[f32], outputs: &mut [[f32; 2]], config: &ConfigSnapshot, ctx: &ProcessContext) {
167        let freq_target = config.get("frequency").and_then(|v| v.as_float()).unwrap_or(1000.0) as f32;
168        let q = config.get("q").and_then(|v| v.as_float()).unwrap_or(0.707) as f32;
169        let filter_type = config.get("type").and_then(|v| v.as_string());
170
171        let smooth = self.freq_smooth.get_or_insert_with(|| SmoothedValue::new(freq_target, ctx.sample_rate, 10.0));
172        let freq = smooth.next();
173
174        // Simple RBJ Biquad coefficients
175        let w0 = 2.0 * std::f32::consts::PI * freq / ctx.sample_rate;
176        let alpha = w0.sin() / (2.0 * q);
177        let cos_w0 = w0.cos();
178
179        let (b0, b1, b2, a0, a1, a2) = match filter_type.map(|s| s.as_str()).unwrap_or("lpf") {
180            "hpf" => {
181                let b0 = (1.0 + cos_w0) / 2.0;
182                let b1 = -(1.0 + cos_w0);
183                let b2 = (1.0 + cos_w0) / 2.0;
184                let a0 = 1.0 + alpha;
185                let a1 = -2.0 * cos_w0;
186                let a2 = 1.0 - alpha;
187                (b0, b1, b2, a0, a1, a2)
188            }
189            "bandpass" => {
190                let b0 = alpha;
191                let b1 = 0.0;
192                let b2 = -alpha;
193                let a0 = 1.0 + alpha;
194                let a1 = -2.0 * cos_w0;
195                let a2 = 1.0 - alpha;
196                (b0, b1, b2, a0, a1, a2)
197            }
198            "notch" => {
199                let b0 = 1.0;
200                let b1 = -2.0 * cos_w0;
201                let b2 = 1.0;
202                let a0 = 1.0 + alpha;
203                let a1 = -2.0 * cos_w0;
204                let a2 = 1.0 - alpha;
205                (b0, b1, b2, a0, a1, a2)
206            }
207            "peak" => {
208                let gain_db = config.get("gain_db").and_then(|v| v.as_float()).unwrap_or(0.0) as f32;
209                let a_val = 10.0_f32.powf(gain_db / 40.0);
210                let b0 = 1.0 + alpha * a_val;
211                let b1 = -2.0 * cos_w0;
212                let b2 = 1.0 - alpha * a_val;
213                let a0 = 1.0 + alpha / a_val;
214                let a1 = -2.0 * cos_w0;
215                let a2 = 1.0 - alpha / a_val;
216                (b0, b1, b2, a0, a1, a2)
217            }
218            _ => { // LPF
219                let b0 = (1.0 - cos_w0) / 2.0;
220                let b1 = 1.0 - cos_w0;
221                let b2 = (1.0 - cos_w0) / 2.0;
222                let a0 = 1.0 + alpha;
223                let a1 = -2.0 * cos_w0;
224                let a2 = 1.0 - alpha;
225                (b0, b1, b2, a0, a1, a2)
226            }
227        };
228
229        let inv_a0 = 1.0 / a0;
230        let ff0 = b0 * inv_a0;
231        let ff1 = b1 * inv_a0;
232        let ff2 = b2 * inv_a0;
233        let fb1 = a1 * inv_a0;
234        let fb2 = a2 * inv_a0;
235
236        for i in 0..2 {
237            let x = if inputs.len() > i { inputs[i] } else { 0.0 };
238            let y = ff0 * x + self.z1[i];
239            self.z1[i] = ff1 * x - fb1 * y + self.z2[i];
240            self.z2[i] = ff2 * x - fb2 * y;
241            outputs[0][i] = y;
242        }
243    }
244
245    fn update_parameter(&mut self, param: &str, value: f32) {
246        if param == "frequency" {
247            if let Some(s) = &mut self.freq_smooth {
248                s.set_target(value);
249            }
250        }
251    }
252}
253
254pub struct CompressorNode {
255    envelope: f32,
256}
257
258impl CompressorNode {
259    pub fn new() -> Self {
260        Self { envelope: 0.0 }
261    }
262}
263
264impl DspNode for CompressorNode {
265    fn process(&mut self, inputs: &[f32], outputs: &mut [[f32; 2]], config: &ConfigSnapshot, ctx: &ProcessContext) {
266        let threshold_db = config.get("threshold_db").and_then(|v| v.as_float()).unwrap_or(-20.0) as f32;
267        let ratio = config.get("ratio").and_then(|v| v.as_float()).unwrap_or(4.0) as f32;
268        let attack_ms = config.get("attack_ms").and_then(|v| v.as_float()).unwrap_or(10.0) as f32;
269        let release_ms = config.get("release_ms").and_then(|v| v.as_float()).unwrap_or(100.0) as f32;
270
271        let threshold = 10.0_f32.powf(threshold_db / 20.0);
272        let attack_alpha = 1.0 - (-1.0 / (attack_ms * ctx.sample_rate / 1000.0)).exp();
273        let release_alpha = 1.0 - (-1.0 / (release_ms * ctx.sample_rate / 1000.0)).exp();
274
275        let (l, r) = if inputs.len() >= 2 {
276            (inputs[0], inputs[1])
277        } else if inputs.len() == 1 {
278            (inputs[0], inputs[0])
279        } else {
280            (0.0, 0.0)
281        };
282
283        let peak = l.abs().max(r.abs());
284        let alpha = if peak > self.envelope { attack_alpha } else { release_alpha };
285        self.envelope += alpha * (peak - self.envelope);
286
287        let gain = if self.envelope > threshold {
288            let over_db = 20.0 * (self.envelope / threshold).log10();
289            let reduction_db = over_db * (1.0 - 1.0 / ratio);
290            10.0_f32.powf(-reduction_db / 20.0)
291        } else {
292            1.0
293        };
294
295        outputs[0][0] = l * gain;
296        outputs[0][1] = r * gain;
297    }
298}
299
300pub struct ForeignNode {
301    host: Option<PluginHost>,
302    plugin_name: String,
303    buffer_size: usize,
304    in_buffer: Vec<f32>,
305    out_buffer: Vec<f32>,
306    buffer_idx: usize,
307    has_crashed: bool,
308}
309
310impl ForeignNode {
311    pub fn new(plugin_name: String, buffer_size: usize) -> Self {
312        Self {
313            host: None,
314            plugin_name,
315            buffer_size,
316            in_buffer: vec![0.0; buffer_size],
317            out_buffer: vec![0.0; buffer_size],
318            buffer_idx: 0,
319            has_crashed: false,
320        }
321    }
322
323    fn ensure_host(&mut self) -> bool {
324        if self.has_crashed { return false; }
325        if self.host.is_some() { return true; }
326        
327        match PluginHost::new(&self.plugin_name, self.buffer_size) {
328            Ok(h) => {
329                self.host = Some(h);
330                true
331            }
332            Err(_) => {
333                self.has_crashed = true;
334                false
335            }
336        }
337    }
338}
339
340impl DspNode for ForeignNode {
341    fn process(&mut self, inputs: &[f32], outputs: &mut [[f32; 2]], _config: &ConfigSnapshot, _ctx: &ProcessContext) {
342        if !self.ensure_host() {
343            // Fallback: Silence or pass through
344            outputs[0] = [0.0, 0.0];
345            return;
346        }
347
348        let input_val = if !inputs.is_empty() { inputs[0] } else { 0.0 };
349        self.in_buffer[self.buffer_idx] = input_val;
350        
351        // We output the delayed sample from the previous block's processing
352        // This introduces 1-block latency, which is expected for out-of-process
353        outputs[0][0] = self.out_buffer[self.buffer_idx];
354        outputs[0][1] = self.out_buffer[self.buffer_idx];
355
356        self.buffer_idx += 1;
357        if self.buffer_idx >= self.buffer_size {
358            self.buffer_idx = 0;
359            // Process the block
360            if let Some(host) = self.host.as_mut() {
361                if host.process(&self.in_buffer, &mut self.out_buffer).is_err() {
362                    self.has_crashed = true;
363                    self.host = None;
364                    if let Some(flag) = _ctx.crash_flag {
365                        flag.store(true, std::sync::atomic::Ordering::SeqCst);
366                    }
367                }
368            }
369        }
370    }
371
372    fn update_parameter(&mut self, param: &str, value: f32) {
373        if let Some(host) = self.host.as_mut() {
374            if let Ok(id) = param.parse::<u32>() {
375                let _ = host.set_parameter(id, value);
376            }
377        }
378    }
379}
380
381pub struct DelayNode {
382    buffer: Vec<[f32; 2]>,
383    write_pos: usize,
384}
385
386impl DelayNode {
387    pub fn new(max_delay_samples: usize) -> Self {
388        Self {
389            buffer: vec![[0.0, 0.0]; max_delay_samples],
390            write_pos: 0,
391        }
392    }
393}
394
395impl DspNode for DelayNode {
396    fn process(&mut self, inputs: &[f32], outputs: &mut [[f32; 2]], config: &ConfigSnapshot, _ctx: &ProcessContext) {
397        let delay_samples = config.get("delay_samples").and_then(|v| v.as_float()).unwrap_or(4410.0) as usize;
398        let feedback = config.get("feedback").and_then(|v| v.as_float()).unwrap_or(0.5) as f32;
399        
400        let read_pos = (self.write_pos + self.buffer.len() - delay_samples) % self.buffer.len();
401        let delayed = self.buffer[read_pos];
402        
403        outputs[0][0] = delayed[0];
404        outputs[0][1] = delayed[1];
405
406        let in_l = if inputs.len() >= 1 { inputs[0] } else { 0.0 };
407        let in_r = if inputs.len() >= 2 { inputs[1] } else { 0.0 };
408
409        self.buffer[self.write_pos] = [
410            in_l + delayed[0] * feedback,
411            in_r + delayed[1] * feedback,
412        ];
413        
414        self.write_pos = (self.write_pos + 1) % self.buffer.len();
415    }
416}
417
418// ──────────────────────────────────────────────
419// §3 — Math
420// ──────────────────────────────────────────────
421
422pub struct AddNode;
423impl AddNode { pub fn new() -> Self { Self } }
424
425impl DspNode for AddNode {
426    fn process(&mut self, inputs: &[f32], outputs: &mut [[f32; 2]], _config: &ConfigSnapshot, _ctx: &ProcessContext) {
427        // Sum all stereo input pairs
428        let mut l = 0.0;
429        let mut r = 0.0;
430        for chunk in inputs.chunks_exact(2) {
431            l += chunk[0];
432            r += chunk[1];
433        }
434        outputs[0][0] = l;
435        outputs[0][1] = r;
436    }
437}
438
439pub struct MultiplyNode;
440impl MultiplyNode { pub fn new() -> Self { Self } }
441
442impl DspNode for MultiplyNode {
443    fn process(&mut self, inputs: &[f32], outputs: &mut [[f32; 2]], _config: &ConfigSnapshot, _ctx: &ProcessContext) {
444        if inputs.len() >= 4 {
445            outputs[0][0] = inputs[0] * inputs[2];
446            outputs[0][1] = inputs[1] * inputs[3];
447        } else {
448            outputs[0][0] = 0.0;
449            outputs[0][1] = 0.0;
450        }
451    }
452}
453
454pub struct ClipNode;
455impl ClipNode { pub fn new() -> Self { Self } }
456
457impl DspNode for ClipNode {
458    fn process(&mut self, inputs: &[f32], outputs: &mut [[f32; 2]], config: &ConfigSnapshot, _ctx: &ProcessContext) {
459        let min = config.get("min").and_then(|v| v.as_float()).unwrap_or(-1.0) as f32;
460        let max = config.get("max").and_then(|v| v.as_float()).unwrap_or(1.0) as f32;
461        
462        if inputs.len() >= 2 {
463            outputs[0][0] = inputs[0].clamp(min, max);
464            outputs[0][1] = inputs[1].clamp(min, max);
465        }
466    }
467}
468
469// ──────────────────────────────────────────────
470// §4 — Alchemy (Modulation & Time)
471// ──────────────────────────────────────────────
472
473pub struct TriggerNode;
474impl TriggerNode { pub fn new() -> Self { Self } }
475
476impl DspNode for TriggerNode {
477    fn process(&mut self, _inputs: &[f32], outputs: &mut [[f32; 2]], config: &ConfigSnapshot, ctx: &ProcessContext) {
478        let trigger_sample = config.get("sample").and_then(|v| v.as_float()).unwrap_or(0.0) as u64;
479        let val = if ctx.global_sample_index == trigger_sample { 1.0 } else { 0.0 };
480        outputs[0][0] = val;
481        outputs[0][1] = val;
482    }
483}
484
485#[derive(Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
486enum EnvState { Idle, Attack, Decay, Sustain, Release, FastRelease }
487
488pub struct EnvelopeNode {
489    state: EnvState,
490    level: f32,
491}
492
493impl EnvelopeNode {
494    pub fn new() -> Self {
495        Self { state: EnvState::Idle, level: 0.0 }
496    }
497
498    pub fn is_idle(&self) -> bool {
499        self.state == EnvState::Idle
500    }
501}
502
503impl DspNode for EnvelopeNode {
504    fn process(&mut self, inputs: &[f32], outputs: &mut [[f32; 2]], config: &ConfigSnapshot, ctx: &ProcessContext) {
505        let a = config.get("attack").and_then(|v| v.as_float()).unwrap_or(0.1) as f32;
506        let d = config.get("decay").and_then(|v| v.as_float()).unwrap_or(0.1) as f32;
507        let s = config.get("sustain").and_then(|v| v.as_float()).unwrap_or(0.5) as f32;
508        let r = config.get("release").and_then(|v| v.as_float()).unwrap_or(0.5) as f32;
509
510        let gate = inputs.get(0).cloned().unwrap_or(0.0) > 0.0;
511
512        match self.state {
513            EnvState::Idle => {
514                if gate { self.state = EnvState::Attack; }
515            }
516            EnvState::Attack => {
517                if !gate { self.state = EnvState::Release; }
518                else {
519                    self.level += 1.0 / (a * ctx.sample_rate);
520                    if self.level >= 1.0 {
521                        self.level = 1.0;
522                        self.state = EnvState::Decay;
523                    }
524                }
525            }
526            EnvState::Decay => {
527                if !gate { self.state = EnvState::Release; }
528                else {
529                    self.level -= (1.0 - s) / (d * ctx.sample_rate);
530                    if self.level <= s {
531                        self.level = s;
532                        self.state = EnvState::Sustain;
533                    }
534                }
535            }
536            EnvState::Sustain => {
537                if !gate { self.state = EnvState::Release; }
538            }
539            EnvState::Release => {
540                if gate { self.state = EnvState::Attack; }
541                else {
542                    // C9 fix: Release rate based on current level, not sustain level.
543                    // This ensures release works correctly even when sustain = 0.
544                    let release_rate = 1.0 / (r.max(0.001) * ctx.sample_rate);
545                    self.level -= release_rate;
546                    if self.level <= 0.0 {
547                        self.level = 0.0;
548                        self.state = EnvState::Idle;
549                    }
550                }
551            }
552            EnvState::FastRelease => {
553                // Fade out in 5ms to avoid pops
554                let fade_out_rate = 1.0 / (0.005 * ctx.sample_rate);
555                self.level -= fade_out_rate;
556                if self.level <= 0.0 {
557                    self.level = 0.0;
558                    self.state = EnvState::Idle;
559                }
560            }
561        }
562
563        outputs[0][0] = self.level;
564        outputs[0][1] = self.level;
565    }
566
567    fn update_parameter(&mut self, param: &str, _value: f32) {
568        if param == "steal" {
569            self.state = EnvState::FastRelease;
570        }
571    }
572
573    fn extract_state(&self) -> NodeState {
574        NodeState::from_json(serde_json::json!({
575            "state": self.state,
576            "level": self.level
577        }))
578    }
579
580    fn inject_state(&mut self, state: &NodeState) {
581        if let Some(data) = state.to_json::<serde_json::Value>() {
582            if let Some(s) = data.get("state").and_then(|v| serde_json::from_value::<EnvState>(v.clone()).ok()) {
583                self.state = s;
584            }
585            if let Some(l) = data.get("level").and_then(|v| v.as_f64()) {
586                self.level = l as f32;
587            }
588        }
589    }
590}
591
592pub struct SequencerNode {
593    last_step_idx: i32,
594}
595
596impl SequencerNode {
597    pub fn new() -> Self {
598        Self { last_step_idx: -1 }
599    }
600}
601
602impl DspNode for SequencerNode {
603    fn process(&mut self, _inputs: &[f32], outputs: &mut [[f32; 2]], config: &ConfigSnapshot, ctx: &ProcessContext) {
604        let bpm = config.get("bpm").and_then(|v| v.as_float()).unwrap_or(120.0) as f32;
605        let steps_data = config.get("steps").and_then(|v| v.as_list());
606        
607        let samples_per_step = (60.0 / (bpm * 4.0)) * ctx.sample_rate;
608        let current_step_idx = ((ctx.global_sample_index as f32 / samples_per_step) as i32) % 16;
609        
610        outputs[0] = [0.0, 0.0];
611
612        if current_step_idx != self.last_step_idx {
613            // Step boundary!
614            if let Some(steps) = steps_data {
615                let step = &steps[current_step_idx as usize];
616                if let Some(note_val) = step.as_float() {
617                    // Simple protocol: L=1.0 (NoteOn), R=(Note<<8 | Velocity)
618                    // For now, velocity is fixed at 100
619                    let note = note_val as u32;
620                    let vel = 100u32;
621                    outputs[0][0] = 1.0; // NoteOn
622                    outputs[0][1] = ((note << 8) | vel) as f32;
623                } else {
624                    // NoteOff if the previous step had a note?
625                    // For now, let's just send NoteOff for ALL notes if step is empty
626                    // Or more precisely, we need to track what note we started.
627                    outputs[0][0] = 2.0; // NoteOff (All or specific)
628                }
629            }
630            self.last_step_idx = current_step_idx;
631        }
632    }
633}
634
635pub struct AutomationNode;
636impl AutomationNode { pub fn new() -> Self { Self } }
637
638impl DspNode for AutomationNode {
639    fn process(&mut self, _inputs: &[f32], outputs: &mut [[f32; 2]], config: &ConfigSnapshot, ctx: &ProcessContext) {
640        let keyframes = config.get("keyframes").and_then(|v| v.as_list());
641        let current_time = ctx.global_sample_index as f64 / ctx.sample_rate as f64;
642
643        let mut val = 0.0;
644
645        if let Some(keys) = keyframes {
646            let mut prev_t = 0.0;
647            let mut prev_v = 0.0;
648            let mut found = false;
649
650            for key in keys {
651                if let Some(pair) = key.as_list() {
652                    if pair.len() >= 2 {
653                        let t = pair[0].as_float().unwrap_or(0.0);
654                        let v = pair[1].as_float().unwrap_or(0.0) as f32;
655
656                        if current_time < t {
657                            let dt = t - prev_t;
658                            if dt > 0.0 {
659                                let frac = ((current_time - prev_t) / dt) as f32;
660                                val = prev_v + (v - prev_v) * frac;
661                            } else {
662                                val = v;
663                            }
664                            found = true;
665                            break;
666                        }
667                        prev_t = t;
668                        prev_v = v;
669                    }
670                }
671            }
672            if !found {
673                val = prev_v;
674            }
675        }
676
677        outputs[0][0] = val;
678        outputs[0][1] = val;
679    }
680}
681
682pub struct MidiEvent {
683    pub sample_index: u64,
684    pub message: [u8; 3],
685}
686
687pub struct MidiInNode {
688    event_rx: crossbeam_channel::Receiver<MidiEvent>,
689    gate: f32,
690    pitch_hz: f32,
691    velocity: f32,
692    pending_events: Vec<MidiEvent>,
693}
694
695impl MidiInNode {
696    pub fn new(event_rx: crossbeam_channel::Receiver<MidiEvent>) -> Self {
697        Self {
698            event_rx,
699            gate: 0.0,
700            pitch_hz: 440.0,
701            velocity: 0.0,
702            pending_events: Vec::new(),
703        }
704    }
705}
706
707impl DspNode for MidiInNode {
708    fn process(&mut self, _inputs: &[f32], outputs: &mut [[f32; 2]], _config: &ConfigSnapshot, ctx: &ProcessContext) {
709        // 1. Drain queue into pending
710        while let Ok(event) = self.event_rx.try_recv() {
711            self.pending_events.push(event);
712        }
713
714        // 2. Process events for current sample
715        self.pending_events.retain(|event| {
716            if event.sample_index <= ctx.global_sample_index {
717                let status = event.message[0] & 0xF0;
718                match status {
719                    0x90 => { // Note On
720                        let note = event.message[1];
721                        let vel = event.message[2];
722                        if vel > 0 {
723                            self.gate = 1.0;
724                            self.pitch_hz = 440.0 * 2.0_f32.powf((note as f32 - 69.0) / 12.0);
725                            self.velocity = vel as f32 / 127.0;
726                        } else {
727                            self.gate = 0.0;
728                        }
729                    }
730                    0x80 => { // Note Off
731                        self.gate = 0.0;
732                    }
733                    _ => {}
734                }
735                false // Handled
736            } else {
737                true // Future
738            }
739        });
740
741        // Port 0: Gate
742        outputs[0][0] = self.gate;
743        outputs[0][1] = self.gate;
744        // Port 1: Pitch
745        if outputs.len() > 1 {
746            outputs[1][0] = self.pitch_hz;
747            outputs[1][1] = self.pitch_hz;
748        }
749        // Port 2: Velocity
750        if outputs.len() > 2 {
751            outputs[2][0] = self.velocity;
752            outputs[2][1] = self.velocity;
753        }
754    }
755}
756
757
758
759// ──────────────────────────────────────────────
760// §4 — Advanced & Chaos
761// ──────────────────────────────────────────────
762
763pub struct WavefolderNode {
764    _stages: usize,
765}
766
767impl WavefolderNode {
768    pub fn new() -> Self { Self { _stages: 4 } }
769}
770
771impl DspNode for WavefolderNode {
772    fn process(&mut self, inputs: &[f32], outputs: &mut [[f32; 2]], config: &ConfigSnapshot, _ctx: &ProcessContext) {
773        let gain = config.get("gain").and_then(|v| v.as_float()).unwrap_or(1.0) as f32;
774        let stages = config.get("stages").and_then(|v| match v {
775            dirtydata_core::types::ConfigValue::Int(i) => Some(*i as usize),
776            _ => None,
777        }).unwrap_or(4);
778        
779        for i in 0..outputs.len() {
780            let mut l = inputs.get(i * 2).cloned().unwrap_or(0.0) * gain;
781            let mut r = inputs.get(i * 2 + 1).cloned().unwrap_or(0.0) * gain;
782            
783            for _ in 0..stages {
784                l = (l * std::f32::consts::PI * 0.5).sin();
785                r = (r * std::f32::consts::PI * 0.5).sin();
786            }
787            outputs[i] = [l, r];
788        }
789    }
790}
791
792pub struct LorenzNode {
793    state: [f32; 3],
794    sigma: f32,
795    rho: f32,
796    beta: f32,
797}
798
799impl LorenzNode {
800    pub fn new() -> Self {
801        Self { state: [0.1, 0.0, 0.0], sigma: 10.0, rho: 28.0, beta: 8.0/3.0 }
802    }
803}
804
805impl DspNode for LorenzNode {
806    fn process(&mut self, _inputs: &[f32], outputs: &mut [[f32; 2]], config: &ConfigSnapshot, ctx: &ProcessContext) {
807        let speed = config.get("speed").and_then(|v| v.as_float()).unwrap_or(1.0) as f32;
808        let dt = speed / ctx.sample_rate;
809
810        let sigma = self.sigma;
811        let rho = self.rho;
812        let beta = self.beta;
813
814        // P1: Use zero-allocation fixed-size RK4
815        rk4_step_fixed(&mut self.state, dt, 0.0, |state, _t| {
816            [
817                sigma * (state[1] - state[0]),
818                state[0] * (rho - state[2]) - state[1],
819                state[0] * state[1] - beta * state[2],
820            ]
821        });
822
823        // Soft clamp to prevent blow-up
824        for s in &mut self.state {
825            *s = s.clamp(-100.0, 100.0);
826            if !s.is_finite() { *s = 0.1; }
827        }
828
829        // Output X, Y, Z as 3 mono signals (mapped to stereo ports)
830        outputs[0] = [self.state[0] * 0.05, self.state[1] * 0.05];
831        if outputs.len() > 1 {
832            outputs[1] = [self.state[2] * 0.05, 0.0];
833        }
834    }
835}
836
837pub struct MackeyGlassNode {
838    history: VecDeque<f32>,
839    _tau_samples: usize,
840    beta: f32,
841    gamma: f32,
842    n: f32,
843    current_x: f32,
844}
845
846impl MackeyGlassNode {
847    pub fn new(tau_ms: f32, sample_rate: f32) -> Self {
848        let tau_samples = (tau_ms * 0.001 * sample_rate) as usize;
849        let mut history = VecDeque::with_capacity(tau_samples + 1);
850        for _ in 0..=tau_samples { history.push_back(0.5); }
851        Self { history, _tau_samples: tau_samples, beta: 2.0, gamma: 1.0, n: 10.0, current_x: 0.5 }
852    }
853}
854
855impl DspNode for MackeyGlassNode {
856    fn process(&mut self, _inputs: &[f32], outputs: &mut [[f32; 2]], config: &ConfigSnapshot, ctx: &ProcessContext) {
857        let speed = config.get("speed").and_then(|v| v.as_float()).unwrap_or(1.0) as f32;
858        let dt = speed / ctx.sample_rate;
859        
860        let x_tau = *self.history.front().unwrap();
861        
862        // Simple integration for Mackey-Glass (RK4-ish applied locally)
863        let f = |x: f32, xt: f32| self.beta * xt / (1.0 + xt.powf(self.n)) - self.gamma * x;
864        
865        let k1 = f(self.current_x, x_tau);
866        let k2 = f(self.current_x + k1 * dt * 0.5, x_tau);
867        let k3 = f(self.current_x + k2 * dt * 0.5, x_tau);
868        let k4 = f(self.current_x + k3 * dt, x_tau);
869        
870        self.current_x += (dt / 6.0) * (k1 + 2.0 * k2 + 2.0 * k3 + k4);
871        self.history.push_back(self.current_x);
872        self.history.pop_front();
873        
874        outputs[0] = [self.current_x, self.current_x];
875    }
876}
877
878pub struct GrayScottNode {
879    u: [Vec<f32>; 2],  // P2: Double buffer (no per-sample clone)
880    v: [Vec<f32>; 2],
881    current: usize,
882    size: usize,
883    f: f32,
884    k: f32,
885    du: f32,
886    dv: f32,
887}
888
889impl GrayScottNode {
890    pub fn new(size: usize) -> Self {
891        let u0 = vec![1.0; size];
892        let mut v0 = vec![0.0; size];
893        for i in (size/2 - 5)..(size/2 + 5) { v0[i] = 0.5; }
894        Self {
895            u: [u0.clone(), vec![0.0; size]],
896            v: [v0.clone(), vec![0.0; size]],
897            current: 0,
898            size, f: 0.0545, k: 0.062, du: 0.1, dv: 0.05,
899        }
900    }
901}
902
903impl DspNode for GrayScottNode {
904    fn process(&mut self, _inputs: &[f32], outputs: &mut [[f32; 2]], _config: &ConfigSnapshot, _ctx: &ProcessContext) {
905        let cur = self.current;
906        let nxt = 1 - cur;
907        
908        for i in 0..self.size {
909            let prev = if i == 0 { self.size - 1 } else { i - 1 };
910            let next = if i == self.size - 1 { 0 } else { i + 1 };
911            
912            let u_val = self.u[cur][i];
913            let v_val = self.v[cur][i];
914            let lap_u = self.u[cur][prev] + self.u[cur][next] - 2.0 * u_val;
915            let lap_v = self.v[cur][prev] + self.v[cur][next] - 2.0 * v_val;
916            let uv2 = u_val * v_val * v_val;
917            
918            self.u[nxt][i] = (u_val + self.du * lap_u - uv2 + self.f * (1.0 - u_val)).clamp(0.0, 1.5);
919            self.v[nxt][i] = (v_val + self.dv * lap_v + uv2 - (self.f + self.k) * v_val).clamp(0.0, 1.5);
920        }
921        
922        self.current = nxt;
923        outputs[0] = [self.u[nxt][self.size/2] * 2.0 - 1.0, self.v[nxt][self.size/2] * 2.0 - 1.0];
924    }
925}
926
927pub struct SlewLimiterNode {
928    current: f32,
929}
930
931impl SlewLimiterNode {
932    pub fn new() -> Self { Self { current: 0.0 } }
933}
934
935impl DspNode for SlewLimiterNode {
936    fn process(&mut self, inputs: &[f32], outputs: &mut [[f32; 2]], config: &ConfigSnapshot, ctx: &ProcessContext) {
937        let rise = config.get("rise").and_then(|v| v.as_float()).unwrap_or(0.1) as f32;
938        let fall = config.get("fall").and_then(|v| v.as_float()).unwrap_or(0.1) as f32;
939        
940        for i in 0..outputs.len() {
941            let target = inputs.get(i * 2).cloned().unwrap_or(0.0);
942            let diff = target - self.current;
943            let limit = if diff > 0.0 { rise } else { fall };
944            let step = diff.clamp(-limit / ctx.sample_rate, limit / ctx.sample_rate);
945            self.current += step;
946            outputs[i] = [self.current, self.current];
947        }
948    }
949}
950
951pub struct SampleHoldNode {
952    last_val: [f32; 2],
953    last_trig: f32,
954}
955
956impl SampleHoldNode {
957    pub fn new() -> Self { Self { last_val: [0.0, 0.0], last_trig: 0.0 } }
958}
959
960impl DspNode for SampleHoldNode {
961    fn process(&mut self, inputs: &[f32], outputs: &mut [[f32; 2]], _config: &ConfigSnapshot, _ctx: &ProcessContext) {
962        for i in 0..outputs.len() {
963            let sig_l = inputs.get(i * 2).cloned().unwrap_or(0.0);
964            let sig_r = inputs.get(i * 2 + 1).cloned().unwrap_or(0.0);
965            let trig = inputs.get(i * 2 + 2).cloned().unwrap_or(0.0); // Assume 3rd input is trigger
966            
967            if trig > 0.5 && self.last_trig <= 0.5 {
968                self.last_val = [sig_l, sig_r];
969            }
970            self.last_trig = trig;
971            outputs[i] = self.last_val;
972        }
973    }
974}
975
976pub struct ClockNode {
977    phase: f32,
978}
979
980impl ClockNode {
981    pub fn new() -> Self { Self { phase: 0.0 } }
982}
983
984impl DspNode for ClockNode {
985    fn process(&mut self, _inputs: &[f32], outputs: &mut [[f32; 2]], config: &ConfigSnapshot, ctx: &ProcessContext) {
986        let bpm = config.get("bpm").and_then(|v| v.as_float()).unwrap_or(120.0) as f32;
987        let division = config.get("division").and_then(|v| v.as_float()).unwrap_or(4.0) as f32; // Default 1/4
988        
989        let freq = (bpm / 60.0) * (division / 4.0);
990        let phase_step = freq / ctx.sample_rate;
991        
992        for i in 0..outputs.len() {
993            let old_phase = self.phase;
994            self.phase = (self.phase + phase_step).fract();
995            
996            let trigger = if self.phase < old_phase { 1.0 } else { 0.0 };
997            outputs[i] = [trigger, trigger];
998        }
999    }
1000}
1001
1002pub struct ProbabilityGateNode {
1003    rng: Pcg32,
1004}
1005
1006impl ProbabilityGateNode {
1007    pub fn new() -> Self { Self { rng: Pcg32::seed_from_u64(42) } }
1008}
1009
1010impl DspNode for ProbabilityGateNode {
1011    fn process(&mut self, inputs: &[f32], outputs: &mut [[f32; 2]], config: &ConfigSnapshot, _ctx: &ProcessContext) {
1012        let prob = config.get("probability").and_then(|v| v.as_float()).unwrap_or(0.5) as f32;
1013        
1014        for i in 0..outputs.len() {
1015            let trig = inputs.get(i * 2).cloned().unwrap_or(0.0);
1016            let mut out = 0.0;
1017            if trig > 0.5 {
1018                if self.rng.gen::<f32>() < prob {
1019                    out = 1.0;
1020                }
1021            }
1022            outputs[i] = [out, out];
1023        }
1024    }
1025}
1026
1027pub struct ReverbNode {
1028    delays: Vec<VecDeque<f32>>,
1029    feedback_matrix: [[f32; 4]; 4],
1030}
1031
1032impl ReverbNode {
1033    pub fn new(sample_rate: f32) -> Self {
1034        let delay_times = [0.037, 0.043, 0.051, 0.061]; // Primes in seconds
1035        let delays = delay_times.iter().map(|&t| {
1036            let size = (t * sample_rate) as usize;
1037            let mut dq = VecDeque::with_capacity(size);
1038            for _ in 0..size { dq.push_back(0.0); }
1039            dq
1040        }).collect();
1041
1042        // 4x4 Hadamard matrix for diffusion
1043        let h = 0.5;
1044        let feedback_matrix = [
1045            [h, h, h, h],
1046            [h, -h, h, -h],
1047            [h, h, -h, -h],
1048            [h, -h, -h, h],
1049        ];
1050
1051        Self { delays, feedback_matrix }
1052    }
1053}
1054
1055impl DspNode for ReverbNode {
1056    fn process(&mut self, inputs: &[f32], outputs: &mut [[f32; 2]], config: &ConfigSnapshot, _ctx: &ProcessContext) {
1057        let decay = config.get("decay").and_then(|v| v.as_float()).unwrap_or(0.5) as f32;
1058        let mix = config.get("mix").and_then(|v| v.as_float()).unwrap_or(0.3) as f32;
1059
1060        for i in 0..outputs.len() {
1061            let input_l = inputs.get(i * 2).cloned().unwrap_or(0.0);
1062            let input_r = inputs.get(i * 2 + 1).cloned().unwrap_or(0.0);
1063            let mono_in = (input_l + input_r) * 0.5;
1064
1065            // 1. Read delay outputs
1066            let mut y = [0.0; 4];
1067            for j in 0..4 {
1068                y[j] = *self.delays[j].front().unwrap();
1069            }
1070
1071            // 2. Compute feedback
1072            let mut fb = [0.0; 4];
1073            for row in 0..4 {
1074                for col in 0..4 {
1075                    fb[row] += self.feedback_matrix[row][col] * y[col];
1076                }
1077            }
1078
1079            // 3. Inject input and write back to delays
1080            for j in 0..4 {
1081                self.delays[j].push_back(mono_in + fb[j] * decay);
1082                self.delays[j].pop_front();
1083            }
1084
1085            // 4. Output mix (L=Y0+Y1, R=Y2+Y3 for pseudo-stereo)
1086            let wet_l = y[0] + y[1];
1087            let wet_r = y[2] + y[3];
1088            
1089            outputs[i] = [
1090                input_l * (1.0 - mix) + wet_l * mix,
1091                input_r * (1.0 - mix) + wet_r * mix
1092            ];
1093        }
1094    }
1095}
1096
1097pub struct Grain {
1098    pos: f32,
1099    duration_samples: f32,
1100    current_sample: f32,
1101    active: bool,
1102}
1103
1104pub struct GranularNode {
1105    buffer: Vec<[f32; 2]>,
1106    write_pos: usize,
1107    grains: Vec<Grain>,
1108    next_grain_samples: f32,
1109}
1110
1111impl GranularNode {
1112    pub fn new(sample_rate: f32) -> Self {
1113        let buf_size = (sample_rate * 2.0) as usize; // 2 seconds buffer
1114        let mut grains = Vec::new();
1115        for _ in 0..16 {
1116            grains.push(Grain { pos: 0.0, duration_samples: 0.0, current_sample: 0.0, active: false });
1117        }
1118        Self {
1119            buffer: vec![[0.0, 0.0]; buf_size],
1120            write_pos: 0,
1121            grains,
1122            next_grain_samples: 0.0,
1123        }
1124    }
1125}
1126
1127impl DspNode for GranularNode {
1128    fn process(&mut self, inputs: &[f32], outputs: &mut [[f32; 2]], config: &ConfigSnapshot, ctx: &ProcessContext) {
1129        let pos_norm = config.get("position").and_then(|v| v.as_float()).unwrap_or(0.5) as f32;
1130        let size_ms = config.get("size").and_then(|v| v.as_float()).unwrap_or(50.0) as f32;
1131        let density = config.get("density").and_then(|v| v.as_float()).unwrap_or(0.5) as f32;
1132        
1133        let size_samples = (size_ms * 0.001 * ctx.sample_rate) as f32;
1134        
1135        // 1. Record input
1136        for i in 0..outputs.len() {
1137            let in_l = inputs.get(i * 2).cloned().unwrap_or(0.0);
1138            let in_r = inputs.get(i * 2 + 1).cloned().unwrap_or(0.0);
1139            self.buffer[self.write_pos] = [in_l, in_r];
1140            self.write_pos = (self.write_pos + 1) % self.buffer.len();
1141
1142            // 2. Schedule new grain
1143            self.next_grain_samples -= 1.0;
1144            if self.next_grain_samples <= 0.0 {
1145                if let Some(grain) = self.grains.iter_mut().find(|g| !g.active) {
1146                    grain.active = true;
1147                    grain.current_sample = 0.0;
1148                    grain.duration_samples = size_samples;
1149                    // Jittered position
1150                    let jitter = (rand::random::<f32>() - 0.5) * 0.05;
1151                    grain.pos = (pos_norm + jitter).clamp(0.0, 1.0);
1152                }
1153                self.next_grain_samples = (1.0 - density) * size_samples * 0.5 + 100.0;
1154            }
1155
1156            // 3. Process grains
1157            let mut mixed = [0.0, 0.0];
1158            for grain in self.grains.iter_mut().filter(|g| g.active) {
1159                let norm_idx = grain.current_sample / grain.duration_samples;
1160                
1161                // Simple triangle window
1162                let window = 1.0 - (2.0 * norm_idx - 1.0).abs();
1163                
1164                let read_base = (grain.pos * (self.buffer.len() as f32 - 1.0)) as usize;
1165                let read_idx = (read_base + grain.current_sample as usize) % self.buffer.len();
1166                let val = self.buffer[read_idx];
1167                
1168                mixed[0] += val[0] * window;
1169                mixed[1] += val[1] * window;
1170                
1171                grain.current_sample += 1.0;
1172                if grain.current_sample >= grain.duration_samples {
1173                    grain.active = false;
1174                }
1175            }
1176
1177            outputs[i] = mixed;
1178        }
1179    }
1180}
1181
1182pub struct WasmNode {
1183    instance: Option<wasmtime::Instance>,
1184    store: Option<wasmtime::Store<()>>,
1185    process_fn: Option<wasmtime::TypedFunc<(f32, f32), i64>>,
1186    failed: bool,
1187}
1188
1189impl WasmNode {
1190    pub fn new() -> Self {
1191        Self { instance: None, store: None, process_fn: None, failed: false }
1192    }
1193
1194    fn init(&mut self, path: &str) -> anyhow::Result<()> {
1195        let engine = wasmtime::Engine::default();
1196        let module = wasmtime::Module::from_file(&engine, path)?;
1197        let mut store = wasmtime::Store::new(&engine, ());
1198        let instance = wasmtime::Instance::new(&mut store, &module, &[])?;
1199        
1200        let process_fn = instance.get_typed_func::<(f32, f32), i64>(&mut store, "process")?;
1201        
1202        self.instance = Some(instance);
1203        self.store = Some(store);
1204        self.process_fn = Some(process_fn);
1205        Ok(())
1206    }
1207}
1208
1209impl DspNode for WasmNode {
1210    fn process(&mut self, inputs: &[f32], outputs: &mut [[f32; 2]], config: &ConfigSnapshot, _ctx: &ProcessContext) {
1211        if self.instance.is_none() && !self.failed {
1212            if let Some(path) = config.get("path").and_then(|v| v.as_string()) {
1213                if let Err(e) = self.init(path) {
1214                    eprintln!("Failed to init WasmNode: {}", e);
1215                    self.failed = true;
1216                }
1217            }
1218        }
1219
1220        if let (Some(store), Some(f)) = (self.store.as_mut(), self.process_fn.as_mut()) {
1221            for i in 0..outputs.len() {
1222                let in_l = inputs.get(i * 2).cloned().unwrap_or(0.0);
1223                let in_r = inputs.get(i * 2 + 1).cloned().unwrap_or(0.0);
1224                
1225                match f.call(&mut *store, (in_l, in_r)) {
1226                    Ok(res) => {
1227                        // Unpack two f32 from i64
1228                        let out_l = f32::from_bits((res >> 32) as u32);
1229                        let out_r = f32::from_bits(res as u32);
1230                        outputs[i] = [out_l, out_r];
1231                    }
1232                    Err(_) => {
1233                        outputs[i] = [in_l, in_r];
1234                    }
1235                }
1236            }
1237        } else {
1238            // Bypass
1239            for i in 0..outputs.len() {
1240                outputs[i] = [
1241                    inputs.get(i * 2).cloned().unwrap_or(0.0),
1242                    inputs.get(i * 2 + 1).cloned().unwrap_or(0.0)
1243                ];
1244            }
1245        }
1246    }
1247}
1248
1249// ──────────────────────────────────────────────
1250// §7.3 Missing Gaps Implementation
1251// ──────────────────────────────────────────────
1252
1253/// Logic operations on signals (Gate/CV logic).
1254pub struct LogicNode;
1255impl LogicNode { pub fn new() -> Self { Self } }
1256impl DspNode for LogicNode {
1257    fn process(&mut self, inputs: &[f32], outputs: &mut [[f32; 2]], config: &ConfigSnapshot, _ctx: &ProcessContext) {
1258        let mode = config.get("mode").and_then(|v| v.as_string()).map(|s| s.as_str()).unwrap_or("AND");
1259        let threshold = config.get("threshold").and_then(|v| v.as_float()).unwrap_or(0.5) as f32;
1260
1261        let a = inputs.get(0).cloned().unwrap_or(0.0) > threshold;
1262        let b = inputs.get(1).cloned().unwrap_or(0.0) > threshold;
1263
1264        let res = match mode {
1265            "AND" => a && b,
1266            "OR" => a || b,
1267            "XOR" => a ^ b,
1268            "NOT" => !a,
1269            _ => a && b,
1270        };
1271
1272        let val = if res { 1.0 } else { 0.0 };
1273        for out in outputs.iter_mut() {
1274            *out = [val, val];
1275        }
1276    }
1277}
1278
1279use rustfft::{FftPlanner, num_complex::Complex};
1280
1281/// Spectral Freeze Node.
1282pub struct SpectralFreezeNode {
1283    size: usize,
1284    buffer: Vec<f32>,
1285    fft_result: Vec<Complex<f32>>,
1286    frozen: bool,
1287    write_pos: usize,
1288    read_pos: usize,
1289}
1290
1291impl SpectralFreezeNode {
1292    pub fn new(size: usize) -> Self {
1293        Self {
1294            size,
1295            buffer: vec![0.0; size],
1296            fft_result: vec![Complex::default(); size],
1297            frozen: false,
1298            write_pos: 0,
1299            read_pos: 0,
1300        }
1301    }
1302}
1303
1304impl DspNode for SpectralFreezeNode {
1305    fn process(&mut self, inputs: &[f32], outputs: &mut [[f32; 2]], config: &ConfigSnapshot, _ctx: &ProcessContext) {
1306        let freeze = config.get("freeze").and_then(|v| v.as_bool()).unwrap_or(false);
1307        let input = inputs.get(0).cloned().unwrap_or(0.0);
1308
1309        if freeze && !self.frozen {
1310            // Perform FFT once and freeze
1311            let mut planner = FftPlanner::new();
1312            let fft = planner.plan_fft_forward(self.size);
1313            let mut complex_buf: Vec<Complex<f32>> = self.buffer.iter().map(|&x| Complex::new(x, 0.0)).collect();
1314            fft.process(&mut complex_buf);
1315            self.fft_result = complex_buf;
1316            self.frozen = true;
1317            
1318            // Generate time-domain frozen signal via IFFT
1319            let ifft = planner.plan_fft_inverse(self.size);
1320            let mut inv_buf = self.fft_result.clone();
1321            ifft.process(&mut inv_buf);
1322            for (i, c) in inv_buf.iter().enumerate() {
1323                self.buffer[i] = c.re / self.size as f32;
1324            }
1325        } else if !freeze {
1326            self.frozen = false;
1327        }
1328
1329        // Fill input buffer if not frozen
1330        if !self.frozen {
1331            self.buffer[self.write_pos] = input;
1332            self.write_pos = (self.write_pos + 1) % self.size;
1333        }
1334
1335        // Output logic: if frozen, loop the frozen buffer
1336        let out_val = if self.frozen {
1337             let v = self.buffer[self.read_pos];
1338             self.read_pos = (self.read_pos + 1) % self.size;
1339             v
1340        } else {
1341            input
1342        };
1343
1344        for out in outputs.iter_mut() {
1345            *out = [out_val, out_val];
1346        }
1347    }
1348}
1349
1350/// FFT Convolution Node.
1351pub struct FFTConvolveNode {
1352    size: usize,
1353    input_buffer: Vec<f32>,
1354    impulse_buffer: Vec<f32>,
1355    result_buffer: Vec<f32>,
1356    pos: usize,
1357}
1358
1359impl FFTConvolveNode {
1360    pub fn new(size: usize) -> Self {
1361        Self {
1362            size,
1363            input_buffer: vec![0.0; size],
1364            impulse_buffer: vec![0.0; size],
1365            result_buffer: vec![0.0; size],
1366            pos: 0,
1367        }
1368    }
1369}
1370
1371impl DspNode for FFTConvolveNode {
1372    fn process(&mut self, inputs: &[f32], outputs: &mut [[f32; 2]], _config: &ConfigSnapshot, _ctx: &ProcessContext) {
1373        let input = inputs.get(0).cloned().unwrap_or(0.0);
1374        let impulse = inputs.get(1).cloned().unwrap_or(0.0);
1375
1376        self.input_buffer[self.pos] = input;
1377        self.impulse_buffer[self.pos] = impulse;
1378        self.pos += 1;
1379
1380        if self.pos >= self.size {
1381            // Block process
1382            let mut planner = FftPlanner::new();
1383            let fft = planner.plan_fft_forward(self.size);
1384            
1385            let mut in_complex: Vec<Complex<f32>> = self.input_buffer.iter().map(|&x| Complex::new(x, 0.0)).collect();
1386            let mut imp_complex: Vec<Complex<f32>> = self.impulse_buffer.iter().map(|&x| Complex::new(x, 0.0)).collect();
1387            
1388            fft.process(&mut in_complex);
1389            fft.process(&mut imp_complex);
1390            
1391            // Multiply in frequency domain
1392            for i in 0..self.size {
1393                in_complex[i] *= imp_complex[i];
1394            }
1395            
1396            let ifft = planner.plan_fft_inverse(self.size);
1397            ifft.process(&mut in_complex);
1398            
1399            for (i, c) in in_complex.iter().enumerate() {
1400                self.result_buffer[i] = c.re / self.size as f32;
1401            }
1402            self.pos = 0;
1403        }
1404
1405        let out_val = self.result_buffer[self.pos];
1406
1407        for out in outputs.iter_mut() {
1408            *out = [out_val, out_val];
1409        }
1410    }
1411}
1412
1413/// OSC Output Node.
1414pub struct OscOutNode {
1415    last_sent_val: f32,
1416    threshold: f32,
1417}
1418
1419impl OscOutNode {
1420    pub fn new() -> Self {
1421        Self {
1422            last_sent_val: 0.0,
1423            threshold: 0.001,
1424        }
1425    }
1426}
1427
1428impl DspNode for OscOutNode {
1429    fn process(&mut self, inputs: &[f32], _outputs: &mut [[f32; 2]], config: &ConfigSnapshot, ctx: &ProcessContext) {
1430        let addr = config.get("address").and_then(|v| v.as_string()).map(|s| s.as_str()).unwrap_or("/dirtydata/out");
1431        let val = inputs.get(0).cloned().unwrap_or(0.0);
1432
1433        // Rate-limited sending: only send if value changed significantly
1434        if (val - self.last_sent_val).abs() > self.threshold {
1435            if let Some(tx) = ctx.osc_tx {
1436                let _ = tx.try_send(OscMessage {
1437                    addr: addr.to_string(),
1438                    args: vec![rosc::OscType::Float(val)],
1439                });
1440                self.last_sent_val = val;
1441            }
1442        }
1443    }
1444}
1445
1446// ──────────────────────────────────────────────
1447// Phase 5.5 — Feedback Hell
1448// ──────────────────────────────────────────────
1449
1450/// A node that provides a 1-sample delay, enabling explicit feedback loops.
1451/// Use this to break causal cycles in the graph.
1452pub struct FeedbackNode {
1453    latch: [f32; 2],
1454}
1455
1456impl FeedbackNode {
1457    pub fn new() -> Self {
1458        Self { latch: [0.0, 0.0] }
1459    }
1460}
1461
1462impl DspNode for FeedbackNode {
1463    fn process(&mut self, inputs: &[f32], outputs: &mut [[f32; 2]], _config: &ConfigSnapshot, _ctx: &ProcessContext) {
1464        // Output the value from the PREVIOUS sample
1465        outputs[0] = self.latch;
1466        
1467        // Capture the CURRENT sample for the next cycle
1468        if inputs.len() >= 2 {
1469            self.latch = [inputs[0], inputs[1]];
1470        }
1471    }
1472}
1473
1474// ──────────────────────────────────────────────
1475// §7 — Containers (Encapsulation)
1476// ──────────────────────────────────────────────
1477
1478pub struct InputProxyNode { value: f32 }
1479impl InputProxyNode { pub fn new() -> Self { Self { value: 0.0 } } }
1480impl DspNode for InputProxyNode {
1481    fn process(&mut self, _inputs: &[f32], outputs: &mut [[f32; 2]], _config: &ConfigSnapshot, _ctx: &ProcessContext) {
1482        outputs[0] = [self.value, self.value];
1483    }
1484    fn update_parameter(&mut self, _param: &str, value: f32) { self.value = value; }
1485}
1486
1487pub struct OutputProxyNode;
1488impl OutputProxyNode { pub fn new() -> Self { Self } }
1489impl DspNode for OutputProxyNode {
1490    fn process(&mut self, inputs: &[f32], outputs: &mut [[f32; 2]], _config: &ConfigSnapshot, _ctx: &ProcessContext) {
1491        let val = inputs.get(0).cloned().unwrap_or(0.0);
1492        outputs[0] = [val, val];
1493    }
1494}
1495
1496pub struct SubGraphNode {
1497    runner: Option<crate::DspRunner>,
1498    last_graph_hash: String,
1499}
1500
1501impl SubGraphNode {
1502    pub fn new() -> Self {
1503        Self { runner: None, last_graph_hash: String::new() }
1504    }
1505}
1506
1507impl DspNode for SubGraphNode {
1508    fn process(&mut self, inputs: &[f32], outputs: &mut [[f32; 2]], config: &ConfigSnapshot, ctx: &ProcessContext) {
1509        let graph_json = config.get("graph_json").and_then(|v| v.as_string()).map(|s| s.as_str()).unwrap_or("");
1510        let hash = blake3::hash(graph_json.as_bytes()).to_string();
1511
1512        if hash != self.last_graph_hash && !graph_json.is_empty() {
1513            if let Ok(graph) = serde_json::from_str::<dirtydata_core::ir::Graph>(&graph_json) {
1514                self.runner = Some(crate::DspRunner::new(graph, None, ctx.sample_rate));
1515                self.last_graph_hash = hash;
1516            }
1517        }
1518
1519        if let Some(runner) = &mut self.runner {
1520            let mut proxy_ids = Vec::new();
1521            for (id, n) in &runner.get_graph().nodes {
1522                if n.kind == dirtydata_core::types::NodeKind::InputProxy {
1523                    proxy_ids.push(*id);
1524                }
1525            }
1526            for (id, node) in runner.nodes_mut() {
1527                if proxy_ids.contains(id) {
1528                    node.update_parameter("value", inputs.get(0).cloned().unwrap_or(0.0));
1529                }
1530            }
1531            
1532            let sub_out = runner.process_sample(ctx);
1533            outputs[0] = sub_out;
1534        } else {
1535            for o in outputs { *o = [0.0, 0.0]; }
1536        }
1537    }
1538}
1539
1540// ──────────────────────────────────────────────
1541// Tier S Analog DSP Nodes (Topology Preserving)
1542// ──────────────────────────────────────────────
1543
1544pub struct ZdfLadderNode {
1545    inner: dirtydata_dsp_zdf::ZdfLadder,
1546}
1547impl ZdfLadderNode {
1548    pub fn new(sample_rate: f32) -> Self {
1549        Self { inner: dirtydata_dsp_zdf::ZdfLadder::new(sample_rate) }
1550    }
1551}
1552impl DspNode for ZdfLadderNode {
1553    fn process(&mut self, inputs: &[f32], outputs: &mut [[f32; 2]], config: &ConfigSnapshot, _ctx: &ProcessContext) {
1554        let input = inputs.get(0).copied().unwrap_or(0.0);
1555        let cutoff = config.get("cutoff").and_then(|v| v.as_float()).unwrap_or(1000.0) as f32;
1556        let res = config.get("resonance").and_then(|v| v.as_float()).unwrap_or(0.0) as f32;
1557        let drive = config.get("drive").and_then(|v| v.as_float()).unwrap_or(0.0) as f32;
1558        
1559        let out = self.inner.process(input, cutoff, res, drive);
1560        for o in outputs { *o = [out, out]; }
1561    }
1562}
1563
1564pub struct SvfNode {
1565    inner: dirtydata_dsp_svf::Svf,
1566}
1567impl SvfNode {
1568    pub fn new(sample_rate: f32) -> Self {
1569        Self { inner: dirtydata_dsp_svf::Svf::new(sample_rate) }
1570    }
1571}
1572impl DspNode for SvfNode {
1573    fn process(&mut self, inputs: &[f32], outputs: &mut [[f32; 2]], config: &ConfigSnapshot, _ctx: &ProcessContext) {
1574        let input = inputs.get(0).copied().unwrap_or(0.0);
1575        let cutoff = config.get("cutoff").and_then(|v| v.as_float()).unwrap_or(1000.0) as f32;
1576        let q = config.get("q").and_then(|v| v.as_float()).unwrap_or(0.707) as f32;
1577        let mode = config.get("mode").and_then(|v| v.as_float()).unwrap_or(0.0) as f32;
1578        let drive = config.get("drive").and_then(|v| v.as_float()).unwrap_or(0.0) as f32;
1579        
1580        let svf_out = if drive > 0.01 {
1581            self.inner.process_nonlinear(input, cutoff, q, drive)
1582        } else {
1583            self.inner.process(input, cutoff, q)
1584        };
1585        let out = match mode as i32 {
1586            0 => svf_out.lp,
1587            1 => svf_out.hp,
1588            2 => svf_out.bp,
1589            3 => svf_out.notch,
1590            4 => svf_out.ap,
1591            _ => svf_out.peak,
1592        };
1593        for o in outputs { *o = [out, out]; }
1594    }
1595}
1596
1597pub struct DiodeClipperNode {
1598    inner: dirtydata_dsp_clipper::DiodeClipper,
1599}
1600impl DiodeClipperNode {
1601    pub fn new() -> Self {
1602        Self { inner: dirtydata_dsp_clipper::DiodeClipper::new() }
1603    }
1604}
1605impl DspNode for DiodeClipperNode {
1606    fn process(&mut self, inputs: &[f32], outputs: &mut [[f32; 2]], config: &ConfigSnapshot, _ctx: &ProcessContext) {
1607        let input = inputs.get(0).copied().unwrap_or(0.0);
1608        let drive = config.get("drive").and_then(|v| v.as_float()).unwrap_or(1.0) as f32;
1609        let asymmetry = config.get("asymmetry").and_then(|v| v.as_float()).unwrap_or(0.0) as f32;
1610        
1611        let out = self.inner.process(input, drive, asymmetry);
1612        for o in outputs { *o = [out, out]; }
1613    }
1614}
1615
1616pub struct BbdDelayNode {
1617    inner: dirtydata_dsp_bbd::BbdDelay,
1618}
1619impl BbdDelayNode {
1620    pub fn new(sample_rate: f32) -> Self {
1621        Self { inner: dirtydata_dsp_bbd::BbdDelay::new(sample_rate, 2.0) }
1622    }
1623}
1624impl DspNode for BbdDelayNode {
1625    fn process(&mut self, inputs: &[f32], outputs: &mut [[f32; 2]], config: &ConfigSnapshot, _ctx: &ProcessContext) {
1626        let input = inputs.get(0).copied().unwrap_or(0.0);
1627        let time_ms = config.get("time_ms").and_then(|v| v.as_float()).unwrap_or(300.0) as f32;
1628        let feedback = config.get("feedback").and_then(|v| v.as_float()).unwrap_or(0.3) as f32;
1629        let dirt = config.get("dirt").and_then(|v| v.as_float()).unwrap_or(0.5) as f32;
1630        
1631        let out = self.inner.process(input, time_ms, feedback, dirt);
1632        for o in outputs { *o = [out, out]; }
1633    }
1634}
1635
1636// ──────────────────────────────────────────────
1637// Tier A Physical Modeling DSP Nodes
1638// ──────────────────────────────────────────────
1639
1640pub struct WdfSimpleRcNode {
1641    inner: dirtydata_dsp_wdf::WdfSimpleRc,
1642}
1643impl WdfSimpleRcNode {
1644    pub fn new(sample_rate: f32) -> Self {
1645        Self { inner: dirtydata_dsp_wdf::WdfSimpleRc::new(1000.0, 1e-6, sample_rate) }
1646    }
1647}
1648impl DspNode for WdfSimpleRcNode {
1649    fn process(&mut self, inputs: &[f32], outputs: &mut [[f32; 2]], _config: &ConfigSnapshot, _ctx: &ProcessContext) {
1650        let input = inputs.get(0).copied().unwrap_or(0.0);
1651        let out = self.inner.process(input);
1652        for o in outputs { *o = [out, out]; }
1653    }
1654}
1655
1656pub struct WdfDiodeClipperNode {
1657    inner: dirtydata_dsp_wdf::WdfDiodeClipper,
1658}
1659impl WdfDiodeClipperNode {
1660    pub fn new(sample_rate: f32) -> Self {
1661        Self { inner: dirtydata_dsp_wdf::WdfDiodeClipper::new(4700.0, 10e-9, sample_rate) }
1662    }
1663}
1664impl DspNode for WdfDiodeClipperNode {
1665    fn process(&mut self, inputs: &[f32], outputs: &mut [[f32; 2]], _config: &ConfigSnapshot, _ctx: &ProcessContext) {
1666        let input = inputs.get(0).copied().unwrap_or(0.0);
1667        let out = self.inner.process(input);
1668        for o in outputs { *o = [out, out]; }
1669    }
1670}
1671
1672pub struct KarplusStrongNode {
1673    inner: dirtydata_dsp_ks::KarplusStrong,
1674}
1675impl KarplusStrongNode {
1676    pub fn new(sample_rate: f32) -> Self {
1677        Self { inner: dirtydata_dsp_ks::KarplusStrong::new(sample_rate) }
1678    }
1679}
1680impl DspNode for KarplusStrongNode {
1681    fn process(&mut self, inputs: &[f32], outputs: &mut [[f32; 2]], config: &ConfigSnapshot, _ctx: &ProcessContext) {
1682        let input = inputs.get(0).copied().unwrap_or(0.0);
1683        let freq = config.get("freq").and_then(|v| v.as_float()).unwrap_or(440.0) as f32;
1684        let damping = config.get("damping").and_then(|v| v.as_float()).unwrap_or(0.5) as f32;
1685        let dispersion = config.get("dispersion").and_then(|v| v.as_float()).unwrap_or(0.5) as f32;
1686        let pick_pos = config.get("pick_pos").and_then(|v| v.as_float()).unwrap_or(0.2) as f32;
1687        
1688        let out = self.inner.process(input, freq, damping, dispersion, pick_pos);
1689        for o in outputs { *o = [out, out]; }
1690    }
1691}
1692
1693pub struct ModalResonatorNode {
1694    inner: dirtydata_dsp_modal::ModalResonatorBank,
1695    last_material: u32,
1696    last_freq: f32,
1697    last_bright: f32,
1698}
1699impl ModalResonatorNode {
1700    pub fn new(sample_rate: f32) -> Self {
1701        Self { 
1702            inner: dirtydata_dsp_modal::ModalResonatorBank::new(sample_rate),
1703            last_material: 999,
1704            last_freq: -1.0,
1705            last_bright: -1.0,
1706        }
1707    }
1708}
1709impl DspNode for ModalResonatorNode {
1710    fn process(&mut self, inputs: &[f32], outputs: &mut [[f32; 2]], config: &ConfigSnapshot, _ctx: &ProcessContext) {
1711        let input = inputs.get(0).copied().unwrap_or(0.0);
1712        
1713        let material = config.get("material").and_then(|v| v.as_float()).unwrap_or(0.0) as u32;
1714        let freq = config.get("base_freq").and_then(|v| v.as_float()).unwrap_or(440.0) as f32;
1715        let bright = config.get("brightness").and_then(|v| v.as_float()).unwrap_or(0.5) as f32;
1716        
1717        if material != self.last_material || (freq - self.last_freq).abs() > 0.1 || (bright - self.last_bright).abs() > 0.01 {
1718            self.inner.set_material(material, freq, bright);
1719            self.last_material = material;
1720            self.last_freq = freq;
1721            self.last_bright = bright;
1722        }
1723        
1724        let out = self.inner.process(input);
1725        for o in outputs { *o = [out, out]; }
1726    }
1727}
1728
1729pub struct SpringReverbNode {
1730    inner: dirtydata_dsp_spring::SpringReverb,
1731}
1732impl SpringReverbNode {
1733    pub fn new(sample_rate: f32) -> Self {
1734        Self { inner: dirtydata_dsp_spring::SpringReverb::new(sample_rate) }
1735    }
1736}
1737impl DspNode for SpringReverbNode {
1738    fn process(&mut self, inputs: &[f32], outputs: &mut [[f32; 2]], config: &ConfigSnapshot, _ctx: &ProcessContext) {
1739        let input = inputs.get(0).copied().unwrap_or(0.0);
1740        let decay = config.get("decay").and_then(|v| v.as_float()).unwrap_or(0.8) as f32;
1741        let dispersion = config.get("dispersion").and_then(|v| v.as_float()).unwrap_or(0.6) as f32;
1742        
1743        let out = self.inner.process(input, decay, dispersion);
1744        for o in outputs { *o = [out, out]; }
1745    }
1746}
1747
1748// ──────────────────────────────────────────────
1749// Tier B "For Madmen" DSP Nodes (Chaos, Ecosystems, Degradation)
1750// ──────────────────────────────────────────────
1751
1752pub struct ChuaCircuitNode {
1753    inner: dirtydata_dsp_chaos::ChuaCircuit,
1754}
1755impl ChuaCircuitNode {
1756    pub fn new(sample_rate: f32) -> Self {
1757        Self { inner: dirtydata_dsp_chaos::ChuaCircuit::new(sample_rate) }
1758    }
1759}
1760impl DspNode for ChuaCircuitNode {
1761    fn process(&mut self, _inputs: &[f32], outputs: &mut [[f32; 2]], config: &ConfigSnapshot, _ctx: &ProcessContext) {
1762        let alpha = config.get("alpha").and_then(|v| v.as_float()).unwrap_or(15.6) as f32;
1763        let beta = config.get("beta").and_then(|v| v.as_float()).unwrap_or(28.0) as f32;
1764        let rate = config.get("rate").and_then(|v| v.as_float()).unwrap_or(1.0) as f32;
1765        
1766        let out = self.inner.process(alpha, beta, rate);
1767        for o in outputs { *o = [out, out]; }
1768    }
1769}
1770
1771pub struct ReactionDiffusionNode {
1772    inner: dirtydata_dsp_reaction::ReactionDiffusion,
1773}
1774impl ReactionDiffusionNode {
1775    pub fn new() -> Self {
1776        Self { inner: dirtydata_dsp_reaction::ReactionDiffusion::new(256) }
1777    }
1778}
1779impl DspNode for ReactionDiffusionNode {
1780    fn process(&mut self, inputs: &[f32], outputs: &mut [[f32; 2]], config: &ConfigSnapshot, _ctx: &ProcessContext) {
1781        let input = inputs.get(0).copied().unwrap_or(0.0);
1782        let da = config.get("da").and_then(|v| v.as_float()).unwrap_or(1.0) as f32;
1783        let db = config.get("db").and_then(|v| v.as_float()).unwrap_or(0.5) as f32;
1784        let f = config.get("f").and_then(|v| v.as_float()).unwrap_or(0.055) as f32;
1785        let k = config.get("k").and_then(|v| v.as_float()).unwrap_or(0.062) as f32;
1786        
1787        let out = self.inner.process(input, da, db, f, k);
1788        for o in outputs { *o = [out, out]; }
1789    }
1790}
1791
1792pub struct TapeMachineNode {
1793    inner: dirtydata_dsp_tape::TapeMachine,
1794}
1795impl TapeMachineNode {
1796    pub fn new(sample_rate: f32) -> Self {
1797        Self { inner: dirtydata_dsp_tape::TapeMachine::new(sample_rate) }
1798    }
1799}
1800impl DspNode for TapeMachineNode {
1801    fn process(&mut self, inputs: &[f32], outputs: &mut [[f32; 2]], config: &ConfigSnapshot, _ctx: &ProcessContext) {
1802        let input = inputs.get(0).copied().unwrap_or(0.0);
1803        let drive = config.get("drive").and_then(|v| v.as_float()).unwrap_or(1.0) as f32;
1804        let wow = config.get("wow").and_then(|v| v.as_float()).unwrap_or(0.1) as f32;
1805        let flutter = config.get("flutter").and_then(|v| v.as_float()).unwrap_or(0.1) as f32;
1806        let bias = config.get("bias").and_then(|v| v.as_float()).unwrap_or(0.5) as f32;
1807        
1808        let out = self.inner.process(input, drive, wow, flutter, bias);
1809        for o in outputs { *o = [out, out]; }
1810    }
1811}
1812
1813// ──────────────────────────────────────────────
1814// Priority SSS: Matrix / Routing Hell
1815// ──────────────────────────────────────────────
1816
1817pub struct MatrixMixerNode {
1818    inner: dirtydata_dsp_matrix::MatrixMixer,
1819}
1820impl MatrixMixerNode {
1821    pub fn new(num_in: usize, num_out: usize) -> Self {
1822        Self { inner: dirtydata_dsp_matrix::MatrixMixer::new(num_in, num_out) }
1823    }
1824}
1825impl DspNode for MatrixMixerNode {
1826    fn process(&mut self, inputs: &[f32], outputs: &mut [[f32; 2]], config: &ConfigSnapshot, _ctx: &ProcessContext) {
1827        // Simple 2x2 matrix for now
1828        let g00 = config.get("g00").and_then(|v| v.as_float()).unwrap_or(1.0) as f32;
1829        let g01 = config.get("g01").and_then(|v| v.as_float()).unwrap_or(0.0) as f32;
1830        let g10 = config.get("g10").and_then(|v| v.as_float()).unwrap_or(0.0) as f32;
1831        let g11 = config.get("g11").and_then(|v| v.as_float()).unwrap_or(1.0) as f32;
1832        
1833        self.inner.set_gain(0, 0, g00);
1834        self.inner.set_gain(1, 0, g01);
1835        self.inner.set_gain(0, 1, g10);
1836        self.inner.set_gain(1, 1, g11);
1837        
1838        let in_flat: Vec<f32> = inputs.iter().copied().collect();
1839        let mut out_flat = vec![0.0; outputs.len() * 2];
1840        self.inner.process(&in_flat, &mut out_flat);
1841        
1842        for (i, o) in outputs.iter_mut().enumerate() {
1843            o[0] = out_flat[i * 2];
1844            o[1] = out_flat[i * 2 + 1];
1845        }
1846    }
1847}
1848
1849// ──────────────────────────────────────────────
1850// Priority SS: CV Civilization
1851// ──────────────────────────────────────────────
1852
1853pub struct SlewNode {
1854    inner: dirtydata_dsp_cv::Slew,
1855}
1856impl SlewNode {
1857    pub fn new() -> Self { Self { inner: dirtydata_dsp_cv::Slew::new() } }
1858}
1859impl DspNode for SlewNode {
1860    fn process(&mut self, inputs: &[f32], outputs: &mut [[f32; 2]], config: &ConfigSnapshot, ctx: &ProcessContext) {
1861        let input = inputs.get(0).copied().unwrap_or(0.0);
1862        let rise = config.get("rise").and_then(|v| v.as_float()).unwrap_or(0.1) as f32;
1863        let fall = config.get("fall").and_then(|v| v.as_float()).unwrap_or(0.1) as f32;
1864        let out = self.inner.process(input, rise, fall, ctx.sample_rate);
1865        for o in outputs { *o = [out, out]; }
1866    }
1867}
1868
1869pub struct EuclideanSequencerNode {
1870    inner: dirtydata_dsp_cv::EuclideanSequencer,
1871}
1872impl EuclideanSequencerNode {
1873    pub fn new() -> Self { Self { inner: dirtydata_dsp_cv::EuclideanSequencer::new() } }
1874}
1875impl DspNode for EuclideanSequencerNode {
1876    fn process(&mut self, inputs: &[f32], outputs: &mut [[f32; 2]], config: &ConfigSnapshot, _ctx: &ProcessContext) {
1877        let clock = inputs.get(0).copied().unwrap_or(0.0);
1878        self.inner.steps = config.get("steps").and_then(|v| v.as_float()).unwrap_or(16.0) as u32;
1879        self.inner.hits = config.get("hits").and_then(|v| v.as_float()).unwrap_or(4.0) as u32;
1880        let out = self.inner.process(clock);
1881        for o in outputs { *o = [out, out]; }
1882    }
1883}
1884
1885// ──────────────────────────────────────────────
1886// Priority S: Destruction
1887// ──────────────────────────────────────────────
1888
1889pub struct BitCrushNode {
1890    inner: dirtydata_dsp_destruction::BitCrush,
1891}
1892impl BitCrushNode {
1893    pub fn new() -> Self { Self { inner: dirtydata_dsp_destruction::BitCrush::new() } }
1894}
1895impl DspNode for BitCrushNode {
1896    fn process(&mut self, inputs: &[f32], outputs: &mut [[f32; 2]], config: &ConfigSnapshot, _ctx: &ProcessContext) {
1897        let input = inputs.get(0).copied().unwrap_or(0.0);
1898        let bits = config.get("bits").and_then(|v| v.as_float()).unwrap_or(8.0) as f32;
1899        let srr = config.get("srr").and_then(|v| v.as_float()).unwrap_or(1.0) as f32;
1900        let out = self.inner.process(input, bits, srr);
1901        for o in outputs { *o = [out, out]; }
1902    }
1903}
1904
1905// ──────────────────────────────────────────────
1906// Priority S: Control as Instrument (Maths)
1907// ──────────────────────────────────────────────
1908
1909pub struct FunctionGeneratorNode {
1910    inner: dirtydata_dsp_control::FunctionGenerator,
1911}
1912impl FunctionGeneratorNode {
1913    pub fn new() -> Self { Self { inner: dirtydata_dsp_control::FunctionGenerator::new() } }
1914}
1915impl DspNode for FunctionGeneratorNode {
1916    fn process(&mut self, inputs: &[f32], outputs: &mut [[f32; 2]], config: &ConfigSnapshot, ctx: &ProcessContext) {
1917        let trigger = inputs.get(0).copied().unwrap_or(0.0);
1918        let rise = config.get("rise").and_then(|v| v.as_float()).unwrap_or(0.1) as f32;
1919        let fall = config.get("fall").and_then(|v| v.as_float()).unwrap_or(0.5) as f32;
1920        let cycle = config.get("cycle").and_then(|v| v.as_bool()).unwrap_or(false);
1921        let out = self.inner.process(trigger, rise, fall, cycle, ctx.sample_rate);
1922        for o in outputs { *o = [out, out]; }
1923    }
1924}
1925
1926// ──────────────────────────────────────────────
1927// Priority GOD: Circuit Sandbox (MNA Solver)
1928// ──────────────────────────────────────────────
1929
1930pub struct CircuitSandboxNode {
1931    solver: dirtydata_dsp_circuit::MnaSolver,
1932    // Live probe history (ring buffer for popup oscilloscope)
1933    probe_voltages: Vec<f32>,
1934}
1935
1936impl CircuitSandboxNode {
1937    pub fn new(sample_rate: f32) -> Self {
1938        let mut solver = dirtydata_dsp_circuit::MnaSolver::new(1.0 / sample_rate as f64);
1939        
1940        // --- PRESET: Transistor-based Diode Ladder (Moog-ish) ---
1941        // Node 0: Ground
1942        // Node 1: Signal In (Voltage Source)
1943        // Node 2: Cutoff Control (Voltage Source)
1944        // Nodes 3-6: Ladder Stages
1945        solver.set_num_nodes(7);
1946        
1947        // Signal Input
1948        solver.add_element(dirtydata_dsp_circuit::CircuitElement::VoltageSource {
1949            pos: dirtydata_dsp_circuit::NodeId(1), neg: dirtydata_dsp_circuit::NodeId(0), voltage: 0.0,
1950        });
1951        
1952        // Cutoff Control (thermal voltage biasing)
1953        solver.add_element(dirtydata_dsp_circuit::CircuitElement::VoltageSource {
1954            pos: dirtydata_dsp_circuit::NodeId(2), neg: dirtydata_dsp_circuit::NodeId(0), voltage: 0.7,
1955        });
1956
1957        // 4-stage Diode Ladder (discrete components)
1958        for i in 0..4 {
1959            let n_in = if i == 0 { 1 } else { 3 + i - 1 };
1960            let n_out = 3 + i;
1961            
1962            // Diode Pair (nonlinear saturation)
1963            solver.add_element(dirtydata_dsp_circuit::CircuitElement::Diode {
1964                a: dirtydata_dsp_circuit::NodeId(n_in), 
1965                k: dirtydata_dsp_circuit::NodeId(n_out), 
1966                material: dirtydata_dsp_circuit::Material::Silicon,
1967                is: 1e-12,
1968            });
1969            // Stage Capacitor
1970            solver.add_element(dirtydata_dsp_circuit::CircuitElement::Capacitor {
1971                a: dirtydata_dsp_circuit::NodeId(n_out), 
1972                b: dirtydata_dsp_circuit::NodeId(0), 
1973                value: 1e-8, 
1974                state_v: 0.0,
1975                tolerance: 0.1,
1976                material: dirtydata_dsp_circuit::Material::Ceramic,
1977            });
1978        }
1979        
1980        Self { solver, probe_voltages: vec![0.0; 256] }
1981    }
1982}
1983
1984impl DspNode for CircuitSandboxNode {
1985    fn process(&mut self, inputs: &[f32], outputs: &mut [[f32; 2]], config: &ConfigSnapshot, ctx: &ProcessContext) {
1986        let input = inputs.get(0).copied().unwrap_or(0.0) as f64;
1987        let cutoff = config.get("cutoff").and_then(|v| v.as_float()).unwrap_or(0.7) as f64;
1988        
1989        // --- Priority MONSTER: Environmental Sabotage ---
1990        if let Some(temp) = config.get("temp_c").and_then(|v| v.as_float()) {
1991            self.solver.context.temperature_c = temp as f64;
1992        }
1993        if let Some(drift) = config.get("drift").and_then(|v| v.as_float()) {
1994            self.solver.context.global_drift = drift as f64;
1995        }
1996        if let Some(vcc) = config.get("vcc").and_then(|v| v.as_float()) {
1997            self.solver.context.vcc = vcc as f64;
1998        }
1999
2000        // Set parameters via handles...
2001        if let Some(dirtydata_dsp_circuit::CircuitElement::VoltageSource { voltage, .. }) = self.solver.add_element_dummy_handle(0) {
2002            *voltage = input;
2003        }
2004        if let Some(dirtydata_dsp_circuit::CircuitElement::VoltageSource { voltage, .. }) = self.solver.add_element_dummy_handle(1) {
2005            *voltage = cutoff;
2006        }
2007
2008        let state = self.solver.solve();
2009        let out = state.voltages.get(6).copied().unwrap_or(0.0) as f32; // Last stage
2010        
2011        // --- §SSS: Visual Nodal Probing ---
2012        // Expose state to GUI via ctx.shared_state
2013        if ctx.sample_rate > 0.0 {
2014            // Push to ring buffer for "Nodal Probing"
2015            self.probe_voltages.rotate_left(1);
2016            if let Some(last) = self.probe_voltages.last_mut() { *last = out; }
2017            
2018            // If iterations is high, the circuit is "screaming" (vibrate UI)
2019            if state.iterations > 40 {
2020                // Trigger visual vibration event (conceptual)
2021            }
2022        }
2023        
2024        for o in outputs { *o = [out, out]; }
2025    }
2026}
2027
2028// ──────────────────────────────────────────────
2029// Priority SSS: Circuit Module (Custom reusable MNA nodes)
2030// ──────────────────────────────────────────────
2031
2032pub struct CircuitModuleNode {
2033    solver: dirtydata_dsp_circuit::MnaSolver,
2034    /// Maps audio input index to internal voltage source index
2035    input_v_sources: Vec<usize>,
2036    /// Maps internal node IDs to audio output indices
2037    output_nodes: Vec<usize>,
2038}
2039
2040impl CircuitModuleNode {
2041    pub fn new(sample_rate: f32, definition_json: &str) -> Option<Self> {
2042        let def: dirtydata_core::types::CircuitDefinition = serde_json::from_str(definition_json).ok()?;
2043        let elements: Vec<dirtydata_dsp_circuit::CircuitElement> = serde_json::from_str(&def.elements_json).ok()?;
2044        
2045        let mut solver = dirtydata_dsp_circuit::MnaSolver::new(1.0 / sample_rate as f64);
2046        
2047        // Find max node ID to set_num_nodes
2048        let mut max_node = 0;
2049        for el in &elements {
2050            match el {
2051                dirtydata_dsp_circuit::CircuitElement::Resistor { a, b, .. } => { max_node = max_node.max(a.0).max(b.0); }
2052                dirtydata_dsp_circuit::CircuitElement::Capacitor { a, b, .. } => { max_node = max_node.max(a.0).max(b.0); }
2053                dirtydata_dsp_circuit::CircuitElement::Diode { a, k, .. } => { max_node = max_node.max(a.0).max(k.0); }
2054                dirtydata_dsp_circuit::CircuitElement::VoltageSource { pos, neg, .. } => { max_node = max_node.max(pos.0).max(neg.0); }
2055            }
2056        }
2057        solver.set_num_nodes(max_node + 1);
2058
2059        let mut input_v_sources = Vec::new();
2060        for (_, &node_id) in &def.input_mappings {
2061            let idx = solver.num_elements(); // Track position of voltage source
2062            solver.add_element(dirtydata_dsp_circuit::CircuitElement::VoltageSource {
2063                pos: dirtydata_dsp_circuit::NodeId(node_id),
2064                neg: dirtydata_dsp_circuit::NodeId(0), // Ground reference
2065                voltage: 0.0,
2066            });
2067            input_v_sources.push(idx);
2068        }
2069
2070        for el in elements { solver.add_element(el); }
2071        
2072        let mut output_nodes = Vec::new();
2073        for (_, &node_id) in &def.output_mappings {
2074            output_nodes.push(node_id);
2075        }
2076
2077        Some(Self { solver, input_v_sources, output_nodes })
2078    }
2079}
2080
2081impl DspNode for CircuitModuleNode {
2082    fn process(&mut self, inputs: &[f32], outputs: &mut [[f32; 2]], _config: &ConfigSnapshot, ctx: &ProcessContext) {
2083        // 1. Inject samples as voltages
2084        for (i, &v_idx) in self.input_v_sources.iter().enumerate() {
2085            if let Some(val) = inputs.get(i) {
2086                if let Some(dirtydata_dsp_circuit::CircuitElement::VoltageSource { voltage, .. }) = self.solver.add_element_dummy_handle(v_idx) {
2087                    *voltage = *val as f64;
2088                }
2089            }
2090        }
2091
2092        // 2. Step the physics
2093        let state = self.solver.solve();
2094        
2095        if let (Some(info), Some(id)) = (ctx.convergence_info.as_ref(), ctx.node_id) {
2096            info.insert(id, state.iterations);
2097        }
2098
2099        if !state.converged {
2100            if let (Some(diag), Some(id)) = (ctx.node_diagnostics.as_ref(), ctx.node_id) {
2101                diag.insert(id, crate::DiagnosticRecord {
2102                    message: state.failure_culprit.clone().unwrap_or_default(),
2103                    severity: crate::DiagnosticSeverity::Error,
2104                    timestamp: ctx.global_sample_index,
2105                });
2106            }
2107        }
2108
2109        // 3. Extract voltages as samples
2110        for (i, &node_id) in self.output_nodes.iter().enumerate() {
2111            if let Some(out_pair) = outputs.get_mut(i) {
2112                let v = state.voltages.get(node_id).copied().unwrap_or(0.0) as f32;
2113                *out_pair = [v, v];
2114            }
2115        }
2116    }
2117}
2118
2119// ──────────────────────────────────────────────
2120// Priority GOD: Vocal Tract Physical Modeling
2121// ──────────────────────────────────────────────
2122
2123pub struct VocalTractNode {
2124    inner: dirtydata_dsp_vocal::VocalTract,
2125}
2126impl VocalTractNode {
2127    pub fn new(sample_rate: f32) -> Self {
2128        let _ = sample_rate;
2129        Self { inner: dirtydata_dsp_vocal::VocalTract::new(44) } // 44 sections
2130    }
2131}
2132impl DspNode for VocalTractNode {
2133    fn process(&mut self, _inputs: &[f32], outputs: &mut [[f32; 2]], config: &ConfigSnapshot, ctx: &ProcessContext) {
2134        let freq = config.get("pitch").and_then(|v| v.as_float()).unwrap_or(110.0) as f32;
2135        let tongue_x = config.get("tongue_x").and_then(|v| v.as_float()).unwrap_or(0.5) as f32;
2136        let tongue_y = config.get("tongue_y").and_then(|v| v.as_float()).unwrap_or(0.5) as f32;
2137        let tension = config.get("tension").and_then(|v| v.as_float()).unwrap_or(0.8) as f32;
2138        let velum = config.get("velum").and_then(|v| v.as_float()).unwrap_or(0.0) as f32;
2139        
2140        // Check for vowel preset
2141        let vowel = config.get("vowel").and_then(|v| v.as_string());
2142        if let Some(v) = vowel {
2143            if let Some(ch) = v.chars().next() {
2144                self.inner.set_vowel(ch);
2145            }
2146        } else {
2147            self.inner.glottis.set_freq(freq);
2148            self.inner.set_tongue(tongue_x, tongue_y);
2149            self.inner.set_velum(velum);
2150        }
2151        
2152        let out = self.inner.process(ctx.sample_rate, tension);
2153        for o in outputs { *o = [out, out]; }
2154    }
2155}
2156
2157
2158