devalang_core/core/audio/engine/sample/
insert.rs1use devalang_types::Value;
2use devalang_types::VariableTable;
3use devalang_utils::path::normalize_path;
4use rodio::{Decoder, Source};
5use std::{collections::HashMap, fs::File, io::BufReader, path::Path};
6
7pub fn insert_sample_impl(
8 engine: &mut crate::core::audio::engine::driver::AudioEngine,
9 filepath: &str,
10 time_secs: f32,
11 dur_sec: f32,
12 effects: Option<HashMap<String, Value>>,
13 variable_table: &VariableTable,
14) {
15 if filepath.is_empty() {
16 eprintln!("❌ Empty file path provided for audio sample.");
17 return;
18 }
19
20 let module_root = Path::new(&engine.module_name);
21 let root = match devalang_utils::path::get_project_root() {
22 Ok(p) => p,
23 Err(_) => std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
24 };
25 let resolved_path: String;
26
27 let mut var_path = filepath.to_string();
28 if let Some(Value::String(variable_path)) = variable_table.variables.get(filepath) {
29 var_path = variable_path.clone();
30 } else if let Some(Value::Sample(sample_path)) = variable_table.variables.get(filepath) {
31 var_path = sample_path.clone();
32 }
33
34 if var_path.starts_with("devalang://") {
35 let path_after_protocol = var_path.replace("devalang://", "");
36 let parts: Vec<&str> = path_after_protocol.split('/').collect();
37
38 if parts.len() < 3 {
39 eprintln!(
40 "❌ Invalid devalang:// path format. Expected devalang://<type>/<author>.<bank>/<entity>"
41 );
42 return;
43 }
44
45 let obj_type = parts[0];
46 let bank_name = parts[1];
47 let entity_name = parts[2..].join("/");
50
51 let deva_dir = match devalang_utils::path::get_deva_dir() {
52 Ok(dir) => dir,
53 Err(e) => {
54 eprintln!("❌ {}", e);
55 return;
56 }
57 };
58 let subdir = match obj_type {
59 "bank" => "banks",
60 "plugin" => "plugins",
61 "preset" => "presets",
62 "template" => "templates",
63 other => other,
64 };
65
66 let mut audio_dir = deva_dir.join(subdir).join(bank_name).join("audio");
70 let bank_toml = deva_dir.join(subdir).join(bank_name).join("bank.toml");
72 if bank_toml.exists() {
73 if let Ok(content) = std::fs::read_to_string(&bank_toml) {
74 if let Ok(parsed) = toml::from_str::<toml::Value>(&content) {
75 if let Some(ap) = parsed
76 .get("audioPath")
77 .or_else(|| parsed.get("audio_path"))
78 .and_then(|v| v.as_str())
79 {
80 let ap_norm = ap.replace("\\", "/");
82 audio_dir = deva_dir.join(subdir).join(bank_name).join(ap_norm);
83 }
84 }
85 }
86 }
87 let bank_base = audio_dir;
91 let candidate = bank_base.join(&entity_name);
92
93 if candidate.exists() {
94 resolved_path = candidate.to_string_lossy().to_string();
95 } else {
96 let has_extension = std::path::Path::new(&entity_name).extension().is_some();
98
99 if !has_extension {
100 let wav_candidate = bank_base.join(format!("{}.wav", entity_name));
102 if wav_candidate.exists() {
103 resolved_path = wav_candidate.to_string_lossy().to_string();
104 } else {
105 resolved_path = deva_dir
107 .join(subdir)
108 .join(bank_name)
109 .join(format!("{}.wav", entity_name))
110 .to_string_lossy()
111 .to_string();
112 }
113 } else {
114 let legacy_candidate = deva_dir.join(subdir).join(bank_name).join(&entity_name);
116
117 if legacy_candidate.exists() {
118 resolved_path = legacy_candidate.to_string_lossy().to_string();
119 } else {
120 resolved_path = candidate.to_string_lossy().to_string();
122 }
123 }
124 }
125 } else {
126 let entry_dir = module_root.parent().unwrap_or(&root);
127 let absolute_path = root.join(entry_dir).join(&var_path);
128
129 resolved_path = normalize_path(absolute_path.to_string_lossy().to_string());
130 }
131
132 if !Path::new(&resolved_path).exists() {
133 eprintln!("❌ Unknown trigger or missing audio file: {}", filepath);
134 return;
135 }
136
137 let file = match File::open(&resolved_path) {
138 Ok(f) => BufReader::new(f),
139 Err(e) => {
140 eprintln!("❌ Failed to open audio file {}: {}", resolved_path, e);
141 return;
142 }
143 };
144
145 let decoder = match Decoder::new(file) {
146 Ok(d) => d,
147 Err(e) => {
148 eprintln!("❌ Failed to decode audio file {}: {}", resolved_path, e);
149 return;
150 }
151 };
152
153 let sample_rate = engine.sample_rate as f32;
155 let channels = engine.channels as usize;
156
157 let max_frames = (dur_sec * sample_rate) 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 * (channels as f32)) as usize;
191 let required_len = offset + samples.len() * (channels as usize);
192 if engine.buffer.len() < required_len {
193 engine.buffer.resize(required_len, 0);
194 }
195
196 crate::core::audio::engine::sample::padding::pad_samples_impl(
197 engine, &samples, time_secs, effects,
198 );
199}