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    /// Map target synth -> last inserted note sample ranges (start_sample, total_samples)
39    pub last_notes: std::collections::HashMap<String, Vec<(usize, usize)>>,
40    /// Logical module name used for error traces/diagnostics.
41    pub module_name: String,
42    /// Simple diagnostic counter for inserted notes.
43    pub note_count: usize,
44    /// Sample rate (can be overridden per-engine)
45    pub sample_rate: u32,
46    /// Number of channels (interleaved). Defaults to 2.
47    pub channels: u16,
48}
49
50impl AudioEngine {
51    pub fn new(module_name: String) -> Self {
52        AudioEngine {
53            volume: 1.0,
54            buffer: vec![],
55            midi_events: Vec::new(),
56            module_name,
57            note_count: 0,
58            sample_rate: SAMPLE_RATE,
59            channels: CHANNELS,
60            last_notes: std::collections::HashMap::new(),
61        }
62    }
63
64    pub fn get_buffer(&self) -> &[i16] {
65        &self.buffer
66    }
67
68    pub fn get_normalized_buffer(&self) -> Vec<f32> {
69        self.buffer.iter().map(|&s| (s as f32) / 32768.0).collect()
70    }
71
72    pub fn mix(&mut self, other: &AudioEngine) {
73        let max_len = self.buffer.len().max(other.buffer.len());
74        self.buffer.resize(max_len, 0);
75
76        for (i, &sample) in other.buffer.iter().enumerate() {
77            self.buffer[i] = self.buffer[i].saturating_add(sample);
78        }
79    }
80
81    pub fn merge_with(&mut self, other: AudioEngine) {
82        // If the other buffer is empty, simply return without warning (common for spawns that produced nothing)
83        if other.buffer.is_empty() {
84            return;
85        }
86
87        // If the other buffer is present but contains only zeros, warn and skip merge
88        if other.buffer.iter().all(|&s| s == 0) {
89            eprintln!("⚠️ Skipping merge: other buffer is silent");
90            return;
91        }
92
93        if self.buffer.iter().all(|&s| s == 0) {
94            self.buffer = other.buffer;
95            return;
96        }
97
98        self.mix(&other);
99    }
100
101    pub fn set_duration(&mut self, duration_secs: f32) {
102        let total_samples =
103            (duration_secs * (self.sample_rate as f32) * (self.channels as f32)) as usize;
104
105        if self.buffer.len() < total_samples {
106            self.buffer.resize(total_samples, 0);
107        }
108    }
109
110    pub fn generate_midi_file(
111        &mut self,
112        output_path: &String,
113        bpm: Option<f32>,
114        tpqn: Option<u16>,
115    ) -> Result<(), String> {
116        crate::core::audio::engine::export::generate_midi_file_impl(
117            &self.midi_events,
118            output_path,
119            bpm,
120            tpqn,
121        )
122    }
123
124    pub fn generate_wav_file(
125        &mut self,
126        output_dir: &String,
127        audio_format: Option<String>,
128        sample_rate: Option<u32>,
129    ) -> Result<(), String> {
130        crate::core::audio::engine::export::generate_wav_file_impl(
131            &mut self.buffer,
132            output_dir,
133            audio_format,
134            sample_rate,
135        )
136    }
137
138    pub fn insert_note(
139        &mut self,
140        owner: Option<String>,
141        waveform: String,
142        freq: f32,
143        amp: f32,
144        start_time_ms: f32,
145        duration_ms: f32,
146        synth_params: HashMap<String, Value>,
147        note_params: HashMap<String, Value>,
148        automation: Option<HashMap<String, Value>>,
149    ) -> Vec<(usize, usize)> {
150        // Delegated implementation lives in notes.rs
151        crate::core::audio::engine::notes::insert_note_impl(
152            self,
153            owner,
154            waveform,
155            freq,
156            amp,
157            start_time_ms,
158            duration_ms,
159            synth_params,
160            note_params,
161            automation,
162        )
163    }
164
165    pub fn record_last_note_range(
166        &mut self,
167        owner: &str,
168        start_sample: usize,
169        total_samples: usize,
170    ) {
171        self.last_notes
172            .entry(owner.to_string())
173            .or_default()
174            .push((start_sample, total_samples));
175    }
176
177    // helper extraction functions left in this struct for now
178    pub(crate) fn extract_f32(&self, map: &HashMap<String, Value>, key: &str) -> Option<f32> {
179        match map.get(key) {
180            Some(Value::Number(n)) => Some(*n),
181            Some(Value::String(s)) => s.parse::<f32>().ok(),
182            Some(Value::Boolean(b)) => Some(if *b { 1.0 } else { 0.0 }),
183            _ => None,
184        }
185    }
186
187    pub(crate) fn extract_boolean(&self, map: &HashMap<String, Value>, key: &str) -> Option<bool> {
188        match map.get(key) {
189            Some(Value::Boolean(b)) => Some(*b),
190            Some(Value::Number(n)) => Some(*n != 0.0),
191            Some(Value::Identifier(s)) => {
192                if s == "true" {
193                    Some(true)
194                } else if s == "false" {
195                    Some(false)
196                } else {
197                    None
198                }
199            }
200            Some(Value::String(s)) => {
201                if s == "true" {
202                    Some(true)
203                } else if s == "false" {
204                    Some(false)
205                } else {
206                    None
207                }
208            }
209            _ => None,
210        }
211    }
212}
213
214// Parse simple musical fraction strings like "1/16" into seconds using bpm
215pub fn parse_fraction_to_seconds(s: &str, bpm: f32) -> Option<f32> {
216    let trimmed = s.trim();
217    if let Some((num, den)) = trimmed.split_once('/') {
218        if let (Ok(n), Ok(d)) = (num.parse::<f32>(), den.parse::<f32>()) {
219            if d != 0.0 {
220                let beats = n / d; // e.g. 1/16 -> 0.0625 beats
221                let secs_per_beat = 60.0 / bpm.max(1.0);
222                return Some(beats * secs_per_beat);
223            }
224        }
225    }
226    None
227}
228
229// Convert a devalang_types::Duration to seconds using bpm when relevant.
230pub fn duration_to_seconds(d: &devalang_types::Duration, bpm: f32) -> Option<f32> {
231    use devalang_types::Duration as D;
232    match d {
233        D::Number(s) => Some(*s),
234        D::Beat(frac) | D::Identifier(frac) => parse_fraction_to_seconds(frac, bpm),
235        _ => None,
236    }
237}