@devaloop/devalang 0.0.1-alpha.9 → 0.0.1-beta.1

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 (271) hide show
  1. package/.cargo/config.toml +2 -0
  2. package/.devalang +10 -4
  3. package/.github/workflows/ci.yml +103 -0
  4. package/Cargo.toml +80 -48
  5. package/README.md +135 -154
  6. package/docs/CHANGELOG.md +386 -1
  7. package/docs/CONTRIBUTING.md +101 -0
  8. package/docs/ROADMAP.md +10 -7
  9. package/docs/TODO.md +21 -9
  10. package/examples/automation.deva +42 -0
  11. package/examples/bank.deva +7 -0
  12. package/examples/duration.deva +9 -0
  13. package/examples/events.deva +12 -0
  14. package/examples/function.deva +15 -0
  15. package/examples/index.deva +57 -12
  16. package/examples/loop.deva +5 -12
  17. package/examples/pattern.deva +8 -0
  18. package/examples/plugin.deva +16 -0
  19. package/examples/variables.deva +1 -1
  20. package/out-tsc/bin/index.d.ts +2 -0
  21. package/out-tsc/bin/index.js +51 -7
  22. package/out-tsc/core/functions/index.d.ts +37 -0
  23. package/out-tsc/core/functions/index.js +76 -0
  24. package/out-tsc/core/index.d.ts +6 -0
  25. package/out-tsc/core/index.js +22 -0
  26. package/out-tsc/core/types/index.d.ts +4 -0
  27. package/out-tsc/core/types/index.js +20 -0
  28. package/out-tsc/core/types/plugin.d.ts +18 -0
  29. package/out-tsc/core/types/plugin.js +2 -0
  30. package/out-tsc/core/types/result.d.ts +27 -0
  31. package/out-tsc/core/types/result.js +2 -0
  32. package/out-tsc/core/types/statement.d.ts +106 -0
  33. package/out-tsc/core/types/statement.js +2 -0
  34. package/out-tsc/core/types/value.d.ts +43 -0
  35. package/out-tsc/core/types/value.js +2 -0
  36. package/out-tsc/index.d.ts +7 -0
  37. package/out-tsc/index.js +42 -1
  38. package/out-tsc/pkg/devalang_core.d.ts +13 -0
  39. package/out-tsc/pkg/devalang_core.js +50 -0
  40. package/out-tsc/pkg/devalang_core_bg.wasm.d.ts +33 -0
  41. package/out-tsc/scripts/copy-wasm-dts.d.ts +1 -0
  42. package/out-tsc/scripts/copy-wasm-dts.js +73 -0
  43. package/out-tsc/scripts/postinstall.d.ts +1 -0
  44. package/out-tsc/scripts/postinstall.js +83 -0
  45. package/out-tsc/scripts/version/bump.d.ts +1 -0
  46. package/out-tsc/scripts/version/fetch.d.ts +1 -0
  47. package/out-tsc/scripts/version/index.d.ts +1 -0
  48. package/out-tsc/scripts/version/sync.d.ts +1 -0
  49. package/package.json +28 -7
  50. package/project-version.json +4 -4
  51. package/rust/cli/bank/api.rs +122 -0
  52. package/rust/cli/bank/commands.rs +275 -0
  53. package/rust/cli/bank/mod.rs +29 -0
  54. package/rust/cli/build/commands.rs +103 -0
  55. package/rust/cli/build/mod.rs +2 -0
  56. package/rust/cli/build/process.rs +146 -0
  57. package/rust/cli/check/mod.rs +208 -0
  58. package/rust/cli/discover/commands.rs +253 -0
  59. package/rust/cli/discover/config.rs +111 -0
  60. package/rust/cli/discover/fs.rs +19 -0
  61. package/rust/cli/discover/install.rs +103 -0
  62. package/rust/cli/discover/metadata.rs +48 -0
  63. package/rust/cli/discover/mod.rs +5 -0
  64. package/rust/cli/{init.rs → init/commands.rs} +32 -23
  65. package/rust/cli/init/mod.rs +1 -0
  66. package/rust/cli/install/addon.rs +118 -0
  67. package/rust/cli/install/bank.rs +53 -0
  68. package/rust/cli/install/commands.rs +35 -0
  69. package/rust/cli/install/mod.rs +4 -0
  70. package/rust/cli/install/plugin.rs +61 -0
  71. package/rust/cli/login/commands.rs +124 -0
  72. package/rust/cli/login/mod.rs +1 -0
  73. package/rust/cli/mod.rs +12 -205
  74. package/rust/cli/parser.rs +314 -0
  75. package/rust/cli/play/commands.rs +324 -0
  76. package/rust/cli/play/io.rs +17 -0
  77. package/rust/cli/play/mod.rs +5 -0
  78. package/rust/cli/play/process.rs +150 -0
  79. package/rust/cli/play/realtime.rs +91 -0
  80. package/rust/cli/play/utils.rs +23 -0
  81. package/rust/cli/telemetry/commands.rs +22 -0
  82. package/rust/cli/telemetry/event_creator.rs +80 -0
  83. package/rust/cli/telemetry/mod.rs +3 -0
  84. package/rust/cli/telemetry/send.rs +51 -0
  85. package/rust/cli/{template.rs → template/commands.rs} +69 -57
  86. package/rust/cli/template/mod.rs +1 -0
  87. package/rust/cli/update/commands.rs +6 -0
  88. package/rust/cli/update/mod.rs +1 -0
  89. package/rust/config/driver.rs +103 -0
  90. package/rust/config/mod.rs +3 -16
  91. package/rust/config/ops.rs +26 -0
  92. package/rust/config/settings.rs +101 -0
  93. package/rust/core/audio/engine/helpers.rs +170 -0
  94. package/rust/core/audio/engine/mod.rs +7 -0
  95. package/rust/core/audio/engine/sample.rs +366 -0
  96. package/rust/core/audio/engine/synth.rs +325 -0
  97. package/rust/core/audio/evaluator.rs +310 -31
  98. package/rust/core/audio/interpreter/arrow_call.rs +311 -129
  99. package/rust/core/audio/interpreter/automate.rs +18 -0
  100. package/rust/core/audio/interpreter/call.rs +294 -64
  101. package/rust/core/audio/interpreter/condition.rs +71 -69
  102. package/rust/core/audio/interpreter/driver.rs +542 -216
  103. package/rust/core/audio/interpreter/function.rs +26 -0
  104. package/rust/core/audio/interpreter/let_.rs +38 -19
  105. package/rust/core/audio/interpreter/load.rs +19 -18
  106. package/rust/core/audio/interpreter/loop_.rs +114 -67
  107. package/rust/core/audio/interpreter/mod.rs +14 -12
  108. package/rust/core/audio/interpreter/sleep.rs +28 -36
  109. package/rust/core/audio/interpreter/spawn.rs +252 -66
  110. package/rust/core/audio/interpreter/tempo.rs +40 -16
  111. package/rust/core/audio/interpreter/trigger.rs +239 -69
  112. package/rust/core/audio/loader/mod.rs +1 -1
  113. package/rust/core/audio/loader/trigger.rs +97 -52
  114. package/rust/core/audio/mod.rs +7 -6
  115. package/rust/core/audio/player.rs +70 -54
  116. package/rust/core/audio/renderer.rs +54 -54
  117. package/rust/core/audio/special/easing.rs +189 -0
  118. package/rust/core/audio/special/env.rs +45 -0
  119. package/rust/core/audio/special/math.rs +134 -0
  120. package/rust/core/audio/special/mod.rs +9 -0
  121. package/rust/core/audio/special/modulator.rs +143 -0
  122. package/rust/core/builder/mod.rs +86 -80
  123. package/rust/core/debugger/lexer.rs +27 -27
  124. package/rust/core/debugger/mod.rs +30 -21
  125. package/rust/core/debugger/module.rs +55 -0
  126. package/rust/core/debugger/preprocessor.rs +27 -27
  127. package/rust/core/debugger/store.rs +40 -25
  128. package/rust/core/error/mod.rs +269 -60
  129. package/rust/core/lexer/driver.rs +61 -0
  130. package/rust/core/lexer/handler/arrow.rs +82 -31
  131. package/rust/core/lexer/handler/at.rs +21 -21
  132. package/rust/core/lexer/handler/brace.rs +41 -41
  133. package/rust/core/lexer/handler/colon.rs +21 -21
  134. package/rust/core/lexer/handler/comment.rs +30 -30
  135. package/rust/core/lexer/handler/dot.rs +21 -21
  136. package/rust/core/lexer/handler/driver.rs +337 -226
  137. package/rust/core/lexer/handler/identifier.rs +47 -41
  138. package/rust/core/lexer/handler/indent.rs +66 -52
  139. package/rust/core/lexer/handler/mod.rs +15 -14
  140. package/rust/core/lexer/handler/newline.rs +23 -23
  141. package/rust/core/lexer/handler/number.rs +31 -31
  142. package/rust/core/lexer/handler/operator.rs +46 -44
  143. package/rust/core/lexer/handler/parenthesis.rs +41 -0
  144. package/rust/core/lexer/handler/slash.rs +21 -0
  145. package/rust/core/lexer/handler/string.rs +63 -63
  146. package/rust/core/lexer/mod.rs +3 -51
  147. package/rust/core/lexer/token.rs +17 -12
  148. package/rust/core/mod.rs +10 -10
  149. package/rust/core/parser/driver.rs +584 -331
  150. package/rust/core/parser/handler/arrow_call.rs +253 -126
  151. package/rust/core/parser/handler/at.rs +279 -162
  152. package/rust/core/parser/handler/bank.rs +104 -41
  153. package/rust/core/parser/handler/condition.rs +83 -74
  154. package/rust/core/parser/handler/dot.rs +148 -112
  155. package/rust/core/parser/handler/identifier/automate.rs +254 -0
  156. package/rust/core/parser/handler/identifier/call.rs +91 -41
  157. package/rust/core/parser/handler/identifier/emit.rs +70 -0
  158. package/rust/core/parser/handler/identifier/function.rs +113 -0
  159. package/rust/core/parser/handler/identifier/group.rs +89 -75
  160. package/rust/core/parser/handler/identifier/let_.rs +173 -133
  161. package/rust/core/parser/handler/identifier/mod.rs +55 -51
  162. package/rust/core/parser/handler/identifier/on.rs +107 -0
  163. package/rust/core/parser/handler/identifier/print.rs +49 -0
  164. package/rust/core/parser/handler/identifier/sleep.rs +43 -33
  165. package/rust/core/parser/handler/identifier/spawn.rs +91 -41
  166. package/rust/core/parser/handler/identifier/synth.rs +135 -65
  167. package/rust/core/parser/handler/loop_.rs +194 -72
  168. package/rust/core/parser/handler/mod.rs +9 -8
  169. package/rust/core/parser/handler/pattern.rs +74 -0
  170. package/rust/core/parser/handler/tempo.rs +57 -47
  171. package/rust/core/parser/mod.rs +3 -4
  172. package/rust/core/parser/statement.rs +11 -96
  173. package/rust/core/plugin/loader.rs +137 -0
  174. package/rust/core/plugin/mod.rs +2 -0
  175. package/rust/core/plugin/runner.rs +347 -0
  176. package/rust/core/preprocessor/loader.rs +637 -193
  177. package/rust/core/preprocessor/mod.rs +4 -4
  178. package/rust/core/preprocessor/module.rs +60 -50
  179. package/rust/core/preprocessor/processor.rs +114 -76
  180. package/rust/core/preprocessor/resolver/bank.rs +49 -47
  181. package/rust/core/preprocessor/resolver/call.rs +124 -123
  182. package/rust/core/preprocessor/resolver/condition.rs +95 -92
  183. package/rust/core/preprocessor/resolver/driver.rs +324 -227
  184. package/rust/core/preprocessor/resolver/function.rs +69 -0
  185. package/rust/core/preprocessor/resolver/group.rs +94 -61
  186. package/rust/core/preprocessor/resolver/let_.rs +32 -31
  187. package/rust/core/preprocessor/resolver/loop_.rs +318 -91
  188. package/rust/core/preprocessor/resolver/mod.rs +16 -14
  189. package/rust/core/preprocessor/resolver/pattern.rs +83 -0
  190. package/rust/core/preprocessor/resolver/spawn.rs +99 -58
  191. package/rust/core/preprocessor/resolver/synth.rs +54 -50
  192. package/rust/core/preprocessor/resolver/tempo.rs +48 -49
  193. package/rust/core/preprocessor/resolver/trigger.rs +116 -112
  194. package/rust/core/preprocessor/resolver/value.rs +176 -78
  195. package/rust/core/store/export.rs +28 -28
  196. package/rust/core/store/function.rs +40 -0
  197. package/rust/core/store/global.rs +61 -39
  198. package/rust/core/store/import.rs +28 -28
  199. package/rust/core/store/mod.rs +5 -4
  200. package/rust/core/store/variable.rs +51 -28
  201. package/rust/core/utils/mod.rs +1 -2
  202. package/rust/core/utils/path.rs +37 -31
  203. package/rust/lib.rs +308 -117
  204. package/rust/main.rs +364 -65
  205. package/rust/types/Cargo.toml +11 -0
  206. package/rust/types/src/addons.rs +55 -0
  207. package/rust/types/src/ast.rs +202 -0
  208. package/rust/types/src/config.rs +74 -0
  209. package/rust/types/src/lib.rs +12 -0
  210. package/rust/types/src/telemetry.rs +85 -0
  211. package/rust/utils/Cargo.toml +26 -0
  212. package/rust/utils/src/error.rs +186 -0
  213. package/rust/utils/src/file.rs +94 -0
  214. package/rust/utils/src/first_usage.rs +97 -0
  215. package/rust/utils/{mod.rs → src/lib.rs} +9 -6
  216. package/rust/utils/{logger.rs → src/logger.rs} +200 -123
  217. package/rust/utils/src/path.rs +88 -0
  218. package/rust/utils/src/signature.rs +41 -0
  219. package/rust/utils/{spinner.rs → src/spinner.rs} +20 -21
  220. package/rust/utils/src/version.rs +27 -0
  221. package/rust/utils/{watcher.rs → src/watcher.rs} +46 -33
  222. package/rust/web/api.rs +5 -0
  223. package/rust/web/cdn.rs +34 -0
  224. package/rust/web/mod.rs +3 -0
  225. package/rust/web/sso.rs +5 -0
  226. package/templates/minimal/README.md +143 -127
  227. package/templates/welcome/README.md +143 -127
  228. package/templates/welcome/src/index.deva +56 -8
  229. package/templates/welcome/src/variables.deva +2 -4
  230. package/tests/integration.rs +21 -0
  231. package/tests/rust/cli_check_build.rs +21 -0
  232. package/tests/rust/cli_help.rs +12 -0
  233. package/tests/rust/cli_template_list.rs +10 -0
  234. package/tests/rust/cli_version.rs +11 -0
  235. package/tests/typescript/index.spec.ts +136 -0
  236. package/tests/typescript/playhead.spec.ts +36 -0
  237. package/tests/typescript/render_e2e.spec.ts +77 -0
  238. package/tsconfig.json +12 -10
  239. package/typescript/bin/index.ts +19 -5
  240. package/typescript/core/functions/index.ts +83 -0
  241. package/typescript/core/index.ts +6 -0
  242. package/typescript/core/types/index.ts +4 -0
  243. package/typescript/core/types/plugin.ts +19 -0
  244. package/typescript/core/types/result.ts +29 -0
  245. package/typescript/core/types/statement.ts +47 -0
  246. package/typescript/core/types/value.ts +29 -0
  247. package/typescript/index.ts +8 -1
  248. package/typescript/pkg/devalang_core.d.ts +4 -0
  249. package/typescript/pkg/devalang_core.ts +49 -0
  250. package/typescript/scripts/copy-wasm-dts.ts +41 -0
  251. package/typescript/scripts/postinstall.ts +85 -0
  252. package/typescript/scripts/version/bump.ts +0 -1
  253. package/typescript/scripts/version/index.ts +0 -1
  254. package/docs/COMMANDS.md +0 -85
  255. package/docs/CONFIG.md +0 -30
  256. package/docs/SYNTAX.md +0 -210
  257. package/out-tsc/bin/devalang.exe +0 -0
  258. package/out-tsc/scripts/postbuild.js +0 -11
  259. package/rust/cli/build.rs +0 -137
  260. package/rust/cli/check.rs +0 -117
  261. package/rust/cli/play.rs +0 -193
  262. package/rust/config/loader.rs +0 -13
  263. package/rust/core/audio/engine.rs +0 -203
  264. package/rust/core/shared/duration.rs +0 -8
  265. package/rust/core/shared/mod.rs +0 -2
  266. package/rust/core/shared/value.rs +0 -18
  267. package/rust/core/utils/validation.rs +0 -37
  268. package/rust/utils/file.rs +0 -35
  269. package/rust/utils/signature.rs +0 -17
  270. package/rust/utils/version.rs +0 -15
  271. package/typescript/scripts/postbuild.ts +0 -8
