devalang_core/core/audio/engine/sample/
insert.rs1use crate::config::ops::load_config;
2use devalang_types::Value;
3use devalang_types::VariableTable;
4use devalang_utils::path::normalize_path;
5use rodio::{Decoder, Source};
6use std::path::PathBuf;
7use std::{collections::HashMap, fs::File, io::BufReader, path::Path};
8
9pub fn insert_sample_impl(
10 engine: &mut crate::core::audio::engine::driver::AudioEngine,
11 filepath: &str,
12 time_secs: f32,
13 dur_sec: f32,
14 effects: Option<HashMap<String, Value>>,
15 variable_table: &VariableTable,
16) {
17 if filepath.is_empty() {
18 eprintln!("❌ Empty file path provided for audio sample.");
19 return;
20 }
21
22 let module_root = Path::new(&engine.module_name);
23 let root = match devalang_utils::path::get_project_root() {
24 Ok(p) => p,
25 Err(_) => std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
26 };
27 let resolved_path: String;
28
29 let mut var_path = filepath.to_string();
30 if let Some(Value::String(variable_path)) = variable_table.variables.get(filepath) {
31 var_path = variable_path.clone();
32 } else if let Some(Value::Sample(sample_path)) = variable_table.variables.get(filepath) {
33 var_path = sample_path.clone();
34 }
35
36 if var_path.starts_with("devalang://") {
37 let path_after_protocol = var_path.replace("devalang://", "");
38 let parts: Vec<&str> = path_after_protocol.split('/').collect();
39
40 if parts.len() < 3 {
41 eprintln!(
42 "❌ Invalid devalang:// path format. Expected devalang://<type>/<author>.<bank>/<entity>"
43 );
44 return;
45 }
46
47 let obj_type = parts[0];
48 let bank_name = parts[1];
49 let entity_name = parts[2..].join("/");
52
53 let deva_dir = match devalang_utils::path::get_deva_dir() {
54 Ok(dir) => dir,
55 Err(e) => {
56 eprintln!("❌ {}", e);
57 return;
58 }
59 };
60 let subdir = match obj_type {
61 "bank" => "banks",
62 "plugin" => "plugins",
63 "preset" => "presets",
64 "template" => "templates",
65 other => other,
66 };
67
68 let singular = if subdir.ends_with('s') {
70 &subdir[..subdir.len() - 1]
71 } else {
72 subdir
73 };
74
75 let mut candidate_roots: Vec<PathBuf> = Vec::new();
80 for sd in &[subdir, singular] {
81 let base = deva_dir.join(sd).join(bank_name);
82 candidate_roots.push(base.clone());
83
84 if bank_name.contains('.') {
85 let mut it = bank_name.splitn(2, '.');
86 let pubr = it.next().unwrap_or("");
87 let nm = it.next().unwrap_or("");
88 candidate_roots.push(deva_dir.join(sd).join(pubr).join(nm));
89 }
90 }
91
92 let resolve_from_root = |root: &PathBuf| -> Option<String> {
97 let mut audio_dir = root.join("audio");
99 let bank_toml = root.join("bank.toml");
100 if bank_toml.exists() {
101 if let Ok(content) = std::fs::read_to_string(&bank_toml) {
102 if let Ok(parsed) = toml::from_str::<toml::Value>(&content) {
103 if let Some(ap) = parsed
104 .get("audioPath")
105 .or_else(|| parsed.get("audio_path"))
106 .and_then(|v| v.as_str())
107 {
108 let ap_norm = ap.replace("\\", "/");
109 audio_dir = root.join(ap_norm);
110 }
111 }
112 }
113 }
114
115 let candidate = audio_dir.join(&entity_name);
116 if candidate.exists() {
117 return Some(candidate.to_string_lossy().to_string());
118 }
119
120 let has_extension = std::path::Path::new(&entity_name).extension().is_some();
121 if !has_extension {
122 let wav_candidate = audio_dir.join(format!("{}.wav", entity_name));
123 if wav_candidate.exists() {
124 return Some(wav_candidate.to_string_lossy().to_string());
125 }
126
127 let legacy_candidate = root.join(format!("{}.wav", entity_name));
128 if legacy_candidate.exists() {
129 return Some(legacy_candidate.to_string_lossy().to_string());
130 }
131 } else {
132 let legacy_candidate = root.join(&entity_name);
133 if legacy_candidate.exists() {
134 return Some(legacy_candidate.to_string_lossy().to_string());
135 }
136 }
137
138 None
139 };
140
141 let mut found: Option<String> = None;
142 for root in &candidate_roots {
143 if let Some(p) = resolve_from_root(root) {
144 found = Some(p);
145 break;
146 }
147 }
148
149 if found.is_none() {
151 if let Ok(config_path) = devalang_utils::path::get_devalang_config_path() {
152 if let Some(cfg) = load_config(Some(&config_path)) {
153 if obj_type == "bank" {
155 if let Some(banks) = cfg.banks {
156 for b in banks {
157 if let Some(name_in_path) = b.path.strip_prefix("devalang://bank/")
158 {
159 if name_in_path == bank_name
161 || name_in_path.ends_with(bank_name)
162 {
163 let root = deva_dir.join(subdir).join(name_in_path);
164 if let Some(p) = resolve_from_root(&root) {
165 found = Some(p);
166 break;
167 }
168 if name_in_path.contains('.') {
170 let mut it = name_in_path.splitn(2, '.');
171 let pubr = it.next().unwrap_or("");
172 let nm = it.next().unwrap_or("");
173 let root2 = deva_dir.join(subdir).join(pubr).join(nm);
174 if let Some(p) = resolve_from_root(&root2) {
175 found = Some(p);
176 break;
177 }
178 }
179 }
180 }
181 }
182 }
183 } else if obj_type == "plugin" {
184 if let Some(plugins) = cfg.plugins {
185 for p in plugins {
186 if let Some(name_in_path) =
187 p.path.strip_prefix("devalang://plugin/")
188 {
189 if name_in_path == bank_name
190 || name_in_path.ends_with(bank_name)
191 {
192 let root = deva_dir.join(subdir).join(name_in_path);
193 if let Some(path_found) = resolve_from_root(&root) {
194 found = Some(path_found);
195 break;
196 }
197 if name_in_path.contains('.') {
198 let mut it = name_in_path.splitn(2, '.');
199 let pubr = it.next().unwrap_or("");
200 let nm = it.next().unwrap_or("");
201 let root2 = deva_dir.join(subdir).join(pubr).join(nm);
202 if let Some(path_found) = resolve_from_root(&root2) {
203 found = Some(path_found);
204 break;
205 }
206 }
207 }
208 }
209 }
210 }
211 }
212 }
213 }
214 }
215
216 if let Some(p) = found {
217 resolved_path = p;
218 } else {
219 let legacy = deva_dir
221 .join(subdir)
222 .join(bank_name)
223 .join(format!("{}.wav", entity_name));
224 resolved_path = legacy.to_string_lossy().to_string();
225 }
226 } else {
227 let entry_dir = module_root.parent().unwrap_or(&root);
228 let absolute_path = root.join(entry_dir).join(&var_path);
229
230 resolved_path = normalize_path(absolute_path.to_string_lossy().to_string());
231 }
232
233 if !Path::new(&resolved_path).exists() {
234 eprintln!("❌ Unknown trigger or missing audio file: {}", filepath);
235 return;
236 }
237
238 let file = match File::open(&resolved_path) {
239 Ok(f) => BufReader::new(f),
240 Err(e) => {
241 eprintln!("❌ Failed to open audio file {}: {}", resolved_path, e);
242 return;
243 }
244 };
245
246 let decoder = match Decoder::new(file) {
247 Ok(d) => d,
248 Err(e) => {
249 eprintln!("❌ Failed to decode audio file {}: {}", resolved_path, e);
250 return;
251 }
252 };
253
254 let sample_rate = engine.sample_rate as f32;
256 let channels = engine.channels as usize;
257
258 let max_frames = (dur_sec * sample_rate) as usize;
259 let dec_channels = decoder.channels() as usize;
260 let max_raw_samples = max_frames.saturating_mul(dec_channels.max(1));
261 let raw_samples: Vec<i16> = decoder.convert_samples().take(max_raw_samples).collect();
262
263 let actual_frames = if dec_channels > 0 {
266 raw_samples.len() / dec_channels
267 } else {
268 0
269 };
270 let mut samples: Vec<i16> = Vec::with_capacity(actual_frames);
271 let rms_scale = (dec_channels as f32).sqrt();
272 for frame in 0..actual_frames {
273 let mut sum: i32 = 0;
274 for ch in 0..dec_channels {
275 sum += raw_samples[frame * dec_channels + ch] as i32;
276 }
277 if dec_channels > 0 {
278 let avg = (sum / (dec_channels as i32)) as f32;
279 let scaled = (avg * rms_scale).clamp(i16::MIN as f32, i16::MAX as f32) as i16;
280 samples.push(scaled);
281 } else {
282 samples.push(0);
283 }
284 }
285
286 if samples.is_empty() {
287 eprintln!("❌ No samples read from {}", resolved_path);
288 return;
289 }
290
291 let offset = (time_secs * sample_rate * (channels as f32)) as usize;
292 let required_len = offset + samples.len() * (channels as usize);
293 if engine.buffer.len() < required_len {
294 engine.buffer.resize(required_len, 0);
295 }
296
297 crate::core::audio::engine::sample::padding::pad_samples_impl(
298 engine, &samples, time_secs, effects,
299 );
300}