devalang_wasm/engine/audio/samples/
mod.rs1#![cfg(feature = "cli")]
7
8use anyhow::{Context, Result};
9use once_cell::sync::Lazy;
10use serde::Deserialize;
11use std::collections::HashMap;
12use std::fs;
13use std::path::{Path, PathBuf};
14use std::sync::{Arc, Mutex};
15
16static SAMPLE_REGISTRY: Lazy<Arc<Mutex<SampleRegistry>>> =
18 Lazy::new(|| Arc::new(Mutex::new(SampleRegistry::new())));
19
20#[derive(Debug, Deserialize)]
22struct BankManifest {
23 bank: BankInfo,
24 triggers: Vec<TriggerInfo>,
25}
26
27#[derive(Debug, Deserialize)]
28struct BankInfo {
29 name: String,
30 publisher: String,
31 audio_path: String,
32 #[allow(dead_code)]
33 description: Option<String>,
34 #[allow(dead_code)]
35 version: Option<String>,
36 #[allow(dead_code)]
37 access: Option<String>,
38}
39
40#[derive(Debug, Deserialize)]
41struct TriggerInfo {
42 name: String,
43 path: String,
44}
45
46#[derive(Clone, Debug)]
48pub struct SampleData {
49 pub samples: Vec<f32>,
50 pub sample_rate: u32,
51}
52
53#[derive(Debug, Clone)]
55pub struct BankMetadata {
56 bank_id: String,
57 bank_path: PathBuf,
58 audio_path: String,
59 triggers: HashMap<String, String>, }
61
62#[derive(Debug)]
64pub struct SampleRegistry {
65 samples: HashMap<String, SampleData>, banks: HashMap<String, BankMetadata>, loaded_samples: HashMap<String, bool>, }
69
70impl SampleRegistry {
71 fn new() -> Self {
72 Self {
73 samples: HashMap::new(),
74 banks: HashMap::new(),
75 loaded_samples: HashMap::new(),
76 }
77 }
78
79 pub fn register_sample(&mut self, uri: String, data: SampleData) {
81 self.samples.insert(uri.clone(), data);
82 self.loaded_samples.insert(uri, true);
83 }
84
85 pub fn register_bank_metadata(&mut self, metadata: BankMetadata) {
87 self.banks.insert(metadata.bank_id.clone(), metadata);
88 }
89
90 pub fn get_sample(&mut self, uri: &str) -> Option<SampleData> {
92 if let Some(data) = self.samples.get(uri) {
94 return Some(data.clone());
95 }
96
97 if !self.loaded_samples.contains_key(uri) {
99 if let Some(data) = self.try_lazy_load(uri) {
100 self.samples.insert(uri.to_string(), data.clone());
101 self.loaded_samples.insert(uri.to_string(), true);
102 return Some(data);
103 }
104 self.loaded_samples.insert(uri.to_string(), false);
106 }
107
108 None
109 }
110
111 fn try_lazy_load(&self, uri: &str) -> Option<SampleData> {
113 if !uri.starts_with("devalang://bank/") {
115 return None;
116 }
117
118 let path = &uri["devalang://bank/".len()..];
119 let parts: Vec<&str> = path.split('/').collect();
120 if parts.len() != 2 {
121 return None;
122 }
123
124 let bank_id = parts[0];
125 let trigger_name = parts[1];
126
127 let bank_meta = self.banks.get(bank_id)?;
129
130 let file_relative_path = bank_meta.triggers.get(trigger_name)?;
132
133 let audio_dir = bank_meta.bank_path.join(&bank_meta.audio_path);
135 let wav_path = audio_dir.join(file_relative_path);
136
137 match load_wav_file(&wav_path) {
139 Ok(data) => {
140 Some(data)
142 }
143 Err(e) => {
144 eprintln!("Failed to lazy load {:?}: {}", wav_path, e);
145 None
146 }
147 }
148 }
149
150 pub fn has_bank(&self, bank_id: &str) -> bool {
152 self.banks.contains_key(bank_id)
153 }
154
155 pub fn stats(&self) -> (usize, usize, usize) {
157 let total_banks = self.banks.len();
158 let total_samples: usize = self.banks.values().map(|b| b.triggers.len()).sum();
159 let loaded_samples = self.samples.len();
160 (total_banks, total_samples, loaded_samples)
161 }
162}
163
164pub fn load_bank_from_directory(bank_path: &Path) -> Result<String> {
167 let manifest_path = bank_path.join("bank.toml");
168 if !manifest_path.exists() {
169 anyhow::bail!("bank.toml not found in {:?}", bank_path);
170 }
171
172 let manifest_content = fs::read_to_string(&manifest_path)
173 .with_context(|| format!("Failed to read {:?}", manifest_path))?;
174
175 let manifest: BankManifest = toml::from_str(&manifest_content)
176 .with_context(|| format!("Failed to parse {:?}", manifest_path))?;
177
178 let bank_id = format!("{}.{}", manifest.bank.publisher, manifest.bank.name);
179
180 let mut triggers = HashMap::new();
182 for trigger in &manifest.triggers {
183 let clean_path = trigger.path.trim_start_matches("./").to_string();
185 triggers.insert(trigger.name.clone(), clean_path);
186 }
187
188 let metadata = BankMetadata {
190 bank_id: bank_id.clone(),
191 bank_path: bank_path.to_path_buf(),
192 audio_path: manifest.bank.audio_path.clone(),
193 triggers: triggers.clone(),
194 };
195
196 let mut registry = SAMPLE_REGISTRY.lock().unwrap();
198 registry.register_bank_metadata(metadata);
199
200 Ok(bank_id)
203}
204
205fn load_wav_file(path: &Path) -> Result<SampleData> {
207 let bytes = fs::read(path)?;
208
209 let parser_result = crate::utils::wav_parser::parse_wav_generic(&bytes)
211 .map_err(|e| anyhow::anyhow!("WAV parse error: {}", e))?;
212
213 let (_channels, sample_rate, mono_i16) = parser_result;
214
215 let samples: Vec<f32> = mono_i16.iter().map(|&s| s as f32 / 32768.0).collect();
217
218 Ok(SampleData {
219 samples,
220 sample_rate,
221 })
222}
223
224fn load_audio_file(path: &Path) -> Result<SampleData> {
228 if let Ok(data) = load_wav_file(path) {
230 return Ok(data);
231 }
232
233 use rodio::Decoder;
236 use rodio::Source;
237 use std::fs::File;
238 use std::io::BufReader; let file = File::open(path).with_context(|| format!("Failed to open {:?}", path))?;
241 let reader = BufReader::new(file);
242
243 let decoder = Decoder::new(reader).map_err(|e| anyhow::anyhow!("rodio decode error: {}", e))?;
244
245 let sample_rate = decoder.sample_rate();
246 let channels = decoder.channels();
247
248 let samples_f32: Vec<f32> = decoder.convert_samples::<f32>().collect();
250
251 let mono_f32 = if channels > 1 {
252 let ch = channels as usize;
253 let frames = samples_f32.len() / ch;
254 let mut mono = Vec::with_capacity(frames);
255 for f in 0..frames {
256 let mut acc = 0.0f32;
257 for c in 0..ch {
258 acc += samples_f32[f * ch + c];
259 }
260 mono.push(acc / ch as f32);
261 }
262 mono
263 } else {
264 samples_f32
265 };
266
267 Ok(SampleData {
269 samples: mono_f32,
270 sample_rate,
271 })
272}
273
274pub fn get_sample(uri: &str) -> Option<SampleData> {
276 let mut registry = SAMPLE_REGISTRY.lock().unwrap();
277 registry.get_sample(uri)
278}
279
280pub fn register_sample(uri: &str, data: SampleData) {
282 let mut registry = SAMPLE_REGISTRY.lock().unwrap();
283 registry.register_sample(uri.to_string(), data);
284}
285
286pub fn register_sample_from_path(path: &std::path::Path) -> Result<String, anyhow::Error> {
289 let abs = if path.is_absolute() {
290 path.to_path_buf()
291 } else {
292 std::env::current_dir()?.join(path)
293 };
294 let abs_norm = abs.canonicalize().unwrap_or(abs);
295 let uri = abs_norm.to_string_lossy().to_string();
296
297 match load_audio_file(&abs_norm) {
299 Ok(data) => {
300 register_sample(&uri, data);
301 Ok(uri)
302 }
303 Err(e) => Err(e),
304 }
305}
306
307pub fn get_stats() -> (usize, usize, usize) {
309 let registry = SAMPLE_REGISTRY.lock().unwrap();
310 registry.stats()
311}
312
313pub fn auto_load_banks() -> Result<()> {
315 let mut possible_paths = Vec::new();
316
317 if let Ok(cwd) = std::env::current_dir() {
319 possible_paths.push(cwd.join("addons").join("banks"));
320 possible_paths.push(cwd.join(".deva").join("banks"));
321 }
322
323 if let Some(home_dir) = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE")) {
325 let home_path = PathBuf::from(home_dir);
326 possible_paths.push(home_path.join(".deva").join("banks"));
327 }
328
329 if let Ok(cwd) = std::env::current_dir() {
331 let mut current = cwd.as_path();
332 for _ in 0..3 {
333 if let Some(parent) = current.parent() {
334 possible_paths.push(parent.join("addons").join("banks"));
335 possible_paths.push(parent.join("static").join("addons").join("banks"));
336 current = parent;
337 }
338 }
339 }
340
341 for base_path in possible_paths {
342 if !base_path.exists() {
343 continue;
344 }
345
346 if let Ok(publishers) = fs::read_dir(&base_path) {
348 for publisher_entry in publishers.filter_map(Result::ok) {
349 let publisher_path = publisher_entry.path();
350 if !publisher_path.is_dir() {
351 continue;
352 }
353
354 if let Ok(banks) = fs::read_dir(&publisher_path) {
355 for bank_entry in banks.filter_map(Result::ok) {
356 let bank_path = bank_entry.path();
357 if !bank_path.is_dir() {
358 continue;
359 }
360
361 if let Err(e) = load_bank_from_directory(&bank_path) {
363 eprintln!("Failed to load bank from {:?}: {}", bank_path, e);
364 }
365 }
366 }
367 }
368 }
369 }
370
371 Ok(())
372}