devalang_wasm/engine/audio/samples/
mod.rs1use 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 if let Some(data) = registry.get_sample(uri) {
278 return Some(data);
279 }
280
281 generate_synthetic_sample(uri)
283}
284
285pub fn register_sample(uri: &str, data: SampleData) {
287 let mut registry = SAMPLE_REGISTRY.lock().unwrap();
288 registry.register_sample(uri.to_string(), data);
289}
290
291pub fn register_sample_from_path(path: &std::path::Path) -> Result<String, anyhow::Error> {
294 let abs = if path.is_absolute() {
295 path.to_path_buf()
296 } else {
297 std::env::current_dir()?.join(path)
298 };
299 let abs_norm = abs.canonicalize().unwrap_or(abs);
300 let uri = abs_norm.to_string_lossy().to_string();
301
302 match load_audio_file(&abs_norm) {
304 Ok(data) => {
305 register_sample(&uri, data);
306 Ok(uri)
307 }
308 Err(e) => Err(e),
309 }
310}
311
312pub fn get_stats() -> (usize, usize, usize) {
314 let registry = SAMPLE_REGISTRY.lock().unwrap();
315 registry.stats()
316}
317
318pub fn auto_load_banks() -> Result<()> {
320 let mut possible_paths = Vec::new();
321
322 if let Ok(cwd) = std::env::current_dir() {
324 possible_paths.push(cwd.join("addons").join("banks"));
325 possible_paths.push(cwd.join(".deva").join("banks"));
326 }
327
328 if let Some(home_dir) = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE")) {
330 let home_path = PathBuf::from(home_dir);
331 possible_paths.push(home_path.join(".deva").join("banks"));
332 }
333
334 if let Ok(cwd) = std::env::current_dir() {
336 let mut current = cwd.as_path();
337 for _ in 0..3 {
338 if let Some(parent) = current.parent() {
339 possible_paths.push(parent.join("addons").join("banks"));
340 possible_paths.push(parent.join("static").join("addons").join("banks"));
341 current = parent;
342 }
343 }
344 }
345
346 for base_path in possible_paths {
347 if !base_path.exists() {
348 continue;
349 }
350
351 if let Ok(publishers) = fs::read_dir(&base_path) {
353 for publisher_entry in publishers.filter_map(Result::ok) {
354 let publisher_path = publisher_entry.path();
355 if !publisher_path.is_dir() {
356 continue;
357 }
358
359 if let Ok(banks) = fs::read_dir(&publisher_path) {
360 for bank_entry in banks.filter_map(Result::ok) {
361 let bank_path = bank_entry.path();
362 if !bank_path.is_dir() {
363 continue;
364 }
365
366 if let Err(e) = load_bank_from_directory(&bank_path) {
368 eprintln!("Failed to load bank from {:?}: {}", bank_path, e);
369 }
370 }
371 }
372 }
373 }
374 }
375
376 Ok(())
377}
378
379fn generate_synthetic_sample(uri: &str) -> Option<SampleData> {
381 if !uri.starts_with("devalang://bank/") {
383 return None;
384 }
385
386 let path = &uri["devalang://bank/".len()..];
387 let parts: Vec<&str> = path.split('/').collect();
388 if parts.len() < 2 {
389 return None;
390 }
391
392 let drum_type = parts[parts.len() - 1]; let sample_rate = 44100;
394
395 let (duration_ms, samples) = match drum_type {
397 "kick" => (500, generate_kick(sample_rate, 500)),
398 "snare" => (200, generate_snare(sample_rate, 200)),
399 "hihat" | "hi-hat" => (150, generate_hihat(sample_rate, 150)),
400 "clap" => (200, generate_clap(sample_rate, 200)),
401 "tom" | "tom-high" => (300, generate_tom(sample_rate, 300, 250.0)),
402 "tom-mid" => (350, generate_tom(sample_rate, 350, 180.0)),
403 "tom-low" => (400, generate_tom(sample_rate, 400, 120.0)),
404 "perc" | "percussion" => (100, generate_hihat(sample_rate, 100)),
405 "cowbell" => (150, generate_cowbell(sample_rate, 150)),
406 "cymbal" => (250, generate_cymbal(sample_rate, 250)),
407 _ => {
408 eprintln!(
409 "[SAMPLES] Unknown drum type: {}, using kick fallback",
410 drum_type
411 );
412 (500, generate_kick(sample_rate, 500))
413 }
414 };
415
416 eprintln!(
417 "[SAMPLES] Generated synthetic drum: {} (duration: {}ms, samples: {})",
418 drum_type,
419 duration_ms,
420 samples.len()
421 );
422
423 Some(SampleData {
424 samples,
425 sample_rate,
426 })
427}
428
429fn generate_kick(sample_rate: u32, duration_ms: u32) -> Vec<f32> {
431 let num_samples = ((duration_ms as f32 / 1000.0) * sample_rate as f32) as usize;
432 let mut samples = Vec::with_capacity(num_samples);
433
434 for i in 0..num_samples {
435 let t = i as f32 / sample_rate as f32;
436 let progress = t / (duration_ms as f32 / 1000.0);
437
438 let pitch_start = 150.0;
440 let pitch_end = 50.0;
441 let pitch = pitch_start + (pitch_end - pitch_start) * progress;
442 let phase = 2.0 * std::f32::consts::PI * pitch * t;
443
444 let amp = (1.0 - progress * progress).max(0.0);
446
447 let sample = (phase.sin() * amp * 0.7).tanh();
449 samples.push(sample);
450 }
451
452 samples
453}
454
455fn generate_snare(sample_rate: u32, duration_ms: u32) -> Vec<f32> {
457 let num_samples = ((duration_ms as f32 / 1000.0) * sample_rate as f32) as usize;
458 let mut samples = Vec::with_capacity(num_samples);
459
460 for i in 0..num_samples {
461 let t = i as f32 / sample_rate as f32;
462 let progress = t / (duration_ms as f32 / 1000.0);
463
464 let amp = (1.0 - progress * 3.0).max(0.0);
465
466 let pitch = 200.0;
467 let phase = 2.0 * std::f32::consts::PI * pitch * t;
468 let pitched = phase.sin() * 0.3;
469
470 let seed = (i as u32).wrapping_mul(12345);
471 let random = ((seed >> 16) & 0x7fff) as f32 / 32768.0;
472 let noise = (random * 2.0 - 1.0) * 0.7;
473
474 let sample = (pitched + noise) * amp;
475 samples.push(sample.clamp(-1.0, 1.0));
476 }
477
478 samples
479}
480
481fn generate_hihat(sample_rate: u32, duration_ms: u32) -> Vec<f32> {
483 let num_samples = ((duration_ms as f32 / 1000.0) * sample_rate as f32) as usize;
484 let mut samples = Vec::with_capacity(num_samples);
485
486 for i in 0..num_samples {
487 let t = i as f32 / sample_rate as f32;
488 let progress = t / (duration_ms as f32 / 1000.0);
489
490 let amp = (1.0 - progress * 6.0).max(0.0);
491
492 let seed = (i as u32).wrapping_mul(65537);
493 let random = ((seed >> 16) & 0x7fff) as f32 / 32768.0;
494 let noise = random * 2.0 - 1.0;
495
496 let sample = noise * amp * 0.5;
497 samples.push(sample.clamp(-1.0, 1.0));
498 }
499
500 samples
501}
502
503fn generate_clap(sample_rate: u32, duration_ms: u32) -> Vec<f32> {
505 let num_samples = ((duration_ms as f32 / 1000.0) * sample_rate as f32) as usize;
506 let mut samples = Vec::with_capacity(num_samples);
507
508 for i in 0..num_samples {
509 let t = i as f32 / sample_rate as f32;
510 let progress = t / (duration_ms as f32 / 1000.0);
511
512 let amp = if progress < 0.2 {
513 1.0 - (progress / 0.2) * 0.5
514 } else {
515 (0.5 - (progress - 0.2) * 0.4).max(0.0)
516 };
517
518 let pitch1 = 300.0;
519 let pitch2 = 100.0;
520 let phase1 = 2.0 * std::f32::consts::PI * pitch1 * t;
521 let phase2 = 2.0 * std::f32::consts::PI * pitch2 * t;
522
523 let pitched = phase1.sin() * 0.2 + phase2.sin() * 0.3;
524
525 let seed = (i as u32).wrapping_mul(12345);
526 let random = ((seed >> 16) & 0x7fff) as f32 / 32768.0;
527 let noise = (random * 2.0 - 1.0) * 0.5;
528
529 let sample = (pitched + noise) * amp;
530 samples.push(sample.clamp(-1.0, 1.0));
531 }
532
533 samples
534}
535
536fn generate_tom(sample_rate: u32, duration_ms: u32, pitch: f32) -> Vec<f32> {
538 let num_samples = ((duration_ms as f32 / 1000.0) * sample_rate as f32) as usize;
539 let mut samples = Vec::with_capacity(num_samples);
540
541 for i in 0..num_samples {
542 let t = i as f32 / sample_rate as f32;
543 let progress = t / (duration_ms as f32 / 1000.0);
544
545 let pitch_start = pitch * 1.5;
546 let pitch_end = pitch * 0.5;
547 let current_pitch = pitch_start + (pitch_end - pitch_start) * progress;
548 let phase = 2.0 * std::f32::consts::PI * current_pitch * t;
549
550 let amp = (1.0 - progress * progress * 2.0).max(0.0);
551
552 let sample = phase.sin() * amp * 0.7;
553 samples.push(sample);
554 }
555
556 samples
557}
558
559fn generate_cowbell(sample_rate: u32, duration_ms: u32) -> Vec<f32> {
561 let num_samples = ((duration_ms as f32 / 1000.0) * sample_rate as f32) as usize;
562 let mut samples = Vec::with_capacity(num_samples);
563
564 for i in 0..num_samples {
565 let t = i as f32 / sample_rate as f32;
566 let progress = t / (duration_ms as f32 / 1000.0);
567
568 let freq1 = 540.0;
569 let freq2 = 810.0;
570 let freq3 = 1200.0;
571
572 let phase1 = 2.0 * std::f32::consts::PI * freq1 * t;
573 let phase2 = 2.0 * std::f32::consts::PI * freq2 * t;
574 let phase3 = 2.0 * std::f32::consts::PI * freq3 * t;
575
576 let amp = (1.0 - progress * 2.0).max(0.0);
577
578 let pitched = phase1.sin() * 0.3 + phase2.sin() * 0.25 + phase3.sin() * 0.2;
579 let sample = pitched * amp * 0.7;
580 samples.push(sample.clamp(-1.0, 1.0));
581 }
582
583 samples
584}
585
586fn generate_cymbal(sample_rate: u32, duration_ms: u32) -> Vec<f32> {
588 let num_samples = ((duration_ms as f32 / 1000.0) * sample_rate as f32) as usize;
589 let mut samples = Vec::with_capacity(num_samples);
590
591 for i in 0..num_samples {
592 let t = i as f32 / sample_rate as f32;
593 let progress = t / (duration_ms as f32 / 1000.0);
594
595 let seed1 = (i as u32).wrapping_mul(12345);
596 let seed2 = (i as u32).wrapping_mul(54321);
597
598 let random1 = ((seed1 >> 16) & 0x7fff) as f32 / 32768.0;
599 let random2 = ((seed2 >> 16) & 0x7fff) as f32 / 32768.0;
600
601 let noise = (random1 * 2.0 - 1.0) * 0.4 + (random2 * 2.0 - 1.0) * 0.3;
602
603 let freq1 = 8000.0;
604 let freq2 = 6000.0;
605 let phase1 = 2.0 * std::f32::consts::PI * freq1 * t;
606 let phase2 = 2.0 * std::f32::consts::PI * freq2 * t;
607
608 let pitched = phase1.sin() * 0.1 + phase2.sin() * 0.1;
609
610 let amp = (1.0 - progress * 0.7).max(0.0);
611
612 let sample = (noise + pitched) * amp * 0.6;
613 samples.push(sample.clamp(-1.0, 1.0));
614 }
615
616 samples
617}