devalang_core/core/audio/
engine.rs

1use crate::core::{
2    shared::value::Value, store::variable::VariableTable, utils::path::normalize_path,
3};
4use hound::{SampleFormat, WavSpec, WavWriter};
5use rodio::{Decoder, Source};
6use std::{collections::HashMap, fs::File, io::BufReader, path::Path};
7
8const SAMPLE_RATE: u32 = 44100;
9const CHANNELS: u16 = 2;
10
11#[derive(Debug, Clone, PartialEq)]
12pub struct AudioEngine {
13    pub volume: f32,
14    pub buffer: Vec<i16>,
15    pub module_name: String,
16}
17
18impl AudioEngine {
19    pub fn new(module_name: String) -> Self {
20        AudioEngine {
21            volume: 1.0,
22            buffer: vec![],
23            module_name,
24        }
25    }
26
27    pub fn get_buffer(&self) -> &[i16] {
28        &self.buffer
29    }
30
31    pub fn get_normalized_buffer(&self) -> Vec<f32> {
32        self.buffer.iter().map(|&s| (s as f32) / 32768.0).collect()
33    }
34
35    pub fn mix(&mut self, other: &AudioEngine) {
36        let max_len = self.buffer.len().max(other.buffer.len());
37        self.buffer.resize(max_len, 0);
38
39        for (i, &sample) in other.buffer.iter().enumerate() {
40            self.buffer[i] = self.buffer[i].saturating_add(sample);
41        }
42    }
43
44    pub fn merge_with(&mut self, other: AudioEngine) {
45        if other.buffer.iter().all(|&s| s == 0) {
46            eprintln!("⚠️ Skipping merge: other buffer is silent");
47            return;
48        }
49
50        if self.buffer.iter().all(|&s| s == 0) {
51            self.buffer = other.buffer;
52            return;
53        }
54
55        self.mix(&other);
56    }
57
58    pub fn set_duration(&mut self, duration_secs: f32) {
59        let total_samples = (duration_secs * (SAMPLE_RATE as f32) * (CHANNELS as f32)) as usize;
60
61        if self.buffer.len() < total_samples {
62            self.buffer.resize(total_samples, 0);
63        }
64    }
65
66    pub fn generate_wav_file(&mut self, output_dir: &String) -> Result<(), String> {
67        if self.buffer.len() % (CHANNELS as usize) != 0 {
68            self.buffer.push(0);
69            println!("Completed buffer to respect stereo format.");
70        }
71
72        let spec = WavSpec {
73            channels: CHANNELS,
74            sample_rate: SAMPLE_RATE,
75            bits_per_sample: 16,
76            sample_format: SampleFormat::Int,
77        };
78
79        let mut writer = WavWriter::create(output_dir, spec)
80            .map_err(|e| format!("Error creating WAV file: {}", e))?;
81
82        for sample in &self.buffer {
83            writer
84                .write_sample(*sample)
85                .map_err(|e| format!("Error writing sample: {:?}", e))?;
86        }
87
88        writer
89            .finalize()
90            .map_err(|e| format!("Error finalizing WAV: {:?}", e))?;
91
92        Ok(())
93    }
94
95    pub fn insert_note(
96        &mut self,
97        waveform: String,
98        freq: f32,
99        amp: f32,
100        start_time_ms: f32,
101        duration_ms: f32,
102        synth_params: HashMap<String, Value>,
103        note_params: HashMap<String, Value>,
104        automation: Option<HashMap<String, Value>>,
105    ) {
106        let valid_synth_params = vec!["attack", "decay", "sustain", "release"];
107        let valid_note_params = vec![
108            "duration",
109            "velocity",
110            "glide",
111            "slide",
112            "amp",
113            "target_freq",
114            "target_amp",
115            "modulation",
116            "expression",
117            // allow per-note automation map
118            "automate",
119        ];
120
121        // Synth params validation
122        for key in synth_params.keys() {
123            if !valid_synth_params.contains(&key.as_str()) {
124                eprintln!("⚠️ Unknown synth parameter: '{}'", key);
125            }
126        }
127
128        // Note params validation
129        for key in note_params.keys() {
130            if !valid_note_params.contains(&key.as_str()) {
131                eprintln!("⚠️ Unknown note parameter: '{}'", key);
132            }
133        }
134
135        // Synth parameters
136        let attack = self.extract_f32(&synth_params, "attack").unwrap_or(0.0);
137        let decay = self.extract_f32(&synth_params, "decay").unwrap_or(0.0);
138        let sustain = self.extract_f32(&synth_params, "sustain").unwrap_or(1.0);
139        let release = self.extract_f32(&synth_params, "release").unwrap_or(0.0);
140        let attack_s = if attack > 10.0 {
141            attack / 1000.0
142        } else {
143            attack
144        };
145        let decay_s = if decay > 10.0 { decay / 1000.0 } else { decay };
146        let release_s = if release > 10.0 {
147            release / 1000.0
148        } else {
149            release
150        };
151        let sustain_level = if sustain > 1.0 {
152            (sustain / 100.0).clamp(0.0, 1.0)
153        } else {
154            sustain.clamp(0.0, 1.0)
155        };
156
157        // Note parameters
158        let duration_ms = self
159            .extract_f32(&note_params, "duration")
160            .unwrap_or(duration_ms);
161        let velocity = self.extract_f32(&note_params, "velocity").unwrap_or(1.0);
162        let glide = self.extract_boolean(&note_params, "glide").unwrap_or(false);
163        let slide = self.extract_boolean(&note_params, "slide").unwrap_or(false);
164
165        let _amplitude = (i16::MAX as f32) * amp.clamp(0.0, 1.0) * velocity.clamp(0.0, 1.0);
166
167        // Logic for glide and slide
168        let freq_start = freq;
169        let mut freq_end = freq;
170        let amp_start = amp * velocity.clamp(0.0, 1.0);
171        let mut amp_end = amp_start;
172
173        if glide {
174            if let Some(Value::Number(target_freq)) = note_params.get("target_freq") {
175                freq_end = *target_freq;
176            } else {
177                freq_end = freq * 1.5; // By default, glide to a perfect fifth
178            }
179        }
180
181        if slide {
182            if let Some(Value::Number(target_amp)) = note_params.get("target_amp") {
183                amp_end = *target_amp * velocity.clamp(0.0, 1.0);
184            } else {
185                amp_end = amp_start * 0.5; // By default, slide to half the amplitude
186            }
187        }
188        let sample_rate = SAMPLE_RATE as f32;
189        let channels = CHANNELS as usize;
190
191        let total_samples = ((duration_ms / 1000.0) * sample_rate) as usize;
192        let start_sample = ((start_time_ms / 1000.0) * sample_rate) as usize;
193
194        // Precompute automation envelopes
195        let (volume_env, pan_env, pitch_env) = Self::env_maps_from_automation(&automation);
196
197        let mut stereo_samples: Vec<i16> = Vec::with_capacity(total_samples * 2);
198        let fade_len = (sample_rate * 0.01) as usize; // 10 ms fade
199
200        let attack_samples = (attack_s * sample_rate) as usize;
201        let decay_samples = (decay_s * sample_rate) as usize;
202        let release_samples = (release_s * sample_rate) as usize;
203        let sustain_samples = if total_samples > attack_samples + decay_samples + release_samples {
204            total_samples - attack_samples - decay_samples - release_samples
205        } else {
206            0
207        };
208
209        for i in 0..total_samples {
210            let t = ((start_sample + i) as f32) / sample_rate;
211
212            // Glide
213            let current_freq = if glide {
214                freq_start + ((freq_end - freq_start) * (i as f32)) / (total_samples as f32)
215            } else {
216                freq
217            };
218
219            // Pitch automation (in semitones), applied as frequency multiplier
220            let pitch_semi =
221                Self::eval_env_map(&pitch_env, (i as f32) / (total_samples as f32), 0.0);
222            let current_freq = current_freq * (2.0_f32).powf(pitch_semi / 12.0);
223
224            // Slide
225            let current_amp = if slide {
226                amp_start + ((amp_end - amp_start) * (i as f32)) / (total_samples as f32)
227            } else {
228                amp_start
229            };
230
231            let mut value = Self::oscillator_sample(&waveform, current_freq, t);
232
233            // ADSR envelope
234            let envelope = Self::adsr_envelope_value(
235                i,
236                attack_samples,
237                decay_samples,
238                sustain_samples,
239                release_samples,
240                sustain_level,
241            );
242
243            // Fade in/out
244            if i < fade_len {
245                value *= (i as f32) / (fade_len as f32);
246            } else if i >= total_samples - fade_len {
247                value *= ((total_samples - i) as f32) / (fade_len as f32);
248            }
249
250            value *= envelope;
251            // Apply dynamic amplitude (slide + velocity)
252            let mut sample_val = value * (i16::MAX as f32) * current_amp;
253
254            // Volume automation multiplier
255            let vol_mul = Self::eval_env_map(&volume_env, (i as f32) / (total_samples as f32), 1.0)
256                .clamp(0.0, 10.0);
257            sample_val *= vol_mul;
258
259            // Pan automation [-1..1]; consistency with pad_samples method
260            let pan_val = Self::eval_env_map(&pan_env, (i as f32) / (total_samples as f32), 0.0)
261                .clamp(-1.0, 1.0);
262            let (left_gain, right_gain) = Self::pan_gains(pan_val);
263
264            let left = (sample_val * left_gain)
265                .round()
266                .clamp(i16::MIN as f32, i16::MAX as f32) as i16;
267            let right = (sample_val * right_gain)
268                .round()
269                .clamp(i16::MIN as f32, i16::MAX as f32) as i16;
270
271            stereo_samples.push(left);
272            stereo_samples.push(right);
273        }
274
275        self.mix_stereo_samples_into_buffer(start_sample, channels, &stereo_samples);
276    }
277
278    pub fn insert_sample(
279        &mut self,
280        filepath: &str,
281        time_secs: f32,
282        dur_sec: f32,
283        effects: Option<HashMap<String, Value>>,
284        variable_table: &VariableTable,
285    ) {
286        if filepath.is_empty() {
287            eprintln!("❌ Empty file path provided for audio sample.");
288            return;
289        }
290
291        let root = Path::new(env!("CARGO_MANIFEST_DIR"));
292        let module_root = Path::new(&self.module_name);
293        let resolved_path: String;
294
295        // Get the variable path from the variable table
296        let mut var_path = filepath.to_string();
297        if let Some(Value::String(variable_path)) = variable_table.variables.get(filepath) {
298            var_path = variable_path.clone();
299        } else if let Some(Value::Sample(sample_path)) = variable_table.variables.get(filepath) {
300            var_path = sample_path.clone();
301        }
302
303        // Handle devalang:// protocol
304        if var_path.starts_with("devalang://") {
305            let path_after_protocol = var_path.replace("devalang://", "");
306            let parts: Vec<&str> = path_after_protocol.split('/').collect();
307
308            if parts.len() < 3 {
309                eprintln!(
310                    "❌ Invalid devalang:// path format. Expected devalang://<type>/<author>.<bank>/<entity>"
311                );
312                return;
313            }
314
315            let obj_type = parts[0];
316            let bank_name = parts[1];
317            let entity_name = parts[2];
318
319            resolved_path = root
320                .join(".deva")
321                .join(obj_type)
322                .join(bank_name)
323                .join(format!("{}.wav", entity_name))
324                .to_string_lossy()
325                .to_string();
326        } else {
327            // Else, resolve as a relative path
328            let entry_dir = module_root.parent().unwrap_or(root);
329            let absolute_path = root.join(entry_dir).join(&var_path);
330
331            resolved_path = normalize_path(absolute_path.to_string_lossy().to_string());
332        }
333
334        // Verify if the file exists
335        if !Path::new(&resolved_path).exists() {
336            eprintln!("❌ Unknown trigger or missing audio file: {}", filepath);
337            return;
338        }
339
340        let file = match File::open(&resolved_path) {
341            Ok(f) => BufReader::new(f),
342            Err(e) => {
343                eprintln!("❌ Failed to open audio file {}: {}", resolved_path, e);
344                return;
345            }
346        };
347
348        let decoder = match Decoder::new(file) {
349            Ok(d) => d,
350            Err(e) => {
351                eprintln!("❌ Failed to decode audio file {}: {}", resolved_path, e);
352                return;
353            }
354        };
355
356        let max_mono_samples = (dur_sec * (SAMPLE_RATE as f32)) as usize;
357        let samples: Vec<i16> = decoder.convert_samples().take(max_mono_samples).collect();
358
359        if samples.is_empty() {
360            eprintln!("❌ No samples read from {}", resolved_path);
361            return;
362        }
363
364        // Calculate buffer offset and size
365        let offset = (time_secs * (SAMPLE_RATE as f32) * (CHANNELS as f32)) as usize;
366        let required_len = offset + samples.len() * (CHANNELS as usize);
367        if self.buffer.len() < required_len {
368            self.buffer.resize(required_len, 0);
369        }
370
371        // Apply effects and mix
372        if let Some(effects_map) = effects {
373            self.pad_samples(&samples, time_secs, Some(effects_map));
374        } else {
375            self.pad_samples(&samples, time_secs, None);
376        }
377    }
378
379    fn pad_samples(
380        &mut self,
381        samples: &[i16],
382        time_secs: f32,
383        effects_map: Option<HashMap<String, Value>>,
384    ) {
385        let offset = (time_secs * (SAMPLE_RATE as f32) * (CHANNELS as f32)) as usize;
386        let total_samples = samples.len();
387
388        // Default values
389        let mut gain = 1.0;
390        let mut pan = 0.0;
391        let mut fade_in = 0.0;
392        let mut fade_out = 0.0;
393        let mut pitch = 1.0;
394        let mut drive = 0.0;
395        let mut reverb = 0.0;
396        let mut delay = 0.0; // delay time in seconds
397        let delay_feedback = 0.35; // default feedback
398
399        if let Some(map) = &effects_map {
400            for (key, val) in map {
401                match (key.as_str(), val) {
402                    ("gain", Value::Number(v)) => {
403                        gain = *v;
404                    }
405                    ("pan", Value::Number(v)) => {
406                        pan = *v;
407                    }
408                    ("fadeIn", Value::Number(v)) => {
409                        fade_in = *v;
410                    }
411                    ("fadeOut", Value::Number(v)) => {
412                        fade_out = *v;
413                    }
414                    ("pitch", Value::Number(v)) => {
415                        pitch = *v;
416                    }
417                    ("drive", Value::Number(v)) => {
418                        drive = *v;
419                    }
420                    ("reverb", Value::Number(v)) => {
421                        reverb = *v;
422                    }
423                    ("delay", Value::Number(v)) => {
424                        delay = *v;
425                    }
426                    _ => eprintln!("⚠️ Unknown or invalid effect '{}'", key),
427                }
428            }
429        }
430
431        let fade_in_samples = (fade_in * (SAMPLE_RATE as f32)) as usize;
432        let fade_out_samples = (fade_out * (SAMPLE_RATE as f32)) as usize;
433
434        let delay_samples = if delay > 0.0 {
435            (delay * (SAMPLE_RATE as f32)) as usize
436        } else {
437            0
438        };
439        let mut delay_buffer: Vec<f32> = vec![0.0; total_samples + delay_samples];
440
441        for i in 0..total_samples {
442            // PITCH FIRST
443            let pitch_index = if pitch != 1.0 {
444                ((i as f32) / pitch) as usize
445            } else {
446                i
447            };
448
449            let mut adjusted = if pitch_index < total_samples {
450                samples[pitch_index] as f32
451            } else {
452                0.0
453            };
454
455            // GAIN
456            adjusted *= gain;
457
458            // FADE IN/OUT
459            if fade_in_samples > 0 && i < fade_in_samples {
460                adjusted *= (i as f32) / (fade_in_samples as f32);
461            }
462            if fade_out_samples > 0 && i >= total_samples.saturating_sub(fade_out_samples) {
463                adjusted *= ((total_samples - i) as f32) / (fade_out_samples as f32);
464            }
465
466            // DRIVE (soft)
467            if drive > 0.0 {
468                let normalized = adjusted / (i16::MAX as f32);
469                let pre_gain = (10f32).powf(drive / 20.0); // dB mapping
470                let driven = (normalized * pre_gain).tanh();
471                adjusted = driven * (i16::MAX as f32);
472            }
473
474            // DELAY
475            if delay_samples > 0 && i >= delay_samples {
476                let echo = delay_buffer[i - delay_samples] * delay_feedback;
477                adjusted += echo;
478            }
479            if delay_samples > 0 {
480                delay_buffer[i] = adjusted;
481            }
482
483            // REVERB
484            if reverb > 0.0 {
485                let reverb_delay = (0.03 * (SAMPLE_RATE as f32)) as usize;
486                if i >= reverb_delay {
487                    adjusted += (self.buffer[offset + i - reverb_delay] as f32) * reverb;
488                }
489            }
490
491            // CLAMP
492            let adjusted_sample = adjusted.round().clamp(i16::MIN as f32, i16::MAX as f32) as i16;
493
494            // PAN
495            let (left_gain, right_gain) = Self::pan_gains(pan);
496
497            let left = ((adjusted_sample as f32) * left_gain) as i16;
498            let right = ((adjusted_sample as f32) * right_gain) as i16;
499
500            let left_pos = offset + i * 2;
501            let right_pos = left_pos + 1;
502
503            if right_pos < self.buffer.len() {
504                self.buffer[left_pos] = self.buffer[left_pos].saturating_add(left);
505                self.buffer[right_pos] = self.buffer[right_pos].saturating_add(right);
506            }
507        }
508    }
509
510    // ===== Helper methods to keep long functions modular and readable =====
511
512    fn env_maps_from_automation(
513        automation: &Option<HashMap<String, Value>>,
514    ) -> (
515        Option<HashMap<String, Value>>,
516        Option<HashMap<String, Value>>,
517        Option<HashMap<String, Value>>,
518    ) {
519        if let Some(auto) = automation {
520            let vol = match auto.get("volume") {
521                Some(Value::Map(m)) => Some(m.clone()),
522                _ => None,
523            };
524            let pan = match auto.get("pan") {
525                Some(Value::Map(m)) => Some(m.clone()),
526                _ => None,
527            };
528            let pit = match auto.get("pitch") {
529                Some(Value::Map(m)) => Some(m.clone()),
530                _ => None,
531            };
532            (vol, pan, pit)
533        } else {
534            (None, None, None)
535        }
536    }
537
538    // Evaluate envelope map at progress [0,1]
539    fn eval_env_map(
540        env_opt: &Option<HashMap<String, Value>>,
541        progress: f32,
542        default_val: f32,
543    ) -> f32 {
544        let env = match env_opt {
545            Some(m) => m,
546            None => {
547                return default_val;
548            }
549        };
550        let mut points: Vec<(f32, f32)> = Vec::with_capacity(env.len());
551        for (k, v) in env.iter() {
552            // accept keys like "0" or "0%"
553            let key = if k.ends_with('%') {
554                &k[..k.len() - 1]
555            } else {
556                &k[..]
557            };
558            if let Ok(mut p) = key.parse::<f32>() {
559                p = (p / 100.0).clamp(0.0, 1.0);
560                let val = match v {
561                    Value::Number(n) => *n,
562                    Value::String(s) => s.parse::<f32>().unwrap_or(default_val),
563                    Value::Identifier(s) => s.parse::<f32>().unwrap_or(default_val),
564                    _ => default_val,
565                };
566                points.push((p, val));
567            }
568        }
569        if points.is_empty() {
570            return default_val;
571        }
572        points.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
573        let t = progress.clamp(0.0, 1.0);
574        if t <= points[0].0 {
575            return points[0].1;
576        }
577        if t >= points[points.len() - 1].0 {
578            return points[points.len() - 1].1;
579        }
580        for w in points.windows(2) {
581            let (p0, v0) = w[0];
582            let (p1, v1) = w[1];
583            if t >= p0 && t <= p1 {
584                let ratio = if (p1 - p0).abs() < std::f32::EPSILON {
585                    0.0
586                } else {
587                    (t - p0) / (p1 - p0)
588                };
589                return v0 + (v1 - v0) * ratio;
590            }
591        }
592        default_val
593    }
594
595    fn oscillator_sample(waveform: &str, current_freq: f32, t: f32) -> f32 {
596        let phase = 2.0 * std::f32::consts::PI * current_freq * t;
597        match waveform {
598            "sine" => phase.sin(),
599            "square" => {
600                if phase.sin() >= 0.0 {
601                    1.0
602                } else {
603                    -1.0
604                }
605            }
606            "saw" => 2.0 * (current_freq * t - (current_freq * t + 0.5).floor()),
607            "triangle" => (2.0 * (2.0 * (current_freq * t).fract() - 1.0)).abs() * 2.0 - 1.0,
608            _ => 0.0,
609        }
610    }
611
612    fn adsr_envelope_value(
613        i: usize,
614        attack_samples: usize,
615        decay_samples: usize,
616        sustain_samples: usize,
617        release_samples: usize,
618        sustain_level: f32,
619    ) -> f32 {
620        if i < attack_samples {
621            (i as f32) / (attack_samples as f32)
622        } else if i < attack_samples + decay_samples {
623            1.0 - (1.0 - sustain_level) * (((i - attack_samples) as f32) / (decay_samples as f32))
624        } else if i < attack_samples + decay_samples + sustain_samples {
625            sustain_level
626        } else if release_samples > 0 {
627            sustain_level
628                * (1.0
629                    - ((i - attack_samples - decay_samples - sustain_samples) as f32)
630                        / (release_samples as f32))
631        } else {
632            0.0
633        }
634    }
635
636    fn pan_gains(pan_val: f32) -> (f32, f32) {
637        let left_gain = 1.0 - pan_val.max(0.0);
638        let right_gain = 1.0 + pan_val.min(0.0);
639        (left_gain, right_gain)
640    }
641
642    fn mix_stereo_samples_into_buffer(
643        &mut self,
644        start_sample: usize,
645        channels: usize,
646        stereo_samples: &[i16],
647    ) {
648        let offset = start_sample * channels;
649        let required_len = offset + stereo_samples.len();
650
651        if self.buffer.len() < required_len {
652            self.buffer.resize(required_len, 0);
653        }
654
655        for (i, sample) in stereo_samples.iter().enumerate() {
656            // Debug: track if we hit non-zero samples (to trace silent buffers)
657            // if i == 0 { eprintln!("[debug] first stereo sample: {}", sample); }
658            self.buffer[offset + i] = self.buffer[offset + i].saturating_add(*sample);
659        }
660    }
661
662    fn extract_f32(&self, map: &HashMap<String, Value>, key: &str) -> Option<f32> {
663        match map.get(key) {
664            Some(Value::Number(n)) => Some(*n),
665            Some(Value::String(s)) => s.parse::<f32>().ok(),
666            Some(Value::Boolean(b)) => Some(if *b { 1.0 } else { 0.0 }),
667            _ => None,
668        }
669    }
670
671    fn extract_boolean(&self, map: &HashMap<String, Value>, key: &str) -> Option<bool> {
672        match map.get(key) {
673            Some(Value::Boolean(b)) => Some(*b),
674            Some(Value::Number(n)) => Some(*n != 0.0),
675            Some(Value::Identifier(s)) => {
676                if s == "true" {
677                    Some(true)
678                } else if s == "false" {
679                    Some(false)
680                } else {
681                    None
682                }
683            }
684            Some(Value::String(s)) => {
685                if s == "true" {
686                    Some(true)
687                } else if s == "false" {
688                    Some(false)
689                } else {
690                    None
691                }
692            }
693            _ => None,
694        }
695    }
696}