Skip to main content

dirtydata_runtime/
offline.rs

1use crate::nodes::ProcessContext;
2use crate::DspRunner;
3use dirtydata_core::ir::Graph;
4use dirtydata_observer::divergence::{DivergenceMap, DivergencePoint};
5
6/// A renderer for offline (faster than real-time) audio generation.
7pub struct OfflineRenderer {
8    runner: DspRunner,
9    sample_rate: f32,
10}
11
12impl OfflineRenderer {
13    pub fn new(graph: Graph, sample_rate: f32) -> Self {
14        // Offline rendering currently doesn't support live MIDI input
15        let runner = DspRunner::new(graph, None, sample_rate);
16        Self { runner, sample_rate }
17    }
18
19    /// Renders the specified duration of audio.
20    /// Returns interleaved stereo samples [L, R, L, R, ...].
21    pub fn render(&mut self, duration_secs: f32) -> Vec<f32> {
22        let num_samples = (duration_secs * self.sample_rate) as usize;
23        let mut output = Vec::with_capacity(num_samples * 2);
24
25        for i in 0..num_samples {
26            let ctx = ProcessContext {
27                sample_rate: self.sample_rate,
28                global_sample_index: i as u64,
29                crash_flag: None,
30                osc_tx: None,
31                convergence_info: None,
32                node_diagnostics: None,
33                node_id: None,
34            };
35            let sample = self.runner.process_sample(&ctx);
36            output.push(sample[0]);
37            output.push(sample[1]);
38        }
39        output
40    }
41
42    /// Verifies that two independent runs produce the exact same output.
43    pub fn verify_determinism(graph: Graph, duration_secs: f32, sample_rate: f32) -> Result<bool, String> {
44        let mut r1 = Self::new(graph.clone(), sample_rate);
45        let mut r2 = Self::new(graph, sample_rate);
46        
47        let out1 = r1.render(duration_secs);
48        let out2 = r2.render(duration_secs);
49        
50        if out1.len() != out2.len() {
51            return Err("Output length mismatch between identical runs".into());
52        }
53        
54        for (s1, s2) in out1.iter().zip(out2.iter()) {
55            if (*s1 - *s2).abs() > 0.0 {
56                return Ok(false);
57            }
58        }
59        Ok(true)
60    }
61
62    /// Compares two graphs sample-by-sample and node-by-node.
63    /// This is the heart of the "Replay Divergence Map".
64    pub fn compare(
65        graph_a: Graph,
66        graph_b: Graph,
67        duration_secs: f32,
68        sample_rate: f32,
69    ) -> DivergenceMap {
70        let mut r_a = DspRunner::new(graph_a, None, sample_rate);
71        let mut r_b = DspRunner::new(graph_b, None, sample_rate);
72
73        let num_samples = (duration_secs * sample_rate) as usize;
74        let mut map = DivergenceMap::new();
75
76        for i in 0..num_samples {
77            let ctx = ProcessContext {
78                sample_rate,
79                global_sample_index: i as u64,
80                crash_flag: None,
81                osc_tx: None,
82                convergence_info: None,
83                node_diagnostics: None,
84                node_id: None,
85            };
86
87            r_a.process_sample(&ctx);
88            r_b.process_sample(&ctx);
89
90            // Compare outputs of all nodes that exist in both runners
91            let ids: Vec<_> = r_a.get_graph().nodes.keys().cloned().collect();
92            for id in ids {
93                if let (Some(out_a), Some(out_b)) = (r_a.get_node_outputs(&id), r_b.get_node_outputs(&id)) {
94                    for (p_idx, (v_a, v_b)) in out_a.iter().zip(out_b.iter()).enumerate() {
95                        let diff_l = (v_a[0] - v_b[0]).abs();
96                        let diff_r = (v_a[1] - v_b[1]).abs();
97                        let mag = diff_l.max(diff_r);
98
99                        if mag > 1e-7 { // Tolerance for floating point
100                            map.add_point(DivergencePoint {
101                                sample_index: i as u64,
102                                node_id: id,
103                                node_name: "Unknown".into(), // Should fetch from graph
104                                port_idx: p_idx,
105                                expected_value: *v_a,
106                                actual_value: *v_b,
107                                diff_magnitude: mag,
108                            });
109                            
110                            if map.points.len() > 100 {
111                                return map; // Cap it for now
112                            }
113                        }
114                    }
115                }
116            }
117        }
118
119        map
120    }
121}