devalang_core/core/audio/engine/
driver.rs1use devalang_types::Value;
2use std::collections::HashMap;
3
4#[derive(Debug, Clone, PartialEq)]
6pub struct MidiNoteEvent {
7 pub key: u8,
9 pub vel: u8,
11 pub start_ms: u32,
13 pub duration_ms: u32,
15 pub channel: u8,
17}
18
19pub const SAMPLE_RATE: u32 = 44100;
21pub const CHANNELS: u16 = 2;
22
23#[derive(Debug, Clone, PartialEq)]
31pub struct AudioEngine {
32 pub volume: f32,
34 pub buffer: Vec<i16>,
36 pub midi_events: Vec<MidiNoteEvent>,
38 pub module_name: String,
40 pub note_count: usize,
42 pub sample_rate: u32,
44 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 other.buffer.is_empty() {
81 return;
82 }
83
84 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 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 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
197pub 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; let secs_per_beat = 60.0 / bpm.max(1.0);
205 return Some(beats * secs_per_beat);
206 }
207 }
208 }
209 None
210}
211
212pub 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}