@devaloop/devalang 0.0.1-beta.2 → 0.0.1-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +84 -81
- package/README.md +3 -2
- package/docs/CHANGELOG.md +41 -0
- package/docs/ROADMAP.md +3 -3
- package/examples/chain.deva +19 -0
- package/examples/plugin.deva +10 -10
- package/examples/routing.deva +23 -0
- package/out-tsc/bin/project-version.json +6 -0
- package/out-tsc/pkg/devalang_core_bg.wasm.d.ts +8 -8
- package/out-tsc/scripts/version/copy-to-binary.d.ts +1 -0
- package/out-tsc/scripts/version/copy-to-binary.js +79 -0
- package/package.json +23 -10
- package/project-version.json +3 -3
- package/rust/bindings/Cargo.toml +9 -0
- package/rust/bindings/src/lib.rs +86 -0
- package/rust/cli/addon/commands.rs +35 -0
- package/rust/cli/addon/download.rs +234 -0
- package/rust/cli/addon/install.rs +33 -0
- package/rust/cli/addon/list.rs +224 -0
- package/rust/cli/addon/metadata.rs +124 -0
- package/rust/cli/addon/mod.rs +8 -0
- package/rust/cli/addon/remove.rs +271 -0
- package/rust/cli/addon/update.rs +305 -0
- package/rust/cli/{install/addon.rs → addon/utils.rs} +109 -118
- package/rust/cli/build/commands.rs +153 -153
- package/rust/cli/build/process.rs +165 -165
- package/rust/cli/check/mod.rs +208 -208
- package/rust/cli/discover/commands.rs +275 -253
- package/rust/cli/discover/config.rs +109 -111
- package/rust/cli/discover/fs.rs +19 -19
- package/rust/cli/discover/install.rs +214 -103
- package/rust/cli/discover/metadata.rs +48 -48
- package/rust/cli/discover/mod.rs +5 -5
- package/rust/cli/me/commands.rs +52 -0
- package/rust/cli/me/mod.rs +1 -0
- package/rust/cli/mod.rs +12 -12
- package/rust/cli/parser.rs +30 -69
- package/rust/cli/play/commands.rs +375 -375
- package/rust/cli/play/process.rs +159 -159
- package/rust/core/audio/engine/driver.rs +19 -2
- package/rust/core/audio/engine/export.rs +169 -169
- package/rust/core/audio/engine/mod.rs +56 -56
- package/rust/core/audio/engine/notes/dsp.rs +88 -85
- package/rust/core/audio/engine/notes/mod.rs +53 -44
- package/rust/core/audio/engine/notes/params.rs +294 -294
- package/rust/core/audio/engine/sample/insert.rs +148 -47
- package/rust/core/audio/engine/sample/mod.rs +40 -40
- package/rust/core/audio/engine/sample/padding.rs +170 -170
- package/rust/core/audio/evaluator/condition.rs +61 -61
- package/rust/core/audio/evaluator/numeric.rs +152 -152
- package/rust/core/audio/evaluator/rhs.rs +16 -16
- package/rust/core/audio/evaluator/string_expr.rs +94 -94
- package/rust/core/audio/interpreter/driver.rs +574 -574
- package/rust/core/audio/interpreter/mod.rs +2 -2
- package/rust/core/audio/interpreter/statements/arrow_call/interprete.rs +9 -5
- package/rust/core/audio/interpreter/statements/arrow_call/methods/chord.rs +398 -384
- package/rust/core/audio/interpreter/statements/arrow_call/methods/effects.rs +323 -0
- package/rust/core/audio/interpreter/statements/arrow_call/methods/mod.rs +1 -0
- package/rust/core/audio/interpreter/statements/arrow_call/methods/note.rs +66 -11
- package/rust/core/audio/interpreter/statements/arrow_call/mod.rs +3 -3
- package/rust/core/audio/interpreter/statements/arrow_call/types/arp.rs +192 -192
- package/rust/core/audio/interpreter/statements/arrow_call/types/mod.rs +24 -24
- package/rust/core/audio/interpreter/statements/arrow_call/types/pad.rs +116 -116
- package/rust/core/audio/interpreter/statements/arrow_call/types/pluck.rs +97 -97
- package/rust/core/audio/interpreter/statements/arrow_call/types/sub.rs +100 -100
- package/rust/core/audio/interpreter/statements/automate.rs +16 -16
- package/rust/core/audio/interpreter/statements/call.rs +31 -1
- package/rust/core/audio/interpreter/statements/condition.rs +72 -72
- package/rust/core/audio/interpreter/statements/function.rs +24 -24
- package/rust/core/audio/interpreter/statements/let_.rs +36 -36
- package/rust/core/audio/interpreter/statements/load.rs +17 -17
- package/rust/core/audio/interpreter/statements/loop_.rs +115 -115
- package/rust/core/audio/interpreter/statements/spawn.rs +51 -2
- package/rust/core/audio/interpreter/statements/trigger.rs +242 -239
- package/rust/core/audio/loader/trigger.rs +98 -98
- package/rust/core/audio/player.rs +70 -70
- package/rust/core/audio/special/mod.rs +9 -9
- package/rust/core/builder/mod.rs +129 -129
- package/rust/core/debugger/lexer.rs +27 -27
- package/rust/core/debugger/logs.rs +52 -52
- package/rust/core/debugger/preprocessor.rs +27 -27
- package/rust/core/debugger/store.rs +38 -38
- package/rust/core/lexer/driver.rs +59 -59
- package/rust/core/lexer/handler/arrow.rs +82 -82
- package/rust/core/lexer/handler/at.rs +21 -21
- package/rust/core/lexer/handler/brace.rs +41 -41
- package/rust/core/lexer/handler/colon.rs +21 -21
- package/rust/core/lexer/handler/comment.rs +30 -30
- package/rust/core/lexer/handler/dot.rs +21 -21
- package/rust/core/lexer/handler/driver.rs +337 -337
- package/rust/core/lexer/handler/identifier.rs +47 -47
- package/rust/core/lexer/handler/indent.rs +66 -66
- package/rust/core/lexer/handler/mod.rs +15 -15
- package/rust/core/lexer/handler/newline.rs +23 -23
- package/rust/core/lexer/handler/number.rs +31 -31
- package/rust/core/lexer/handler/operator.rs +46 -46
- package/rust/core/lexer/handler/parenthesis.rs +41 -41
- package/rust/core/lexer/handler/slash.rs +21 -21
- package/rust/core/lexer/handler/string.rs +63 -63
- package/rust/core/lexer/mod.rs +3 -3
- package/rust/core/mod.rs +9 -9
- package/rust/core/parser/driver/block.rs +111 -111
- package/rust/core/parser/driver/cursor.rs +82 -82
- package/rust/core/parser/driver/driver_impl.rs +21 -1
- package/rust/core/parser/driver/mod.rs +6 -6
- package/rust/core/parser/driver/parse_array.rs +120 -120
- package/rust/core/parser/driver/parse_map.rs +247 -223
- package/rust/core/parser/driver/parser.rs +160 -160
- package/rust/core/parser/handler/arrow_call.rs +65 -14
- package/rust/core/parser/handler/identifier/synth.rs +171 -135
- package/rust/core/parser/handler/mod.rs +9 -9
- package/rust/core/parser/handler/pattern.rs +24 -1
- package/rust/core/plugin/loader.rs +137 -137
- package/rust/core/plugin/mod.rs +2 -2
- package/rust/core/plugin/runner/non_wasm.rs +481 -297
- package/rust/core/plugin/runner/wasm32.rs +1 -0
- package/rust/core/preprocessor/loader/inject.rs +313 -278
- package/rust/core/preprocessor/loader/loader_helpers.rs +110 -110
- package/rust/core/preprocessor/loader/mod.rs +235 -235
- package/rust/core/preprocessor/module.rs +55 -55
- package/rust/core/preprocessor/processor/handlers.rs +107 -107
- package/rust/core/preprocessor/resolver/bank.rs +49 -49
- package/rust/core/preprocessor/resolver/call.rs +124 -124
- package/rust/core/preprocessor/resolver/condition.rs +95 -95
- package/rust/core/preprocessor/resolver/driver.rs +324 -324
- package/rust/core/preprocessor/resolver/function.rs +69 -69
- package/rust/core/preprocessor/resolver/group.rs +122 -122
- package/rust/core/preprocessor/resolver/let_.rs +32 -32
- package/rust/core/preprocessor/resolver/loop_.rs +318 -318
- package/rust/core/preprocessor/resolver/mod.rs +16 -16
- package/rust/core/preprocessor/resolver/pattern.rs +95 -83
- package/rust/core/preprocessor/resolver/spawn.rs +99 -99
- package/rust/core/preprocessor/resolver/synth.rs +54 -54
- package/rust/core/preprocessor/resolver/tempo.rs +48 -48
- package/rust/core/preprocessor/resolver/trigger.rs +116 -116
- package/rust/core/preprocessor/resolver/value.rs +176 -176
- package/rust/core/store/global.rs +57 -57
- package/rust/lib.rs +323 -323
- package/rust/macros/Cargo.toml +14 -0
- package/rust/macros/src/lib.rs +52 -0
- package/rust/main.rs +311 -142
- package/rust/types/Cargo.toml +1 -1
- package/rust/types/src/addons.rs +3 -1
- package/rust/types/src/config.rs +1 -3
- package/rust/utils/Cargo.toml +5 -2
- package/rust/utils/src/file.rs +397 -14
- package/rust/utils/src/path.rs +31 -2
- package/rust/utils/src/version.rs +38 -7
- package/rust/web/auth.rs +5 -0
- package/rust/web/forge.rs +5 -0
- package/rust/web/mod.rs +5 -3
- package/typescript/scripts/version/copy-to-binary.ts +82 -0
- package/rust/cli/bank/api.rs +0 -122
- package/rust/cli/bank/commands.rs +0 -306
- package/rust/cli/bank/mod.rs +0 -29
- package/rust/cli/install/bank.rs +0 -72
- package/rust/cli/install/commands.rs +0 -35
- package/rust/cli/install/mod.rs +0 -4
- package/rust/cli/install/plugin.rs +0 -80
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
use crate::config::ops::load_config;
|
|
1
2
|
use devalang_types::Value;
|
|
2
3
|
use devalang_types::VariableTable;
|
|
3
4
|
use devalang_utils::path::normalize_path;
|
|
4
5
|
use rodio::{Decoder, Source};
|
|
6
|
+
use std::path::PathBuf;
|
|
5
7
|
use std::{collections::HashMap, fs::File, io::BufReader, path::Path};
|
|
6
8
|
|
|
7
9
|
pub fn insert_sample_impl(
|
|
@@ -63,65 +65,164 @@ pub fn insert_sample_impl(
|
|
|
63
65
|
other => other,
|
|
64
66
|
};
|
|
65
67
|
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
68
|
+
// Try both plural and singular folder names (some installs use 'bank' instead of 'banks')
|
|
69
|
+
let singular = if subdir.ends_with('s') {
|
|
70
|
+
&subdir[..subdir.len() - 1]
|
|
71
|
+
} else {
|
|
72
|
+
subdir
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Build a list of candidate addon roots to support different layouts:
|
|
76
|
+
// - legacy flat: .deva/<subdir>/<publisher>.<name>
|
|
77
|
+
// - nested: .deva/<subdir>/<publisher>/<name>
|
|
78
|
+
// Test both plural and singular folder names.
|
|
79
|
+
let mut candidate_roots: Vec<PathBuf> = Vec::new();
|
|
80
|
+
for sd in &[subdir, singular] {
|
|
81
|
+
let base = deva_dir.join(sd).join(bank_name);
|
|
82
|
+
candidate_roots.push(base.clone());
|
|
83
|
+
|
|
84
|
+
if bank_name.contains('.') {
|
|
85
|
+
let mut it = bank_name.splitn(2, '.');
|
|
86
|
+
let pubr = it.next().unwrap_or("");
|
|
87
|
+
let nm = it.next().unwrap_or("");
|
|
88
|
+
candidate_roots.push(deva_dir.join(sd).join(pubr).join(nm));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// If none of the candidate roots yields the asset, we will also
|
|
93
|
+
// try to lookup referenced addons from the project config.
|
|
94
|
+
|
|
95
|
+
// Helper to resolve audio path for a given addon root
|
|
96
|
+
let resolve_from_root = |root: &PathBuf| -> Option<String> {
|
|
97
|
+
// Determine audio dir: prefer audioPath in bank.toml, else audio/
|
|
98
|
+
let mut audio_dir = root.join("audio");
|
|
99
|
+
let bank_toml = root.join("bank.toml");
|
|
100
|
+
if bank_toml.exists() {
|
|
101
|
+
if let Ok(content) = std::fs::read_to_string(&bank_toml) {
|
|
102
|
+
if let Ok(parsed) = toml::from_str::<toml::Value>(&content) {
|
|
103
|
+
if let Some(ap) = parsed
|
|
104
|
+
.get("audioPath")
|
|
105
|
+
.or_else(|| parsed.get("audio_path"))
|
|
106
|
+
.and_then(|v| v.as_str())
|
|
107
|
+
{
|
|
108
|
+
let ap_norm = ap.replace("\\", "/");
|
|
109
|
+
audio_dir = root.join(ap_norm);
|
|
110
|
+
}
|
|
83
111
|
}
|
|
84
112
|
}
|
|
85
113
|
}
|
|
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
114
|
|
|
115
|
+
let candidate = audio_dir.join(&entity_name);
|
|
116
|
+
if candidate.exists() {
|
|
117
|
+
return Some(candidate.to_string_lossy().to_string());
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let has_extension = std::path::Path::new(&entity_name).extension().is_some();
|
|
99
121
|
if !has_extension {
|
|
100
|
-
|
|
101
|
-
let wav_candidate = bank_base.join(format!("{}.wav", entity_name));
|
|
122
|
+
let wav_candidate = audio_dir.join(format!("{}.wav", entity_name));
|
|
102
123
|
if wav_candidate.exists() {
|
|
103
|
-
|
|
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();
|
|
124
|
+
return Some(wav_candidate.to_string_lossy().to_string());
|
|
112
125
|
}
|
|
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
126
|
|
|
127
|
+
let legacy_candidate = root.join(format!("{}.wav", entity_name));
|
|
117
128
|
if legacy_candidate.exists() {
|
|
118
|
-
|
|
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();
|
|
129
|
+
return Some(legacy_candidate.to_string_lossy().to_string());
|
|
122
130
|
}
|
|
131
|
+
} else {
|
|
132
|
+
let legacy_candidate = root.join(&entity_name);
|
|
133
|
+
if legacy_candidate.exists() {
|
|
134
|
+
return Some(legacy_candidate.to_string_lossy().to_string());
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
None
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
let mut found: Option<String> = None;
|
|
142
|
+
for root in &candidate_roots {
|
|
143
|
+
if let Some(p) = resolve_from_root(root) {
|
|
144
|
+
found = Some(p);
|
|
145
|
+
break;
|
|
123
146
|
}
|
|
124
147
|
}
|
|
148
|
+
|
|
149
|
+
// If not found in typical layouts, try to find the addon referenced in the project config
|
|
150
|
+
if found.is_none() {
|
|
151
|
+
if let Ok(config_path) = devalang_utils::path::get_devalang_config_path() {
|
|
152
|
+
if let Some(cfg) = load_config(Some(&config_path)) {
|
|
153
|
+
// Scan banks or plugins depending on obj_type
|
|
154
|
+
if obj_type == "bank" {
|
|
155
|
+
if let Some(banks) = cfg.banks {
|
|
156
|
+
for b in banks {
|
|
157
|
+
if let Some(name_in_path) = b.path.strip_prefix("devalang://bank/")
|
|
158
|
+
{
|
|
159
|
+
// match by exact, suffix, or dot notation
|
|
160
|
+
if name_in_path == bank_name
|
|
161
|
+
|| name_in_path.ends_with(bank_name)
|
|
162
|
+
{
|
|
163
|
+
let root = deva_dir.join(subdir).join(name_in_path);
|
|
164
|
+
if let Some(p) = resolve_from_root(&root) {
|
|
165
|
+
found = Some(p);
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
// try nested layout
|
|
169
|
+
if name_in_path.contains('.') {
|
|
170
|
+
let mut it = name_in_path.splitn(2, '.');
|
|
171
|
+
let pubr = it.next().unwrap_or("");
|
|
172
|
+
let nm = it.next().unwrap_or("");
|
|
173
|
+
let root2 = deva_dir.join(subdir).join(pubr).join(nm);
|
|
174
|
+
if let Some(p) = resolve_from_root(&root2) {
|
|
175
|
+
found = Some(p);
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
} else if obj_type == "plugin" {
|
|
184
|
+
if let Some(plugins) = cfg.plugins {
|
|
185
|
+
for p in plugins {
|
|
186
|
+
if let Some(name_in_path) =
|
|
187
|
+
p.path.strip_prefix("devalang://plugin/")
|
|
188
|
+
{
|
|
189
|
+
if name_in_path == bank_name
|
|
190
|
+
|| name_in_path.ends_with(bank_name)
|
|
191
|
+
{
|
|
192
|
+
let root = deva_dir.join(subdir).join(name_in_path);
|
|
193
|
+
if let Some(path_found) = resolve_from_root(&root) {
|
|
194
|
+
found = Some(path_found);
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
if name_in_path.contains('.') {
|
|
198
|
+
let mut it = name_in_path.splitn(2, '.');
|
|
199
|
+
let pubr = it.next().unwrap_or("");
|
|
200
|
+
let nm = it.next().unwrap_or("");
|
|
201
|
+
let root2 = deva_dir.join(subdir).join(pubr).join(nm);
|
|
202
|
+
if let Some(path_found) = resolve_from_root(&root2) {
|
|
203
|
+
found = Some(path_found);
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if let Some(p) = found {
|
|
217
|
+
resolved_path = p;
|
|
218
|
+
} else {
|
|
219
|
+
// Not found; fallback to legacy candidate for error message
|
|
220
|
+
let legacy = deva_dir
|
|
221
|
+
.join(subdir)
|
|
222
|
+
.join(bank_name)
|
|
223
|
+
.join(format!("{}.wav", entity_name));
|
|
224
|
+
resolved_path = legacy.to_string_lossy().to_string();
|
|
225
|
+
}
|
|
125
226
|
} else {
|
|
126
227
|
let entry_dir = module_root.parent().unwrap_or(&root);
|
|
127
228
|
let absolute_path = root.join(entry_dir).join(&var_path);
|
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
pub mod insert;
|
|
2
|
-
pub mod padding;
|
|
3
|
-
|
|
4
|
-
use devalang_types::Value;
|
|
5
|
-
use devalang_types::VariableTable;
|
|
6
|
-
use std::collections::HashMap;
|
|
7
|
-
|
|
8
|
-
impl super::driver::AudioEngine {
|
|
9
|
-
pub fn insert_sample(
|
|
10
|
-
&mut self,
|
|
11
|
-
filepath: &str,
|
|
12
|
-
time_secs: f32,
|
|
13
|
-
dur_sec: f32,
|
|
14
|
-
effects: Option<HashMap<String, Value>>,
|
|
15
|
-
variable_table: &VariableTable,
|
|
16
|
-
) {
|
|
17
|
-
crate::core::audio::engine::sample::insert::insert_sample_impl(
|
|
18
|
-
self,
|
|
19
|
-
filepath,
|
|
20
|
-
time_secs,
|
|
21
|
-
dur_sec,
|
|
22
|
-
effects,
|
|
23
|
-
variable_table,
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
pub fn pad_samples(
|
|
28
|
-
&mut self,
|
|
29
|
-
samples: &[i16],
|
|
30
|
-
time_secs: f32,
|
|
31
|
-
effects_map: Option<HashMap<String, Value>>,
|
|
32
|
-
) {
|
|
33
|
-
crate::core::audio::engine::sample::padding::pad_samples_impl(
|
|
34
|
-
self,
|
|
35
|
-
samples,
|
|
36
|
-
time_secs,
|
|
37
|
-
effects_map,
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
1
|
+
pub mod insert;
|
|
2
|
+
pub mod padding;
|
|
3
|
+
|
|
4
|
+
use devalang_types::Value;
|
|
5
|
+
use devalang_types::VariableTable;
|
|
6
|
+
use std::collections::HashMap;
|
|
7
|
+
|
|
8
|
+
impl super::driver::AudioEngine {
|
|
9
|
+
pub fn insert_sample(
|
|
10
|
+
&mut self,
|
|
11
|
+
filepath: &str,
|
|
12
|
+
time_secs: f32,
|
|
13
|
+
dur_sec: f32,
|
|
14
|
+
effects: Option<HashMap<String, Value>>,
|
|
15
|
+
variable_table: &VariableTable,
|
|
16
|
+
) {
|
|
17
|
+
crate::core::audio::engine::sample::insert::insert_sample_impl(
|
|
18
|
+
self,
|
|
19
|
+
filepath,
|
|
20
|
+
time_secs,
|
|
21
|
+
dur_sec,
|
|
22
|
+
effects,
|
|
23
|
+
variable_table,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
pub fn pad_samples(
|
|
28
|
+
&mut self,
|
|
29
|
+
samples: &[i16],
|
|
30
|
+
time_secs: f32,
|
|
31
|
+
effects_map: Option<HashMap<String, Value>>,
|
|
32
|
+
) {
|
|
33
|
+
crate::core::audio::engine::sample::padding::pad_samples_impl(
|
|
34
|
+
self,
|
|
35
|
+
samples,
|
|
36
|
+
time_secs,
|
|
37
|
+
effects_map,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -1,170 +1,170 @@
|
|
|
1
|
-
use devalang_types::Value;
|
|
2
|
-
use std::collections::HashMap;
|
|
3
|
-
|
|
4
|
-
pub fn pad_samples_impl(
|
|
5
|
-
engine: &mut crate::core::audio::engine::driver::AudioEngine,
|
|
6
|
-
samples: &[i16],
|
|
7
|
-
time_secs: f32,
|
|
8
|
-
effects_map: Option<HashMap<String, Value>>,
|
|
9
|
-
) {
|
|
10
|
-
let sample_rate = engine.sample_rate as f32;
|
|
11
|
-
let channels = engine.channels as usize;
|
|
12
|
-
|
|
13
|
-
let offset = (time_secs * (sample_rate) * (channels as f32)) as usize;
|
|
14
|
-
let total_samples = samples.len();
|
|
15
|
-
|
|
16
|
-
let mut gain = 1.0;
|
|
17
|
-
let mut pan = 0.0;
|
|
18
|
-
let mut fade_in = 0.0;
|
|
19
|
-
let mut fade_out = 0.0;
|
|
20
|
-
let mut pitch = 1.0;
|
|
21
|
-
let mut drive = 0.0;
|
|
22
|
-
let mut reverb = 0.0;
|
|
23
|
-
let mut delay = 0.0; // delay time in seconds
|
|
24
|
-
let delay_feedback = 0.35; // default feedback
|
|
25
|
-
|
|
26
|
-
if let Some(map) = &effects_map {
|
|
27
|
-
for (key, val) in map {
|
|
28
|
-
match (key.as_str(), val) {
|
|
29
|
-
("gain", Value::Number(v)) => {
|
|
30
|
-
gain = *v;
|
|
31
|
-
}
|
|
32
|
-
("pan", Value::Number(v)) => {
|
|
33
|
-
pan = *v;
|
|
34
|
-
}
|
|
35
|
-
("fadeIn", Value::Number(v)) => {
|
|
36
|
-
fade_in = *v;
|
|
37
|
-
}
|
|
38
|
-
("fadeOut", Value::Number(v)) => {
|
|
39
|
-
fade_out = *v;
|
|
40
|
-
}
|
|
41
|
-
("pitch", Value::Number(v)) => {
|
|
42
|
-
pitch = *v;
|
|
43
|
-
}
|
|
44
|
-
("drive", Value::Number(v)) => {
|
|
45
|
-
drive = *v;
|
|
46
|
-
}
|
|
47
|
-
("reverb", Value::Number(v)) => {
|
|
48
|
-
reverb = *v;
|
|
49
|
-
}
|
|
50
|
-
("delay", Value::Number(v)) => {
|
|
51
|
-
delay = *v;
|
|
52
|
-
}
|
|
53
|
-
_ => eprintln!("⚠️ Unknown or invalid effect '{}'", key),
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
let fade_in_samples = (fade_in * (sample_rate)) as usize;
|
|
59
|
-
let fade_out_samples = (fade_out * (sample_rate)) as usize;
|
|
60
|
-
|
|
61
|
-
// If no fade specified, apply a tiny default fade (2 ms) when sample boundaries are non-zero
|
|
62
|
-
let default_boundary_fade_ms = 1.0_f32; // 1 ms
|
|
63
|
-
let default_fade_samples = (default_boundary_fade_ms * (sample_rate)) as usize;
|
|
64
|
-
let mut effective_fade_in = fade_in_samples;
|
|
65
|
-
let mut effective_fade_out = fade_out_samples;
|
|
66
|
-
if effective_fade_in == 0 {
|
|
67
|
-
if let Some(&first) = samples.first() {
|
|
68
|
-
if first.abs() > 64 {
|
|
69
|
-
// increased threshold to detect only strong abrupt starts
|
|
70
|
-
effective_fade_in = default_fade_samples.max(1);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
if effective_fade_out == 0 {
|
|
75
|
-
if let Some(&last) = samples.last() {
|
|
76
|
-
if last.abs() > 64 {
|
|
77
|
-
// increased threshold to detect only strong abrupt ends
|
|
78
|
-
effective_fade_out = default_fade_samples.max(1);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Ensure fades do not exceed half the sample length to avoid silencing short samples
|
|
84
|
-
if total_samples > 0 {
|
|
85
|
-
let cap = total_samples / 2;
|
|
86
|
-
if effective_fade_in > cap {
|
|
87
|
-
effective_fade_in = cap.max(1);
|
|
88
|
-
}
|
|
89
|
-
if effective_fade_out > cap {
|
|
90
|
-
effective_fade_out = cap.max(1);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
let delay_samples = if delay > 0.0 {
|
|
95
|
-
(delay * (sample_rate)) as usize
|
|
96
|
-
} else {
|
|
97
|
-
0
|
|
98
|
-
};
|
|
99
|
-
let mut delay_buffer: Vec<f32> = vec![0.0; total_samples + delay_samples];
|
|
100
|
-
|
|
101
|
-
for i in 0..total_samples {
|
|
102
|
-
let pitch_index = if pitch != 1.0 {
|
|
103
|
-
((i as f32) / pitch) as usize
|
|
104
|
-
} else {
|
|
105
|
-
i
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
let mut adjusted = if pitch_index < total_samples {
|
|
109
|
-
samples[pitch_index] as f32
|
|
110
|
-
} else {
|
|
111
|
-
0.0
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
adjusted *= gain;
|
|
115
|
-
|
|
116
|
-
if effective_fade_in > 0 && i < effective_fade_in {
|
|
117
|
-
if effective_fade_in == 1 {
|
|
118
|
-
adjusted *= 0.0;
|
|
119
|
-
} else {
|
|
120
|
-
adjusted *= (i as f32) / (effective_fade_in as f32);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
if effective_fade_out > 0 && i >= total_samples.saturating_sub(effective_fade_out) {
|
|
124
|
-
if effective_fade_out == 1 {
|
|
125
|
-
adjusted *= 0.0;
|
|
126
|
-
} else {
|
|
127
|
-
adjusted *= ((total_samples - 1 - i) as f32) / ((effective_fade_out - 1) as f32);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if drive > 0.0 {
|
|
132
|
-
let normalized = adjusted / (i16::MAX as f32);
|
|
133
|
-
let pre_gain = (10f32).powf(drive / 20.0);
|
|
134
|
-
let driven = (normalized * pre_gain).tanh();
|
|
135
|
-
adjusted = driven * (i16::MAX as f32);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if delay_samples > 0 && i >= delay_samples {
|
|
139
|
-
let echo = delay_buffer[i - delay_samples] * delay_feedback;
|
|
140
|
-
adjusted += echo;
|
|
141
|
-
}
|
|
142
|
-
if delay_samples > 0 {
|
|
143
|
-
delay_buffer[i] = adjusted;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if reverb > 0.0 {
|
|
147
|
-
let reverb_delay = (0.03 * (sample_rate)) as usize;
|
|
148
|
-
if i >= reverb_delay {
|
|
149
|
-
adjusted += (engine.buffer[offset + i - reverb_delay] as f32) * reverb;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
let adjusted_sample = adjusted.round().clamp(i16::MIN as f32, i16::MAX as f32) as i16;
|
|
154
|
-
|
|
155
|
-
let (left_gain, right_gain) = crate::core::audio::engine::helpers::pan_gains(pan);
|
|
156
|
-
|
|
157
|
-
let left = ((adjusted_sample as f32) * left_gain) as i16;
|
|
158
|
-
let right = ((adjusted_sample as f32) * right_gain) as i16;
|
|
159
|
-
|
|
160
|
-
// For interleaved buffer with `channels` channels, each frame has `channels` samples.
|
|
161
|
-
// left channel at frame i is at offset + i * channels, right at +1.
|
|
162
|
-
let left_pos = offset + i * channels;
|
|
163
|
-
let right_pos = left_pos + 1;
|
|
164
|
-
|
|
165
|
-
if right_pos < engine.buffer.len() {
|
|
166
|
-
engine.buffer[left_pos] = engine.buffer[left_pos].saturating_add(left);
|
|
167
|
-
engine.buffer[right_pos] = engine.buffer[right_pos].saturating_add(right);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
1
|
+
use devalang_types::Value;
|
|
2
|
+
use std::collections::HashMap;
|
|
3
|
+
|
|
4
|
+
pub fn pad_samples_impl(
|
|
5
|
+
engine: &mut crate::core::audio::engine::driver::AudioEngine,
|
|
6
|
+
samples: &[i16],
|
|
7
|
+
time_secs: f32,
|
|
8
|
+
effects_map: Option<HashMap<String, Value>>,
|
|
9
|
+
) {
|
|
10
|
+
let sample_rate = engine.sample_rate as f32;
|
|
11
|
+
let channels = engine.channels as usize;
|
|
12
|
+
|
|
13
|
+
let offset = (time_secs * (sample_rate) * (channels as f32)) as usize;
|
|
14
|
+
let total_samples = samples.len();
|
|
15
|
+
|
|
16
|
+
let mut gain = 1.0;
|
|
17
|
+
let mut pan = 0.0;
|
|
18
|
+
let mut fade_in = 0.0;
|
|
19
|
+
let mut fade_out = 0.0;
|
|
20
|
+
let mut pitch = 1.0;
|
|
21
|
+
let mut drive = 0.0;
|
|
22
|
+
let mut reverb = 0.0;
|
|
23
|
+
let mut delay = 0.0; // delay time in seconds
|
|
24
|
+
let delay_feedback = 0.35; // default feedback
|
|
25
|
+
|
|
26
|
+
if let Some(map) = &effects_map {
|
|
27
|
+
for (key, val) in map {
|
|
28
|
+
match (key.as_str(), val) {
|
|
29
|
+
("gain", Value::Number(v)) => {
|
|
30
|
+
gain = *v;
|
|
31
|
+
}
|
|
32
|
+
("pan", Value::Number(v)) => {
|
|
33
|
+
pan = *v;
|
|
34
|
+
}
|
|
35
|
+
("fadeIn", Value::Number(v)) => {
|
|
36
|
+
fade_in = *v;
|
|
37
|
+
}
|
|
38
|
+
("fadeOut", Value::Number(v)) => {
|
|
39
|
+
fade_out = *v;
|
|
40
|
+
}
|
|
41
|
+
("pitch", Value::Number(v)) => {
|
|
42
|
+
pitch = *v;
|
|
43
|
+
}
|
|
44
|
+
("drive", Value::Number(v)) => {
|
|
45
|
+
drive = *v;
|
|
46
|
+
}
|
|
47
|
+
("reverb", Value::Number(v)) => {
|
|
48
|
+
reverb = *v;
|
|
49
|
+
}
|
|
50
|
+
("delay", Value::Number(v)) => {
|
|
51
|
+
delay = *v;
|
|
52
|
+
}
|
|
53
|
+
_ => eprintln!("⚠️ Unknown or invalid effect '{}'", key),
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let fade_in_samples = (fade_in * (sample_rate)) as usize;
|
|
59
|
+
let fade_out_samples = (fade_out * (sample_rate)) as usize;
|
|
60
|
+
|
|
61
|
+
// If no fade specified, apply a tiny default fade (2 ms) when sample boundaries are non-zero
|
|
62
|
+
let default_boundary_fade_ms = 1.0_f32; // 1 ms
|
|
63
|
+
let default_fade_samples = (default_boundary_fade_ms * (sample_rate)) as usize;
|
|
64
|
+
let mut effective_fade_in = fade_in_samples;
|
|
65
|
+
let mut effective_fade_out = fade_out_samples;
|
|
66
|
+
if effective_fade_in == 0 {
|
|
67
|
+
if let Some(&first) = samples.first() {
|
|
68
|
+
if first.abs() > 64 {
|
|
69
|
+
// increased threshold to detect only strong abrupt starts
|
|
70
|
+
effective_fade_in = default_fade_samples.max(1);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if effective_fade_out == 0 {
|
|
75
|
+
if let Some(&last) = samples.last() {
|
|
76
|
+
if last.abs() > 64 {
|
|
77
|
+
// increased threshold to detect only strong abrupt ends
|
|
78
|
+
effective_fade_out = default_fade_samples.max(1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Ensure fades do not exceed half the sample length to avoid silencing short samples
|
|
84
|
+
if total_samples > 0 {
|
|
85
|
+
let cap = total_samples / 2;
|
|
86
|
+
if effective_fade_in > cap {
|
|
87
|
+
effective_fade_in = cap.max(1);
|
|
88
|
+
}
|
|
89
|
+
if effective_fade_out > cap {
|
|
90
|
+
effective_fade_out = cap.max(1);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let delay_samples = if delay > 0.0 {
|
|
95
|
+
(delay * (sample_rate)) as usize
|
|
96
|
+
} else {
|
|
97
|
+
0
|
|
98
|
+
};
|
|
99
|
+
let mut delay_buffer: Vec<f32> = vec![0.0; total_samples + delay_samples];
|
|
100
|
+
|
|
101
|
+
for i in 0..total_samples {
|
|
102
|
+
let pitch_index = if pitch != 1.0 {
|
|
103
|
+
((i as f32) / pitch) as usize
|
|
104
|
+
} else {
|
|
105
|
+
i
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
let mut adjusted = if pitch_index < total_samples {
|
|
109
|
+
samples[pitch_index] as f32
|
|
110
|
+
} else {
|
|
111
|
+
0.0
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
adjusted *= gain;
|
|
115
|
+
|
|
116
|
+
if effective_fade_in > 0 && i < effective_fade_in {
|
|
117
|
+
if effective_fade_in == 1 {
|
|
118
|
+
adjusted *= 0.0;
|
|
119
|
+
} else {
|
|
120
|
+
adjusted *= (i as f32) / (effective_fade_in as f32);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if effective_fade_out > 0 && i >= total_samples.saturating_sub(effective_fade_out) {
|
|
124
|
+
if effective_fade_out == 1 {
|
|
125
|
+
adjusted *= 0.0;
|
|
126
|
+
} else {
|
|
127
|
+
adjusted *= ((total_samples - 1 - i) as f32) / ((effective_fade_out - 1) as f32);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if drive > 0.0 {
|
|
132
|
+
let normalized = adjusted / (i16::MAX as f32);
|
|
133
|
+
let pre_gain = (10f32).powf(drive / 20.0);
|
|
134
|
+
let driven = (normalized * pre_gain).tanh();
|
|
135
|
+
adjusted = driven * (i16::MAX as f32);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if delay_samples > 0 && i >= delay_samples {
|
|
139
|
+
let echo = delay_buffer[i - delay_samples] * delay_feedback;
|
|
140
|
+
adjusted += echo;
|
|
141
|
+
}
|
|
142
|
+
if delay_samples > 0 {
|
|
143
|
+
delay_buffer[i] = adjusted;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if reverb > 0.0 {
|
|
147
|
+
let reverb_delay = (0.03 * (sample_rate)) as usize;
|
|
148
|
+
if i >= reverb_delay {
|
|
149
|
+
adjusted += (engine.buffer[offset + i - reverb_delay] as f32) * reverb;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let adjusted_sample = adjusted.round().clamp(i16::MIN as f32, i16::MAX as f32) as i16;
|
|
154
|
+
|
|
155
|
+
let (left_gain, right_gain) = crate::core::audio::engine::helpers::pan_gains(pan);
|
|
156
|
+
|
|
157
|
+
let left = ((adjusted_sample as f32) * left_gain) as i16;
|
|
158
|
+
let right = ((adjusted_sample as f32) * right_gain) as i16;
|
|
159
|
+
|
|
160
|
+
// For interleaved buffer with `channels` channels, each frame has `channels` samples.
|
|
161
|
+
// left channel at frame i is at offset + i * channels, right at +1.
|
|
162
|
+
let left_pos = offset + i * channels;
|
|
163
|
+
let right_pos = left_pos + 1;
|
|
164
|
+
|
|
165
|
+
if right_pos < engine.buffer.len() {
|
|
166
|
+
engine.buffer[left_pos] = engine.buffer[left_pos].saturating_add(left);
|
|
167
|
+
engine.buffer[right_pos] = engine.buffer[right_pos].saturating_add(right);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|