@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.
Files changed (159) hide show
  1. package/Cargo.toml +84 -81
  2. package/README.md +3 -2
  3. package/docs/CHANGELOG.md +41 -0
  4. package/docs/ROADMAP.md +3 -3
  5. package/examples/chain.deva +19 -0
  6. package/examples/plugin.deva +10 -10
  7. package/examples/routing.deva +23 -0
  8. package/out-tsc/bin/project-version.json +6 -0
  9. package/out-tsc/pkg/devalang_core_bg.wasm.d.ts +8 -8
  10. package/out-tsc/scripts/version/copy-to-binary.d.ts +1 -0
  11. package/out-tsc/scripts/version/copy-to-binary.js +79 -0
  12. package/package.json +23 -10
  13. package/project-version.json +3 -3
  14. package/rust/bindings/Cargo.toml +9 -0
  15. package/rust/bindings/src/lib.rs +86 -0
  16. package/rust/cli/addon/commands.rs +35 -0
  17. package/rust/cli/addon/download.rs +234 -0
  18. package/rust/cli/addon/install.rs +33 -0
  19. package/rust/cli/addon/list.rs +224 -0
  20. package/rust/cli/addon/metadata.rs +124 -0
  21. package/rust/cli/addon/mod.rs +8 -0
  22. package/rust/cli/addon/remove.rs +271 -0
  23. package/rust/cli/addon/update.rs +305 -0
  24. package/rust/cli/{install/addon.rs → addon/utils.rs} +109 -118
  25. package/rust/cli/build/commands.rs +153 -153
  26. package/rust/cli/build/process.rs +165 -165
  27. package/rust/cli/check/mod.rs +208 -208
  28. package/rust/cli/discover/commands.rs +275 -253
  29. package/rust/cli/discover/config.rs +109 -111
  30. package/rust/cli/discover/fs.rs +19 -19
  31. package/rust/cli/discover/install.rs +214 -103
  32. package/rust/cli/discover/metadata.rs +48 -48
  33. package/rust/cli/discover/mod.rs +5 -5
  34. package/rust/cli/me/commands.rs +52 -0
  35. package/rust/cli/me/mod.rs +1 -0
  36. package/rust/cli/mod.rs +12 -12
  37. package/rust/cli/parser.rs +30 -69
  38. package/rust/cli/play/commands.rs +375 -375
  39. package/rust/cli/play/process.rs +159 -159
  40. package/rust/core/audio/engine/driver.rs +19 -2
  41. package/rust/core/audio/engine/export.rs +169 -169
  42. package/rust/core/audio/engine/mod.rs +56 -56
  43. package/rust/core/audio/engine/notes/dsp.rs +88 -85
  44. package/rust/core/audio/engine/notes/mod.rs +53 -44
  45. package/rust/core/audio/engine/notes/params.rs +294 -294
  46. package/rust/core/audio/engine/sample/insert.rs +148 -47
  47. package/rust/core/audio/engine/sample/mod.rs +40 -40
  48. package/rust/core/audio/engine/sample/padding.rs +170 -170
  49. package/rust/core/audio/evaluator/condition.rs +61 -61
  50. package/rust/core/audio/evaluator/numeric.rs +152 -152
  51. package/rust/core/audio/evaluator/rhs.rs +16 -16
  52. package/rust/core/audio/evaluator/string_expr.rs +94 -94
  53. package/rust/core/audio/interpreter/driver.rs +574 -574
  54. package/rust/core/audio/interpreter/mod.rs +2 -2
  55. package/rust/core/audio/interpreter/statements/arrow_call/interprete.rs +9 -5
  56. package/rust/core/audio/interpreter/statements/arrow_call/methods/chord.rs +398 -384
  57. package/rust/core/audio/interpreter/statements/arrow_call/methods/effects.rs +323 -0
  58. package/rust/core/audio/interpreter/statements/arrow_call/methods/mod.rs +1 -0
  59. package/rust/core/audio/interpreter/statements/arrow_call/methods/note.rs +66 -11
  60. package/rust/core/audio/interpreter/statements/arrow_call/mod.rs +3 -3
  61. package/rust/core/audio/interpreter/statements/arrow_call/types/arp.rs +192 -192
  62. package/rust/core/audio/interpreter/statements/arrow_call/types/mod.rs +24 -24
  63. package/rust/core/audio/interpreter/statements/arrow_call/types/pad.rs +116 -116
  64. package/rust/core/audio/interpreter/statements/arrow_call/types/pluck.rs +97 -97
  65. package/rust/core/audio/interpreter/statements/arrow_call/types/sub.rs +100 -100
  66. package/rust/core/audio/interpreter/statements/automate.rs +16 -16
  67. package/rust/core/audio/interpreter/statements/call.rs +31 -1
  68. package/rust/core/audio/interpreter/statements/condition.rs +72 -72
  69. package/rust/core/audio/interpreter/statements/function.rs +24 -24
  70. package/rust/core/audio/interpreter/statements/let_.rs +36 -36
  71. package/rust/core/audio/interpreter/statements/load.rs +17 -17
  72. package/rust/core/audio/interpreter/statements/loop_.rs +115 -115
  73. package/rust/core/audio/interpreter/statements/spawn.rs +51 -2
  74. package/rust/core/audio/interpreter/statements/trigger.rs +242 -239
  75. package/rust/core/audio/loader/trigger.rs +98 -98
  76. package/rust/core/audio/player.rs +70 -70
  77. package/rust/core/audio/special/mod.rs +9 -9
  78. package/rust/core/builder/mod.rs +129 -129
  79. package/rust/core/debugger/lexer.rs +27 -27
  80. package/rust/core/debugger/logs.rs +52 -52
  81. package/rust/core/debugger/preprocessor.rs +27 -27
  82. package/rust/core/debugger/store.rs +38 -38
  83. package/rust/core/lexer/driver.rs +59 -59
  84. package/rust/core/lexer/handler/arrow.rs +82 -82
  85. package/rust/core/lexer/handler/at.rs +21 -21
  86. package/rust/core/lexer/handler/brace.rs +41 -41
  87. package/rust/core/lexer/handler/colon.rs +21 -21
  88. package/rust/core/lexer/handler/comment.rs +30 -30
  89. package/rust/core/lexer/handler/dot.rs +21 -21
  90. package/rust/core/lexer/handler/driver.rs +337 -337
  91. package/rust/core/lexer/handler/identifier.rs +47 -47
  92. package/rust/core/lexer/handler/indent.rs +66 -66
  93. package/rust/core/lexer/handler/mod.rs +15 -15
  94. package/rust/core/lexer/handler/newline.rs +23 -23
  95. package/rust/core/lexer/handler/number.rs +31 -31
  96. package/rust/core/lexer/handler/operator.rs +46 -46
  97. package/rust/core/lexer/handler/parenthesis.rs +41 -41
  98. package/rust/core/lexer/handler/slash.rs +21 -21
  99. package/rust/core/lexer/handler/string.rs +63 -63
  100. package/rust/core/lexer/mod.rs +3 -3
  101. package/rust/core/mod.rs +9 -9
  102. package/rust/core/parser/driver/block.rs +111 -111
  103. package/rust/core/parser/driver/cursor.rs +82 -82
  104. package/rust/core/parser/driver/driver_impl.rs +21 -1
  105. package/rust/core/parser/driver/mod.rs +6 -6
  106. package/rust/core/parser/driver/parse_array.rs +120 -120
  107. package/rust/core/parser/driver/parse_map.rs +247 -223
  108. package/rust/core/parser/driver/parser.rs +160 -160
  109. package/rust/core/parser/handler/arrow_call.rs +65 -14
  110. package/rust/core/parser/handler/identifier/synth.rs +171 -135
  111. package/rust/core/parser/handler/mod.rs +9 -9
  112. package/rust/core/parser/handler/pattern.rs +24 -1
  113. package/rust/core/plugin/loader.rs +137 -137
  114. package/rust/core/plugin/mod.rs +2 -2
  115. package/rust/core/plugin/runner/non_wasm.rs +481 -297
  116. package/rust/core/plugin/runner/wasm32.rs +1 -0
  117. package/rust/core/preprocessor/loader/inject.rs +313 -278
  118. package/rust/core/preprocessor/loader/loader_helpers.rs +110 -110
  119. package/rust/core/preprocessor/loader/mod.rs +235 -235
  120. package/rust/core/preprocessor/module.rs +55 -55
  121. package/rust/core/preprocessor/processor/handlers.rs +107 -107
  122. package/rust/core/preprocessor/resolver/bank.rs +49 -49
  123. package/rust/core/preprocessor/resolver/call.rs +124 -124
  124. package/rust/core/preprocessor/resolver/condition.rs +95 -95
  125. package/rust/core/preprocessor/resolver/driver.rs +324 -324
  126. package/rust/core/preprocessor/resolver/function.rs +69 -69
  127. package/rust/core/preprocessor/resolver/group.rs +122 -122
  128. package/rust/core/preprocessor/resolver/let_.rs +32 -32
  129. package/rust/core/preprocessor/resolver/loop_.rs +318 -318
  130. package/rust/core/preprocessor/resolver/mod.rs +16 -16
  131. package/rust/core/preprocessor/resolver/pattern.rs +95 -83
  132. package/rust/core/preprocessor/resolver/spawn.rs +99 -99
  133. package/rust/core/preprocessor/resolver/synth.rs +54 -54
  134. package/rust/core/preprocessor/resolver/tempo.rs +48 -48
  135. package/rust/core/preprocessor/resolver/trigger.rs +116 -116
  136. package/rust/core/preprocessor/resolver/value.rs +176 -176
  137. package/rust/core/store/global.rs +57 -57
  138. package/rust/lib.rs +323 -323
  139. package/rust/macros/Cargo.toml +14 -0
  140. package/rust/macros/src/lib.rs +52 -0
  141. package/rust/main.rs +311 -142
  142. package/rust/types/Cargo.toml +1 -1
  143. package/rust/types/src/addons.rs +3 -1
  144. package/rust/types/src/config.rs +1 -3
  145. package/rust/utils/Cargo.toml +5 -2
  146. package/rust/utils/src/file.rs +397 -14
  147. package/rust/utils/src/path.rs +31 -2
  148. package/rust/utils/src/version.rs +38 -7
  149. package/rust/web/auth.rs +5 -0
  150. package/rust/web/forge.rs +5 -0
  151. package/rust/web/mod.rs +5 -3
  152. package/typescript/scripts/version/copy-to-binary.ts +82 -0
  153. package/rust/cli/bank/api.rs +0 -122
  154. package/rust/cli/bank/commands.rs +0 -306
  155. package/rust/cli/bank/mod.rs +0 -29
  156. package/rust/cli/install/bank.rs +0 -72
  157. package/rust/cli/install/commands.rs +0 -35
  158. package/rust/cli/install/mod.rs +0 -4
  159. 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
- // 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);
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
- // Try appending .wav as a fallback for shorthand names without extension
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
- 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();
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
- 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();
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
+ }