@devaloop/devalang 0.0.1-beta.1 → 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 (207) hide show
  1. package/.devalang +9 -10
  2. package/Cargo.toml +84 -80
  3. package/README.md +10 -7
  4. package/docs/CHANGELOG.md +83 -0
  5. package/docs/ROADMAP.md +6 -2
  6. package/docs/TODO.md +3 -14
  7. package/examples/bus.deva +10 -0
  8. package/examples/chain.deva +19 -0
  9. package/examples/effect.deva +2 -0
  10. package/examples/filter.deva +11 -0
  11. package/examples/lfo.deva +9 -0
  12. package/examples/plugin.deva +10 -10
  13. package/examples/routing.deva +23 -0
  14. package/examples/synth.deva +11 -1
  15. package/examples/synth_types.deva +17 -0
  16. package/out-tsc/bin/project-version.json +6 -0
  17. package/out-tsc/core/functions/index.d.ts +5 -0
  18. package/out-tsc/core/functions/index.js +11 -0
  19. package/out-tsc/pkg/devalang_core.d.ts +2 -0
  20. package/out-tsc/pkg/devalang_core.js +17 -2
  21. package/out-tsc/pkg/devalang_core_bg.wasm.d.ts +1 -0
  22. package/out-tsc/scripts/version/copy-to-binary.d.ts +1 -0
  23. package/out-tsc/scripts/version/copy-to-binary.js +79 -0
  24. package/package.json +23 -10
  25. package/project-version.json +3 -3
  26. package/rust/bindings/Cargo.toml +9 -0
  27. package/rust/bindings/src/lib.rs +86 -0
  28. package/rust/cli/addon/commands.rs +35 -0
  29. package/rust/cli/addon/download.rs +234 -0
  30. package/rust/cli/addon/install.rs +33 -0
  31. package/rust/cli/addon/list.rs +224 -0
  32. package/rust/cli/addon/metadata.rs +124 -0
  33. package/rust/cli/addon/mod.rs +8 -0
  34. package/rust/cli/addon/remove.rs +271 -0
  35. package/rust/cli/addon/update.rs +305 -0
  36. package/rust/cli/{install/addon.rs → addon/utils.rs} +34 -43
  37. package/rust/cli/build/commands.rs +153 -103
  38. package/rust/cli/build/mod.rs +2 -2
  39. package/rust/cli/build/process.rs +165 -146
  40. package/rust/cli/check/mod.rs +208 -208
  41. package/rust/cli/discover/commands.rs +53 -31
  42. package/rust/cli/discover/config.rs +2 -4
  43. package/rust/cli/discover/install.rs +139 -28
  44. package/rust/cli/discover/metadata.rs +3 -3
  45. package/rust/cli/login/commands.rs +124 -124
  46. package/rust/cli/me/commands.rs +52 -0
  47. package/rust/cli/me/mod.rs +1 -0
  48. package/rust/cli/mod.rs +2 -2
  49. package/rust/cli/parser.rs +76 -70
  50. package/rust/cli/play/commands.rs +375 -324
  51. package/rust/cli/play/mod.rs +5 -5
  52. package/rust/cli/play/process.rs +159 -150
  53. package/rust/cli/play/realtime.rs +91 -91
  54. package/rust/cli/telemetry/commands.rs +22 -22
  55. package/rust/cli/telemetry/event_creator.rs +80 -80
  56. package/rust/cli/telemetry/mod.rs +3 -3
  57. package/rust/cli/telemetry/send.rs +51 -51
  58. package/rust/cli/template/commands.rs +69 -69
  59. package/rust/config/driver.rs +112 -103
  60. package/rust/config/mod.rs +3 -3
  61. package/rust/config/ops.rs +26 -26
  62. package/rust/config/settings.rs +101 -101
  63. package/rust/core/audio/engine/driver.rs +237 -0
  64. package/rust/core/audio/engine/export.rs +169 -0
  65. package/rust/core/audio/engine/helpers.rs +178 -170
  66. package/rust/core/audio/engine/mod.rs +56 -7
  67. package/rust/core/audio/engine/notes/dsp.rs +88 -0
  68. package/rust/core/audio/engine/notes/mod.rs +53 -0
  69. package/rust/core/audio/engine/notes/params.rs +294 -0
  70. package/rust/core/audio/engine/sample/insert.rs +300 -0
  71. package/rust/core/audio/engine/sample/mod.rs +40 -0
  72. package/rust/core/audio/engine/sample/padding.rs +170 -0
  73. package/rust/core/audio/evaluator/condition.rs +61 -0
  74. package/rust/core/audio/evaluator/mod.rs +9 -0
  75. package/rust/core/audio/{evaluator.rs → evaluator/numeric.rs} +152 -310
  76. package/rust/core/audio/evaluator/rhs.rs +16 -0
  77. package/rust/core/audio/evaluator/string_expr.rs +94 -0
  78. package/rust/core/audio/interpreter/driver.rs +574 -542
  79. package/rust/core/audio/interpreter/mod.rs +2 -14
  80. package/rust/core/audio/interpreter/statements/arrow_call/interprete.rs +179 -0
  81. package/rust/core/audio/interpreter/statements/arrow_call/methods/chord.rs +398 -0
  82. package/rust/core/audio/interpreter/statements/arrow_call/methods/effects.rs +323 -0
  83. package/rust/core/audio/interpreter/statements/arrow_call/methods/mod.rs +3 -0
  84. package/rust/core/audio/interpreter/statements/arrow_call/methods/note.rs +371 -0
  85. package/rust/core/audio/interpreter/statements/arrow_call/mod.rs +3 -0
  86. package/rust/core/audio/interpreter/statements/arrow_call/types/arp.rs +192 -0
  87. package/rust/core/audio/interpreter/statements/arrow_call/types/mod.rs +24 -0
  88. package/rust/core/audio/interpreter/statements/arrow_call/types/pad.rs +116 -0
  89. package/rust/core/audio/interpreter/statements/arrow_call/types/pluck.rs +97 -0
  90. package/rust/core/audio/interpreter/statements/arrow_call/types/sub.rs +100 -0
  91. package/rust/core/audio/interpreter/{automate.rs → statements/automate.rs} +2 -4
  92. package/rust/core/audio/interpreter/{call.rs → statements/call.rs} +36 -5
  93. package/rust/core/audio/interpreter/{condition.rs → statements/condition.rs} +72 -71
  94. package/rust/core/audio/interpreter/{function.rs → statements/function.rs} +24 -26
  95. package/rust/core/audio/interpreter/{let_.rs → statements/let_.rs} +36 -38
  96. package/rust/core/audio/interpreter/{load.rs → statements/load.rs} +17 -19
  97. package/rust/core/audio/interpreter/{loop_.rs → statements/loop_.rs} +115 -114
  98. package/rust/core/audio/interpreter/statements/mod.rs +12 -0
  99. package/rust/core/audio/interpreter/{sleep.rs → statements/sleep.rs} +28 -28
  100. package/rust/core/audio/interpreter/{spawn.rs → statements/spawn.rs} +54 -4
  101. package/rust/core/audio/interpreter/{tempo.rs → statements/tempo.rs} +40 -40
  102. package/rust/core/audio/interpreter/{trigger.rs → statements/trigger.rs} +242 -239
  103. package/rust/core/audio/loader/trigger.rs +98 -97
  104. package/rust/core/audio/mod.rs +6 -7
  105. package/rust/core/audio/special/easing.rs +189 -189
  106. package/rust/core/audio/special/env.rs +45 -45
  107. package/rust/core/audio/special/math.rs +134 -134
  108. package/rust/core/audio/special/modulator.rs +143 -143
  109. package/rust/core/builder/mod.rs +129 -86
  110. package/rust/core/debugger/{module.rs → logs.rs} +52 -55
  111. package/rust/core/debugger/mod.rs +30 -30
  112. package/rust/core/debugger/store.rs +38 -40
  113. package/rust/core/error/mod.rs +269 -269
  114. package/rust/core/lexer/driver.rs +2 -4
  115. package/rust/core/mod.rs +9 -10
  116. package/rust/core/parser/driver/block.rs +111 -0
  117. package/rust/core/parser/driver/cursor.rs +82 -0
  118. package/rust/core/parser/driver/driver_impl.rs +159 -0
  119. package/rust/core/parser/driver/mod.rs +6 -0
  120. package/rust/core/parser/driver/parse_array.rs +120 -0
  121. package/rust/core/parser/driver/parse_map.rs +247 -0
  122. package/rust/core/parser/driver/parser.rs +160 -0
  123. package/rust/core/parser/handler/arrow_call.rs +90 -15
  124. package/rust/core/parser/handler/at.rs +279 -279
  125. package/rust/core/parser/handler/bank.rs +104 -104
  126. package/rust/core/parser/handler/condition.rs +83 -83
  127. package/rust/core/parser/handler/dot.rs +148 -148
  128. package/rust/core/parser/handler/identifier/automate.rs +254 -254
  129. package/rust/core/parser/handler/identifier/call.rs +91 -91
  130. package/rust/core/parser/handler/identifier/emit.rs +70 -70
  131. package/rust/core/parser/handler/identifier/function.rs +113 -113
  132. package/rust/core/parser/handler/identifier/group.rs +89 -89
  133. package/rust/core/parser/handler/identifier/let_.rs +173 -173
  134. package/rust/core/parser/handler/identifier/mod.rs +55 -55
  135. package/rust/core/parser/handler/identifier/on.rs +107 -107
  136. package/rust/core/parser/handler/identifier/print.rs +49 -49
  137. package/rust/core/parser/handler/identifier/sleep.rs +96 -43
  138. package/rust/core/parser/handler/identifier/spawn.rs +91 -91
  139. package/rust/core/parser/handler/identifier/synth.rs +39 -3
  140. package/rust/core/parser/handler/loop_.rs +194 -194
  141. package/rust/core/parser/handler/pattern.rs +25 -2
  142. package/rust/core/parser/handler/tempo.rs +105 -57
  143. package/rust/core/parser/statement.rs +10 -11
  144. package/rust/core/plugin/loader.rs +137 -137
  145. package/rust/core/plugin/runner/mod.rs +11 -0
  146. package/rust/core/plugin/{runner.rs → runner/non_wasm.rs} +206 -72
  147. package/rust/core/plugin/runner/wasm32.rs +44 -0
  148. package/rust/core/preprocessor/loader/inject.rs +313 -0
  149. package/rust/core/preprocessor/loader/loader_helpers.rs +110 -0
  150. package/rust/core/preprocessor/loader/mod.rs +235 -0
  151. package/rust/core/preprocessor/module.rs +55 -60
  152. package/rust/core/preprocessor/{processor.rs → processor/handlers.rs} +107 -114
  153. package/rust/core/preprocessor/processor/mod.rs +1 -0
  154. package/rust/core/preprocessor/resolver/function.rs +69 -69
  155. package/rust/core/preprocessor/resolver/group.rs +122 -94
  156. package/rust/core/preprocessor/resolver/pattern.rs +14 -2
  157. package/rust/core/store/global.rs +57 -61
  158. package/rust/core/store/mod.rs +1 -5
  159. package/rust/lib.rs +323 -308
  160. package/rust/macros/Cargo.toml +14 -0
  161. package/rust/macros/src/lib.rs +52 -0
  162. package/rust/main.rs +336 -143
  163. package/rust/types/Cargo.toml +1 -1
  164. package/rust/types/src/addons.rs +57 -55
  165. package/rust/types/src/config.rs +82 -74
  166. package/rust/types/src/lib.rs +15 -12
  167. package/rust/types/src/plugin.rs +20 -0
  168. package/rust/types/src/store.rs +139 -0
  169. package/rust/types/src/telemetry.rs +85 -85
  170. package/rust/utils/Cargo.toml +5 -2
  171. package/rust/utils/src/file.rs +477 -94
  172. package/rust/utils/src/first_usage.rs +97 -97
  173. package/rust/utils/src/lib.rs +9 -9
  174. package/rust/utils/src/logger.rs +200 -200
  175. package/rust/utils/src/path.rs +158 -88
  176. package/rust/utils/src/signature.rs +41 -41
  177. package/rust/utils/src/spinner.rs +20 -20
  178. package/rust/utils/src/version.rs +58 -27
  179. package/rust/utils/src/watcher.rs +46 -46
  180. package/rust/web/api.rs +5 -5
  181. package/rust/web/auth.rs +5 -0
  182. package/rust/web/cdn.rs +34 -34
  183. package/rust/web/forge.rs +5 -0
  184. package/rust/web/mod.rs +2 -0
  185. package/tests/integration.rs +21 -21
  186. package/typescript/core/functions/index.ts +11 -0
  187. package/typescript/pkg/devalang_core.ts +20 -4
  188. package/typescript/scripts/version/copy-to-binary.ts +82 -0
  189. package/rust/cli/bank/api.rs +0 -122
  190. package/rust/cli/bank/commands.rs +0 -275
  191. package/rust/cli/bank/mod.rs +0 -29
  192. package/rust/cli/install/bank.rs +0 -53
  193. package/rust/cli/install/commands.rs +0 -35
  194. package/rust/cli/install/mod.rs +0 -4
  195. package/rust/cli/install/plugin.rs +0 -61
  196. package/rust/core/audio/engine/sample.rs +0 -366
  197. package/rust/core/audio/engine/synth.rs +0 -325
  198. package/rust/core/audio/interpreter/arrow_call.rs +0 -311
  199. package/rust/core/audio/renderer.rs +0 -54
  200. package/rust/core/parser/driver.rs +0 -584
  201. package/rust/core/preprocessor/loader.rs +0 -637
  202. package/rust/core/store/export.rs +0 -28
  203. package/rust/core/store/function.rs +0 -40
  204. package/rust/core/store/import.rs +0 -28
  205. package/rust/core/store/variable.rs +0 -51
  206. package/rust/core/utils/mod.rs +0 -1
  207. package/rust/core/utils/path.rs +0 -37
