devalang_core/core/audio/engine/
driver.rs

1use devalang_types::Value;
2use std::collections::HashMap;
3
4// Minimal representation of a MIDI note event for export purposes.
5#[derive(Debug, Clone, PartialEq)]
6pub struct MidiNoteEvent {
7    /// MIDI key number 0-127
8    pub key: u8,
9    /// velocity 0-127
10    pub vel: u8,
11    /// start time in milliseconds (absolute)
12    pub start_ms: u32,
13    /// duration in milliseconds
14    pub duration_ms: u32,
15    /// MIDI channel (0-15)
16    pub channel: u8,
17}
18
19// Sample rate and channel constants used throughout the engine.
20pub const SAMPLE_RATE: u32 = 44100;
21pub const CHANNELS: u16 = 2;
22
23/// AudioEngine holds the generated interleaved stereo buffer and
24/// provides simple utilities to mix/merge buffers and export WAV files.
25///
26/// Notes:
27/// - Buffer is interleaved stereo (L,R,L,R...).
28/// - Methods are synchronous and operate on in-memory buffers.
29///
30#[derive(Debug, Clone, PartialEq)]
31pub struct AudioEngine {
32    /// Master volume multiplier (not automatically applied by helpers).
33    pub volume: f32,
34    /// Interleaved i16 PCM buffer.
35    pub buffer: Vec<i16>,
36    /// Collected MIDI note events for export (non-audio representation).
37    pub midi_events: Vec<MidiNoteEvent>,
38    /// Logical module name used for error traces/diagnostics.
39    pub module_name: String,
40    /// Simple diagnostic counter for inserted notes.
41    pub note_count: usize,
42    /// Sample rate (can be overridden per-engine)
43    pub sample_rate: u32,
44    /// Number of channels (interleaved). Defaults to 2.
45    pub channels: u16,
46}
47
48impl AudioEngine {
49    pub fn new(module_name: String) -> Self {
50        AudioEngine {
51            volume: 1.0,
52            buffer: vec![],
53            midi_events: Vec::new(),
54            module_name,
55            note_count: 0,
56            sample_rate: SAMPLE_RATE,
57            channels: CHANNELS,
58        }
59    }
60
61    pub fn get_buffer(&self) -> &[i16] {
62        &self.buffer
63    }
64
65    pub fn get_normalized_buffer(&self) -> Vec<f32> {
66        self.buffer.iter().map(|&s| (s as f32) / 32768.0).collect()
67    }
68
69    pub fn mix(&mut self, other: &AudioEngine) {
70        let max_len = self.buffer.len().max(other.buffer.len());
71        self.buffer.resize(max_len, 0);
72
73        for (i, &sample) in other.buffer.iter().enumerate() {
74            self.buffer[i] = self.buffer[i].saturating_add(sample);
75        }
76    }
77
78    pub fn merge_with(&mut self, other: AudioEngine) {
79        // If the other buffer is empty, simply return without warning (common for spawns that produced nothing)
80        if other.buffer.is_empty() {
81            return;
82        }
83
84        // If the other buffer is present but contains only zeros, warn and skip merge
85        if other.buffer.iter().all(|&s| s == 0) {
86            eprintln!("⚠️ Skipping merge: other buffer is silent");
87            return;
88        }
89
90        if self.buffer.iter().all(|&s| s == 0) {
91            self.buffer = other.buffer;
92            return;
93        }
94
95        self.mix(&other);
96    }
97
98    pub fn set_duration(&mut self, duration_secs: f32) {
99        let total_samples =
100            (duration_secs * (self.sample_rate as f32) * (self.channels as f32)) as usize;
101
102        if self.buffer.len() < total_samples {
103            self.buffer.resize(total_samples, 0);
104        }
105    }
106
107    pub fn generate_midi_file(
108        &mut self,
109        output_path: &String,
110        bpm: Option<f32>,
111        tpqn: Option<u16>,
112    ) -> Result<(), String> {
113        crate::core::audio::engine::export::generate_midi_file_impl(
114            &self.midi_events,
115            output_path,
116            bpm,
117            tpqn,
118        )
119    }
120
121    pub fn generate_wav_file(
122        &mut self,
123        output_dir: &String,
124        audio_format: Option<String>,
125        sample_rate: Option<u32>,
126    ) -> Result<(), String> {
127        crate::core::audio::engine::export::generate_wav_file_impl(
128            &mut self.buffer,
129            output_dir,
130            audio_format,
131            sample_rate,
132        )
133    }
134
135    pub fn insert_note(
136        &mut self,
137        waveform: String,
138        freq: f32,
139        amp: f32,
140        start_time_ms: f32,
141        duration_ms: f32,
142        synth_params: HashMap<String, Value>,
143        note_params: HashMap<String, Value>,
144        automation: Option<HashMap<String, Value>>,
145    ) {
146        // Delegated implementation lives in notes.rs
147        crate::core::audio::engine::notes::insert_note_impl(
148            self,
149            waveform,
150            freq,
151            amp,
152            start_time_ms,
153            duration_ms,
154            synth_params,
155            note_params,
156            automation,
157        );
158    }
159
160    // helper extraction functions left in this struct for now
161    pub(crate) fn extract_f32(&self, map: &HashMap<String, Value>, key: &str) -> Option<f32> {
162        match map.get(key) {
163            Some(Value::Number(n)) => Some(*n),
164            Some(Value::String(s)) => s.parse::<f32>().ok(),
165            Some(Value::Boolean(b)) => Some(if *b { 1.0 } else { 0.0 }),
166            _ => None,
167        }
168    }
169
170    pub(crate) fn extract_boolean(&self, map: &HashMap<String, Value>, key: &str) -> Option<bool> {
171        match map.get(key) {
172            Some(Value::Boolean(b)) => Some(*b),
173            Some(Value::Number(n)) => Some(*n != 0.0),
174            Some(Value::Identifier(s)) => {
175                if s == "true" {
176                    Some(true)
177                } else if s == "false" {
178                    Some(false)
179                } else {
180                    None
181                }
182            }
183            Some(Value::String(s)) => {
184                if s == "true" {
185                    Some(true)
186                } else if s == "false" {
187                    Some(false)
188                } else {
189                    None
190                }
191            }
192            _ => None,
193        }
194    }
195}
196
197// Parse simple musical fraction strings like "1/16" into seconds using bpm
198pub fn parse_fraction_to_seconds(s: &str, bpm: f32) -> Option<f32> {
199    let trimmed = s.trim();
200    if let Some((num, den)) = trimmed.split_once('/') {
201        if let (Ok(n), Ok(d)) = (num.parse::<f32>(), den.parse::<f32>()) {
202            if d != 0.0 {
203                let beats = n / d; // e.g. 1/16 -> 0.0625 beats
204                let secs_per_beat = 60.0 / bpm.max(1.0);
205                return Some(beats * secs_per_beat);
206            }
207        }
208    }
209    None
210}
211
212// Convert a devalang_types::Duration to seconds using bpm when relevant.
213pub fn duration_to_seconds(d: &devalang_types::Duration, bpm: f32) -> Option<f32> {
214    use devalang_types::Duration as D;
215    match d {
216        D::Number(s) => Some(*s),
217        D::Beat(frac) | D::Identifier(frac) => parse_fraction_to_seconds(frac, bpm),
218        _ => None,
219    }
220}