@@ -0,0 +1,325 @@
1
+ use devalang_types::Value;
2
+ use std::collections::HashMap;
3
+
4
+ // Sample rate and channel constants used throughout the engine.
5
+ const SAMPLE_RATE: u32 = 44100;
6
+ const CHANNELS: u16 = 2;
7
+
8
+ /// AudioEngine holds the generated interleaved stereo buffer and
9
+ /// provides simple utilities to mix/merge buffers and export WAV files.
10
+ ///
11
+ /// Notes:
12
+ /// - Buffer is interleaved stereo (L,R,L,R...).
13
+ /// - Methods are synchronous and operate on in-memory buffers.
14
+ #[derive(Debug, Clone, PartialEq)]
15
+ pub struct AudioEngine {
16
+ /// Master volume multiplier (not automatically applied by helpers).
17
+ pub volume: f32,
18
+ /// Interleaved i16 PCM buffer.
19
+ pub buffer: Vec<i16>,
20
+ /// Logical module name used for error traces/diagnostics.
21
+ pub module_name: String,
22
+ /// Simple diagnostic counter for inserted notes.
23
+ pub note_count: usize,
24
+ }
25
+
26
+ impl AudioEngine {
27
+ pub fn new(module_name: String) -> Self {
28
+ AudioEngine {
29
+ volume: 1.0,
30
+ buffer: vec![],
31
+ module_name,
32
+ note_count: 0,
33
+ }
34
+ }
35
+
36
+ pub fn get_buffer(&self) -> &[i16] {
37
+ &self.buffer
38
+ }
39
+
40
+ pub fn get_normalized_buffer(&self) -> Vec<f32> {
41
+ self.buffer.iter().map(|&s| (s as f32) / 32768.0).collect()
42
+ }
43
+
44
+ pub fn mix(&mut self, other: &AudioEngine) {
45
+ let max_len = self.buffer.len().max(other.buffer.len());
46
+ self.buffer.resize(max_len, 0);
47
+
48
+ for (i, &sample) in other.buffer.iter().enumerate() {
49
+ self.buffer[i] = self.buffer[i].saturating_add(sample);
50
+ }
51
+ }
52
+
53
+ pub fn merge_with(&mut self, other: AudioEngine) {
54
+ // If the other buffer is empty, simply return without warning (common for spawns that produced nothing)
55
+ if other.buffer.is_empty() {
56
+ return;
57
+ }
58
+
59
+ // If the other buffer is present but contains only zeros, warn and skip merge
60
+ if other.buffer.iter().all(|&s| s == 0) {
61
+ eprintln!("⚠️ Skipping merge: other buffer is silent");
62
+ return;
63
+ }
64
+
65
+ if self.buffer.iter().all(|&s| s == 0) {
66
+ self.buffer = other.buffer;
67
+ return;
68
+ }
69
+
70
+ self.mix(&other);
71
+ }
72
+
73
+ pub fn set_duration(&mut self, duration_secs: f32) {
74
+ let total_samples = (duration_secs * (SAMPLE_RATE as f32) * (CHANNELS as f32)) as usize;
75
+
76
+ if self.buffer.len() < total_samples {
77
+ self.buffer.resize(total_samples, 0);
78
+ }
79
+ }
80
+
81
+ pub fn generate_wav_file(&mut self, output_dir: &String) -> Result<(), String> {
82
+ if self.buffer.len() % (CHANNELS as usize) != 0 {
83
+ self.buffer.push(0);
84
+ println!("Completed buffer to respect stereo format.");
85
+ }
86
+
87
+ let spec = hound::WavSpec {
88
+ channels: CHANNELS,
89
+ sample_rate: SAMPLE_RATE,
90
+ bits_per_sample: 16,
91
+ sample_format: hound::SampleFormat::Int,
92
+ };
93
+
94
+ let mut writer = hound::WavWriter::create(output_dir, spec)
95
+ .map_err(|e| format!("Error creating WAV file: {}", e))?;
96
+
97
+ for sample in &self.buffer {
98
+ writer
99
+ .write_sample(*sample)
100
+ .map_err(|e| format!("Error writing sample: {:?}", e))?;
101
+ }
102
+
103
+ writer
104
+ .finalize()
105
+ .map_err(|e| format!("Error finalizing WAV: {:?}", e))?;
106
+
107
+ Ok(())
108
+ }
109
+
110
+ // Insert note moved here from original engine.rs
111
+ pub fn insert_note(
112
+ &mut self,
113
+ waveform: String,
114
+ freq: f32,
115
+ amp: f32,
116
+ start_time_ms: f32,
117
+ duration_ms: f32,
118
+ synth_params: HashMap<String, Value>,
119
+ note_params: HashMap<String, Value>,
120
+ automation: Option<HashMap<String, Value>>,
121
+ ) {
122
+ // Keep internal logic; helpers called from helpers module
123
+ let attack = self.extract_f32(&synth_params, "attack").unwrap_or(0.0);
124
+ let decay = self.extract_f32(&synth_params, "decay").unwrap_or(0.0);
125
+ let sustain = self.extract_f32(&synth_params, "sustain").unwrap_or(1.0);
126
+ let release = self.extract_f32(&synth_params, "release").unwrap_or(0.0);
127
+ let attack_s = if attack > 10.0 {
128
+ attack / 1000.0
129
+ } else {
130
+ attack
131
+ };
132
+ let decay_s = if decay > 10.0 { decay / 1000.0 } else { decay };
133
+ let release_s = if release > 10.0 {
134
+ release / 1000.0
135
+ } else {
136
+ release
137
+ };
138
+ let sustain_level = if sustain > 1.0 {
139
+ (sustain / 100.0).clamp(0.0, 1.0)
140
+ } else {
141
+ sustain.clamp(0.0, 1.0)
142
+ };
143
+
144
+ let duration_ms = self
145
+ .extract_f32(&note_params, "duration")
146
+ .unwrap_or(duration_ms);
147
+ let velocity = self.extract_f32(&note_params, "velocity").unwrap_or(1.0);
148
+ let glide = self.extract_boolean(&note_params, "glide").unwrap_or(false);
149
+ let slide = self.extract_boolean(&note_params, "slide").unwrap_or(false);
150
+
151
+ let _amplitude = (i16::MAX as f32) * amp.clamp(0.0, 1.0) * velocity.clamp(0.0, 1.0);
152
+
153
+ let freq_start = freq;
154
+ let mut freq_end = freq;
155
+ let amp_start = amp * velocity.clamp(0.0, 1.0);
156
+ let mut amp_end = amp_start;
157
+
158
+ if glide {
159
+ if let Some(Value::Number(target_freq)) = note_params.get("target_freq") {
160
+ freq_end = *target_freq;
161
+ } else {
162
+ freq_end = freq * 1.5;
163
+ }
164
+ }
165
+
166
+ if slide {
167
+ if let Some(Value::Number(target_amp)) = note_params.get("target_amp") {
168
+ amp_end = *target_amp * velocity.clamp(0.0, 1.0);
169
+ } else {
170
+ amp_end = amp_start * 0.5;
171
+ }
172
+ }
173
+
174
+ let sample_rate = SAMPLE_RATE as f32;
175
+ let channels = CHANNELS as usize;
176
+
177
+ let total_samples = ((duration_ms / 1000.0) * sample_rate) as usize;
178
+ let start_sample = ((start_time_ms / 1000.0) * sample_rate) as usize;
179
+
180
+ let (volume_env, pan_env, pitch_env) =
181
+ crate::core::audio::engine::helpers::env_maps_from_automation(&automation);
182
+
183
+ let mut stereo_samples: Vec<i16> = Vec::with_capacity(total_samples * 2);
184
+ let fade_len = (sample_rate * 0.01) as usize; // 10 ms fade
185
+
186
+ let attack_samples = (attack_s * sample_rate) as usize;
187
+ let decay_samples = (decay_s * sample_rate) as usize;
188
+ let release_samples = (release_s * sample_rate) as usize;
189
+ let sustain_samples = if total_samples > attack_samples + decay_samples + release_samples {
190
+ total_samples - attack_samples - decay_samples - release_samples
191
+ } else {
192
+ 0
193
+ };
194
+
195
+ for i in 0..total_samples {
196
+ let t = ((start_sample + i) as f32) / sample_rate;
197
+
198
+ // Glide
199
+ let current_freq = if glide {
200
+ freq_start + ((freq_end - freq_start) * (i as f32)) / (total_samples as f32)
201
+ } else {
202
+ freq
203
+ };
204
+
205
+ // Pitch automation (in semitones), applied as frequency multiplier
206
+ let pitch_semi = crate::core::audio::engine::helpers::eval_env_map(
207
+ &pitch_env,
208
+ (i as f32) / (total_samples as f32),
209
+ 0.0,
210
+ );
211
+ let current_freq = current_freq * (2.0_f32).powf(pitch_semi / 12.0);
212
+
213
+ // Slide
214
+ let current_amp = if slide {
215
+ amp_start + ((amp_end - amp_start) * (i as f32)) / (total_samples as f32)
216
+ } else {
217
+ amp_start
218
+ };
219
+
220
+ let mut value =
221
+ crate::core::audio::engine::helpers::oscillator_sample(&waveform, current_freq, t);
222
+
223
+ // ADSR envelope
224
+ let envelope = crate::core::audio::engine::helpers::adsr_envelope_value(
225
+ i,
226
+ attack_samples,
227
+ decay_samples,
228
+ sustain_samples,
229
+ release_samples,
230
+ sustain_level,
231
+ );
232
+
233
+ // Fade in/out
234
+ if fade_len > 0 && i < fade_len {
235
+ if fade_len == 1 {
236
+ value *= 0.0;
237
+ } else {
238
+ value *= (i as f32) / (fade_len as f32);
239
+ }
240
+ } else if fade_len > 0 && i >= total_samples.saturating_sub(fade_len) {
241
+ if fade_len == 1 {
242
+ value *= 0.0;
243
+ } else {
244
+ // ensure last sample becomes exactly zero to avoid clicks
245
+ value *= ((total_samples - 1 - i) as f32) / ((fade_len - 1) as f32);
246
+ }
247
+ }
248
+
249
+ value *= envelope;
250
+ let mut sample_val = value * (i16::MAX as f32) * current_amp;
251
+
252
+ let vol_mul = crate::core::audio::engine::helpers::eval_env_map(
253
+ &volume_env,
254
+ (i as f32) / (total_samples as f32),
255
+ 1.0,
256
+ )
257
+ .clamp(0.0, 10.0);
258
+ sample_val *= vol_mul;
259
+
260
+ let pan_val = crate::core::audio::engine::helpers::eval_env_map(
261
+ &pan_env,
262
+ (i as f32) / (total_samples as f32),
263
+ 0.0,
264
+ )
265
+ .clamp(-1.0, 1.0);
266
+ let (left_gain, right_gain) = crate::core::audio::engine::helpers::pan_gains(pan_val);
267
+
268
+ let left = (sample_val * left_gain)
269
+ .round()
270
+ .clamp(i16::MIN as f32, i16::MAX as f32) as i16;
271
+ let right = (sample_val * right_gain)
272
+ .round()
273
+ .clamp(i16::MIN as f32, i16::MAX as f32) as i16;
274
+
275
+ stereo_samples.push(left);
276
+ stereo_samples.push(right);
277
+ }
278
+
279
+ // Increment note counter for diagnostics
280
+ self.note_count = self.note_count.saturating_add(1);
281
+
282
+ crate::core::audio::engine::helpers::mix_stereo_samples_into_buffer(
283
+ self,
284
+ start_sample,
285
+ channels,
286
+ &stereo_samples,
287
+ );
288
+ }
289
+
290
+ // helper extraction functions left in this struct for now
291
+ fn extract_f32(&self, map: &HashMap<String, Value>, key: &str) -> Option<f32> {
292
+ match map.get(key) {
293
+ Some(Value::Number(n)) => Some(*n),
294
+ Some(Value::String(s)) => s.parse::<f32>().ok(),
295
+ Some(Value::Boolean(b)) => Some(if *b { 1.0 } else { 0.0 }),
296
+ _ => None,
297
+ }
298
+ }
299
+
300
+ fn extract_boolean(&self, map: &HashMap<String, Value>, key: &str) -> Option<bool> {
301
+ match map.get(key) {
302
+ Some(Value::Boolean(b)) => Some(*b),
303
+ Some(Value::Number(n)) => Some(*n != 0.0),
304
+ Some(Value::Identifier(s)) => {
305
+ if s == "true" {
306
+ Some(true)
307
+ } else if s == "false" {
308
+ Some(false)
309
+ } else {
310
+ None
311
+ }
312
+ }
313
+ Some(Value::String(s)) => {
314
+ if s == "true" {
315
+ Some(true)
316
+ } else if s == "false" {
317
+ Some(false)
318
+ } else {
319
+ None
320
+ }
321
+ }
322
+ _ => None,
323
+ }
324
+ }
325
+ }
@@ -1,31 +1,310 @@
1
- use crate::core::{ shared::value::Value, store::variable::VariableTable };
2
-
3
- pub fn evaluate_condition_string(expr: &str, vars: &VariableTable) -> bool {
4
- let tokens: Vec<&str> = expr.split_whitespace().collect();
5
- if tokens.len() != 3 {
6
- return false;
7
- }
8
-
9
- let left = tokens[0];
10
- let op = tokens[1];
11
- let right = tokens[2];
12
-
13
- let left_val = match vars.get(left) {
14
- Some(Value::Number(n)) => *n,
15
- _ => {
16
- return false;
17
- }
18
- };
19
-
20
- let right_val: f32 = right.parse().unwrap_or(0.0);
21
-
22
- match op {
23
- ">" => left_val > right_val,
24
- "<" => left_val < right_val,
25
- ">=" => left_val >= right_val,
26
- "<=" => left_val <= right_val,
27
- "==" => (left_val - right_val).abs() < f32::EPSILON,
28
- "!=" => (left_val - right_val).abs() > f32::EPSILON,
29
- _ => false,
30
- }
31
- }
1
+ use crate::core::audio::special::{
2
+ find_and_eval_first_easing_call, find_and_eval_first_math_call, find_and_eval_first_mod_call,
3
+ resolve_env_atom,
4
+ };
5
+ use crate::core::store::variable::VariableTable;
6
+ use devalang_types::Value;
7
+
8
+ pub fn evaluate_condition_string(expr: &str, vars: &VariableTable) -> bool {
9
+ let tokens: Vec<&str> = expr.split_whitespace().collect();
10
+ if tokens.len() != 3 {
11
+ return false;
12
+ }
13
+
14
+ let left = tokens[0];
15
+ let op = tokens[1];
16
+ let right = tokens[2];
17
+
18
+ // Resolve left and right to numeric values where possible. Accept numbers, variables or env atoms.
19
+ fn resolve_for_cond(s: &str, vars: &VariableTable) -> Option<f32> {
20
+ if let Ok(n) = s.parse::<f32>() {
21
+ return Some(n);
22
+ }
23
+ if let Some(Value::Number(n)) = vars.get(s) {
24
+ return Some(*n);
25
+ }
26
+ if let Some(v) = resolve_env_atom(s, 120.0, 1.0) {
27
+ return Some(v);
28
+ }
29
+ None
30
+ }
31
+
32
+ let left_val = match resolve_for_cond(left, vars) {
33
+ Some(v) => v,
34
+ None => return false,
35
+ };
36
+
37
+ let right_val = match resolve_for_cond(right, vars) {
38
+ Some(v) => v,
39
+ None => return false,
40
+ };
41
+
42
+ match op {
43
+ ">" => left_val > right_val,
44
+ "<" => left_val < right_val,
45
+ ">=" => left_val >= right_val,
46
+ "<=" => left_val <= right_val,
47
+ "==" => {
48
+ // relative epsilon for floating comparisons
49
+ let diff = (left_val - right_val).abs();
50
+ let largest = left_val.abs().max(right_val.abs()).max(1.0);
51
+ diff <= (f32::EPSILON * largest)
52
+ }
53
+ "!=" => {
54
+ let diff = (left_val - right_val).abs();
55
+ let largest = left_val.abs().max(right_val.abs()).max(1.0);
56
+ diff > (f32::EPSILON * largest)
57
+ }
58
+ _ => false,
59
+ }
60
+ }
61
+
62
+ // Very small expression evaluator for `$env.*`, `$math.*` and variables.
63
+ // Supports: +, -, *, / and simple parentheses, left-to-right (no precedence), and $math.(sin|cos)(expr)
64
+ pub fn evaluate_numeric_expression(
65
+ expr: &str,
66
+ vars: &VariableTable,
67
+ env_bpm: f32,
68
+ env_beat: f32,
69
+ ) -> Option<f32> {
70
+ let expr = expr.replace(" ", "");
71
+
72
+ // Helper to resolve an atom to a number
73
+ fn resolve_atom(atom: &str, vars: &VariableTable, bpm: f32, beat: f32) -> Option<f32> {
74
+ if let Some(v) = resolve_env_atom(atom, bpm, beat) {
75
+ return Some(v);
76
+ }
77
+ if let Ok(n) = atom.parse::<f32>() {
78
+ return Some(n);
79
+ }
80
+ if let Some(Value::Number(n)) = vars.get(atom) {
81
+ return Some(*n);
82
+ }
83
+ None
84
+ }
85
+
86
+ // Shunting-like, simplified: first evaluate any $math.func(...) calls anywhere in the expression,
87
+ // then fold remaining parentheses and evaluate left-to-right.
88
+ fn eval(expr: &str, vars: &VariableTable, bpm: f32, beat: f32) -> Option<f32> {
89
+ // 1) Replace $math/$easing/$mod calls progressively with a max iteration guard
90
+ let mut s = expr.to_string();
91
+ let mut iterations = 0u32;
92
+ const MAX_ITER: u32 = 64;
93
+
94
+ // Evaluate modulators first (they may feed easing/math)
95
+ while iterations < MAX_ITER {
96
+ if let Some(next) =
97
+ find_and_eval_first_mod_call(&s, evaluate_numeric_expression, vars, bpm, beat)
98
+ {
99
+ s = next;
100
+ iterations += 1;
101
+ continue;
102
+ }
103
+ break;
104
+ }
105
+
106
+ iterations = 0;
107
+ while iterations < MAX_ITER {
108
+ if let Some(next) =
109
+ find_and_eval_first_easing_call(&s, evaluate_numeric_expression, vars, bpm, beat)
110
+ {
111
+ s = next;
112
+ iterations += 1;
113
+ continue;
114
+ }
115
+ break;
116
+ }
117
+
118
+ iterations = 0;
119
+ while iterations < MAX_ITER {
120
+ if let Some(next) =
121
+ find_and_eval_first_math_call(&s, evaluate_numeric_expression, vars, bpm, beat)
122
+ {
123
+ s = next;
124
+ iterations += 1;
125
+ continue;
126
+ }
127
+ break;
128
+ }
129
+
130
+ // 2) Evaluate remaining (pure) parentheses starting from innermost
131
+ if let Some(open) = s.rfind('(') {
132
+ if let Some(close_rel) = s[open..].find(')') {
133
+ // index relatif
134
+ let close = open + close_rel;
135
+ let inner = &s[open + 1..close];
136
+ let val = eval(inner, vars, bpm, beat)?;
137
+ let mut replaced = String::new();
138
+ replaced.push_str(&s[..open]);
139
+ replaced.push_str(&val.to_string());
140
+ replaced.push_str(&s[close + 1..]);
141
+ return eval(&replaced, vars, bpm, beat);
142
+ }
143
+ }
144
+
145
+ // Tokenize by operators left-to-right
146
+ let mut parts: Vec<String> = Vec::new();
147
+ let mut cur = String::new();
148
+ for ch in s.chars() {
149
+ if "+-*/".contains(ch) {
150
+ if !cur.is_empty() {
151
+ parts.push(cur.clone());
152
+ cur.clear();
153
+ }
154
+ parts.push(ch.to_string());
155
+ } else {
156
+ cur.push(ch);
157
+ }
158
+ }
159
+ if !cur.is_empty() {
160
+ parts.push(cur);
161
+ }
162
+ if parts.is_empty() {
163
+ return None;
164
+ }
165
+
166
+ // Resolve atoms and compute
167
+ let mut acc: Option<f32> = None;
168
+ let mut op: Option<char> = None;
169
+ for part in parts {
170
+ if part.len() == 1 && "+-*/".contains(part.chars().next().unwrap()) {
171
+ op = part.chars().next();
172
+ continue;
173
+ }
174
+ let val = if let Some(v) = resolve_atom(&part, vars, bpm, beat) {
175
+ v
176
+ } else if part.starts_with("$env.") {
177
+ // $env atom not handled by resolve_atom (when composed), try recursive eval
178
+ eval(&part, vars, bpm, beat)?
179
+ } else {
180
+ return None;
181
+ };
182
+
183
+ acc = Some(match (acc, op) {
184
+ (None, _) => val,
185
+ (Some(a), Some('+')) => a + val,
186
+ (Some(a), Some('-')) => a - val,
187
+ (Some(a), Some('*')) => a * val,
188
+ (Some(a), Some('/')) => {
189
+ if val != 0.0 {
190
+ a / val
191
+ } else {
192
+ return Some(f32::INFINITY);
193
+ }
194
+ }
195
+ (Some(_), None) => val,
196
+ _ => {
197
+ return None;
198
+ }
199
+ });
200
+ }
201
+
202
+ acc
203
+ }
204
+
205
+ eval(&expr, vars, env_bpm, env_beat)
206
+ }
207
+
208
+ pub fn evaluate_rhs_into_value(
209
+ raw: &str,
210
+ vars: &VariableTable,
211
+ env_bpm: f32,
212
+ env_beat: f32,
213
+ ) -> Value {
214
+ if let Some(num) = evaluate_numeric_expression(raw, vars, env_bpm, env_beat) {
215
+ Value::Number(num)
216
+ } else {
217
+ Value::String(raw.to_string())
218
+ }
219
+ }
220
+
221
+ // Evaluate a simple string concatenation expression like: "hello " + name + "!" + $env.beat
222
+ // - Splits on + outside quotes
223
+ // - Terms can be string literals (double quotes), variables (Number/String/Boolean), or numeric env/math expressions
224
+ // Returns None if parsing fails (fallback to raw print)
225
+ pub fn evaluate_string_expression(
226
+ expr: &str,
227
+ vars: &VariableTable,
228
+ env_bpm: f32,
229
+ env_beat: f32,
230
+ ) -> Option<String> {
231
+ // Quick reject if no '+' present
232
+ if !expr.contains('+') {
233
+ return None;
234
+ }
235
+
236
+ // Split by '+' outside of quotes
237
+ let mut parts: Vec<String> = Vec::new();
238
+ let mut cur = String::new();
239
+ let mut in_quotes = false;
240
+ let mut escape = false;
241
+ for ch in expr.chars() {
242
+ if escape {
243
+ cur.push(ch);
244
+ escape = false;
245
+ continue;
246
+ }
247
+ if ch == '\\' {
248
+ // escape next char
249
+ escape = true;
250
+ continue;
251
+ }
252
+ if ch == '"' {
253
+ in_quotes = !in_quotes;
254
+ cur.push(ch);
255
+ continue;
256
+ }
257
+ if ch == '+' && !in_quotes {
258
+ parts.push(cur.to_string());
259
+ cur.clear();
260
+ continue;
261
+ }
262
+ cur.push(ch);
263
+ }
264
+ if !cur.is_empty() {
265
+ parts.push(cur.to_string());
266
+ }
267
+ if parts.is_empty() {
268
+ return None;
269
+ }
270
+
271
+ // Resolve each part into a string
272
+ fn strip_quotes(s: &str) -> Option<String> {
273
+ let st = s.trim();
274
+ if st.len() >= 2 && st.starts_with('"') && st.ends_with('"') {
275
+ Some(st[1..st.len() - 1].to_string())
276
+ } else {
277
+ None
278
+ }
279
+ }
280
+
281
+ let mut out = String::new();
282
+ for p in parts {
283
+ if p.is_empty() {
284
+ continue;
285
+ }
286
+ if let Some(lit) = strip_quotes(&p) {
287
+ out.push_str(&lit);
288
+ continue;
289
+ }
290
+ // Try variables first
291
+ if let Some(val) = vars.get(&p) {
292
+ match val {
293
+ Value::String(s) => out.push_str(s),
294
+ Value::Number(n) => out.push_str(&format!("{}", n)),
295
+ Value::Boolean(b) => out.push_str(&format!("{}", b)),
296
+ other => out.push_str(&format!("{:?}", other)),
297
+ }
298
+ continue;
299
+ }
300
+ // Try env/math/numeric expression for this term
301
+ if let Some(n) = evaluate_numeric_expression(&p, vars, env_bpm, env_beat) {
302
+ out.push_str(&format!("{}", n));
303
+ continue;
304
+ }
305
+ // Bareword not resolved: include as-is (safe fallback)
306
+ out.push_str(&p);
307
+ }
308
+
309
+ Some(out)
310
+ }