@@ -0,0 +1,323 @@
1
+ use crate::core::audio::engine::AudioEngine;
2
+ use devalang_types::Value;
3
+ use devalang_utils::logger::{LogLevel, Logger};
4
+ use std::collections::HashMap;
5
+
6
+ fn parse_value_to_f32(v: &Value) -> Option<f32> {
7
+ match v {
8
+ Value::Number(n) => Some(*n as f32),
9
+ Value::String(s) => s.parse::<f32>().ok(),
10
+ Value::Identifier(s) => s.parse::<f32>().ok(),
11
+ Value::Boolean(b) => Some(if *b { 1.0 } else { 0.0 }),
12
+ _ => None,
13
+ }
14
+ }
15
+
16
+ // Simple nearest-neighbour resampler that produces an output with the same frame count
17
+ // while applying a time-scaling factor per frame. channels = interleaved channel count.
18
+ fn resample_segment_nearest(
19
+ src: &[i16],
20
+ channels: usize,
21
+ start_rate: f32,
22
+ end_rate: f32,
23
+ ) -> Vec<i16> {
24
+ if src.is_empty() || channels == 0 {
25
+ return Vec::new();
26
+ }
27
+
28
+ let frames = src.len() / channels;
29
+ if frames == 0 {
30
+ return Vec::new();
31
+ }
32
+
33
+ // Copy source into frames x channels matrix access via frame*channels + ch
34
+ let mut out = vec![0i16; frames * channels];
35
+
36
+ for f in 0..frames {
37
+ let t = if frames > 1 {
38
+ f as f32 / (frames - 1) as f32
39
+ } else {
40
+ 0.0
41
+ };
42
+ let rate = start_rate + t * (end_rate - start_rate);
43
+ let inv_rate = if rate == 0.0 { 1.0 } else { 1.0 / rate };
44
+
45
+ // determine source frame position (nearest)
46
+ let src_frame_pos = (f as f32 * inv_rate).clamp(0.0, (frames - 1) as f32);
47
+ let src_idx = src_frame_pos.round() as usize;
48
+
49
+ for ch in 0..channels {
50
+ let s_idx = src_idx.saturating_mul(channels).saturating_add(ch);
51
+ let o_idx = f.saturating_mul(channels).saturating_add(ch);
52
+ let sample = if s_idx < src.len() { src[s_idx] } else { 0 };
53
+ out[o_idx] = sample;
54
+ }
55
+ }
56
+
57
+ out
58
+ }
59
+
60
+ // Basic effect application for chainable effects. This is intentionally minimal:
61
+ // - Accepts a target synth name and args parsed from the arrow call.
62
+ // - Applies effects by mutating the AudioEngine or scheduling transforms.
63
+ // Current effects implemented: echo, reverb, slide (as parameter modifiers).
64
+
65
+ pub fn apply_effect_chain(
66
+ method: &str,
67
+ args: &Vec<Value>,
68
+ target: &str,
69
+ audio_engine: &mut AudioEngine,
70
+ variable_table: &devalang_types::VariableTable,
71
+ ) {
72
+ match method {
73
+ "echo" => apply_echo(args, target, audio_engine, variable_table),
74
+ "reverb" => apply_reverb(args, target, audio_engine, variable_table),
75
+ "slide" => apply_slide(args, target, audio_engine, variable_table),
76
+ "arp" => apply_arp_effect(args, target, audio_engine, variable_table),
77
+ _ => {
78
+ let logger = Logger::new();
79
+ logger.log_message(
80
+ LogLevel::Error,
81
+ &format!("Unknown chainable effect '{}' on '{}'.", method, target),
82
+ );
83
+ }
84
+ }
85
+ }
86
+
87
+ fn parse_map_arg(args: &Vec<Value>) -> Option<HashMap<String, Value>> {
88
+ // Typical usage: method({ key: value }) -> args[0] is a Map
89
+ if let Some(Value::Map(m)) = args.first() {
90
+ return Some(m.clone());
91
+ }
92
+ None
93
+ }
94
+
95
+ fn apply_echo(
96
+ args: &Vec<Value>,
97
+ _target: &str,
98
+ engine: &mut AudioEngine,
99
+ _variable_table: &devalang_types::VariableTable,
100
+ ) {
101
+ let map = parse_map_arg(args);
102
+ let mut delay_ms = 250.0_f32;
103
+ let mut feedback = 0.5_f32;
104
+
105
+ if let Some(m) = map {
106
+ if let Some(Value::Number(n)) = m.get("delay") {
107
+ delay_ms = *n as f32;
108
+ }
109
+ if let Some(Value::Number(n)) = m.get("feedback") {
110
+ feedback = *n as f32;
111
+ }
112
+ }
113
+
114
+ // Very small and cheap echo: we will add a delayed, attenuated copy of the buffer
115
+ let sample_rate = engine.sample_rate as f32;
116
+ let channels = engine.channels as usize;
117
+ let delay_samples = ((delay_ms / 1000.0) * sample_rate) as usize * channels;
118
+
119
+ if delay_samples == 0 || engine.buffer.is_empty() {
120
+ return;
121
+ }
122
+
123
+ // Mix a single echo pass
124
+ let mut out = engine.buffer.clone();
125
+ for i in delay_samples..engine.buffer.len() {
126
+ let src = engine.buffer[i - delay_samples] as f32;
127
+ let added = (src * feedback)
128
+ .round()
129
+ .clamp(i16::MIN as f32, i16::MAX as f32) as i16;
130
+ out[i] = out[i].saturating_add(added);
131
+ }
132
+
133
+ engine.buffer = out;
134
+ }
135
+
136
+ fn apply_reverb(
137
+ args: &Vec<Value>,
138
+ _target: &str,
139
+ engine: &mut AudioEngine,
140
+ _variable_table: &devalang_types::VariableTable,
141
+ ) {
142
+ let map = parse_map_arg(args);
143
+ let mut room_size = 0.5_f32;
144
+ if let Some(m) = map {
145
+ if let Some(Value::Number(n)) = m.get("room_size") {
146
+ room_size = *n as f32;
147
+ }
148
+ }
149
+
150
+ // Cheap reverb: multiple short comb filters (very approximate)
151
+ if engine.buffer.is_empty() {
152
+ return;
153
+ }
154
+
155
+ let sample_rate = engine.sample_rate as f32;
156
+ let channels = engine.channels as usize;
157
+ let reverb_delay_samples = ((0.03 * room_size) * sample_rate) as usize * channels;
158
+ if reverb_delay_samples == 0 {
159
+ return;
160
+ }
161
+
162
+ let mut out = engine.buffer.clone();
163
+ for i in reverb_delay_samples..engine.buffer.len() {
164
+ let src = engine.buffer[i - reverb_delay_samples] as f32;
165
+ let added = (src * room_size * 0.5)
166
+ .round()
167
+ .clamp(i16::MIN as f32, i16::MAX as f32) as i16;
168
+ out[i] = out[i].saturating_add(added);
169
+ }
170
+
171
+ engine.buffer = out;
172
+ }
173
+
174
+ fn apply_slide(
175
+ args: &Vec<Value>,
176
+ _target: &str,
177
+ _engine: &mut AudioEngine,
178
+ _variable_table: &devalang_types::VariableTable,
179
+ ) {
180
+ // Slide: apply a linear pitch glide across the most recently recorded note ranges
181
+ let map = parse_map_arg(args);
182
+ let mut from_semitones = 0.0_f32;
183
+ let mut to_semitones = 0.0_f32;
184
+
185
+ if let Some(m) = map {
186
+ if let Some(v) = m.get("from") {
187
+ if let Some(f) = parse_value_to_f32(v) {
188
+ from_semitones = f;
189
+ }
190
+ }
191
+ if let Some(v) = m.get("to") {
192
+ if let Some(t) = parse_value_to_f32(v) {
193
+ to_semitones = t;
194
+ }
195
+ }
196
+ }
197
+
198
+ // Compute rate multipliers from semitone offsets
199
+ let start_rate = 2f32.powf(from_semitones / 12.0);
200
+ let end_rate = 2f32.powf(to_semitones / 12.0);
201
+
202
+ let channels = _engine.channels as usize;
203
+ if channels == 0 {
204
+ return;
205
+ }
206
+
207
+ // For each recorded last note range for the target, apply a per-frame resample glide
208
+ if let Some(ranges) = _engine.last_notes.get(_target) {
209
+ for (start_sample, total_samples) in ranges.iter() {
210
+ if *total_samples == 0 {
211
+ continue;
212
+ }
213
+ // clamp range
214
+ let end = (*start_sample)
215
+ .saturating_add(*total_samples)
216
+ .min(_engine.buffer.len());
217
+ if *start_sample >= end {
218
+ continue;
219
+ }
220
+
221
+ // work on a copy of the segment
222
+ let seg = _engine.buffer[*start_sample..end].to_vec();
223
+ let processed = resample_segment_nearest(&seg, channels, start_rate, end_rate);
224
+
225
+ // Mix processed back into buffer (replace to preserve duration)
226
+ for i in 0..processed.len().min(seg.len()) {
227
+ _engine.buffer[*start_sample + i] = processed[i];
228
+ }
229
+ }
230
+ } else {
231
+ let logger = Logger::new();
232
+ logger.log_message(
233
+ LogLevel::Warning,
234
+ "Slide requested but no recent notes found for target",
235
+ );
236
+ }
237
+ }
238
+
239
+ fn apply_arp_effect(
240
+ args: &Vec<Value>,
241
+ _target: &str,
242
+ _engine: &mut AudioEngine,
243
+ _variable_table: &devalang_types::VariableTable,
244
+ ) {
245
+ // Arp effect: split the last note into N slices and re-pitch each slice across a spread
246
+ let map = parse_map_arg(args);
247
+ let mut steps: usize = 4;
248
+ let mut spread_semitones: f32 = 0.0;
249
+
250
+ if let Some(m) = map {
251
+ if let Some(v) = m.get("steps") {
252
+ if let Some(s) = parse_value_to_f32(v) {
253
+ steps = (s as usize).max(1);
254
+ }
255
+ }
256
+ if let Some(v) = m.get("spread") {
257
+ if let Some(s) = parse_value_to_f32(v) {
258
+ spread_semitones = s;
259
+ }
260
+ }
261
+ }
262
+
263
+ let channels = _engine.channels as usize;
264
+ if channels == 0 {
265
+ return;
266
+ }
267
+
268
+ if let Some(ranges) = _engine.last_notes.get(_target) {
269
+ for (start_sample, total_samples) in ranges.iter() {
270
+ if *total_samples == 0 {
271
+ continue;
272
+ }
273
+ let end_sample = (*start_sample)
274
+ .saturating_add(*total_samples)
275
+ .min(_engine.buffer.len());
276
+ if *start_sample >= end_sample {
277
+ continue;
278
+ }
279
+
280
+ let seg = _engine.buffer[*start_sample..end_sample].to_vec();
281
+ let frames = seg.len() / channels;
282
+ if frames == 0 {
283
+ continue;
284
+ }
285
+
286
+ // For each step, compute semitone and pitch multiplier and place slice at computed offset
287
+ for step in 0..steps {
288
+ let t = if steps > 1 {
289
+ step as f32 / (steps - 1) as f32
290
+ } else {
291
+ 0.0
292
+ };
293
+ let semis = t * spread_semitones;
294
+ let rate = 2f32.powf(semis / 12.0);
295
+
296
+ // Resample the entire segment to the same duration using nearest approach with rate
297
+ let processed = resample_segment_nearest(&seg, channels, rate, rate);
298
+
299
+ // place the processed slice starting at fractional positions across original segment
300
+ let offset_frames =
301
+ ((t * frames as f32).round() as usize).min(frames.saturating_sub(1));
302
+ let offset_samples = offset_frames.saturating_mul(channels);
303
+
304
+ // mix into engine buffer
305
+ for i in 0..processed.len() {
306
+ let dst_idx = *start_sample + offset_samples + i;
307
+ if dst_idx >= _engine.buffer.len() {
308
+ break;
309
+ }
310
+ // simple additive mix and clamp
311
+ let sum = (_engine.buffer[dst_idx] as i32) + (processed[i] as i32);
312
+ _engine.buffer[dst_idx] = sum.clamp(i16::MIN as i32, i16::MAX as i32) as i16;
313
+ }
314
+ }
315
+ }
316
+ } else {
317
+ let logger = Logger::new();
318
+ logger.log_message(
319
+ LogLevel::Warning,
320
+ "Arp effect requested but no recent notes found for target",
321
+ );
322
+ }
323
+ }
@@ -0,0 +1,3 @@
1
+ pub mod chord;
2
+ pub mod effects;
3
+ pub mod note;
@@ -0,0 +1,371 @@
1
+ use crate::core::{audio::engine::AudioEngine, plugin::runner::WasmPluginRunner};
2
+ use devalang_types::Value;
3
+ use devalang_utils::logger::{LogLevel, Logger};
4
+ use std::collections::HashMap;
5
+
6
+ pub fn interprete_note_method(
7
+ args: &Vec<devalang_types::Value>,
8
+ target: &str,
9
+ audio_engine: &mut AudioEngine,
10
+ variable_table: &devalang_types::VariableTable,
11
+ global_store: &crate::core::store::global::GlobalStore,
12
+ waveform_str: &str,
13
+ synth_params: &HashMap<String, devalang_types::Value>,
14
+ amp: f32,
15
+ base_bpm: f32,
16
+ base_duration: f32,
17
+ max_end_time: &mut f32,
18
+ mut cursor_time: Option<&mut f32>,
19
+ cursor_copy: f32,
20
+ update_cursor: bool,
21
+ ) -> (f32, f32) {
22
+ let filtered_args: Vec<_> = args
23
+ .iter()
24
+ .filter(|arg| !matches!(arg, Value::Unknown))
25
+ .collect();
26
+
27
+ let Some(Value::Identifier(note_name)) = filtered_args.first().map(|v| (*v).clone()) else {
28
+ println!(
29
+ "❌ Invalid or missing argument for 'note' method on '{}'.",
30
+ target
31
+ );
32
+ return (*max_end_time, cursor_copy);
33
+ };
34
+
35
+ let mut note_params = HashMap::new();
36
+ if let Some(arg1) = filtered_args.get(1) {
37
+ if let Value::Map(map) = (*arg1).clone() {
38
+ for (key, value) in map {
39
+ note_params.insert(key, value);
40
+ }
41
+ }
42
+ }
43
+
44
+ // Note parameters and calculations
45
+ let amp_note = extract_f32(&note_params, "amp", base_bpm).unwrap_or(amp);
46
+ let duration_ms =
47
+ extract_f32(&note_params, "duration", base_bpm).unwrap_or(base_duration * 1000.0);
48
+
49
+ let duration_secs = duration_ms / 1000.0;
50
+ let final_freq = note_to_freq(&note_name);
51
+ let start_time = cursor_copy;
52
+ let end_time = start_time + duration_secs;
53
+
54
+ // Fetch automation map if present:
55
+ // - Global (per-synth): key "<target>__automation" => map with key "params"
56
+ // - Per-note: note parameter "automate" => map
57
+ let auto_key = format!("{}__automation", target);
58
+ let synth_automation = match variable_table.get(&auto_key) {
59
+ Some(Value::Map(map)) => match map.get("params") {
60
+ Some(Value::Map(p)) => Some(p.clone()),
61
+ _ => None,
62
+ },
63
+ _ => None,
64
+ };
65
+
66
+ let note_automation = match note_params.get("automate") {
67
+ Some(Value::Map(m)) => Some(m.clone()),
68
+ _ => None,
69
+ };
70
+
71
+ // Merge: per-note overrides synth automation per key (volume/pan/pitch)
72
+ let automation = match (synth_automation, note_automation) {
73
+ (Some(mut a), Some(n)) => {
74
+ for (k, v) in n {
75
+ a.insert(k, v);
76
+ }
77
+ Some(a)
78
+ }
79
+ (None, Some(n)) => Some(n),
80
+ (Some(a), None) => Some(a),
81
+ _ => None,
82
+ };
83
+
84
+ // If waveform references a plugin alias (e.g., alias.synth), use the WASM plugin runner
85
+ if waveform_str.contains('.') && waveform_str.ends_with(".synth") {
86
+ let alias = waveform_str.split('.').next().unwrap_or("");
87
+ if let Some(Value::String(uri)) = variable_table.get(alias) {
88
+ if let Some(id) = uri.strip_prefix("devalang://plugin/") {
89
+ let mut parts = id.split('/');
90
+ let author = parts.next().unwrap_or("");
91
+ let name = parts.next().unwrap_or("");
92
+ let key = format!("{}:{}", author, name);
93
+ if let Some((info, wasm_bytes)) = global_store.plugins.get(&key) {
94
+ // Prepare buffer (stereo f32)
95
+ let sample_rate = 44100.0_f32;
96
+ let total_samples = ((duration_ms / 1000.0) * sample_rate) as usize;
97
+ let channels = 2usize;
98
+ let start_index = ((start_time * sample_rate) as usize) * channels;
99
+ let required_len = start_index + total_samples * channels;
100
+ if audio_engine.buffer.len() < required_len {
101
+ audio_engine.buffer.resize(required_len, 0);
102
+ }
103
+ let mut fbuf = vec![0.0f32; total_samples * channels];
104
+ let runner = WasmPluginRunner::new();
105
+ let mut params_num: std::collections::HashMap<String, f32> =
106
+ std::collections::HashMap::new();
107
+ let mut params_str: std::collections::HashMap<String, String> =
108
+ std::collections::HashMap::new();
109
+ for (k, v) in synth_params.iter() {
110
+ match v {
111
+ Value::Number(n) => {
112
+ params_num.insert(k.clone(), *n);
113
+ }
114
+ Value::String(s) => {
115
+ params_str.insert(k.clone(), s.clone());
116
+ }
117
+ Value::Identifier(s) => {
118
+ params_str.insert(k.clone(), s.clone());
119
+ }
120
+ _ => {}
121
+ }
122
+ }
123
+
124
+ // collect exported names to pass to the runner (preference list)
125
+ let exported_names_vec: Vec<String> =
126
+ info.exports.iter().map(|e| e.name.clone()).collect();
127
+
128
+ // Debug log: exported names and synth param keys
129
+ {
130
+ let logger = devalang_utils::logger::Logger::new();
131
+ logger.log_message(
132
+ devalang_utils::logger::LogLevel::Debug,
133
+ &format!(
134
+ "Calling plugin runner for '{}' with {} exported names and {} synth params",
135
+ key,
136
+ exported_names_vec.len(),
137
+ synth_params.len()
138
+ )
139
+ );
140
+ }
141
+
142
+ let _ = runner.render_note_with_params_in_place(
143
+ wasm_bytes,
144
+ &mut fbuf,
145
+ None,
146
+ final_freq,
147
+ amp_note,
148
+ duration_ms as i32,
149
+ 44100,
150
+ 2,
151
+ &params_num,
152
+ Some(&params_str),
153
+ Some(&exported_names_vec),
154
+ );
155
+
156
+ for (i, sample) in fbuf.iter().enumerate().take(total_samples * channels) {
157
+ let s = (sample.clamp(-1.0, 1.0) * (i16::MAX as f32)) as i16;
158
+ let idx = start_index + i;
159
+ audio_engine.buffer[idx] = audio_engine.buffer[idx].saturating_add(s);
160
+ }
161
+ } else {
162
+ let logger = Logger::new();
163
+ logger.log_message(
164
+ LogLevel::Warning,
165
+ &format!(
166
+ "Plugin bytes not found for key '{}' (alias '{}').",
167
+ key, alias
168
+ ),
169
+ );
170
+ }
171
+ } else {
172
+ let logger = Logger::new();
173
+ logger.log_message(
174
+ LogLevel::Warning,
175
+ &format!("Invalid plugin URI in alias '{}': {}", alias, uri),
176
+ );
177
+ }
178
+ } else {
179
+ let logger = Logger::new();
180
+ logger.log_message(
181
+ LogLevel::Warning,
182
+ &format!("Plugin alias '{}' not found in variable table.", alias),
183
+ );
184
+ }
185
+ } else {
186
+ // Allow types to adjust per-note scheduling/params
187
+ let start_ms = start_time * 1000.0;
188
+ let mut final_amp = amp_note;
189
+ let mut final_note_params = note_params.clone();
190
+
191
+ let mut handled = false;
192
+ if let Some(tval) = synth_params.get("type") {
193
+ let tname = match tval {
194
+ Value::String(s) => s.as_str(),
195
+ Value::Identifier(s) => s.as_str(),
196
+ _ => "",
197
+ };
198
+ match tname {
199
+ "arp" => {
200
+ // compute a step (ms) from synth params (rate/step). compute_arp_step
201
+ // will interpret `rate` as number of notes across the provided duration
202
+ let step_ms =
203
+ crate::core::audio::interpreter::statements::arrow_call::types::arp::compute_arp_step(
204
+ duration_ms,
205
+ 1,
206
+ &synth_params
207
+ );
208
+ let steps = if step_ms > 0.0 {
209
+ ((duration_ms / step_ms).ceil() as usize).max(1)
210
+ } else {
211
+ 1usize
212
+ };
213
+
214
+ // For each arp step, call prepare_note to get per-step params and schedule it
215
+ for idx in 0..steps {
216
+ let (start_abs_ms, freq_step, amp_out, params_out) =
217
+ crate::core::audio::interpreter::statements::arrow_call::types::arp::prepare_note(
218
+ &note_name,
219
+ idx,
220
+ steps,
221
+ start_ms,
222
+ duration_ms,
223
+ amp_note,
224
+ &synth_params,
225
+ &final_note_params,
226
+ &automation
227
+ );
228
+
229
+ // sub-note duration: default to step_ms so arp steps are audible and sequenced
230
+ let sub_duration_ms = if step_ms > 0.0 { step_ms } else { duration_ms };
231
+
232
+ let _ranges = audio_engine.insert_note(
233
+ Some(target.to_string()),
234
+ waveform_str.to_string(),
235
+ freq_step,
236
+ amp_out,
237
+ start_abs_ms,
238
+ sub_duration_ms,
239
+ synth_params.clone(),
240
+ params_out.clone(),
241
+ automation.clone(),
242
+ );
243
+ // Apply per-note effects if present in synth_params or note params
244
+ if let Some(ev) = params_out.get("effects") {
245
+ // for now expect effects as array of maps or identifiers
246
+ match ev {
247
+ Value::Array(arr) => {
248
+ for eff in arr.iter() {
249
+ if let Value::Map(_m) = eff {
250
+ // each map may have single key -> value
251
+ }
252
+ }
253
+ }
254
+ _ => {}
255
+ }
256
+ }
257
+ }
258
+
259
+ // mark handled to avoid the unconditional insert below
260
+ handled = true;
261
+ }
262
+ "pluck" => {
263
+ let (_s, _f, amp_out, params_out) =
264
+ crate::core::audio::interpreter::statements::arrow_call::types::pluck::prepare_note(
265
+ &note_name,
266
+ 0,
267
+ 1,
268
+ start_ms,
269
+ duration_ms,
270
+ amp_note,
271
+ &synth_params,
272
+ &final_note_params,
273
+ &automation
274
+ );
275
+ final_amp = amp_out;
276
+ final_note_params = params_out;
277
+ }
278
+ "pad" => {
279
+ let (_s, _f, amp_out, params_out) =
280
+ crate::core::audio::interpreter::statements::arrow_call::types::pad::prepare_note(
281
+ &note_name,
282
+ 0,
283
+ 1,
284
+ start_ms,
285
+ duration_ms,
286
+ amp_note,
287
+ &synth_params,
288
+ &final_note_params,
289
+ &automation
290
+ );
291
+ final_amp = amp_out;
292
+ final_note_params = params_out;
293
+ }
294
+ _ => {}
295
+ }
296
+ }
297
+
298
+ if !handled {
299
+ let ranges = audio_engine.insert_note(
300
+ Some(target.to_string()),
301
+ waveform_str.to_string(),
302
+ final_freq,
303
+ final_amp,
304
+ start_ms,
305
+ duration_ms,
306
+ synth_params.clone(),
307
+ final_note_params.clone(),
308
+ automation.clone(),
309
+ );
310
+ // apply per-note effects specified in final_note_params
311
+ if let Some(Value::Map(eff_map)) = final_note_params.get("effects") {
312
+ // delegate to effects module per range
313
+ for (_start, _len) in ranges.iter() {
314
+ // for simplicity apply using engine buffer ranges via effects module
315
+ crate::core::audio::interpreter::statements::arrow_call::methods::effects::apply_effect_chain(
316
+ "echo",
317
+ &vec![Value::Map(eff_map.clone())],
318
+ target,
319
+ audio_engine,
320
+ variable_table
321
+ );
322
+ }
323
+ }
324
+ }
325
+ }
326
+
327
+ *max_end_time = (*max_end_time).max(end_time);
328
+
329
+ if update_cursor {
330
+ if let Some(c) = cursor_time.as_mut() {
331
+ **c = end_time;
332
+ }
333
+ }
334
+
335
+ return (*max_end_time, end_time);
336
+ }
337
+
338
+ fn extract_f32(map: &HashMap<String, Value>, key: &str, base_bpm: f32) -> Option<f32> {
339
+ map.get(key).and_then(|v| match v {
340
+ Value::Number(n) => Some(*n),
341
+ Value::Beat(beat_str) => {
342
+ let parts: Vec<&str> = beat_str.split('/').collect();
343
+ if parts.len() == 2 {
344
+ let numerator = parts[0].parse::<f32>().ok()?;
345
+ let denominator = parts[1].parse::<f32>().ok()?;
346
+
347
+ Some((numerator / denominator) * ((60.0 / base_bpm) * 1000.0))
348
+ } else {
349
+ None
350
+ }
351
+ }
352
+ _ => None,
353
+ })
354
+ }
355
+
356
+ fn note_to_freq(note: &str) -> f32 {
357
+ let notes = [
358
+ "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
359
+ ];
360
+
361
+ if note.len() < 2 || note.len() > 3 {
362
+ return 440.0;
363
+ }
364
+
365
+ let (name, octave_str) = note.split_at(note.len() - 1);
366
+ let semitone = notes.iter().position(|&n| n == name).unwrap_or(9) as i32;
367
+ let octave = octave_str.parse::<i32>().unwrap_or(4);
368
+ let midi_note = (octave + 1) * 12 + semitone;
369
+
370
+ 440.0 * (2.0_f32).powf(((midi_note as f32) - 69.0) / 12.0)
371
+ }
@@ -0,0 +1,3 @@
1
+ pub mod interprete;
2
+ pub mod methods;
3
+ pub mod types;