devalang_core/core/audio/engine/
synth.rs

1use devalang_types::Value;
2use std::collections::HashMap;
3
4// Sample rate and channel constants used throughout the engine.
5const SAMPLE_RATE: u32 = 44100;
6const CHANNELS: u16 = 2;
7
8/// AudioEngine holds the generated interleaved stereo buffer and
9/// provides simple utilities to mix/merge buffers and export WAV files.
10///
11/// Notes:
12/// - Buffer is interleaved stereo (L,R,L,R...).
13/// - Methods are synchronous and operate on in-memory buffers.
14#[derive(Debug, Clone, PartialEq)]
15pub struct AudioEngine {
16    /// Master volume multiplier (not automatically applied by helpers).
17    pub volume: f32,
18    /// Interleaved i16 PCM buffer.
19    pub buffer: Vec<i16>,
20    /// Logical module name used for error traces/diagnostics.
21    pub module_name: String,
22    /// Simple diagnostic counter for inserted notes.
23    pub note_count: usize,
24}
25
26impl AudioEngine {
27    pub fn new(module_name: String) -> Self {
28        AudioEngine {
29            volume: 1.0,
30            buffer: vec![],
31            module_name,
32            note_count: 0,
33        }
34    }
35
36    pub fn get_buffer(&self) -> &[i16] {
37        &self.buffer
38    }
39
40    pub fn get_normalized_buffer(&self) -> Vec<f32> {
41        self.buffer.iter().map(|&s| (s as f32) / 32768.0).collect()
42    }
43
44    pub fn mix(&mut self, other: &AudioEngine) {
45        let max_len = self.buffer.len().max(other.buffer.len());
46        self.buffer.resize(max_len, 0);
47
48        for (i, &sample) in other.buffer.iter().enumerate() {
49            self.buffer[i] = self.buffer[i].saturating_add(sample);
50        }
51    }
52
53    pub fn merge_with(&mut self, other: AudioEngine) {
54        if other.buffer.iter().all(|&s| s == 0) {
55            eprintln!("⚠️ Skipping merge: other buffer is silent");
56            return;
57        }
58
59        if self.buffer.iter().all(|&s| s == 0) {
60            self.buffer = other.buffer;
61            return;
62        }
63
64        self.mix(&other);
65    }
66
67    pub fn set_duration(&mut self, duration_secs: f32) {
68        let total_samples = (duration_secs * (SAMPLE_RATE as f32) * (CHANNELS as f32)) as usize;
69
70        if self.buffer.len() < total_samples {
71            self.buffer.resize(total_samples, 0);
72        }
73    }
74
75    pub fn generate_wav_file(&mut self, output_dir: &String) -> Result<(), String> {
76        if self.buffer.len() % (CHANNELS as usize) != 0 {
77            self.buffer.push(0);
78            println!("Completed buffer to respect stereo format.");
79        }
80
81        let spec = hound::WavSpec {
82            channels: CHANNELS,
83            sample_rate: SAMPLE_RATE,
84            bits_per_sample: 16,
85            sample_format: hound::SampleFormat::Int,
86        };
87
88        let mut writer = hound::WavWriter::create(output_dir, spec)
89            .map_err(|e| format!("Error creating WAV file: {}", e))?;
90
91        for sample in &self.buffer {
92            writer
93                .write_sample(*sample)
94                .map_err(|e| format!("Error writing sample: {:?}", e))?;
95        }
96
97        writer
98            .finalize()
99            .map_err(|e| format!("Error finalizing WAV: {:?}", e))?;
100
101        Ok(())
102    }
103
104    // Insert note moved here from original engine.rs
105    pub fn insert_note(
106        &mut self,
107        waveform: String,
108        freq: f32,
109        amp: f32,
110        start_time_ms: f32,
111        duration_ms: f32,
112        synth_params: HashMap<String, Value>,
113        note_params: HashMap<String, Value>,
114        automation: Option<HashMap<String, Value>>,
115    ) {
116        // Keep internal logic; helpers called from helpers module
117        let attack = self.extract_f32(&synth_params, "attack").unwrap_or(0.0);
118        let decay = self.extract_f32(&synth_params, "decay").unwrap_or(0.0);
119        let sustain = self.extract_f32(&synth_params, "sustain").unwrap_or(1.0);
120        let release = self.extract_f32(&synth_params, "release").unwrap_or(0.0);
121        let attack_s = if attack > 10.0 {
122            attack / 1000.0
123        } else {
124            attack
125        };
126        let decay_s = if decay > 10.0 { decay / 1000.0 } else { decay };
127        let release_s = if release > 10.0 {
128            release / 1000.0
129        } else {
130            release
131        };
132        let sustain_level = if sustain > 1.0 {
133            (sustain / 100.0).clamp(0.0, 1.0)
134        } else {
135            sustain.clamp(0.0, 1.0)
136        };
137
138        let duration_ms = self
139            .extract_f32(&note_params, "duration")
140            .unwrap_or(duration_ms);
141        let velocity = self.extract_f32(&note_params, "velocity").unwrap_or(1.0);
142        let glide = self.extract_boolean(&note_params, "glide").unwrap_or(false);
143        let slide = self.extract_boolean(&note_params, "slide").unwrap_or(false);
144
145        let _amplitude = (i16::MAX as f32) * amp.clamp(0.0, 1.0) * velocity.clamp(0.0, 1.0);
146
147        let freq_start = freq;
148        let mut freq_end = freq;
149        let amp_start = amp * velocity.clamp(0.0, 1.0);
150        let mut amp_end = amp_start;
151
152        if glide {
153            if let Some(Value::Number(target_freq)) = note_params.get("target_freq") {
154                freq_end = *target_freq;
155            } else {
156                freq_end = freq * 1.5;
157            }
158        }
159
160        if slide {
161            if let Some(Value::Number(target_amp)) = note_params.get("target_amp") {
162                amp_end = *target_amp * velocity.clamp(0.0, 1.0);
163            } else {
164                amp_end = amp_start * 0.5;
165            }
166        }
167
168        let sample_rate = SAMPLE_RATE as f32;
169        let channels = CHANNELS as usize;
170
171        let total_samples = ((duration_ms / 1000.0) * sample_rate) as usize;
172        let start_sample = ((start_time_ms / 1000.0) * sample_rate) as usize;
173
174        let (volume_env, pan_env, pitch_env) =
175            crate::core::audio::engine::helpers::env_maps_from_automation(&automation);
176
177        let mut stereo_samples: Vec<i16> = Vec::with_capacity(total_samples * 2);
178        let fade_len = (sample_rate * 0.01) as usize; // 10 ms fade
179
180        let attack_samples = (attack_s * sample_rate) as usize;
181        let decay_samples = (decay_s * sample_rate) as usize;
182        let release_samples = (release_s * sample_rate) as usize;
183        let sustain_samples = if total_samples > attack_samples + decay_samples + release_samples {
184            total_samples - attack_samples - decay_samples - release_samples
185        } else {
186            0
187        };
188
189        for i in 0..total_samples {
190            let t = ((start_sample + i) as f32) / sample_rate;
191
192            // Glide
193            let current_freq = if glide {
194                freq_start + ((freq_end - freq_start) * (i as f32)) / (total_samples as f32)
195            } else {
196                freq
197            };
198
199            // Pitch automation (in semitones), applied as frequency multiplier
200            let pitch_semi = crate::core::audio::engine::helpers::eval_env_map(
201                &pitch_env,
202                (i as f32) / (total_samples as f32),
203                0.0,
204            );
205            let current_freq = current_freq * (2.0_f32).powf(pitch_semi / 12.0);
206
207            // Slide
208            let current_amp = if slide {
209                amp_start + ((amp_end - amp_start) * (i as f32)) / (total_samples as f32)
210            } else {
211                amp_start
212            };
213
214            let mut value =
215                crate::core::audio::engine::helpers::oscillator_sample(&waveform, current_freq, t);
216
217            // ADSR envelope
218            let envelope = crate::core::audio::engine::helpers::adsr_envelope_value(
219                i,
220                attack_samples,
221                decay_samples,
222                sustain_samples,
223                release_samples,
224                sustain_level,
225            );
226
227            // Fade in/out
228            if i < fade_len {
229                value *= (i as f32) / (fade_len as f32);
230            } else if i >= total_samples - fade_len {
231                value *= ((total_samples - i) as f32) / (fade_len as f32);
232            }
233
234            value *= envelope;
235            let mut sample_val = value * (i16::MAX as f32) * current_amp;
236
237            let vol_mul = crate::core::audio::engine::helpers::eval_env_map(
238                &volume_env,
239                (i as f32) / (total_samples as f32),
240                1.0,
241            )
242            .clamp(0.0, 10.0);
243            sample_val *= vol_mul;
244
245            let pan_val = crate::core::audio::engine::helpers::eval_env_map(
246                &pan_env,
247                (i as f32) / (total_samples as f32),
248                0.0,
249            )
250            .clamp(-1.0, 1.0);
251            let (left_gain, right_gain) = crate::core::audio::engine::helpers::pan_gains(pan_val);
252
253            let left = (sample_val * left_gain)
254                .round()
255                .clamp(i16::MIN as f32, i16::MAX as f32) as i16;
256            let right = (sample_val * right_gain)
257                .round()
258                .clamp(i16::MIN as f32, i16::MAX as f32) as i16;
259
260            stereo_samples.push(left);
261            stereo_samples.push(right);
262        }
263
264        // Increment note counter for diagnostics
265        self.note_count = self.note_count.saturating_add(1);
266
267        crate::core::audio::engine::helpers::mix_stereo_samples_into_buffer(
268            self,
269            start_sample,
270            channels,
271            &stereo_samples,
272        );
273    }
274
275    // helper extraction functions left in this struct for now
276    fn extract_f32(&self, map: &HashMap<String, Value>, key: &str) -> Option<f32> {
277        match map.get(key) {
278            Some(Value::Number(n)) => Some(*n),
279            Some(Value::String(s)) => s.parse::<f32>().ok(),
280            Some(Value::Boolean(b)) => Some(if *b { 1.0 } else { 0.0 }),
281            _ => None,
282        }
283    }
284
285    fn extract_boolean(&self, map: &HashMap<String, Value>, key: &str) -> Option<bool> {
286        match map.get(key) {
287            Some(Value::Boolean(b)) => Some(*b),
288            Some(Value::Number(n)) => Some(*n != 0.0),
289            Some(Value::Identifier(s)) => {
290                if s == "true" {
291                    Some(true)
292                } else if s == "false" {
293                    Some(false)
294                } else {
295                    None
296                }
297            }
298            Some(Value::String(s)) => {
299                if s == "true" {
300                    Some(true)
301                } else if s == "false" {
302                    Some(false)
303                } else {
304                    None
305                }
306            }
307            _ => None,
308        }
309    }
310}