devalang_core/core/audio/engine/sample/
insert.rs

1use 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        // Rejoin the remainder as the entity path so bank entries can contain
48        // nested paths like "subdir/sample.wav" or plain names.
49        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        // Determine the bank audio base directory. Prefer an optional
67        // `audioPath` declared in the bank's bank.toml (supports keys
68        // `audioPath` or `audio_path`). If absent, fall back to `audio/`.
69        let mut audio_dir = deva_dir.join(subdir).join(bank_name).join("audio");
70        // Try to read bank.toml to get audioPath
71        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                        // normalize separators
81                        let ap_norm = ap.replace("\\", "/");
82                        audio_dir = deva_dir.join(subdir).join(bank_name).join(ap_norm);
83                    }
84                }
85            }
86        }
87        // Force looking into the computed audio_dir. If the entity_name
88        // already contains an extension (e.g. .wav/.mp3) or a nested path,
89        // preserve it as-is. Otherwise, try with a .wav extension.
90        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            // Detect whether the provided entity already includes an extension.
97            let has_extension = std::path::Path::new(&entity_name).extension().is_some();
98
99            if !has_extension {
100                // Try appending .wav as a fallback for shorthand names without extension
101                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                    // Last resort: use the legacy location (no audio/), also with .wav
106                    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                // If an extension was specified, don't append .wav; try legacy location
115                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                    // No file found; fall back to the audio candidate path (even if missing)
121                    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    // Read frames from decoder and convert to mono if needed.
154    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    // Convert interleaved channels to mono by averaging channels per frame.
163    // Apply a small RMS-preserving scale so mono level is similar to mixed stereo.
164    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}