@devaloop/devalang 0.0.1-beta.2 → 0.0.1-beta.3

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