devalang_wasm/engine/audio/
generator.rs

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