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/// Filter definition
8#[derive(Debug, Clone)]
9pub struct FilterDef {
10    pub filter_type: String, // lowpass, highpass, bandpass
11    pub cutoff: f32,         // Hz
12    pub resonance: f32,      // 0.0 - 1.0
13}
14
15/// Parameters for synthesizing a note
16#[derive(Debug, Clone)]
17pub struct SynthParams {
18    pub waveform: String,
19    pub attack: f32,                   // seconds
20    pub decay: f32,                    // seconds
21    pub sustain: f32,                  // level (0.0 - 1.0)
22    pub release: f32,                  // seconds
23    pub synth_type: Option<String>,    // pluck, arp, pad, bass, lead, keys
24    pub filters: Vec<FilterDef>,       // Chain of filters
25    pub options: HashMap<String, f32>, // Configurable options for synth types
26}
27
28impl Default for SynthParams {
29    fn default() -> Self {
30        Self {
31            waveform: "sine".to_string(),
32            attack: 0.01,
33            decay: 0.1,
34            sustain: 0.7,
35            release: 0.2,
36            synth_type: None,
37            filters: Vec::new(),
38            options: HashMap::new(),
39        }
40    }
41}
42
43/// Generate stereo audio samples for a single note
44pub fn generate_note(
45    midi_note: u8,
46    duration_ms: f32,
47    velocity: f32,
48    params: &SynthParams,
49    sample_rate: u32,
50) -> Result<Vec<f32>> {
51    generate_note_with_options(
52        midi_note,
53        duration_ms,
54        velocity,
55        params,
56        sample_rate,
57        0.0,
58        0.0,
59    )
60}
61
62/// Generate stereo audio samples for a single note with pan and detune options
63pub fn generate_note_with_options(
64    midi_note: u8,
65    duration_ms: f32,
66    velocity: f32,
67    params: &SynthParams,
68    sample_rate: u32,
69    pan: f32,    // -1.0 (left) to 1.0 (right), 0.0 = center
70    detune: f32, // cents, -100 to 100
71) -> Result<Vec<f32>> {
72    let base_frequency = midi_to_frequency(midi_note);
73
74    // Apply detune (cents to frequency ratio: 2^(cents/1200))
75    let frequency = if detune.abs() > 0.01 {
76        base_frequency * 2.0_f32.powf(detune / 1200.0)
77    } else {
78        base_frequency
79    };
80
81    let velocity = velocity.clamp(0.0, 1.0);
82
83    // Clone params to allow modification by synth type
84    let mut modified_params = params.clone();
85
86    // Apply synth type modifications
87    let synth_type: Option<Box<dyn SynthType>> = if let Some(ref type_name) = params.synth_type {
88        get_synth_type(type_name)
89    } else {
90        None
91    };
92
93    if let Some(ref stype) = synth_type {
94        stype.modify_params(&mut modified_params);
95    }
96
97    // Convert ADSR times to samples
98    let attack_samples = time_to_samples(modified_params.attack, sample_rate);
99    let decay_samples = time_to_samples(modified_params.decay, sample_rate);
100    let release_samples = time_to_samples(modified_params.release, sample_rate);
101
102    // Calculate sustain duration
103    let duration_seconds = duration_ms / 1000.0;
104    let total_samples = time_to_samples(duration_seconds, sample_rate);
105    let envelope_samples = attack_samples + decay_samples + release_samples;
106    let sustain_samples = total_samples.saturating_sub(envelope_samples);
107
108    let mut samples = Vec::with_capacity(total_samples * 2); // stereo
109
110    // Calculate pan gains (constant power panning)
111    let pan = pan.clamp(-1.0, 1.0);
112    let pan_angle = (pan + 1.0) * 0.25 * std::f32::consts::PI; // 0 to PI/2
113    let left_gain = pan_angle.cos();
114    let right_gain = pan_angle.sin();
115
116    for i in 0..total_samples {
117        let time = i as f32 / sample_rate as f32;
118
119        // Generate oscillator sample
120        let osc_sample = oscillator_sample(&modified_params.waveform, frequency, time);
121
122        // Apply ADSR envelope
123        let envelope = adsr_envelope(
124            i,
125            attack_samples,
126            decay_samples,
127            sustain_samples,
128            release_samples,
129            modified_params.sustain,
130        );
131
132        // Apply velocity and envelope
133        let amplitude = osc_sample * envelope * velocity * 0.3; // 0.3 for headroom
134
135        // Stereo output with panning
136        samples.push(amplitude * left_gain);
137        samples.push(amplitude * right_gain);
138    }
139
140    // Apply filters if any
141    for filter in &modified_params.filters {
142        apply_filter(&mut samples, filter, sample_rate)?;
143    }
144
145    // Apply synth type post-processing
146    if let Some(stype) = synth_type {
147        stype.post_process(&mut samples, sample_rate, &modified_params.options)?;
148    }
149
150    Ok(samples)
151}
152
153/// Apply a filter to audio samples
154fn apply_filter(samples: &mut [f32], filter: &FilterDef, sample_rate: u32) -> Result<()> {
155    match filter.filter_type.to_lowercase().as_str() {
156        "lowpass" => apply_lowpass(samples, filter.cutoff, sample_rate),
157        "highpass" => apply_highpass(samples, filter.cutoff, sample_rate),
158        "bandpass" => apply_bandpass(samples, filter.cutoff, sample_rate),
159        _ => Ok(()),
160    }
161}
162
163/// Simple one-pole lowpass filter
164fn apply_lowpass(samples: &mut [f32], cutoff: f32, sample_rate: u32) -> Result<()> {
165    let dt = 1.0 / sample_rate as f32;
166    let rc = 1.0 / (2.0 * std::f32::consts::PI * cutoff);
167    let alpha = dt / (rc + dt);
168
169    let mut prev = 0.0f32;
170    for i in (0..samples.len()).step_by(2) {
171        // Process left channel
172        let filtered = prev + alpha * (samples[i] - prev);
173        prev = filtered;
174        samples[i] = filtered;
175
176        // Copy to right channel
177        if i + 1 < samples.len() {
178            samples[i + 1] = filtered;
179        }
180    }
181
182    Ok(())
183}
184
185/// Simple one-pole highpass filter
186fn apply_highpass(samples: &mut [f32], cutoff: f32, sample_rate: u32) -> Result<()> {
187    let dt = 1.0 / sample_rate as f32;
188    let rc = 1.0 / (2.0 * std::f32::consts::PI * cutoff);
189    let alpha = rc / (rc + dt);
190
191    let mut prev_input = 0.0f32;
192    let mut prev_output = 0.0f32;
193
194    for i in (0..samples.len()).step_by(2) {
195        let current = samples[i];
196        let filtered = alpha * (prev_output + current - prev_input);
197
198        prev_input = current;
199        prev_output = filtered;
200
201        samples[i] = filtered;
202        if i + 1 < samples.len() {
203            samples[i + 1] = filtered;
204        }
205    }
206
207    Ok(())
208}
209
210/// Simple bandpass filter (combination of lowpass and highpass)
211fn apply_bandpass(samples: &mut [f32], center: f32, sample_rate: u32) -> Result<()> {
212    // Bandpass = highpass below center, then lowpass above center
213    let bandwidth = center * 0.5; // 50% bandwidth
214
215    apply_highpass(samples, center - bandwidth, sample_rate)?;
216    apply_lowpass(samples, center + bandwidth, sample_rate)?;
217
218    Ok(())
219}
220
221/// Generate stereo audio samples for a chord (multiple notes)
222pub fn generate_chord(
223    midi_notes: &[u8],
224    duration_ms: f32,
225    velocity: f32,
226    params: &SynthParams,
227    sample_rate: u32,
228) -> Result<Vec<f32>> {
229    generate_chord_with_options(
230        midi_notes,
231        duration_ms,
232        velocity,
233        params,
234        sample_rate,
235        0.0,
236        0.0,
237        0.0,
238    )
239}
240
241/// Generate stereo audio samples for a chord with pan, detune and spread options
242pub fn generate_chord_with_options(
243    midi_notes: &[u8],
244    duration_ms: f32,
245    velocity: f32,
246    params: &SynthParams,
247    sample_rate: u32,
248    pan: f32,    // -1.0 (left) to 1.0 (right), 0.0 = center
249    detune: f32, // cents, -100 to 100
250    spread: f32, // stereo spread 0.0-1.0 for chord notes
251) -> Result<Vec<f32>> {
252    if midi_notes.is_empty() {
253        return Ok(Vec::new());
254    }
255
256    let num_notes = midi_notes.len();
257    let spread = spread.clamp(0.0, 1.0);
258
259    // Calculate pan position for each note if spread is enabled
260    let mut result: Option<Vec<f32>> = None;
261
262    for (i, &midi_note) in midi_notes.iter().enumerate() {
263        // Calculate individual pan for each note based on spread
264        let note_pan = if num_notes > 1 && spread > 0.0 {
265            // Distribute notes across stereo field
266            let position = i as f32 / (num_notes - 1) as f32; // 0.0 to 1.0
267            let spread_amount = (position - 0.5) * 2.0 * spread; // -spread to +spread
268            (pan + spread_amount).clamp(-1.0, 1.0)
269        } else {
270            pan
271        };
272
273        // Generate note with individual pan
274        let note_samples = generate_note_with_options(
275            midi_note,
276            duration_ms,
277            velocity,
278            params,
279            sample_rate,
280            note_pan,
281            detune,
282        )?;
283
284        // Mix notes together
285        match result {
286            None => {
287                result = Some(note_samples);
288            }
289            Some(ref mut buffer) => {
290                // Mix by averaging (to avoid clipping)
291                for (j, sample) in note_samples.iter().enumerate() {
292                    if j < buffer.len() {
293                        buffer[j] = (buffer[j] + sample) / 2.0;
294                    }
295                }
296            }
297        }
298    }
299
300    Ok(result.unwrap_or_default())
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306
307    #[test]
308    fn test_generate_note() {
309        let params = SynthParams::default();
310        let samples = generate_note(60, 500.0, 0.8, &params, 44100).unwrap();
311
312        // Should have samples (stereo)
313        assert!(samples.len() > 0);
314        assert_eq!(samples.len() % 2, 0); // Must be even (stereo)
315
316        // Check that some samples are non-zero
317        let has_audio = samples.iter().any(|&s| s.abs() > 0.001);
318        assert!(has_audio);
319    }
320
321    #[test]
322    fn test_generate_chord() {
323        let params = SynthParams::default();
324        let samples = generate_chord(&[60, 64, 67], 500.0, 0.8, &params, 44100).unwrap();
325
326        // Should have samples
327        assert!(samples.len() > 0);
328
329        // Check that some samples are non-zero
330        let has_audio = samples.iter().any(|&s| s.abs() > 0.001);
331        assert!(has_audio);
332    }
333}