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
198 .lock()
199 .map_err(|_| anyhow::anyhow!("Sample registry lock poisoned"))?;
200 registry.register_bank_metadata(metadata);
201
202 Ok(bank_id)
205}
206
207fn load_wav_file(path: &Path) -> Result<SampleData> {
209 let bytes = fs::read(path)?;
210
211 let parser_result = crate::utils::wav_parser::parse_wav_generic(&bytes)
213 .map_err(|e| anyhow::anyhow!("WAV parse error: {}", e))?;
214
215 let (_channels, sample_rate, mono_i16) = parser_result;
216
217 let samples: Vec<f32> = mono_i16.iter().map(|&s| s as f32 / 32768.0).collect();
219
220 Ok(SampleData {
221 samples,
222 sample_rate,
223 })
224}
225
226fn load_audio_file(path: &Path) -> Result<SampleData> {
230 if let Ok(data) = load_wav_file(path) {
232 return Ok(data);
233 }
234
235 use rodio::Decoder;
238 use rodio::Source;
239 use std::fs::File;
240 use std::io::BufReader; let file = File::open(path).with_context(|| format!("Failed to open {:?}", path))?;
243 let reader = BufReader::new(file);
244
245 let decoder = Decoder::new(reader).map_err(|e| anyhow::anyhow!("rodio decode error: {}", e))?;
246
247 let sample_rate = decoder.sample_rate();
248 let channels = decoder.channels();
249
250 let samples_f32: Vec<f32> = decoder.convert_samples::<f32>().collect();
252
253 let mono_f32 = if channels > 1 {
254 let ch = channels as usize;
255 let frames = samples_f32.len() / ch;
256 let mut mono = Vec::with_capacity(frames);
257 for f in 0..frames {
258 let mut acc = 0.0f32;
259 for c in 0..ch {
260 acc += samples_f32[f * ch + c];
261 }
262 mono.push(acc / ch as f32);
263 }
264 mono
265 } else {
266 samples_f32
267 };
268
269 Ok(SampleData {
271 samples: mono_f32,
272 sample_rate,
273 })
274}
275
276pub fn get_sample(uri: &str) -> Option<SampleData> {
278 if let Ok(mut registry) = SAMPLE_REGISTRY.lock() {
279 if let Some(data) = registry.get_sample(uri) {
280 return Some(data);
281 }
282 }
283
284 generate_synthetic_sample(uri)
286}
287
288pub fn register_sample(uri: &str, data: SampleData) {
290 if let Ok(mut registry) = SAMPLE_REGISTRY.lock() {
291 registry.register_sample(uri.to_string(), data);
292 }
293}
294
295pub fn register_sample_from_path(path: &std::path::Path) -> Result<String, anyhow::Error> {
298 let abs = if path.is_absolute() {
299 path.to_path_buf()
300 } else {
301 std::env::current_dir()?.join(path)
302 };
303 let abs_norm = abs.canonicalize().unwrap_or(abs);
304 let uri = abs_norm.to_string_lossy().to_string();
305
306 match load_audio_file(&abs_norm) {
308 Ok(data) => {
309 register_sample(&uri, data);
310 Ok(uri)
311 }
312 Err(e) => Err(e),
313 }
314}
315
316pub fn get_stats() -> (usize, usize, usize) {
318 SAMPLE_REGISTRY
319 .lock()
320 .map(|registry| registry.stats())
321 .unwrap_or((0, 0, 0))
322}
323
324pub fn auto_load_banks() -> Result<()> {
326 let mut possible_paths = Vec::new();
327
328 if let Ok(cwd) = std::env::current_dir() {
330 possible_paths.push(cwd.join("addons").join("banks"));
331 possible_paths.push(cwd.join(".deva").join("banks"));
332 }
333
334 if let Some(home_dir) = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE")) {
336 let home_path = PathBuf::from(home_dir);
337 possible_paths.push(home_path.join(".deva").join("banks"));
338 }
339
340 if let Ok(cwd) = std::env::current_dir() {
342 let mut current = cwd.as_path();
343 for _ in 0..3 {
344 if let Some(parent) = current.parent() {
345 possible_paths.push(parent.join("addons").join("banks"));
346 possible_paths.push(parent.join("static").join("addons").join("banks"));
347 current = parent;
348 }
349 }
350 }
351
352 for base_path in possible_paths {
353 if !base_path.exists() {
354 continue;
355 }
356
357 if let Ok(publishers) = fs::read_dir(&base_path) {
359 for publisher_entry in publishers.filter_map(Result::ok) {
360 let publisher_path = publisher_entry.path();
361 if !publisher_path.is_dir() {
362 continue;
363 }
364
365 if let Ok(banks) = fs::read_dir(&publisher_path) {
366 for bank_entry in banks.filter_map(Result::ok) {
367 let bank_path = bank_entry.path();
368 if !bank_path.is_dir() {
369 continue;
370 }
371
372 if let Err(e) = load_bank_from_directory(&bank_path) {
374 eprintln!("Failed to load bank from {:?}: {}", bank_path, e);
375 }
376 }
377 }
378 }
379 }
380 }
381
382 Ok(())
383}
384
385fn generate_synthetic_sample(uri: &str) -> Option<SampleData> {
387 if !uri.starts_with("devalang://bank/") {
389 return None;
390 }
391
392 let path = &uri["devalang://bank/".len()..];
393 let parts: Vec<&str> = path.split('/').collect();
394 if parts.len() < 2 {
395 return None;
396 }
397
398 let drum_type = parts[parts.len() - 1]; let sample_rate = 44100;
400
401 let (duration_ms, samples) = match drum_type {
403 "kick" => (500, generate_kick(sample_rate, 500)),
404 "snare" => (200, generate_snare(sample_rate, 200)),
405 "hihat" | "hi-hat" => (150, generate_hihat(sample_rate, 150)),
406 "clap" => (200, generate_clap(sample_rate, 200)),
407 "tom" | "tom-high" => (300, generate_tom(sample_rate, 300, 250.0)),
408 "tom-mid" => (350, generate_tom(sample_rate, 350, 180.0)),
409 "tom-low" => (400, generate_tom(sample_rate, 400, 120.0)),
410 "perc" | "percussion" => (100, generate_hihat(sample_rate, 100)),
411 "cowbell" => (150, generate_cowbell(sample_rate, 150)),
412 "cymbal" => (250, generate_cymbal(sample_rate, 250)),
413 _ => {
414 eprintln!(
415 "[SAMPLES] Unknown drum type: {}, using kick fallback",
416 drum_type
417 );
418 (500, generate_kick(sample_rate, 500))
419 }
420 };
421
422 eprintln!(
423 "[SAMPLES] Generated synthetic drum: {} (duration: {}ms, samples: {})",
424 drum_type,
425 duration_ms,
426 samples.len()
427 );
428
429 Some(SampleData {
430 samples,
431 sample_rate,
432 })
433}
434
435fn generate_kick(sample_rate: u32, duration_ms: u32) -> Vec<f32> {
437 let num_samples = ((duration_ms as f32 / 1000.0) * sample_rate as f32) as usize;
438 let mut samples = Vec::with_capacity(num_samples);
439
440 for i in 0..num_samples {
441 let t = i as f32 / sample_rate as f32;
442 let progress = t / (duration_ms as f32 / 1000.0);
443
444 let pitch_start = 150.0;
446 let pitch_end = 50.0;
447 let pitch = pitch_start + (pitch_end - pitch_start) * progress;
448 let phase = 2.0 * std::f32::consts::PI * pitch * t;
449
450 let amp = (1.0 - progress * progress).max(0.0);
452
453 let sample = (phase.sin() * amp * 0.7).tanh();
455 samples.push(sample);
456 }
457
458 samples
459}
460
461fn generate_snare(sample_rate: u32, duration_ms: u32) -> Vec<f32> {
463 let num_samples = ((duration_ms as f32 / 1000.0) * sample_rate as f32) as usize;
464 let mut samples = Vec::with_capacity(num_samples);
465
466 for i in 0..num_samples {
467 let t = i as f32 / sample_rate as f32;
468 let progress = t / (duration_ms as f32 / 1000.0);
469
470 let amp = (1.0 - progress * 3.0).max(0.0);
471
472 let pitch = 200.0;
473 let phase = 2.0 * std::f32::consts::PI * pitch * t;
474 let pitched = phase.sin() * 0.3;
475
476 let seed = (i as u32).wrapping_mul(12345);
477 let random = ((seed >> 16) & 0x7fff) as f32 / 32768.0;
478 let noise = (random * 2.0 - 1.0) * 0.7;
479
480 let sample = (pitched + noise) * amp;
481 samples.push(sample.clamp(-1.0, 1.0));
482 }
483
484 samples
485}
486
487fn generate_hihat(sample_rate: u32, duration_ms: u32) -> Vec<f32> {
489 let num_samples = ((duration_ms as f32 / 1000.0) * sample_rate as f32) as usize;
490 let mut samples = Vec::with_capacity(num_samples);
491
492 for i in 0..num_samples {
493 let t = i as f32 / sample_rate as f32;
494 let progress = t / (duration_ms as f32 / 1000.0);
495
496 let amp = (1.0 - progress * 6.0).max(0.0);
497
498 let seed = (i as u32).wrapping_mul(65537);
499 let random = ((seed >> 16) & 0x7fff) as f32 / 32768.0;
500 let noise = random * 2.0 - 1.0;
501
502 let sample = noise * amp * 0.5;
503 samples.push(sample.clamp(-1.0, 1.0));
504 }
505
506 samples
507}
508
509fn generate_clap(sample_rate: u32, duration_ms: u32) -> Vec<f32> {
511 let num_samples = ((duration_ms as f32 / 1000.0) * sample_rate as f32) as usize;
512 let mut samples = Vec::with_capacity(num_samples);
513
514 for i in 0..num_samples {
515 let t = i as f32 / sample_rate as f32;
516 let progress = t / (duration_ms as f32 / 1000.0);
517
518 let amp = if progress < 0.2 {
519 1.0 - (progress / 0.2) * 0.5
520 } else {
521 (0.5 - (progress - 0.2) * 0.4).max(0.0)
522 };
523
524 let pitch1 = 300.0;
525 let pitch2 = 100.0;
526 let phase1 = 2.0 * std::f32::consts::PI * pitch1 * t;
527 let phase2 = 2.0 * std::f32::consts::PI * pitch2 * t;
528
529 let pitched = phase1.sin() * 0.2 + phase2.sin() * 0.3;
530
531 let seed = (i as u32).wrapping_mul(12345);
532 let random = ((seed >> 16) & 0x7fff) as f32 / 32768.0;
533 let noise = (random * 2.0 - 1.0) * 0.5;
534
535 let sample = (pitched + noise) * amp;
536 samples.push(sample.clamp(-1.0, 1.0));
537 }
538
539 samples
540}
541
542fn generate_tom(sample_rate: u32, duration_ms: u32, pitch: f32) -> Vec<f32> {
544 let num_samples = ((duration_ms as f32 / 1000.0) * sample_rate as f32) as usize;
545 let mut samples = Vec::with_capacity(num_samples);
546
547 for i in 0..num_samples {
548 let t = i as f32 / sample_rate as f32;
549 let progress = t / (duration_ms as f32 / 1000.0);
550
551 let pitch_start = pitch * 1.5;
552 let pitch_end = pitch * 0.5;
553 let current_pitch = pitch_start + (pitch_end - pitch_start) * progress;
554 let phase = 2.0 * std::f32::consts::PI * current_pitch * t;
555
556 let amp = (1.0 - progress * progress * 2.0).max(0.0);
557
558 let sample = phase.sin() * amp * 0.7;
559 samples.push(sample);
560 }
561
562 samples
563}
564
565fn generate_cowbell(sample_rate: u32, duration_ms: u32) -> Vec<f32> {
567 let num_samples = ((duration_ms as f32 / 1000.0) * sample_rate as f32) as usize;
568 let mut samples = Vec::with_capacity(num_samples);
569
570 for i in 0..num_samples {
571 let t = i as f32 / sample_rate as f32;
572 let progress = t / (duration_ms as f32 / 1000.0);
573
574 let freq1 = 540.0;
575 let freq2 = 810.0;
576 let freq3 = 1200.0;
577
578 let phase1 = 2.0 * std::f32::consts::PI * freq1 * t;
579 let phase2 = 2.0 * std::f32::consts::PI * freq2 * t;
580 let phase3 = 2.0 * std::f32::consts::PI * freq3 * t;
581
582 let amp = (1.0 - progress * 2.0).max(0.0);
583
584 let pitched = phase1.sin() * 0.3 + phase2.sin() * 0.25 + phase3.sin() * 0.2;
585 let sample = pitched * amp * 0.7;
586 samples.push(sample.clamp(-1.0, 1.0));
587 }
588
589 samples
590}
591
592fn generate_cymbal(sample_rate: u32, duration_ms: u32) -> Vec<f32> {
594 let num_samples = ((duration_ms as f32 / 1000.0) * sample_rate as f32) as usize;
595 let mut samples = Vec::with_capacity(num_samples);
596
597 for i in 0..num_samples {
598 let t = i as f32 / sample_rate as f32;
599 let progress = t / (duration_ms as f32 / 1000.0);
600
601 let seed1 = (i as u32).wrapping_mul(12345);
602 let seed2 = (i as u32).wrapping_mul(54321);
603
604 let random1 = ((seed1 >> 16) & 0x7fff) as f32 / 32768.0;
605 let random2 = ((seed2 >> 16) & 0x7fff) as f32 / 32768.0;
606
607 let noise = (random1 * 2.0 - 1.0) * 0.4 + (random2 * 2.0 - 1.0) * 0.3;
608
609 let freq1 = 8000.0;
610 let freq2 = 6000.0;
611 let phase1 = 2.0 * std::f32::consts::PI * freq1 * t;
612 let phase2 = 2.0 * std::f32::consts::PI * freq2 * t;
613
614 let pitched = phase1.sin() * 0.1 + phase2.sin() * 0.1;
615
616 let amp = (1.0 - progress * 0.7).max(0.0);
617
618 let sample = (noise + pitched) * amp * 0.6;
619 samples.push(sample.clamp(-1.0, 1.0));
620 }
621
622 samples
623}