devalang_core/core/audio/engine/
sample.rs

1use 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            // Rejoin the remainder as the entity path so bank entries can contain
51            // nested paths like "subdir/sample.wav" or plain names.
52            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            // Determine the bank audio base directory. Prefer an optional
70            // `audioPath` declared in the bank's bank.toml (supports keys
71            // `audioPath` or `audio_path`). If absent, fall back to `audio/`.
72            let mut audio_dir = deva_dir.join(subdir).join(bank_name).join("audio");
73            // Try to read bank.toml to get audioPath
74            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                            // normalize separators
84                            let ap_norm = ap.replace("\\", "/");
85                            audio_dir = deva_dir.join(subdir).join(bank_name).join(ap_norm);
86                        }
87                    }
88                }
89            }
90            // Force looking into the computed audio_dir. If the entity_name
91            // already contains an extension (e.g. .wav/.mp3) or a nested path,
92            // preserve it as-is. Otherwise, try with a .wav extension.
93            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                // Detect whether the provided entity already includes an extension.
100                let has_extension = std::path::Path::new(&entity_name).extension().is_some();
101
102                if !has_extension {
103                    // Try appending .wav as a fallback for shorthand names without extension
104                    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                        // Last resort: use the legacy location (no audio/), also with .wav
109                        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                    // If an extension was specified, don't append .wav; try legacy location
118                    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                        // No file found; fall back to the audio candidate path (even if missing)
124                        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        // Read frames from decoder and convert to mono if needed.
157        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        // Convert interleaved channels to mono by averaging channels per frame.
163        // Apply a small RMS-preserving scale so mono level is similar to mixed stereo.
164        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; // delay time in seconds
220        let delay_feedback = 0.35; // default feedback
221
222        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        // If no fade specified, apply a tiny default fade (2 ms) when sample boundaries are non-zero
258        let default_boundary_fade_ms = 1.0_f32; // 1 ms
259        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                    // increased threshold to detect only strong abrupt starts
266                    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                    // increased threshold to detect only strong abrupt ends
274                    effective_fade_out = default_fade_samples.max(1);
275                }
276            }
277        }
278
279        // Ensure fades do not exceed half the sample length to avoid silencing short samples
280        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}