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