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        // Per-note automation flag
29        use_per_note_automation: bool, // Whether to apply per-note automation at render time
30    },
31    Chord {
32        midis: Vec<u8>,
33        start_time: f32,
34        duration: f32,
35        velocity: f32,
36        synth_id: String,
37        synth_def: SynthDefinition, // Snapshot of synth at event creation time
38        // Audio options
39        pan: f32,             // -1.0 to 1.0
40        detune: f32,          // cents
41        spread: f32,          // 0.0 to 1.0
42        gain: f32,            // multiplier
43        attack: Option<f32>,  // ms override
44        release: Option<f32>, // ms override
45        // Effects
46        delay_time: Option<f32>,     // ms
47        delay_feedback: Option<f32>, // 0.0-1.0
48        delay_mix: Option<f32>,      // 0.0-1.0
49        reverb_amount: Option<f32>,  // 0.0-1.0
50        drive_amount: Option<f32>,   // 0.0-1.0
51        drive_color: Option<f32>,    // 0.0-1.0
52        // Per-note automation flag
53        use_per_note_automation: bool, // Whether to apply per-note automation at render time
54    },
55    Sample {
56        uri: String,
57        start_time: f32,
58        velocity: f32,
59    },
60}
61
62/// Audio events collector
63#[derive(Debug, Default)]
64pub struct AudioEventList {
65    pub events: Vec<AudioEvent>,
66    pub synths: HashMap<String, SynthDefinition>,
67}
68
69#[derive(Debug, Clone)]
70pub struct SynthDefinition {
71    pub waveform: String,
72    pub attack: f32,
73    pub decay: f32,
74    pub sustain: f32,
75    pub release: f32,
76    pub synth_type: Option<String>,
77    pub filters: Vec<FilterDef>,
78    pub options: HashMap<String, f32>, // Configurable synth type options
79    pub lfo: Option<crate::engine::audio::lfo::LfoParams>, // Low-Frequency Oscillator
80    // Plugin support
81    pub plugin_author: Option<String>,
82    pub plugin_name: Option<String>,
83    pub plugin_export: Option<String>,
84}
85
86impl Default for SynthDefinition {
87    fn default() -> Self {
88        Self {
89            waveform: "sine".to_string(),
90            attack: 0.01,
91            decay: 0.1,
92            sustain: 0.7,
93            release: 0.2,
94            synth_type: None,
95            filters: Vec::new(),
96            options: HashMap::new(),
97            lfo: None,
98            plugin_author: None,
99            plugin_name: None,
100            plugin_export: None,
101        }
102    }
103}
104
105impl AudioEventList {
106    pub fn new() -> Self {
107        Self {
108            events: Vec::new(),
109            synths: HashMap::new(),
110        }
111    }
112
113    pub fn add_synth(&mut self, name: String, definition: SynthDefinition) {
114        self.synths.insert(name, definition);
115    }
116
117    pub fn add_note_event(
118        &mut self,
119        synth_id: &str,
120        midi: u8,
121        start_time: f32,
122        duration: f32,
123        velocity: f32,
124        pan: f32,
125        detune: f32,
126        gain: f32,
127        attack: Option<f32>,
128        release: Option<f32>,
129        delay_time: Option<f32>,
130        delay_feedback: Option<f32>,
131        delay_mix: Option<f32>,
132        reverb_amount: Option<f32>,
133        drive_amount: Option<f32>,
134        drive_color: Option<f32>,
135    ) {
136        // Capture synth definition snapshot at event creation time
137        let synth_def = self.get_synth(synth_id).cloned().unwrap_or_else(|| {
138            println!("⚠️  Warning: Synth '{}' not found when creating note event. Available synths: {:?}", 
139                     synth_id, self.synths.keys().collect::<Vec<_>>());
140            SynthDefinition::default()
141        });
142
143        self.events.push(AudioEvent::Note {
144            midi,
145            start_time,
146            duration,
147            velocity,
148            synth_id: synth_id.to_string(),
149            synth_def,
150            pan,
151            detune,
152            gain,
153            attack,
154            release,
155            delay_time,
156            delay_feedback,
157            delay_mix,
158            reverb_amount,
159            drive_amount,
160            drive_color,
161            use_per_note_automation: false,
162        });
163    }
164
165    pub fn add_chord_event(
166        &mut self,
167        synth_id: &str,
168        midis: Vec<u8>,
169        start_time: f32,
170        duration: f32,
171        velocity: f32,
172        pan: f32,
173        detune: f32,
174        spread: f32,
175        gain: f32,
176        attack: Option<f32>,
177        release: Option<f32>,
178        delay_time: Option<f32>,
179        delay_feedback: Option<f32>,
180        delay_mix: Option<f32>,
181        reverb_amount: Option<f32>,
182        drive_amount: Option<f32>,
183        drive_color: Option<f32>,
184    ) {
185        // Capture synth definition snapshot at event creation time
186        let synth_def = self.get_synth(synth_id).cloned().unwrap_or_default();
187
188        self.events.push(AudioEvent::Chord {
189            midis,
190            start_time,
191            duration,
192            velocity,
193            synth_id: synth_id.to_string(),
194            synth_def,
195            pan,
196            detune,
197            spread,
198            gain,
199            attack,
200            release,
201            delay_time,
202            delay_feedback,
203            delay_mix,
204            reverb_amount,
205            drive_amount,
206            drive_color,
207            use_per_note_automation: false,
208        });
209    }
210
211    pub fn add_sample_event(&mut self, uri: &str, start_time: f32, velocity: f32) {
212        self.events.push(AudioEvent::Sample {
213            uri: uri.to_string(),
214            start_time,
215            velocity,
216        });
217    }
218
219    pub fn get_synth(&self, name: &str) -> Option<&SynthDefinition> {
220        self.synths.get(name)
221    }
222
223    pub fn total_duration(&self) -> f32 {
224        self.events
225            .iter()
226            .map(|event| match event {
227                AudioEvent::Note {
228                    start_time,
229                    duration,
230                    ..
231                } => start_time + duration,
232                AudioEvent::Chord {
233                    start_time,
234                    duration,
235                    ..
236                } => start_time + duration,
237                AudioEvent::Sample {
238                    start_time, uri, ..
239                } => {
240                    // Get actual sample duration from registry
241                    #[cfg(target_arch = "wasm32")]
242                    {
243                        use crate::web::registry::samples::get_sample;
244                        if let Some(pcm) = get_sample(uri) {
245                            // Assume 44.1kHz sample rate
246                            let duration = pcm.len() as f32 / 44100.0;
247                            start_time + duration
248                        } else {
249                            // Fallback: estimate 2 seconds
250                            start_time + 2.0
251                        }
252                    }
253                    #[cfg(not(target_arch = "wasm32"))]
254                    {
255                        // Fallback for native: estimate 2 seconds
256                        let _ = uri; // Silence unused warning on non-WASM targets
257                        start_time + 2.0
258                    }
259                }
260            })
261            .fold(0.0, f32::max)
262    }
263
264    /// Merge another AudioEventList into this one
265    /// This is used for parallel spawn execution
266    pub fn merge(&mut self, other: AudioEventList) {
267        // Merge synth definitions FIRST (prefer existing definitions on conflict)
268        for (name, def) in other.synths {
269            if !self.synths.contains_key(&name) {
270                self.synths.insert(name, def);
271            }
272        }
273
274        // Merge events and update their synth_def snapshots if needed
275        for mut event in other.events {
276            // Update synth_def snapshot for Note and Chord events
277            match &mut event {
278                AudioEvent::Note {
279                    synth_id,
280                    synth_def,
281                    ..
282                } => {
283                    // If this event's synth now exists in merged synths, update the snapshot
284                    if let Some(updated_def) = self.synths.get(synth_id) {
285                        *synth_def = updated_def.clone();
286                    }
287                }
288                AudioEvent::Chord {
289                    synth_id,
290                    synth_def,
291                    ..
292                } => {
293                    // If this event's synth now exists in merged synths, update the snapshot
294                    if let Some(updated_def) = self.synths.get(synth_id) {
295                        *synth_def = updated_def.clone();
296                    }
297                }
298                _ => {}
299            }
300            self.events.push(event);
301        }
302    }
303}
304
305/// Helper to extract values from Value::Map
306pub fn extract_number(map: &HashMap<String, Value>, key: &str, default: f32) -> f32 {
307    map.get(key)
308        .and_then(|v| {
309            if let Value::Number(n) = v {
310                Some(*n)
311            } else {
312                None
313            }
314        })
315        .unwrap_or(default)
316}
317
318pub fn extract_string(map: &HashMap<String, Value>, key: &str, default: &str) -> String {
319    map.get(key)
320        .and_then(|v| {
321            if let Value::String(s) = v {
322                Some(s.clone())
323            } else {
324                None
325            }
326        })
327        .unwrap_or_else(|| default.to_string())
328}
329
330pub fn extract_filters(filters_arr: &[Value]) -> Vec<FilterDef> {
331    filters_arr
332        .iter()
333        .filter_map(|v| {
334            if let Value::Map(filter_map) = v {
335                let filter_type = extract_string(filter_map, "type", "lowpass");
336                let cutoff = filter_map
337                    .get("cutoff")
338                    .and_then(|v| match v {
339                        Value::Number(n) => Some(*n),
340                        _ => None,
341                    })
342                    .unwrap_or(1000.0);
343                let resonance = filter_map
344                    .get("resonance")
345                    .and_then(|v| match v {
346                        Value::Number(n) => Some(*n),
347                        _ => None,
348                    })
349                    .unwrap_or(1.0);
350
351                Some(FilterDef {
352                    filter_type,
353                    cutoff,
354                    resonance,
355                })
356            } else {
357                None
358            }
359        })
360        .collect()
361}