devalang_wasm/engine/audio/
generator.rs

1use super::synth::types::{SynthType, get_synth_type};
2use super::synth::{adsr_envelope, midi_to_frequency, oscillator_sample, time_to_samples};
3/// Note generator - creates audio samples for synthesized notes
4use anyhow::Result;
5use std::collections::HashMap;
6
7#[cfg(feature = "cli")]
8use crate::engine::plugin::{loader::load_plugin, runner::WasmPluginRunner};
9
10/// Filter definition
11#[derive(Debug, Clone)]
12pub struct FilterDef {
13    pub filter_type: String, // lowpass, highpass, bandpass
14    pub cutoff: f32,         // Hz
15    pub resonance: f32,      // 0.0 - 1.0
16}
17
18/// Parameters for synthesizing a note
19#[derive(Debug, Clone)]
20pub struct SynthParams {
21    pub waveform: String,
22    pub attack: f32,                   // seconds
23    pub decay: f32,                    // seconds
24    pub sustain: f32,                  // level (0.0 - 1.0)
25    pub release: f32,                  // seconds
26    pub synth_type: Option<String>,    // pluck, arp, pad, bass, lead, keys
27    pub filters: Vec<FilterDef>,       // Chain of filters
28    pub options: HashMap<String, f32>, // Configurable options for synth types
29    // Plugin support
30    pub plugin_author: Option<String>,
31    pub plugin_name: Option<String>,
32    pub plugin_export: Option<String>,
33}
34
35impl Default for SynthParams {
36    fn default() -> Self {
37        Self {
38            waveform: "sine".to_string(),
39            attack: 0.01,
40            decay: 0.1,
41            sustain: 0.7,
42            release: 0.2,
43            synth_type: None,
44            filters: Vec::new(),
45            options: HashMap::new(),
46            plugin_author: None,
47            plugin_name: None,
48            plugin_export: None,
49        }
50    }
51}
52
53/// Generate stereo audio samples for a single note
54pub fn generate_note(
55    midi_note: u8,
56    duration_ms: f32,
57    velocity: f32,
58    params: &SynthParams,
59    sample_rate: u32,
60) -> Result<Vec<f32>> {
61    generate_note_with_options(
62        midi_note,
63        duration_ms,
64        velocity,
65        params,
66        sample_rate,
67        0.0,
68        0.0,
69    )
70}
71
72/// Generate stereo audio samples for a single note with pan and detune options
73/// 
74/// If params contains plugin information, it will use the WASM plugin instead of built-in synth
75pub fn generate_note_with_options(
76    midi_note: u8,
77    duration_ms: f32,
78    velocity: f32,
79    params: &SynthParams,
80    sample_rate: u32,
81    pan: f32,    // -1.0 (left) to 1.0 (right), 0.0 = center
82    detune: f32, // cents, -100 to 100
83) -> Result<Vec<f32>> {
84    // Generating note (diagnostics suppressed)
85    
86    // Check if we should use a WASM plugin
87    #[cfg(feature = "cli")]
88    {
89        if let Some(ref author) = params.plugin_author {
90            if let Some(ref name) = params.plugin_name {
91                // Using plugin path
92                return generate_note_with_plugin(
93                    midi_note,
94                    duration_ms,
95                    velocity,
96                    params,
97                    sample_rate,
98                    pan,
99                    detune,
100                    author,
101                    name,
102                    params.plugin_export.as_deref(),
103                );
104            }
105        }
106    }
107    
108    // Using classic synth path
109
110    let base_frequency = midi_to_frequency(midi_note);
111
112    // Apply detune (cents to frequency ratio: 2^(cents/1200))
113    let frequency = if detune.abs() > 0.01 {
114        base_frequency * 2.0_f32.powf(detune / 1200.0)
115    } else {
116        base_frequency
117    };
118
119    let velocity = velocity.clamp(0.0, 1.0);
120
121    // Clone params to allow modification by synth type
122    let mut modified_params = params.clone();
123
124    // Apply synth type modifications
125    let synth_type: Option<Box<dyn SynthType>> = if let Some(ref type_name) = params.synth_type {
126        get_synth_type(type_name)
127    } else {
128        None
129    };
130
131    if let Some(ref stype) = synth_type {
132        stype.modify_params(&mut modified_params);
133    }
134
135    // Convert ADSR times to samples
136    let attack_samples = time_to_samples(modified_params.attack, sample_rate);
137    let decay_samples = time_to_samples(modified_params.decay, sample_rate);
138    let release_samples = time_to_samples(modified_params.release, sample_rate);
139
140    // Calculate sustain duration
141    let duration_seconds = duration_ms / 1000.0;
142    let total_samples = time_to_samples(duration_seconds, sample_rate);
143    let envelope_samples = attack_samples + decay_samples + release_samples;
144    let sustain_samples = total_samples.saturating_sub(envelope_samples);
145
146    let mut samples = Vec::with_capacity(total_samples * 2); // stereo
147
148    // Calculate pan gains (constant power panning)
149    let pan = pan.clamp(-1.0, 1.0);
150    let pan_angle = (pan + 1.0) * 0.25 * std::f32::consts::PI; // 0 to PI/2
151    let left_gain = pan_angle.cos();
152    let right_gain = pan_angle.sin();
153
154    for i in 0..total_samples {
155        let time = i as f32 / sample_rate as f32;
156
157        // Generate oscillator sample
158        let osc_sample = oscillator_sample(&modified_params.waveform, frequency, time);
159
160        // Apply ADSR envelope
161        let envelope = adsr_envelope(
162            i,
163            attack_samples,
164            decay_samples,
165            sustain_samples,
166            release_samples,
167            modified_params.sustain,
168        );
169
170        // Apply velocity and envelope
171        let amplitude = osc_sample * envelope * velocity * 0.3; // 0.3 for headroom
172
173        // Stereo output with panning
174        samples.push(amplitude * left_gain);
175        samples.push(amplitude * right_gain);
176    }
177
178    // Apply filters if any
179    for filter in &modified_params.filters {
180        apply_filter(&mut samples, filter, sample_rate)?;
181    }
182
183    // Apply synth type post-processing
184    if let Some(stype) = synth_type {
185        stype.post_process(&mut samples, sample_rate, &modified_params.options)?;
186    }
187
188    Ok(samples)
189}
190
191/// Apply a filter to audio samples
192fn apply_filter(samples: &mut [f32], filter: &FilterDef, sample_rate: u32) -> Result<()> {
193    match filter.filter_type.to_lowercase().as_str() {
194        "lowpass" => apply_lowpass(samples, filter.cutoff, sample_rate),
195        "highpass" => apply_highpass(samples, filter.cutoff, sample_rate),
196        "bandpass" => apply_bandpass(samples, filter.cutoff, sample_rate),
197        _ => Ok(()),
198    }
199}
200
201/// Simple one-pole lowpass filter
202fn apply_lowpass(samples: &mut [f32], cutoff: f32, sample_rate: u32) -> Result<()> {
203    let dt = 1.0 / sample_rate as f32;
204    let rc = 1.0 / (2.0 * std::f32::consts::PI * cutoff);
205    let alpha = dt / (rc + dt);
206
207    let mut prev = 0.0f32;
208    for i in (0..samples.len()).step_by(2) {
209        // Process left channel
210        let filtered = prev + alpha * (samples[i] - prev);
211        prev = filtered;
212        samples[i] = filtered;
213
214        // Copy to right channel
215        if i + 1 < samples.len() {
216            samples[i + 1] = filtered;
217        }
218    }
219
220    Ok(())
221}
222
223/// Simple one-pole highpass filter
224fn apply_highpass(samples: &mut [f32], cutoff: f32, sample_rate: u32) -> Result<()> {
225    let dt = 1.0 / sample_rate as f32;
226    let rc = 1.0 / (2.0 * std::f32::consts::PI * cutoff);
227    let alpha = rc / (rc + dt);
228
229    let mut prev_input = 0.0f32;
230    let mut prev_output = 0.0f32;
231
232    for i in (0..samples.len()).step_by(2) {
233        let current = samples[i];
234        let filtered = alpha * (prev_output + current - prev_input);
235
236        prev_input = current;
237        prev_output = filtered;
238
239        samples[i] = filtered;
240        if i + 1 < samples.len() {
241            samples[i + 1] = filtered;
242        }
243    }
244
245    Ok(())
246}
247
248/// Simple bandpass filter (combination of lowpass and highpass)
249fn apply_bandpass(samples: &mut [f32], center: f32, sample_rate: u32) -> Result<()> {
250    // Bandpass = highpass below center, then lowpass above center
251    let bandwidth = center * 0.5; // 50% bandwidth
252
253    apply_highpass(samples, center - bandwidth, sample_rate)?;
254    apply_lowpass(samples, center + bandwidth, sample_rate)?;
255
256    Ok(())
257}
258
259/// Generate stereo audio samples for a chord (multiple notes)
260pub fn generate_chord(
261    midi_notes: &[u8],
262    duration_ms: f32,
263    velocity: f32,
264    params: &SynthParams,
265    sample_rate: u32,
266) -> Result<Vec<f32>> {
267    generate_chord_with_options(
268        midi_notes,
269        duration_ms,
270        velocity,
271        params,
272        sample_rate,
273        0.0,
274        0.0,
275        0.0,
276    )
277}
278
279/// Generate stereo audio samples for a chord with pan, detune and spread options
280pub fn generate_chord_with_options(
281    midi_notes: &[u8],
282    duration_ms: f32,
283    velocity: f32,
284    params: &SynthParams,
285    sample_rate: u32,
286    pan: f32,    // -1.0 (left) to 1.0 (right), 0.0 = center
287    detune: f32, // cents, -100 to 100
288    spread: f32, // stereo spread 0.0-1.0 for chord notes
289) -> Result<Vec<f32>> {
290    if midi_notes.is_empty() {
291        return Ok(Vec::new());
292    }
293
294    let num_notes = midi_notes.len();
295    let spread = spread.clamp(0.0, 1.0);
296
297    // Calculate pan position for each note if spread is enabled
298    let mut result: Option<Vec<f32>> = None;
299
300    for (i, &midi_note) in midi_notes.iter().enumerate() {
301        // Calculate individual pan for each note based on spread
302        let note_pan = if num_notes > 1 && spread > 0.0 {
303            // Distribute notes across stereo field
304            let position = i as f32 / (num_notes - 1) as f32; // 0.0 to 1.0
305            let spread_amount = (position - 0.5) * 2.0 * spread; // -spread to +spread
306            (pan + spread_amount).clamp(-1.0, 1.0)
307        } else {
308            pan
309        };
310
311        // Generate note with individual pan
312        let note_samples = generate_note_with_options(
313            midi_note,
314            duration_ms,
315            velocity,
316            params,
317            sample_rate,
318            note_pan,
319            detune,
320        )?;
321
322        // Mix notes together
323        match result {
324            None => {
325                result = Some(note_samples);
326            }
327            Some(ref mut buffer) => {
328                // Mix by averaging (to avoid clipping)
329                for (j, sample) in note_samples.iter().enumerate() {
330                    if j < buffer.len() {
331                        buffer[j] = (buffer[j] + sample) / 2.0;
332                    }
333                }
334            }
335        }
336    }
337
338    Ok(result.unwrap_or_default())
339}
340
341#[cfg(test)]
342mod tests {
343    use super::*;
344
345    #[test]
346    fn test_generate_note() {
347        let params = SynthParams::default();
348        let samples = generate_note(60, 500.0, 0.8, &params, 44100).unwrap();
349
350        // Should have samples (stereo)
351        assert!(samples.len() > 0);
352        assert_eq!(samples.len() % 2, 0); // Must be even (stereo)
353
354        // Check that some samples are non-zero
355        let has_audio = samples.iter().any(|&s| s.abs() > 0.001);
356        assert!(has_audio);
357    }
358
359    #[test]
360    fn test_generate_chord() {
361        let params = SynthParams::default();
362        let samples = generate_chord(&[60, 64, 67], 500.0, 0.8, &params, 44100).unwrap();
363
364        // Should have samples
365        assert!(samples.len() > 0);
366
367        // Check that some samples are non-zero
368        let has_audio = samples.iter().any(|&s| s.abs() > 0.001);
369        assert!(has_audio);
370    }
371}
372
373/// Generate audio using a WASM plugin
374#[cfg(feature = "cli")]
375fn generate_note_with_plugin(
376    midi_note: u8,
377    duration_ms: f32,
378    velocity: f32,
379    params: &SynthParams,
380    sample_rate: u32,
381    pan: f32,
382    detune: f32,
383    plugin_author: &str,
384    plugin_name: &str,
385    plugin_export: Option<&str>,
386) -> Result<Vec<f32>> {
387    use once_cell::sync::Lazy;
388    use std::sync::Mutex;
389
390    println!("🎸 [PLUGIN_GEN] Generating note with plugin: {}.{} (export: {:?})", 
391        plugin_author, plugin_name, plugin_export);
392
393    // Global plugin runner (cached)
394    static PLUGIN_RUNNER: Lazy<Mutex<WasmPluginRunner>> = 
395        Lazy::new(|| Mutex::new(WasmPluginRunner::new()));
396    
397    // Global plugin cache
398    static PLUGIN_CACHE: Lazy<Mutex<HashMap<String, Vec<u8>>>> = 
399        Lazy::new(|| Mutex::new(HashMap::new()));
400
401    // Get or load plugin
402    let plugin_key = format!("{}.{}", plugin_author, plugin_name);
403    let mut cache = PLUGIN_CACHE.lock().unwrap();
404    
405    let wasm_bytes = if let Some(bytes) = cache.get(&plugin_key) {
406        println!("   Using cached plugin ({}  bytes)", bytes.len());
407        bytes.clone()
408    } else {
409        println!("   Loading plugin from disk...");
410        // Load plugin
411        let (info, bytes) = load_plugin(plugin_author, plugin_name)
412            .map_err(|e| anyhow::anyhow!("Failed to load plugin: {}", e))?;
413        
414        eprintln!("✅ Loaded plugin: {}.{} (v{})", 
415            info.author, 
416            info.name, 
417            info.version.as_deref().unwrap_or("unknown")
418        );
419        
420        cache.insert(plugin_key.clone(), bytes.clone());
421        bytes
422    };
423    drop(cache);
424
425    // Calculate buffer size
426    let base_frequency = midi_to_frequency(midi_note);
427    let frequency = if detune.abs() > 0.01 {
428        base_frequency * 2.0_f32.powf(detune / 1200.0)
429    } else {
430        base_frequency
431    };
432    
433    let duration_seconds = duration_ms / 1000.0;
434    let total_samples = (duration_seconds * sample_rate as f32) as usize;
435    let mut buffer = vec![0.0f32; total_samples * 2]; // stereo
436
437    // Prepare options for plugin (merge waveform + custom options)
438    let mut plugin_options = params.options.clone();
439    
440    // Add waveform if not already in options
441    if !plugin_options.contains_key("waveform") {
442        // Try to convert waveform string to numeric value if possible
443        let waveform_value = match params.waveform.as_str() {
444            "sine" => 0.0,
445            "saw" => 1.0,
446            "square" => 2.0,
447            "triangle" => 3.0,
448            _ => 0.0, // default to sine
449        };
450        plugin_options.insert("waveform".to_string(), waveform_value);
451    }
452
453    // Call plugin
454    let runner = PLUGIN_RUNNER.lock().unwrap();
455    let synth_id = format!("{}_{}", plugin_key, plugin_export.unwrap_or("default"));
456    
457    println!("   📋 Buffer before plugin: first 10 samples: {:?}", &buffer[0..10.min(buffer.len())]);
458    
459    runner.render_note_in_place(
460        &wasm_bytes,
461        &mut buffer,
462        Some(&synth_id),
463        plugin_export,
464        frequency,
465        velocity,
466        duration_ms as i32,
467        sample_rate as i32,
468        2, // stereo
469        Some(&plugin_options),
470    ).map_err(|e| anyhow::anyhow!("Plugin render error: {}", e))?;
471
472    println!("   📋 Buffer after plugin: first 10 samples: {:?}", &buffer[0..10.min(buffer.len())]);
473    println!("   📋 Buffer stats: len={}, max={:.4}, rms={:.4}", 
474        buffer.len(), 
475        buffer.iter().map(|s| s.abs()).fold(0.0f32, f32::max),
476        (buffer.iter().map(|s| s * s).sum::<f32>() / buffer.len() as f32).sqrt()
477    );
478
479    // Apply panning if needed
480    if pan.abs() > 0.01 {
481        let pan = pan.clamp(-1.0, 1.0);
482        let pan_angle = (pan + 1.0) * 0.25 * std::f32::consts::PI;
483        let left_gain = pan_angle.cos();
484        let right_gain = pan_angle.sin();
485        
486        for i in (0..buffer.len()).step_by(2) {
487            if i + 1 < buffer.len() {
488                buffer[i] *= left_gain;
489                buffer[i + 1] *= right_gain;
490            }
491        }
492    }
493
494    // Apply filters if any
495    for filter in &params.filters {
496        apply_filter(&mut buffer, filter, sample_rate)?;
497    }
498
499    Ok(buffer)
500}