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 the other buffer is empty, simply return without warning (common for spawns that produced nothing)
55        if other.buffer.is_empty() {
56            return;
57        }
58
59        // If the other buffer is present but contains only zeros, warn and skip merge
60        if other.buffer.iter().all(|&s| s == 0) {
61            eprintln!("⚠️ Skipping merge: other buffer is silent");
62            return;
63        }
64
65        if self.buffer.iter().all(|&s| s == 0) {
66            self.buffer = other.buffer;
67            return;
68        }
69
70        self.mix(&other);
71    }
72
73    pub fn set_duration(&mut self, duration_secs: f32) {
74        let total_samples = (duration_secs * (SAMPLE_RATE as f32) * (CHANNELS as f32)) as usize;
75
76        if self.buffer.len() < total_samples {
77            self.buffer.resize(total_samples, 0);
78        }
79    }
80
81    pub fn generate_wav_file(&mut self, output_dir: &String) -> Result<(), String> {
82        if self.buffer.len() % (CHANNELS as usize) != 0 {
83            self.buffer.push(0);
84            println!("Completed buffer to respect stereo format.");
85        }
86
87        let spec = hound::WavSpec {
88            channels: CHANNELS,
89            sample_rate: SAMPLE_RATE,
90            bits_per_sample: 16,
91            sample_format: hound::SampleFormat::Int,
92        };
93
94        let mut writer = hound::WavWriter::create(output_dir, spec)
95            .map_err(|e| format!("Error creating WAV file: {}", e))?;
96
97        for sample in &self.buffer {
98            writer
99                .write_sample(*sample)
100                .map_err(|e| format!("Error writing sample: {:?}", e))?;
101        }
102
103        writer
104            .finalize()
105            .map_err(|e| format!("Error finalizing WAV: {:?}", e))?;
106
107        Ok(())
108    }
109
110    // Insert note moved here from original engine.rs
111    pub fn insert_note(
112        &mut self,
113        waveform: String,
114        freq: f32,
115        amp: f32,
116        start_time_ms: f32,
117        duration_ms: f32,
118        synth_params: HashMap<String, Value>,
119        note_params: HashMap<String, Value>,
120        automation: Option<HashMap<String, Value>>,
121    ) {
122        // Keep internal logic; helpers called from helpers module
123        let attack = self.extract_f32(&synth_params, "attack").unwrap_or(0.0);
124        let decay = self.extract_f32(&synth_params, "decay").unwrap_or(0.0);
125        let sustain = self.extract_f32(&synth_params, "sustain").unwrap_or(1.0);
126        let release = self.extract_f32(&synth_params, "release").unwrap_or(0.0);
127        let attack_s = if attack > 10.0 {
128            attack / 1000.0
129        } else {
130            attack
131        };
132        let decay_s = if decay > 10.0 { decay / 1000.0 } else { decay };
133        let release_s = if release > 10.0 {
134            release / 1000.0
135        } else {
136            release
137        };
138        let sustain_level = if sustain > 1.0 {
139            (sustain / 100.0).clamp(0.0, 1.0)
140        } else {
141            sustain.clamp(0.0, 1.0)
142        };
143
144        let duration_ms = self
145            .extract_f32(&note_params, "duration")
146            .unwrap_or(duration_ms);
147        let velocity = self.extract_f32(&note_params, "velocity").unwrap_or(1.0);
148        let glide = self.extract_boolean(&note_params, "glide").unwrap_or(false);
149        let slide = self.extract_boolean(&note_params, "slide").unwrap_or(false);
150
151        let _amplitude = (i16::MAX as f32) * amp.clamp(0.0, 1.0) * velocity.clamp(0.0, 1.0);
152
153        let freq_start = freq;
154        let mut freq_end = freq;
155        let amp_start = amp * velocity.clamp(0.0, 1.0);
156        let mut amp_end = amp_start;
157
158        if glide {
159            if let Some(Value::Number(target_freq)) = note_params.get("target_freq") {
160                freq_end = *target_freq;
161            } else {
162                freq_end = freq * 1.5;
163            }
164        }
165
166        if slide {
167            if let Some(Value::Number(target_amp)) = note_params.get("target_amp") {
168                amp_end = *target_amp * velocity.clamp(0.0, 1.0);
169            } else {
170                amp_end = amp_start * 0.5;
171            }
172        }
173
174        let sample_rate = SAMPLE_RATE as f32;
175        let channels = CHANNELS as usize;
176
177        let total_samples = ((duration_ms / 1000.0) * sample_rate) as usize;
178        let start_sample = ((start_time_ms / 1000.0) * sample_rate) as usize;
179
180        let (volume_env, pan_env, pitch_env) =
181            crate::core::audio::engine::helpers::env_maps_from_automation(&automation);
182
183        let mut stereo_samples: Vec<i16> = Vec::with_capacity(total_samples * 2);
184        let fade_len = (sample_rate * 0.01) as usize; // 10 ms fade
185
186        let attack_samples = (attack_s * sample_rate) as usize;
187        let decay_samples = (decay_s * sample_rate) as usize;
188        let release_samples = (release_s * sample_rate) as usize;
189        let sustain_samples = if total_samples > attack_samples + decay_samples + release_samples {
190            total_samples - attack_samples - decay_samples - release_samples
191        } else {
192            0
193        };
194
195        for i in 0..total_samples {
196            let t = ((start_sample + i) as f32) / sample_rate;
197
198            // Glide
199            let current_freq = if glide {
200                freq_start + ((freq_end - freq_start) * (i as f32)) / (total_samples as f32)
201            } else {
202                freq
203            };
204
205            // Pitch automation (in semitones), applied as frequency multiplier
206            let pitch_semi = crate::core::audio::engine::helpers::eval_env_map(
207                &pitch_env,
208                (i as f32) / (total_samples as f32),
209                0.0,
210            );
211            let current_freq = current_freq * (2.0_f32).powf(pitch_semi / 12.0);
212
213            // Slide
214            let current_amp = if slide {
215                amp_start + ((amp_end - amp_start) * (i as f32)) / (total_samples as f32)
216            } else {
217                amp_start
218            };
219
220            let mut value =
221                crate::core::audio::engine::helpers::oscillator_sample(&waveform, current_freq, t);
222
223            // ADSR envelope
224            let envelope = crate::core::audio::engine::helpers::adsr_envelope_value(
225                i,
226                attack_samples,
227                decay_samples,
228                sustain_samples,
229                release_samples,
230                sustain_level,
231            );
232
233            // Fade in/out
234            if fade_len > 0 && i < fade_len {
235                if fade_len == 1 {
236                    value *= 0.0;
237                } else {
238                    value *= (i as f32) / (fade_len as f32);
239                }
240            } else if fade_len > 0 && i >= total_samples.saturating_sub(fade_len) {
241                if fade_len == 1 {
242                    value *= 0.0;
243                } else {
244                    // ensure last sample becomes exactly zero to avoid clicks
245                    value *= ((total_samples - 1 - i) as f32) / ((fade_len - 1) as f32);
246                }
247            }
248
249            value *= envelope;
250            let mut sample_val = value * (i16::MAX as f32) * current_amp;
251
252            let vol_mul = crate::core::audio::engine::helpers::eval_env_map(
253                &volume_env,
254                (i as f32) / (total_samples as f32),
255                1.0,
256            )
257            .clamp(0.0, 10.0);
258            sample_val *= vol_mul;
259
260            let pan_val = crate::core::audio::engine::helpers::eval_env_map(
261                &pan_env,
262                (i as f32) / (total_samples as f32),
263                0.0,
264            )
265            .clamp(-1.0, 1.0);
266            let (left_gain, right_gain) = crate::core::audio::engine::helpers::pan_gains(pan_val);
267
268            let left = (sample_val * left_gain)
269                .round()
270                .clamp(i16::MIN as f32, i16::MAX as f32) as i16;
271            let right = (sample_val * right_gain)
272                .round()
273                .clamp(i16::MIN as f32, i16::MAX as f32) as i16;
274
275            stereo_samples.push(left);
276            stereo_samples.push(right);
277        }
278
279        // Increment note counter for diagnostics
280        self.note_count = self.note_count.saturating_add(1);
281
282        crate::core::audio::engine::helpers::mix_stereo_samples_into_buffer(
283            self,
284            start_sample,
285            channels,
286            &stereo_samples,
287        );
288    }
289
290    // helper extraction functions left in this struct for now
291    fn extract_f32(&self, map: &HashMap<String, Value>, key: &str) -> Option<f32> {
292        match map.get(key) {
293            Some(Value::Number(n)) => Some(*n),
294            Some(Value::String(s)) => s.parse::<f32>().ok(),
295            Some(Value::Boolean(b)) => Some(if *b { 1.0 } else { 0.0 }),
296            _ => None,
297        }
298    }
299
300    fn extract_boolean(&self, map: &HashMap<String, Value>, key: &str) -> Option<bool> {
301        match map.get(key) {
302            Some(Value::Boolean(b)) => Some(*b),
303            Some(Value::Number(n)) => Some(*n != 0.0),
304            Some(Value::Identifier(s)) => {
305                if s == "true" {
306                    Some(true)
307                } else if s == "false" {
308                    Some(false)
309                } else {
310                    None
311                }
312            }
313            Some(Value::String(s)) => {
314                if s == "true" {
315                    Some(true)
316                } else if s == "false" {
317                    Some(false)
318                } else {
319                    None
320                }
321            }
322            _ => None,
323        }
324    }
325}