@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
@@ -1,239 +1,242 @@
1
- use std::collections::HashMap;
2
-
3
- use crate::core::{
4
- audio::{engine::AudioEngine, loader::trigger::load_trigger},
5
- parser::statement::{Statement, StatementKind},
6
- store::variable::VariableTable,
7
- };
8
- use devalang_types::{Duration, Value};
9
- use devalang_utils::logger::Logger;
10
-
11
- pub fn interprete_trigger_statement(
12
- stmt: &Statement,
13
- audio_engine: &mut AudioEngine,
14
- variable_table: &VariableTable,
15
- base_duration: f32,
16
- cursor_time: f32,
17
- max_end_time: f32,
18
- ) -> Option<(f32, f32, AudioEngine)> {
19
- if let StatementKind::Trigger {
20
- entity,
21
- duration,
22
- effects,
23
- } = &stmt.kind
24
- {
25
- let mut trigger_val = Value::String(entity.clone());
26
- let mut trigger_src = String::new();
27
-
28
- match variable_table.variables.get(entity) {
29
- Some(Value::Identifier(id)) => {
30
- // Get real value from global variable table
31
- if let Some(global_table) = &variable_table.parent {
32
- if let Some(val) = global_table.get(id) {
33
- trigger_val = val.clone();
34
- } else {
35
- eprintln!(
36
- "❌ Trigger entity '{}' not found in global variable table",
37
- id
38
- );
39
- return None;
40
- }
41
- } else if let Some(val) = variable_table.get(id) {
42
- trigger_val = val.clone();
43
- } else {
44
- eprintln!("❌ Trigger entity '{}' not found in variable table", id);
45
- return None;
46
- }
47
- }
48
- Some(Value::Sample(sample_src)) => {
49
- // If the entity is a sample, we use its path directly
50
- trigger_src = sample_src.clone();
51
- }
52
- Some(Value::Map(map)) => {
53
- // If the entity is a map, we assume it contains an "entity" key
54
- if let Some(Value::String(src)) = map.get("entity") {
55
- trigger_val = Value::String(src.clone());
56
- } else if let Some(Value::Identifier(src)) = map.get("entity") {
57
- trigger_val = Value::Identifier(src.clone());
58
- } else {
59
- eprintln!(
60
- "❌ Trigger map must contain an 'entity' key with a string or identifier value."
61
- );
62
- return None;
63
- }
64
- }
65
- _ => {
66
- trigger_val = Value::String(entity.clone());
67
- }
68
- }
69
-
70
- // If trigger could not be resolved to a known mapping or explicit path, abort early
71
- if let Value::String(s) = &trigger_val {
72
- let is_protocol = s.starts_with("devalang://");
73
- let is_var = variable_table.get(s).is_some()
74
- || variable_table
75
- .parent
76
- .as_ref()
77
- .and_then(|p| p.get(s))
78
- .is_some();
79
- let looks_like_path = s.contains('/')
80
- || s.ends_with(".wav")
81
- || s.ends_with(".mp3")
82
- || s.ends_with(".ogg");
83
- if !is_protocol && !is_var && !looks_like_path {
84
- let logger = Logger::new();
85
- logger.log_error_with_stacktrace(
86
- &format!("unknown trigger: {}", s),
87
- &format!("{}:{}:{}", audio_engine.module_name, stmt.line, stmt.column),
88
- );
89
- return None;
90
- }
91
- }
92
-
93
- let duration_secs = match duration {
94
- Duration::Number(n) => *n,
95
-
96
- Duration::Identifier(id) => {
97
- if id == "auto" {
98
- 1.0
99
- } else {
100
- match variable_table.get(id) {
101
- Some(Value::Number(n)) => *n,
102
- Some(Value::Identifier(other)) if other == "auto" => 1.0,
103
- Some(other) => {
104
- eprintln!(
105
- "❌ Invalid duration reference '{}': expected number, got {:?}",
106
- id, other
107
- );
108
- return None;
109
- }
110
- None => {
111
- eprintln!("❌ Duration identifier '{}' not found", id);
112
- return None;
113
- }
114
- }
115
- }
116
- }
117
-
118
- Duration::Beat(beat_str) => {
119
- let parts: Vec<&str> = beat_str.split('/').collect();
120
- if parts.len() != 2 {
121
- eprintln!("❌ Invalid beat duration format: {}", beat_str);
122
- return None;
123
- }
124
-
125
- let numerator: f32 = parts[0].parse().unwrap_or(1.0);
126
- let denominator: f32 = parts[1].parse().unwrap_or(4.0);
127
-
128
- let beats = (numerator / denominator) * 4.0;
129
-
130
- beats * base_duration
131
- }
132
-
133
- Duration::Auto => base_duration,
134
- };
135
-
136
- let final_variable_table = if let Some(parent) = &variable_table.parent {
137
- VariableTable {
138
- variables: parent.variables.clone(),
139
- parent: None,
140
- }
141
- } else {
142
- variable_table.clone()
143
- };
144
-
145
- let (src, sample_length) = load_trigger(
146
- &trigger_val,
147
- duration,
148
- effects,
149
- base_duration,
150
- final_variable_table.clone(),
151
- );
152
-
153
- if trigger_src.is_empty() {
154
- trigger_src = src;
155
- }
156
-
157
- let effects = extract_effects(stmt.value.clone());
158
- let one_shot = effects
159
- .as_ref()
160
- .and_then(|map| map.get("one_shot"))
161
- .and_then(|v| match v {
162
- Value::Identifier(id) if id == "true" => Some(true),
163
- Value::String(s) if s == "true" => Some(true),
164
- _ => None,
165
- })
166
- .unwrap_or(false);
167
-
168
- let play_length = if one_shot {
169
- sample_length // play entire sample
170
- } else {
171
- duration_secs.min(sample_length)
172
- };
173
-
174
- let trigger_src = match trigger_val.get("entity") {
175
- Some(Value::String(src)) => src.clone(),
176
- Some(Value::Identifier(id)) => id.clone(),
177
- Some(Value::Statement(stmt)) => {
178
- if let StatementKind::Trigger { entity, .. } = &stmt.kind {
179
- entity.clone()
180
- } else {
181
- eprintln!("❌ Invalid trigger statement in map: expected 'Trigger' kind");
182
- return None;
183
- }
184
- }
185
- _ => trigger_src,
186
- };
187
-
188
- if let Some(effects_map) = effects {
189
- audio_engine.insert_sample(
190
- &trigger_src,
191
- cursor_time,
192
- play_length,
193
- Some(effects_map),
194
- &final_variable_table,
195
- );
196
- } else {
197
- audio_engine.insert_sample(
198
- &trigger_src,
199
- cursor_time,
200
- play_length,
201
- None,
202
- &final_variable_table,
203
- );
204
- }
205
-
206
- let new_cursor_time = cursor_time + duration_secs; // advance by beat duration
207
- let new_max_end_time = (cursor_time + play_length).max(max_end_time);
208
-
209
- let updated_engine = audio_engine.clone();
210
-
211
- return Some((new_cursor_time, new_max_end_time, updated_engine));
212
- }
213
-
214
- None
215
- }
216
-
217
- fn extract_effects(value: Value) -> Option<HashMap<String, Value>> {
218
- if let Value::Map(map) = value {
219
- let mut effects = HashMap::new();
220
-
221
- for (key, val) in map {
222
- if key == "effects" {
223
- if let Value::Map(effect_map) = val {
224
- for (effect_key, effect_val) in effect_map {
225
- effects.insert(effect_key, effect_val);
226
- }
227
- } else {
228
- return None; // effects must be a map
229
- }
230
- } else {
231
- return Some(effects);
232
- }
233
- }
234
-
235
- Some(effects)
236
- } else {
237
- None
238
- }
239
- }
1
+ use std::collections::HashMap;
2
+
3
+ use crate::core::{
4
+ audio::{engine::AudioEngine, loader::trigger::load_trigger},
5
+ parser::statement::{Statement, StatementKind},
6
+ };
7
+ use devalang_types::store::VariableTable;
8
+ use devalang_types::{Duration, Value};
9
+ use devalang_utils::logger::Logger;
10
+
11
+ pub fn interprete_trigger_statement(
12
+ stmt: &Statement,
13
+ audio_engine: &mut AudioEngine,
14
+ variable_table: &VariableTable,
15
+ base_duration: f32,
16
+ cursor_time: f32,
17
+ max_end_time: f32,
18
+ ) -> Option<(f32, f32, AudioEngine)> {
19
+ if let StatementKind::Trigger {
20
+ entity,
21
+ duration,
22
+ effects,
23
+ } = &stmt.kind
24
+ {
25
+ let mut trigger_val = Value::String(entity.clone());
26
+ let mut trigger_src = String::new();
27
+
28
+ match variable_table.variables.get(entity) {
29
+ Some(Value::String(s)) => {
30
+ trigger_val = Value::String(s.clone());
31
+ }
32
+ Some(Value::Identifier(id)) => {
33
+ // Get real value from global variable table
34
+ if let Some(global_table) = &variable_table.parent {
35
+ if let Some(val) = global_table.get(id) {
36
+ trigger_val = val.clone();
37
+ } else {
38
+ eprintln!(
39
+ "❌ Trigger entity '{}' not found in global variable table",
40
+ id
41
+ );
42
+ return None;
43
+ }
44
+ } else if let Some(val) = variable_table.get(id) {
45
+ trigger_val = val.clone();
46
+ } else {
47
+ eprintln!("❌ Trigger entity '{}' not found in variable table", id);
48
+ return None;
49
+ }
50
+ }
51
+ Some(Value::Sample(sample_src)) => {
52
+ // If the entity is a sample, we use its path directly
53
+ trigger_src = sample_src.clone();
54
+ }
55
+ Some(Value::Map(map)) => {
56
+ // If the entity is a map, we assume it contains an "entity" key
57
+ if let Some(Value::String(src)) = map.get("entity") {
58
+ trigger_val = Value::String(src.clone());
59
+ } else if let Some(Value::Identifier(src)) = map.get("entity") {
60
+ trigger_val = Value::Identifier(src.clone());
61
+ } else {
62
+ eprintln!(
63
+ "❌ Trigger map must contain an 'entity' key with a string or identifier value."
64
+ );
65
+ return None;
66
+ }
67
+ }
68
+ _ => {
69
+ trigger_val = Value::String(entity.clone());
70
+ }
71
+ }
72
+
73
+ // If trigger could not be resolved to a known mapping or explicit path, abort early
74
+ if let Value::String(s) = &trigger_val {
75
+ let is_protocol = s.starts_with("devalang://");
76
+ let is_var = variable_table.get(s).is_some()
77
+ || variable_table
78
+ .parent
79
+ .as_ref()
80
+ .and_then(|p| p.get(s))
81
+ .is_some();
82
+ let looks_like_path = s.contains('/')
83
+ || s.ends_with(".wav")
84
+ || s.ends_with(".mp3")
85
+ || s.ends_with(".ogg");
86
+ if !is_protocol && !is_var && !looks_like_path {
87
+ let logger = Logger::new();
88
+ logger.log_error_with_stacktrace(
89
+ &format!("unknown trigger: {}", s),
90
+ &format!("{}:{}:{}", audio_engine.module_name, stmt.line, stmt.column),
91
+ );
92
+ return None;
93
+ }
94
+ }
95
+
96
+ let duration_secs = match duration {
97
+ Duration::Number(n) => *n,
98
+
99
+ Duration::Identifier(id) => {
100
+ if id == "auto" {
101
+ 1.0
102
+ } else {
103
+ match variable_table.get(id) {
104
+ Some(Value::Number(n)) => *n,
105
+ Some(Value::Identifier(other)) if other == "auto" => 1.0,
106
+ Some(other) => {
107
+ eprintln!(
108
+ "❌ Invalid duration reference '{}': expected number, got {:?}",
109
+ id, other
110
+ );
111
+ return None;
112
+ }
113
+ None => {
114
+ eprintln!("❌ Duration identifier '{}' not found", id);
115
+ return None;
116
+ }
117
+ }
118
+ }
119
+ }
120
+
121
+ Duration::Beat(beat_str) => {
122
+ let parts: Vec<&str> = beat_str.split('/').collect();
123
+ if parts.len() != 2 {
124
+ eprintln!("❌ Invalid beat duration format: {}", beat_str);
125
+ return None;
126
+ }
127
+
128
+ let numerator: f32 = parts[0].parse().unwrap_or(1.0);
129
+ let denominator: f32 = parts[1].parse().unwrap_or(4.0);
130
+
131
+ let beats = (numerator / denominator) * 4.0;
132
+
133
+ beats * base_duration
134
+ }
135
+
136
+ Duration::Auto => base_duration,
137
+ };
138
+
139
+ let final_variable_table = if let Some(parent) = &variable_table.parent {
140
+ VariableTable {
141
+ variables: parent.variables.clone(),
142
+ parent: None,
143
+ }
144
+ } else {
145
+ variable_table.clone()
146
+ };
147
+
148
+ let (src, sample_length) = load_trigger(
149
+ &trigger_val,
150
+ duration,
151
+ effects,
152
+ base_duration,
153
+ final_variable_table.clone(),
154
+ );
155
+
156
+ if trigger_src.is_empty() {
157
+ trigger_src = src;
158
+ }
159
+
160
+ let effects = extract_effects(stmt.value.clone());
161
+ let one_shot = effects
162
+ .as_ref()
163
+ .and_then(|map| map.get("one_shot"))
164
+ .and_then(|v| match v {
165
+ Value::Identifier(id) if id == "true" => Some(true),
166
+ Value::String(s) if s == "true" => Some(true),
167
+ _ => None,
168
+ })
169
+ .unwrap_or(false);
170
+
171
+ let play_length = if one_shot {
172
+ sample_length // play entire sample
173
+ } else {
174
+ duration_secs.min(sample_length)
175
+ };
176
+
177
+ let trigger_src = match trigger_val.get("entity") {
178
+ Some(Value::String(src)) => src.clone(),
179
+ Some(Value::Identifier(id)) => id.clone(),
180
+ Some(Value::Statement(stmt)) => {
181
+ if let StatementKind::Trigger { entity, .. } = &stmt.kind {
182
+ entity.clone()
183
+ } else {
184
+ eprintln!("❌ Invalid trigger statement in map: expected 'Trigger' kind");
185
+ return None;
186
+ }
187
+ }
188
+ _ => trigger_src,
189
+ };
190
+
191
+ if let Some(effects_map) = effects {
192
+ audio_engine.insert_sample(
193
+ &trigger_src,
194
+ cursor_time,
195
+ play_length,
196
+ Some(effects_map),
197
+ &final_variable_table,
198
+ );
199
+ } else {
200
+ audio_engine.insert_sample(
201
+ &trigger_src,
202
+ cursor_time,
203
+ play_length,
204
+ None,
205
+ &final_variable_table,
206
+ );
207
+ }
208
+
209
+ let new_cursor_time = cursor_time + duration_secs; // advance by beat duration
210
+ let new_max_end_time = (cursor_time + play_length).max(max_end_time);
211
+
212
+ let updated_engine = audio_engine.clone();
213
+
214
+ return Some((new_cursor_time, new_max_end_time, updated_engine));
215
+ }
216
+
217
+ None
218
+ }
219
+
220
+ fn extract_effects(value: Value) -> Option<HashMap<String, Value>> {
221
+ if let Value::Map(map) = value {
222
+ let mut effects = HashMap::new();
223
+
224
+ for (key, val) in map {
225
+ if key == "effects" {
226
+ if let Value::Map(effect_map) = val {
227
+ for (effect_key, effect_val) in effect_map {
228
+ effects.insert(effect_key, effect_val);
229
+ }
230
+ } else {
231
+ return None; // effects must be a map
232
+ }
233
+ } else {
234
+ return Some(effects);
235
+ }
236
+ }
237
+
238
+ Some(effects)
239
+ } else {
240
+ None
241
+ }
242
+ }