devalang_core/core/audio/engine/
synth.rs1use devalang_types::Value;
2use std::collections::HashMap;
3
4const SAMPLE_RATE: u32 = 44100;
6const CHANNELS: u16 = 2;
7
8#[derive(Debug, Clone, PartialEq)]
15pub struct AudioEngine {
16 pub volume: f32,
18 pub buffer: Vec<i16>,
20 pub module_name: String,
22 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 other.buffer.is_empty() {
56 return;
57 }
58
59 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 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 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(¬e_params, "duration")
146 .unwrap_or(duration_ms);
147 let velocity = self.extract_f32(¬e_params, "velocity").unwrap_or(1.0);
148 let glide = self.extract_boolean(¬e_params, "glide").unwrap_or(false);
149 let slide = self.extract_boolean(¬e_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; 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 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 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 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 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 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 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 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 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}