devalang_core/core/audio/engine/
sample.rs1use crate::core::{store::variable::VariableTable, utils::path::normalize_path};
2use devalang_types::Value;
3use rodio::{Decoder, Source};
4use std::{collections::HashMap, fs::File, io::BufReader, path::Path};
5
6const SAMPLE_RATE: u32 = 44100;
7const CHANNELS: u16 = 2;
8
9impl super::synth::AudioEngine {
10 pub fn insert_sample(
11 &mut self,
12 filepath: &str,
13 time_secs: f32,
14 dur_sec: f32,
15 effects: Option<HashMap<String, Value>>,
16 variable_table: &VariableTable,
17 ) {
18 if filepath.is_empty() {
19 eprintln!("❌ Empty file path provided for audio sample.");
20 return;
21 }
22
23 let module_root = Path::new(&self.module_name);
24 let root = match devalang_utils::path::get_project_root() {
25 Ok(p) => p,
26 Err(_) => std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
27 };
28 let resolved_path: String;
29
30 let mut var_path = filepath.to_string();
31 if let Some(Value::String(variable_path)) = variable_table.variables.get(filepath) {
32 var_path = variable_path.clone();
33 } else if let Some(Value::Sample(sample_path)) = variable_table.variables.get(filepath) {
34 var_path = sample_path.clone();
35 }
36
37 if var_path.starts_with("devalang://") {
38 let path_after_protocol = var_path.replace("devalang://", "");
39 let parts: Vec<&str> = path_after_protocol.split('/').collect();
40
41 if parts.len() < 3 {
42 eprintln!(
43 "❌ Invalid devalang:// path format. Expected devalang://<type>/<author>.<bank>/<entity>"
44 );
45 return;
46 }
47
48 let obj_type = parts[0];
49 let bank_name = parts[1];
50 let entity_name = parts[2..].join("/");
53
54 let deva_dir = match devalang_utils::path::get_deva_dir() {
55 Ok(dir) => dir,
56 Err(e) => {
57 eprintln!("❌ {}", e);
58 return;
59 }
60 };
61 let subdir = match obj_type {
62 "bank" => "banks",
63 "plugin" => "plugins",
64 "preset" => "presets",
65 "template" => "templates",
66 other => other,
67 };
68
69 let mut audio_dir = deva_dir.join(subdir).join(bank_name).join("audio");
73 let bank_toml = deva_dir.join(subdir).join(bank_name).join("bank.toml");
75 if bank_toml.exists() {
76 if let Ok(content) = std::fs::read_to_string(&bank_toml) {
77 if let Ok(parsed) = toml::from_str::<toml::Value>(&content) {
78 if let Some(ap) = parsed
79 .get("audioPath")
80 .or_else(|| parsed.get("audio_path"))
81 .and_then(|v| v.as_str())
82 {
83 let ap_norm = ap.replace("\\", "/");
85 audio_dir = deva_dir.join(subdir).join(bank_name).join(ap_norm);
86 }
87 }
88 }
89 }
90 let bank_base = audio_dir;
94 let candidate = bank_base.join(&entity_name);
95
96 if candidate.exists() {
97 resolved_path = candidate.to_string_lossy().to_string();
98 } else {
99 let has_extension = std::path::Path::new(&entity_name).extension().is_some();
101
102 if !has_extension {
103 let wav_candidate = bank_base.join(format!("{}.wav", entity_name));
105 if wav_candidate.exists() {
106 resolved_path = wav_candidate.to_string_lossy().to_string();
107 } else {
108 resolved_path = deva_dir
110 .join(subdir)
111 .join(bank_name)
112 .join(format!("{}.wav", entity_name))
113 .to_string_lossy()
114 .to_string();
115 }
116 } else {
117 let legacy_candidate = deva_dir.join(subdir).join(bank_name).join(&entity_name);
119
120 if legacy_candidate.exists() {
121 resolved_path = legacy_candidate.to_string_lossy().to_string();
122 } else {
123 resolved_path = candidate.to_string_lossy().to_string();
125 }
126 }
127 }
128 } else {
129 let entry_dir = module_root.parent().unwrap_or(&root);
130 let absolute_path = root.join(entry_dir).join(&var_path);
131
132 resolved_path = normalize_path(absolute_path.to_string_lossy().to_string());
133 }
134
135 if !Path::new(&resolved_path).exists() {
136 eprintln!("❌ Unknown trigger or missing audio file: {}", filepath);
137 return;
138 }
139
140 let file = match File::open(&resolved_path) {
141 Ok(f) => BufReader::new(f),
142 Err(e) => {
143 eprintln!("❌ Failed to open audio file {}: {}", resolved_path, e);
144 return;
145 }
146 };
147
148 let decoder = match Decoder::new(file) {
149 Ok(d) => d,
150 Err(e) => {
151 eprintln!("❌ Failed to decode audio file {}: {}", resolved_path, e);
152 return;
153 }
154 };
155
156 let max_frames = (dur_sec * (SAMPLE_RATE as f32)) as usize;
158 let dec_channels = decoder.channels() as usize;
159 let max_raw_samples = max_frames.saturating_mul(dec_channels.max(1));
160 let raw_samples: Vec<i16> = decoder.convert_samples().take(max_raw_samples).collect();
161
162 let actual_frames = if dec_channels > 0 {
165 raw_samples.len() / dec_channels
166 } else {
167 0
168 };
169 let mut samples: Vec<i16> = Vec::with_capacity(actual_frames);
170 let rms_scale = (dec_channels as f32).sqrt();
171 for frame in 0..actual_frames {
172 let mut sum: i32 = 0;
173 for ch in 0..dec_channels {
174 sum += raw_samples[frame * dec_channels + ch] as i32;
175 }
176 if dec_channels > 0 {
177 let avg = (sum / (dec_channels as i32)) as f32;
178 let scaled = (avg * rms_scale).clamp(i16::MIN as f32, i16::MAX as f32) as i16;
179 samples.push(scaled);
180 } else {
181 samples.push(0);
182 }
183 }
184
185 if samples.is_empty() {
186 eprintln!("❌ No samples read from {}", resolved_path);
187 return;
188 }
189
190 let offset = (time_secs * (SAMPLE_RATE as f32) * (CHANNELS as f32)) as usize;
191 let required_len = offset + samples.len() * (CHANNELS as usize);
192 if self.buffer.len() < required_len {
193 self.buffer.resize(required_len, 0);
194 }
195
196 if let Some(effects_map) = effects {
197 self.pad_samples(&samples, time_secs, Some(effects_map));
198 } else {
199 self.pad_samples(&samples, time_secs, None);
200 }
201 }
202
203 fn pad_samples(
204 &mut self,
205 samples: &[i16],
206 time_secs: f32,
207 effects_map: Option<HashMap<String, Value>>,
208 ) {
209 let offset = (time_secs * (SAMPLE_RATE as f32) * (CHANNELS as f32)) as usize;
210 let total_samples = samples.len();
211
212 let mut gain = 1.0;
213 let mut pan = 0.0;
214 let mut fade_in = 0.0;
215 let mut fade_out = 0.0;
216 let mut pitch = 1.0;
217 let mut drive = 0.0;
218 let mut reverb = 0.0;
219 let mut delay = 0.0; let delay_feedback = 0.35; if let Some(map) = &effects_map {
223 for (key, val) in map {
224 match (key.as_str(), val) {
225 ("gain", Value::Number(v)) => {
226 gain = *v;
227 }
228 ("pan", Value::Number(v)) => {
229 pan = *v;
230 }
231 ("fadeIn", Value::Number(v)) => {
232 fade_in = *v;
233 }
234 ("fadeOut", Value::Number(v)) => {
235 fade_out = *v;
236 }
237 ("pitch", Value::Number(v)) => {
238 pitch = *v;
239 }
240 ("drive", Value::Number(v)) => {
241 drive = *v;
242 }
243 ("reverb", Value::Number(v)) => {
244 reverb = *v;
245 }
246 ("delay", Value::Number(v)) => {
247 delay = *v;
248 }
249 _ => eprintln!("⚠️ Unknown or invalid effect '{}'", key),
250 }
251 }
252 }
253
254 let fade_in_samples = (fade_in * (SAMPLE_RATE as f32)) as usize;
255 let fade_out_samples = (fade_out * (SAMPLE_RATE as f32)) as usize;
256
257 let default_boundary_fade_ms = 1.0_f32; let default_fade_samples = (default_boundary_fade_ms * (SAMPLE_RATE as f32)) as usize;
260 let mut effective_fade_in = fade_in_samples;
261 let mut effective_fade_out = fade_out_samples;
262 if effective_fade_in == 0 {
263 if let Some(&first) = samples.first() {
264 if first.abs() > 64 {
265 effective_fade_in = default_fade_samples.max(1);
267 }
268 }
269 }
270 if effective_fade_out == 0 {
271 if let Some(&last) = samples.last() {
272 if last.abs() > 64 {
273 effective_fade_out = default_fade_samples.max(1);
275 }
276 }
277 }
278
279 if total_samples > 0 {
281 let cap = total_samples / 2;
282 if effective_fade_in > cap {
283 effective_fade_in = cap.max(1);
284 }
285 if effective_fade_out > cap {
286 effective_fade_out = cap.max(1);
287 }
288 }
289
290 let delay_samples = if delay > 0.0 {
291 (delay * (SAMPLE_RATE as f32)) as usize
292 } else {
293 0
294 };
295 let mut delay_buffer: Vec<f32> = vec![0.0; total_samples + delay_samples];
296
297 for i in 0..total_samples {
298 let pitch_index = if pitch != 1.0 {
299 ((i as f32) / pitch) as usize
300 } else {
301 i
302 };
303
304 let mut adjusted = if pitch_index < total_samples {
305 samples[pitch_index] as f32
306 } else {
307 0.0
308 };
309
310 adjusted *= gain;
311
312 if effective_fade_in > 0 && i < effective_fade_in {
313 if effective_fade_in == 1 {
314 adjusted *= 0.0;
315 } else {
316 adjusted *= (i as f32) / (effective_fade_in as f32);
317 }
318 }
319 if effective_fade_out > 0 && i >= total_samples.saturating_sub(effective_fade_out) {
320 if effective_fade_out == 1 {
321 adjusted *= 0.0;
322 } else {
323 adjusted *=
324 ((total_samples - 1 - i) as f32) / ((effective_fade_out - 1) as f32);
325 }
326 }
327
328 if drive > 0.0 {
329 let normalized = adjusted / (i16::MAX as f32);
330 let pre_gain = (10f32).powf(drive / 20.0);
331 let driven = (normalized * pre_gain).tanh();
332 adjusted = driven * (i16::MAX as f32);
333 }
334
335 if delay_samples > 0 && i >= delay_samples {
336 let echo = delay_buffer[i - delay_samples] * delay_feedback;
337 adjusted += echo;
338 }
339 if delay_samples > 0 {
340 delay_buffer[i] = adjusted;
341 }
342
343 if reverb > 0.0 {
344 let reverb_delay = (0.03 * (SAMPLE_RATE as f32)) as usize;
345 if i >= reverb_delay {
346 adjusted += (self.buffer[offset + i - reverb_delay] as f32) * reverb;
347 }
348 }
349
350 let adjusted_sample = adjusted.round().clamp(i16::MIN as f32, i16::MAX as f32) as i16;
351
352 let (left_gain, right_gain) = crate::core::audio::engine::helpers::pan_gains(pan);
353
354 let left = ((adjusted_sample as f32) * left_gain) as i16;
355 let right = ((adjusted_sample as f32) * right_gain) as i16;
356
357 let left_pos = offset + i * 2;
358 let right_pos = left_pos + 1;
359
360 if right_pos < self.buffer.len() {
361 self.buffer[left_pos] = self.buffer[left_pos].saturating_add(left);
362 self.buffer[right_pos] = self.buffer[right_pos].saturating_add(right);
363 }
364 }
365 }
366}