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