Skip to main content

devalang_wasm/engine/audio/
events.rs

1use crate::engine::audio::generator::FilterDef;
2/// Audio events system - stores note/chord events to be rendered
3use crate::language::syntax::ast::Value;
4use std::collections::HashMap;
5
6#[derive(Debug, Clone)]
7pub enum AudioEvent {
8    Note {
9        node_id: Option<String>,
10        midi: u8,
11        start_time: f32,
12        duration: f32,
13        velocity: f32,
14        synth_id: String,
15        synth_def: SynthDefinition, // Snapshot of synth at event creation time
16        // Audio options
17        pan: f32,             // -1.0 to 1.0
18        detune: f32,          // cents
19        gain: f32,            // multiplier
20        attack: Option<f32>,  // ms override
21        release: Option<f32>, // ms override
22        // Effects
23        delay_time: Option<f32>,     // ms
24        delay_feedback: Option<f32>, // 0.0-1.0
25        delay_mix: Option<f32>,      // 0.0-1.0
26        reverb_amount: Option<f32>,  // 0.0-1.0
27        drive_amount: Option<f32>,   // 0.0-1.0
28        drive_color: Option<f32>,    // 0.0-1.0
29        // Full effects map (optional) - may contain chained effects for this event
30        effects: Option<crate::language::syntax::ast::Value>,
31        // Per-note automation flag
32        use_per_note_automation: bool, // Whether to apply per-note automation at render time
33    },
34    Chord {
35        node_id: Option<String>,
36        midis: Vec<u8>,
37        start_time: f32,
38        duration: f32,
39        velocity: f32,
40        synth_id: String,
41        synth_def: SynthDefinition, // Snapshot of synth at event creation time
42        // Audio options
43        pan: f32,             // -1.0 to 1.0
44        detune: f32,          // cents
45        spread: f32,          // 0.0 to 1.0
46        gain: f32,            // multiplier
47        attack: Option<f32>,  // ms override
48        release: Option<f32>, // ms override
49        // Effects
50        delay_time: Option<f32>,     // ms
51        delay_feedback: Option<f32>, // 0.0-1.0
52        delay_mix: Option<f32>,      // 0.0-1.0
53        reverb_amount: Option<f32>,  // 0.0-1.0
54        drive_amount: Option<f32>,   // 0.0-1.0
55        drive_color: Option<f32>,    // 0.0-1.0
56        // Full effects map (optional)
57        effects: Option<crate::language::syntax::ast::Value>,
58        // Per-note automation flag
59        use_per_note_automation: bool, // Whether to apply per-note automation at render time
60    },
61    Sample {
62        node_id: Option<String>,
63        uri: String,
64        start_time: f32,
65        velocity: f32,
66        // Effects to apply to this sample (trigger effects)
67        effects: Option<crate::language::syntax::ast::Value>,
68        // Source identifier (pattern name) for routing purposes
69        source: Option<String>,
70    },
71    Stop {
72        target: Option<String>, // None = stop all, Some = stop specific entity
73        time: f32,              // Time when to stop
74    },
75}
76
77/// Audio events collector
78#[derive(Debug, Default)]
79pub struct AudioEventList {
80    pub events: Vec<AudioEvent>,
81    /// Print/log messages recorded during interpretation. These are not audio events
82    /// and should not affect loop termination logic.
83    pub logs: Vec<(f32, String)>,
84    pub synths: HashMap<String, SynthDefinition>,
85}
86
87#[derive(Debug, Clone)]
88pub struct SynthDefinition {
89    pub waveform: String,
90    pub attack: f32,
91    pub decay: f32,
92    pub sustain: f32,
93    pub release: f32,
94    pub synth_type: Option<String>,
95    pub filters: Vec<FilterDef>,
96    pub options: HashMap<String, f32>, // Configurable synth type options
97    pub lfo: Option<crate::engine::audio::lfo::LfoParams>, // Low-Frequency Oscillator
98    pub bpm: f32, // Tempo snapshot used by beat-synced modulation during rendering
99    // Plugin support
100    pub plugin_author: Option<String>,
101    pub plugin_name: Option<String>,
102    pub plugin_export: Option<String>,
103}
104
105impl Default for SynthDefinition {
106    fn default() -> Self {
107        Self {
108            waveform: "sine".to_string(),
109            attack: 0.01,
110            decay: 0.1,
111            sustain: 0.7,
112            release: 0.2,
113            synth_type: None,
114            filters: Vec::new(),
115            options: HashMap::new(),
116            lfo: None,
117            bpm: 120.0,
118            plugin_author: None,
119            plugin_name: None,
120            plugin_export: None,
121        }
122    }
123}
124
125impl AudioEventList {
126    pub fn new() -> Self {
127        Self {
128            events: Vec::new(),
129            logs: Vec::new(),
130            synths: HashMap::new(),
131        }
132    }
133
134    pub fn add_synth(&mut self, name: String, definition: SynthDefinition) {
135        self.synths.insert(name, definition);
136    }
137
138    pub fn add_note_event(
139        &mut self,
140        synth_id: &str,
141        midi: u8,
142        start_time: f32,
143        duration: f32,
144        velocity: f32,
145        pan: f32,
146        detune: f32,
147        gain: f32,
148        attack: Option<f32>,
149        release: Option<f32>,
150        delay_time: Option<f32>,
151        delay_feedback: Option<f32>,
152        delay_mix: Option<f32>,
153        reverb_amount: Option<f32>,
154        drive_amount: Option<f32>,
155        drive_color: Option<f32>,
156    ) {
157        // Capture synth definition snapshot at event creation time
158        let synth_def = self.get_synth(synth_id).cloned().unwrap_or_else(|| {
159            println!("⚠️  Warning: Synth '{}' not found when creating note event. Available synths: {:?}", 
160                     synth_id, self.synths.keys().collect::<Vec<_>>());
161            SynthDefinition::default()
162        });
163
164        self.events.push(AudioEvent::Note {
165            node_id: Some("master".to_string()),
166            midi,
167            start_time,
168            duration,
169            velocity,
170            synth_id: synth_id.to_string(),
171            synth_def,
172            pan,
173            detune,
174            gain,
175            attack,
176            release,
177            delay_time,
178            delay_feedback,
179            delay_mix,
180            reverb_amount,
181            drive_amount,
182            drive_color,
183            effects: None,
184            use_per_note_automation: false,
185        });
186    }
187
188    pub fn add_chord_event(
189        &mut self,
190        synth_id: &str,
191        midis: Vec<u8>,
192        start_time: f32,
193        duration: f32,
194        velocity: f32,
195        pan: f32,
196        detune: f32,
197        spread: f32,
198        gain: f32,
199        attack: Option<f32>,
200        release: Option<f32>,
201        delay_time: Option<f32>,
202        delay_feedback: Option<f32>,
203        delay_mix: Option<f32>,
204        reverb_amount: Option<f32>,
205        drive_amount: Option<f32>,
206        drive_color: Option<f32>,
207    ) {
208        // Capture synth definition snapshot at event creation time
209        let synth_def = self.get_synth(synth_id).cloned().unwrap_or_default();
210
211        self.events.push(AudioEvent::Chord {
212            node_id: Some("master".to_string()),
213            midis,
214            start_time,
215            duration,
216            velocity,
217            synth_id: synth_id.to_string(),
218            synth_def,
219            pan,
220            detune,
221            spread,
222            gain,
223            attack,
224            release,
225            delay_time,
226            delay_feedback,
227            delay_mix,
228            reverb_amount,
229            drive_amount,
230            drive_color,
231            effects: None,
232            use_per_note_automation: false,
233        });
234    }
235
236    pub fn add_sample_event(&mut self, uri: &str, start_time: f32, velocity: f32) {
237        self.events.push(AudioEvent::Sample {
238            node_id: Some("master".to_string()),
239            uri: uri.to_string(),
240            start_time,
241            velocity,
242            effects: None,
243            source: None,
244        });
245    }
246
247    /// Add a sample event with an attached effects map (owned)
248    pub fn add_sample_event_with_effects(
249        &mut self,
250        uri: &str,
251        start_time: f32,
252        velocity: f32,
253        effects: Option<Value>,
254    ) {
255        self.events.push(AudioEvent::Sample {
256            node_id: Some("master".to_string()),
257            uri: uri.to_string(),
258            start_time,
259            velocity,
260            effects,
261            source: None,
262        });
263    }
264
265    /// Add a log message (created by `print` statements). Time is in seconds from start.
266    /// Logs are stored separately from audio events so they don't affect rendering/loop logic.
267    pub fn add_log_event(&mut self, message: String, time: f32) {
268        self.logs.push((time, message));
269    }
270
271    pub fn get_synth(&self, name: &str) -> Option<&SynthDefinition> {
272        self.synths.get(name)
273    }
274
275    pub fn total_duration(&self) -> f32 {
276        // Find the earliest stop time (if any)
277        let stop_time = self.events.iter().find_map(|event| match event {
278            AudioEvent::Stop { target: None, time } => Some(*time), // Only consider "stop all"
279            _ => None,
280        });
281
282        // Calculate total duration from all events
283        let max_duration = self
284            .events
285            .iter()
286            .map(|event| match event {
287                AudioEvent::Note {
288                    start_time,
289                    duration,
290                    ..
291                } => start_time + duration,
292                AudioEvent::Chord {
293                    start_time,
294                    duration,
295                    ..
296                } => start_time + duration,
297                AudioEvent::Sample {
298                    start_time, uri, ..
299                } => {
300                    // Get actual sample duration from registry
301                    #[cfg(target_arch = "wasm32")]
302                    {
303                        use crate::web::registry::samples::get_sample;
304                        if let Some(pcm) = get_sample(uri) {
305                            // Assume 44.1kHz sample rate
306                            let duration = pcm.len() as f32 / 44100.0;
307                            start_time + duration
308                        } else {
309                            // Fallback: estimate 2 seconds
310                            start_time + 2.0
311                        }
312                    }
313                    #[cfg(not(target_arch = "wasm32"))]
314                    {
315                        // Fallback for native: estimate 2 seconds
316                        let _ = uri; // Silence unused warning on non-WASM targets
317                        start_time + 2.0
318                    }
319                }
320                AudioEvent::Stop { target: None, time } => *time, // Include stop all times
321                AudioEvent::Stop {
322                    target: Some(_), ..
323                } => 0.0, // Ignore stop specific
324            })
325            .fold(0.0, f32::max);
326
327        // If there's a stop all event, limit duration to it
328        if let Some(stop_t) = stop_time {
329            max_duration.min(stop_t)
330        } else {
331            max_duration
332        }
333    }
334
335    /// Merge another AudioEventList into this one
336    /// This is used for parallel spawn execution
337    pub fn merge(&mut self, other: AudioEventList) {
338        // Merge synth definitions FIRST (prefer existing definitions on conflict)
339        for (name, def) in other.synths {
340            if !self.synths.contains_key(&name) {
341                self.synths.insert(name, def);
342            }
343        }
344
345        // Merge events and update their synth_def snapshots if needed
346        for mut event in other.events {
347            // Update synth_def snapshot for Note and Chord events
348            match &mut event {
349                AudioEvent::Note {
350                    synth_id,
351                    synth_def,
352                    ..
353                } => {
354                    // If this event's synth now exists in merged synths, update the snapshot
355                    if let Some(updated_def) = self.synths.get(synth_id) {
356                        *synth_def = updated_def.clone();
357                    }
358                }
359                AudioEvent::Chord {
360                    synth_id,
361                    synth_def,
362                    ..
363                } => {
364                    // If this event's synth now exists in merged synths, update the snapshot
365                    if let Some(updated_def) = self.synths.get(synth_id) {
366                        *synth_def = updated_def.clone();
367                    }
368                }
369                _ => {}
370            }
371            self.events.push(event);
372        }
373
374        // Merge logs (print messages) as well
375        for log in other.logs {
376            // Avoid inserting duplicate or near-duplicate log entries (same message
377            // and timestamps within a small epsilon). Multiple workers or interpreter
378            // snapshots may produce the same scheduled print with slightly different
379            // float timestamps; deduplicate those here.
380            let mut duplicated = false;
381            for existing in &self.logs {
382                let time_diff = (existing.0 - log.0).abs();
383                if existing.1 == log.1 && time_diff <= 0.001 {
384                    duplicated = true;
385                    break;
386                }
387            }
388            if !duplicated {
389                self.logs.push(log);
390            }
391        }
392
393        // Keep logs time-ordered for predictable playback
394        self.logs
395            .sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
396    }
397}
398
399/// Helper to extract values from Value::Map
400pub fn extract_number(map: &HashMap<String, Value>, key: &str, default: f32) -> f32 {
401    map.get(key)
402        .and_then(|v| {
403            if let Value::Number(n) = v {
404                Some(*n)
405            } else {
406                None
407            }
408        })
409        .unwrap_or(default)
410}
411
412pub fn extract_string(map: &HashMap<String, Value>, key: &str, default: &str) -> String {
413    map.get(key)
414        .and_then(|v| {
415            if let Value::String(s) = v {
416                Some(s.clone())
417            } else {
418                None
419            }
420        })
421        .unwrap_or_else(|| default.to_string())
422}
423
424pub fn extract_filters(filters_arr: &[Value]) -> Vec<FilterDef> {
425    filters_arr
426        .iter()
427        .filter_map(|v| {
428            if let Value::Map(filter_map) = v {
429                let filter_type = extract_string(filter_map, "type", "lowpass");
430                let cutoff = filter_map
431                    .get("cutoff")
432                    .and_then(|v| match v {
433                        Value::Number(n) => Some(*n),
434                        _ => None,
435                    })
436                    .unwrap_or(1000.0);
437                let resonance = filter_map
438                    .get("resonance")
439                    .and_then(|v| match v {
440                        Value::Number(n) => Some(*n),
441                        _ => None,
442                    })
443                    .unwrap_or(1.0);
444
445                Some(FilterDef {
446                    filter_type,
447                    cutoff,
448                    resonance,
449                })
450            } else {
451                None
452            }
453        })
454        .collect()
455}