devalang_wasm/engine/audio/samples/
mod.rs1#![cfg(not(target_arch = "wasm32"))]
7
8use anyhow::{Context, Result};
9use serde::Deserialize;
10use std::collections::HashMap;
11use std::fs;
12use std::path::{Path, PathBuf};
13use std::sync::{Arc, Mutex};
14use once_cell::sync::Lazy;
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 println!("🔄 Lazy loaded sample: {}/{}", bank_id, trigger_name);
141 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 println!("✅ Bank registered: {} ({} triggers available for lazy loading)", bank_id, triggers.len());
201
202 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
224pub fn get_sample(uri: &str) -> Option<SampleData> {
226 let mut registry = SAMPLE_REGISTRY.lock().unwrap();
227 registry.get_sample(uri)
228}
229
230pub fn get_stats() -> (usize, usize, usize) {
232 let registry = SAMPLE_REGISTRY.lock().unwrap();
233 registry.stats()
234}
235
236pub fn auto_load_banks() -> Result<()> {
238 let mut possible_paths = Vec::new();
239
240 if let Ok(cwd) = std::env::current_dir() {
242 possible_paths.push(cwd.join("addons").join("banks"));
243 possible_paths.push(cwd.join(".deva").join("banks"));
244 }
245
246 if let Some(home_dir) = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE")) {
248 let home_path = PathBuf::from(home_dir);
249 possible_paths.push(home_path.join(".deva").join("banks"));
250 }
251
252 if let Ok(cwd) = std::env::current_dir() {
254 let mut current = cwd.as_path();
255 for _ in 0..3 {
256 if let Some(parent) = current.parent() {
257 possible_paths.push(parent.join("addons").join("banks"));
258 possible_paths.push(parent.join("static").join("addons").join("banks"));
259 current = parent;
260 }
261 }
262 }
263
264 for base_path in possible_paths {
265 if !base_path.exists() {
266 continue;
267 }
268
269 if let Ok(publishers) = fs::read_dir(&base_path) {
271 for publisher_entry in publishers.filter_map(Result::ok) {
272 let publisher_path = publisher_entry.path();
273 if !publisher_path.is_dir() {
274 continue;
275 }
276
277 if let Ok(banks) = fs::read_dir(&publisher_path) {
278 for bank_entry in banks.filter_map(Result::ok) {
279 let bank_path = bank_entry.path();
280 if !bank_path.is_dir() {
281 continue;
282 }
283
284 if let Err(e) = load_bank_from_directory(&bank_path) {
286 eprintln!("Failed to load bank from {:?}: {}", bank_path, e);
287 }
288 }
289 }
290 }
291 }
292 }
293
294 Ok(())
295}