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