@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.
- package/Cargo.toml +84 -81
- package/README.md +3 -2
- package/docs/CHANGELOG.md +41 -0
- package/docs/ROADMAP.md +3 -3
- package/examples/chain.deva +19 -0
- package/examples/plugin.deva +10 -10
- package/examples/routing.deva +23 -0
- package/out-tsc/bin/project-version.json +6 -0
- package/out-tsc/pkg/devalang_core_bg.wasm.d.ts +8 -8
- package/out-tsc/scripts/version/copy-to-binary.d.ts +1 -0
- package/out-tsc/scripts/version/copy-to-binary.js +79 -0
- package/package.json +23 -10
- package/project-version.json +3 -3
- package/rust/bindings/Cargo.toml +9 -0
- package/rust/bindings/src/lib.rs +86 -0
- package/rust/cli/addon/commands.rs +35 -0
- package/rust/cli/addon/download.rs +234 -0
- package/rust/cli/addon/install.rs +33 -0
- package/rust/cli/addon/list.rs +224 -0
- package/rust/cli/addon/metadata.rs +124 -0
- package/rust/cli/addon/mod.rs +8 -0
- package/rust/cli/addon/remove.rs +271 -0
- package/rust/cli/addon/update.rs +305 -0
- package/rust/cli/{install/addon.rs → addon/utils.rs} +109 -118
- package/rust/cli/build/commands.rs +153 -153
- package/rust/cli/build/process.rs +165 -165
- package/rust/cli/check/mod.rs +208 -208
- package/rust/cli/discover/commands.rs +275 -253
- package/rust/cli/discover/config.rs +109 -111
- package/rust/cli/discover/fs.rs +19 -19
- package/rust/cli/discover/install.rs +214 -103
- package/rust/cli/discover/metadata.rs +48 -48
- package/rust/cli/discover/mod.rs +5 -5
- package/rust/cli/me/commands.rs +52 -0
- package/rust/cli/me/mod.rs +1 -0
- package/rust/cli/mod.rs +12 -12
- package/rust/cli/parser.rs +30 -69
- package/rust/cli/play/commands.rs +375 -375
- package/rust/cli/play/process.rs +159 -159
- package/rust/core/audio/engine/driver.rs +19 -2
- package/rust/core/audio/engine/export.rs +169 -169
- package/rust/core/audio/engine/mod.rs +56 -56
- package/rust/core/audio/engine/notes/dsp.rs +88 -85
- package/rust/core/audio/engine/notes/mod.rs +53 -44
- package/rust/core/audio/engine/notes/params.rs +294 -294
- package/rust/core/audio/engine/sample/insert.rs +148 -47
- package/rust/core/audio/engine/sample/mod.rs +40 -40
- package/rust/core/audio/engine/sample/padding.rs +170 -170
- package/rust/core/audio/evaluator/condition.rs +61 -61
- package/rust/core/audio/evaluator/numeric.rs +152 -152
- package/rust/core/audio/evaluator/rhs.rs +16 -16
- package/rust/core/audio/evaluator/string_expr.rs +94 -94
- package/rust/core/audio/interpreter/driver.rs +574 -574
- package/rust/core/audio/interpreter/mod.rs +2 -2
- package/rust/core/audio/interpreter/statements/arrow_call/interprete.rs +9 -5
- package/rust/core/audio/interpreter/statements/arrow_call/methods/chord.rs +398 -384
- package/rust/core/audio/interpreter/statements/arrow_call/methods/effects.rs +323 -0
- package/rust/core/audio/interpreter/statements/arrow_call/methods/mod.rs +1 -0
- package/rust/core/audio/interpreter/statements/arrow_call/methods/note.rs +66 -11
- package/rust/core/audio/interpreter/statements/arrow_call/mod.rs +3 -3
- package/rust/core/audio/interpreter/statements/arrow_call/types/arp.rs +192 -192
- package/rust/core/audio/interpreter/statements/arrow_call/types/mod.rs +24 -24
- package/rust/core/audio/interpreter/statements/arrow_call/types/pad.rs +116 -116
- package/rust/core/audio/interpreter/statements/arrow_call/types/pluck.rs +97 -97
- package/rust/core/audio/interpreter/statements/arrow_call/types/sub.rs +100 -100
- package/rust/core/audio/interpreter/statements/automate.rs +16 -16
- package/rust/core/audio/interpreter/statements/call.rs +31 -1
- package/rust/core/audio/interpreter/statements/condition.rs +72 -72
- package/rust/core/audio/interpreter/statements/function.rs +24 -24
- package/rust/core/audio/interpreter/statements/let_.rs +36 -36
- package/rust/core/audio/interpreter/statements/load.rs +17 -17
- package/rust/core/audio/interpreter/statements/loop_.rs +115 -115
- package/rust/core/audio/interpreter/statements/spawn.rs +51 -2
- package/rust/core/audio/interpreter/statements/trigger.rs +242 -239
- package/rust/core/audio/loader/trigger.rs +98 -98
- package/rust/core/audio/player.rs +70 -70
- package/rust/core/audio/special/mod.rs +9 -9
- package/rust/core/builder/mod.rs +129 -129
- package/rust/core/debugger/lexer.rs +27 -27
- package/rust/core/debugger/logs.rs +52 -52
- package/rust/core/debugger/preprocessor.rs +27 -27
- package/rust/core/debugger/store.rs +38 -38
- package/rust/core/lexer/driver.rs +59 -59
- package/rust/core/lexer/handler/arrow.rs +82 -82
- package/rust/core/lexer/handler/at.rs +21 -21
- package/rust/core/lexer/handler/brace.rs +41 -41
- package/rust/core/lexer/handler/colon.rs +21 -21
- package/rust/core/lexer/handler/comment.rs +30 -30
- package/rust/core/lexer/handler/dot.rs +21 -21
- package/rust/core/lexer/handler/driver.rs +337 -337
- package/rust/core/lexer/handler/identifier.rs +47 -47
- package/rust/core/lexer/handler/indent.rs +66 -66
- package/rust/core/lexer/handler/mod.rs +15 -15
- package/rust/core/lexer/handler/newline.rs +23 -23
- package/rust/core/lexer/handler/number.rs +31 -31
- package/rust/core/lexer/handler/operator.rs +46 -46
- package/rust/core/lexer/handler/parenthesis.rs +41 -41
- package/rust/core/lexer/handler/slash.rs +21 -21
- package/rust/core/lexer/handler/string.rs +63 -63
- package/rust/core/lexer/mod.rs +3 -3
- package/rust/core/mod.rs +9 -9
- package/rust/core/parser/driver/block.rs +111 -111
- package/rust/core/parser/driver/cursor.rs +82 -82
- package/rust/core/parser/driver/driver_impl.rs +21 -1
- package/rust/core/parser/driver/mod.rs +6 -6
- package/rust/core/parser/driver/parse_array.rs +120 -120
- package/rust/core/parser/driver/parse_map.rs +247 -223
- package/rust/core/parser/driver/parser.rs +160 -160
- package/rust/core/parser/handler/arrow_call.rs +65 -14
- package/rust/core/parser/handler/identifier/synth.rs +171 -135
- package/rust/core/parser/handler/mod.rs +9 -9
- package/rust/core/parser/handler/pattern.rs +24 -1
- package/rust/core/plugin/loader.rs +137 -137
- package/rust/core/plugin/mod.rs +2 -2
- package/rust/core/plugin/runner/non_wasm.rs +481 -297
- package/rust/core/plugin/runner/wasm32.rs +1 -0
- package/rust/core/preprocessor/loader/inject.rs +313 -278
- package/rust/core/preprocessor/loader/loader_helpers.rs +110 -110
- package/rust/core/preprocessor/loader/mod.rs +235 -235
- package/rust/core/preprocessor/module.rs +55 -55
- package/rust/core/preprocessor/processor/handlers.rs +107 -107
- package/rust/core/preprocessor/resolver/bank.rs +49 -49
- package/rust/core/preprocessor/resolver/call.rs +124 -124
- package/rust/core/preprocessor/resolver/condition.rs +95 -95
- package/rust/core/preprocessor/resolver/driver.rs +324 -324
- package/rust/core/preprocessor/resolver/function.rs +69 -69
- package/rust/core/preprocessor/resolver/group.rs +122 -122
- package/rust/core/preprocessor/resolver/let_.rs +32 -32
- package/rust/core/preprocessor/resolver/loop_.rs +318 -318
- package/rust/core/preprocessor/resolver/mod.rs +16 -16
- package/rust/core/preprocessor/resolver/pattern.rs +95 -83
- package/rust/core/preprocessor/resolver/spawn.rs +99 -99
- package/rust/core/preprocessor/resolver/synth.rs +54 -54
- package/rust/core/preprocessor/resolver/tempo.rs +48 -48
- package/rust/core/preprocessor/resolver/trigger.rs +116 -116
- package/rust/core/preprocessor/resolver/value.rs +176 -176
- package/rust/core/store/global.rs +57 -57
- package/rust/lib.rs +323 -323
- package/rust/macros/Cargo.toml +14 -0
- package/rust/macros/src/lib.rs +52 -0
- package/rust/main.rs +311 -142
- package/rust/types/Cargo.toml +1 -1
- package/rust/types/src/addons.rs +3 -1
- package/rust/types/src/config.rs +1 -3
- package/rust/utils/Cargo.toml +5 -2
- package/rust/utils/src/file.rs +397 -14
- package/rust/utils/src/path.rs +31 -2
- package/rust/utils/src/version.rs +38 -7
- package/rust/web/auth.rs +5 -0
- package/rust/web/forge.rs +5 -0
- package/rust/web/mod.rs +5 -3
- package/typescript/scripts/version/copy-to-binary.ts +82 -0
- package/rust/cli/bank/api.rs +0 -122
- package/rust/cli/bank/commands.rs +0 -306
- package/rust/cli/bank/mod.rs +0 -29
- package/rust/cli/install/bank.rs +0 -72
- package/rust/cli/install/commands.rs +0 -35
- package/rust/cli/install/mod.rs +0 -4
- package/rust/cli/install/plugin.rs +0 -80
|
@@ -1,574 +1,574 @@
|
|
|
1
|
-
use devalang_types::Value;
|
|
2
|
-
use devalang_utils::logger::{LogLevel, Logger};
|
|
3
|
-
use rayon::prelude::*;
|
|
4
|
-
|
|
5
|
-
use crate::core::{
|
|
6
|
-
audio::{
|
|
7
|
-
engine::AudioEngine,
|
|
8
|
-
interpreter::statements::{
|
|
9
|
-
arrow_call::interprete::interprete_arrow_call_statement,
|
|
10
|
-
call::interprete_call_statement, function::interprete_function_statement,
|
|
11
|
-
let_::interprete_let_statement, load::interprete_load_statement,
|
|
12
|
-
loop_::interprete_loop_statement, sleep::interprete_sleep_statement,
|
|
13
|
-
spawn::interprete_spawn_statement, tempo::interprete_tempo_statement,
|
|
14
|
-
trigger::interprete_trigger_statement,
|
|
15
|
-
},
|
|
16
|
-
},
|
|
17
|
-
parser::statement::{Statement, StatementKind},
|
|
18
|
-
store::global::GlobalStore,
|
|
19
|
-
};
|
|
20
|
-
use devalang_types::{FunctionTable, VariableTable};
|
|
21
|
-
|
|
22
|
-
// WASM playhead callback support (only compiled for wasm32 target)
|
|
23
|
-
#[cfg(target_arch = "wasm32")]
|
|
24
|
-
use serde::Serialize;
|
|
25
|
-
|
|
26
|
-
#[cfg(target_arch = "wasm32")]
|
|
27
|
-
#[derive(Serialize, Clone)]
|
|
28
|
-
pub struct PlayheadEvent {
|
|
29
|
-
time: f32,
|
|
30
|
-
line: usize,
|
|
31
|
-
column: usize,
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
#[cfg(target_arch = "wasm32")]
|
|
35
|
-
use wasm_bindgen::prelude::JsValue;
|
|
36
|
-
|
|
37
|
-
#[cfg(target_arch = "wasm32")]
|
|
38
|
-
use std::cell::RefCell;
|
|
39
|
-
|
|
40
|
-
#[cfg(target_arch = "wasm32")]
|
|
41
|
-
thread_local! {
|
|
42
|
-
static PLAYHEAD_CB: RefCell<Option<js_sys::Function>> = RefCell::new(None);
|
|
43
|
-
static PLAYHEAD_EVENTS: RefCell<Vec<PlayheadEvent>> = RefCell::new(Vec::new());
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
#[cfg(target_arch = "wasm32")]
|
|
47
|
-
pub fn register_playhead_callback(cb: js_sys::Function) {
|
|
48
|
-
PLAYHEAD_CB.with(|c| {
|
|
49
|
-
*c.borrow_mut() = Some(cb);
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
#[cfg(target_arch = "wasm32")]
|
|
54
|
-
pub fn unregister_playhead_callback() {
|
|
55
|
-
PLAYHEAD_CB.with(|c| {
|
|
56
|
-
*c.borrow_mut() = None;
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
#[cfg(target_arch = "wasm32")]
|
|
61
|
-
pub fn collect_playhead_events() -> Vec<PlayheadEvent> {
|
|
62
|
-
PLAYHEAD_EVENTS.with(|c| c.borrow().clone())
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
#[cfg(target_arch = "wasm32")]
|
|
66
|
-
fn emit_playhead(time: f32, line: usize, column: usize) {
|
|
67
|
-
let ev = PlayheadEvent { time, line, column };
|
|
68
|
-
PLAYHEAD_EVENTS.with(|c| c.borrow_mut().push(ev.clone()));
|
|
69
|
-
|
|
70
|
-
if let Ok(v) = serde_wasm_bindgen::to_value(&ev) {
|
|
71
|
-
PLAYHEAD_CB.with(|c| {
|
|
72
|
-
if let Some(f) = c.borrow().as_ref() {
|
|
73
|
-
let _ = f.call1(&JsValue::NULL, &v);
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
pub fn run_audio_program(
|
|
80
|
-
statements: &[Statement],
|
|
81
|
-
audio_engine: &mut AudioEngine,
|
|
82
|
-
_entry: String,
|
|
83
|
-
_output: String,
|
|
84
|
-
_module_variables: VariableTable,
|
|
85
|
-
_module_functions: FunctionTable,
|
|
86
|
-
global_store: &mut GlobalStore,
|
|
87
|
-
) -> (f32, f32) {
|
|
88
|
-
// Clear any previously collected playhead events for wasm target so each
|
|
89
|
-
// run starts with an empty events buffer.
|
|
90
|
-
#[cfg(target_arch = "wasm32")]
|
|
91
|
-
{
|
|
92
|
-
PLAYHEAD_EVENTS.with(|c| c.borrow_mut().clear());
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
let base_bpm = 120.0;
|
|
96
|
-
let base_duration = 60.0 / base_bpm;
|
|
97
|
-
|
|
98
|
-
let (max_end_time, cursor_time) = execute_audio_block(
|
|
99
|
-
audio_engine,
|
|
100
|
-
global_store,
|
|
101
|
-
global_store.variables.clone(),
|
|
102
|
-
global_store.functions.clone(),
|
|
103
|
-
statements,
|
|
104
|
-
base_bpm,
|
|
105
|
-
base_duration,
|
|
106
|
-
0.0,
|
|
107
|
-
0.0,
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
(max_end_time, cursor_time)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/// Execute a block of statements and schedule audio into the provided
|
|
114
|
-
/// AudioEngine. This function is the core of offline rendering and
|
|
115
|
-
/// performs the following responsibilities:
|
|
116
|
-
/// - sequential evaluation of statements (load, let, trigger, loop, etc.)
|
|
117
|
-
/// - parallel execution of `spawn` blocks (using rayon) and merging results
|
|
118
|
-
/// - scheduling of periodic events (beat/bar) once at the root depth
|
|
119
|
-
/// - emitting playhead events (when compiled for wasm32) after each sequential statement
|
|
120
|
-
pub fn execute_audio_block(
|
|
121
|
-
audio_engine: &mut AudioEngine,
|
|
122
|
-
global_store: &GlobalStore,
|
|
123
|
-
mut variable_table: VariableTable,
|
|
124
|
-
mut functions_table: FunctionTable,
|
|
125
|
-
statements: &[Statement],
|
|
126
|
-
mut base_bpm: f32,
|
|
127
|
-
mut base_duration: f32,
|
|
128
|
-
mut max_end_time: f32,
|
|
129
|
-
mut cursor_time: f32,
|
|
130
|
-
) -> (f32, f32) {
|
|
131
|
-
// Track nested depth of execute_audio_block to avoid scheduling periodic events multiple times
|
|
132
|
-
let current_depth = match variable_table.get("__depth") {
|
|
133
|
-
Some(Value::Number(n)) => *n,
|
|
134
|
-
_ => 0.0,
|
|
135
|
-
};
|
|
136
|
-
variable_table.set("__depth".to_string(), Value::Number(current_depth + 1.0));
|
|
137
|
-
let (spawns, others): (Vec<_>, Vec<_>) = statements
|
|
138
|
-
.iter()
|
|
139
|
-
.partition(|stmt| matches!(stmt.kind, StatementKind::Spawn { .. }));
|
|
140
|
-
|
|
141
|
-
// Execute sequential statements first
|
|
142
|
-
for stmt in others {
|
|
143
|
-
match &stmt.kind {
|
|
144
|
-
StatementKind::Load { .. } => {
|
|
145
|
-
if let Some(new_table) = interprete_load_statement(stmt, &mut variable_table) {
|
|
146
|
-
variable_table = new_table;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
StatementKind::On { .. } => {
|
|
150
|
-
// already registered in global store during parsing; nothing to do at runtime
|
|
151
|
-
}
|
|
152
|
-
StatementKind::Emit { event, payload: _ } => {
|
|
153
|
-
if let Some(handlers) = global_store.get_event_handlers(event) {
|
|
154
|
-
for h in handlers {
|
|
155
|
-
if let StatementKind::On {
|
|
156
|
-
event: _,
|
|
157
|
-
args,
|
|
158
|
-
body,
|
|
159
|
-
} = &h.kind
|
|
160
|
-
{
|
|
161
|
-
// Create a derived variable table with event context
|
|
162
|
-
let mut vt = variable_table.clone();
|
|
163
|
-
let mut ctx = std::collections::HashMap::new();
|
|
164
|
-
ctx.insert("name".to_string(), Value::String(event.clone()));
|
|
165
|
-
if let Some(arg_list) = args.clone() {
|
|
166
|
-
ctx.insert("args".to_string(), Value::Array(arg_list));
|
|
167
|
-
}
|
|
168
|
-
// Attach payload if any on the Emit statement value
|
|
169
|
-
ctx.insert("payload".to_string(), stmt.value.clone());
|
|
170
|
-
vt.set("event".to_string(), Value::Map(ctx));
|
|
171
|
-
// Mark we're inside an event handler to avoid re-scheduling periodic events recursively
|
|
172
|
-
vt.set("__in_event".to_string(), Value::Boolean(true));
|
|
173
|
-
|
|
174
|
-
let (_max, _cursor) = execute_audio_block(
|
|
175
|
-
audio_engine,
|
|
176
|
-
global_store,
|
|
177
|
-
vt,
|
|
178
|
-
functions_table.clone(),
|
|
179
|
-
body,
|
|
180
|
-
base_bpm,
|
|
181
|
-
base_duration,
|
|
182
|
-
max_end_time,
|
|
183
|
-
cursor_time,
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
StatementKind::Let { .. } => {
|
|
190
|
-
if let Some(new_table) = interprete_let_statement(stmt, &mut variable_table) {
|
|
191
|
-
variable_table = new_table;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
StatementKind::Function { .. } => {
|
|
195
|
-
if let Some(new_functions) =
|
|
196
|
-
interprete_function_statement(stmt, &mut functions_table)
|
|
197
|
-
{
|
|
198
|
-
functions_table = new_functions;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
StatementKind::Tempo => {
|
|
202
|
-
if let Some((new_bpm, new_duration)) = interprete_tempo_statement(stmt) {
|
|
203
|
-
base_bpm = new_bpm;
|
|
204
|
-
base_duration = new_duration;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
StatementKind::Trigger { .. } => {
|
|
208
|
-
if let Some((new_cursor, new_max, _)) = interprete_trigger_statement(
|
|
209
|
-
stmt,
|
|
210
|
-
audio_engine,
|
|
211
|
-
&variable_table,
|
|
212
|
-
base_duration,
|
|
213
|
-
cursor_time,
|
|
214
|
-
max_end_time,
|
|
215
|
-
) {
|
|
216
|
-
cursor_time = new_cursor;
|
|
217
|
-
max_end_time = new_max;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
StatementKind::Sleep => {
|
|
221
|
-
let (new_cursor, new_max) =
|
|
222
|
-
interprete_sleep_statement(stmt, cursor_time, max_end_time);
|
|
223
|
-
cursor_time = new_cursor;
|
|
224
|
-
max_end_time = new_max;
|
|
225
|
-
}
|
|
226
|
-
StatementKind::Loop => {
|
|
227
|
-
let (new_max, new_cursor) = interprete_loop_statement(
|
|
228
|
-
stmt,
|
|
229
|
-
audio_engine,
|
|
230
|
-
global_store,
|
|
231
|
-
&variable_table,
|
|
232
|
-
&functions_table,
|
|
233
|
-
base_bpm,
|
|
234
|
-
base_duration,
|
|
235
|
-
max_end_time,
|
|
236
|
-
cursor_time,
|
|
237
|
-
);
|
|
238
|
-
cursor_time = new_cursor;
|
|
239
|
-
max_end_time = new_max;
|
|
240
|
-
}
|
|
241
|
-
StatementKind::Call { .. } => {
|
|
242
|
-
let (new_max, _) = interprete_call_statement(
|
|
243
|
-
stmt,
|
|
244
|
-
audio_engine,
|
|
245
|
-
&variable_table,
|
|
246
|
-
&functions_table,
|
|
247
|
-
global_store,
|
|
248
|
-
base_bpm,
|
|
249
|
-
base_duration,
|
|
250
|
-
max_end_time,
|
|
251
|
-
cursor_time,
|
|
252
|
-
);
|
|
253
|
-
cursor_time = new_max;
|
|
254
|
-
max_end_time = new_max;
|
|
255
|
-
}
|
|
256
|
-
StatementKind::ArrowCall { .. } => {
|
|
257
|
-
let (new_max, new_cursor) = interprete_arrow_call_statement(
|
|
258
|
-
stmt,
|
|
259
|
-
audio_engine,
|
|
260
|
-
&variable_table,
|
|
261
|
-
global_store,
|
|
262
|
-
base_bpm,
|
|
263
|
-
base_duration,
|
|
264
|
-
&mut max_end_time,
|
|
265
|
-
Some(&mut cursor_time),
|
|
266
|
-
true,
|
|
267
|
-
);
|
|
268
|
-
cursor_time = new_cursor;
|
|
269
|
-
|
|
270
|
-
if new_max > max_end_time {
|
|
271
|
-
max_end_time = new_max;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
StatementKind::Automate { .. } => {
|
|
275
|
-
if
|
|
276
|
-
let Some(new_table) =
|
|
277
|
-
crate::core::audio::interpreter::statements::automate::interprete_automate_statement(
|
|
278
|
-
stmt,
|
|
279
|
-
&mut variable_table
|
|
280
|
-
)
|
|
281
|
-
{
|
|
282
|
-
variable_table = new_table;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
StatementKind::Print => {
|
|
286
|
-
// Only print in real-time mode (during playback), not during offline render.
|
|
287
|
-
let is_realtime = matches!(variable_table.get("__rt"), Some(Value::Boolean(true)));
|
|
288
|
-
if is_realtime {
|
|
289
|
-
let logger = Logger::new();
|
|
290
|
-
match &stmt.value {
|
|
291
|
-
Value::String(s) => {
|
|
292
|
-
let bpm = if let Some(Value::Number(n)) = variable_table.get("bpm") {
|
|
293
|
-
*n
|
|
294
|
-
} else {
|
|
295
|
-
120.0
|
|
296
|
-
};
|
|
297
|
-
let beat = if let Some(Value::Number(n)) = variable_table.get("beat") {
|
|
298
|
-
*n
|
|
299
|
-
} else {
|
|
300
|
-
0.0
|
|
301
|
-
};
|
|
302
|
-
// First try JS-like string concatenation: "str " + var + 1 + $env.*
|
|
303
|
-
if let Some(res) =
|
|
304
|
-
crate::core::audio::evaluator::evaluate_string_expression(
|
|
305
|
-
s,
|
|
306
|
-
&variable_table,
|
|
307
|
-
bpm,
|
|
308
|
-
beat,
|
|
309
|
-
)
|
|
310
|
-
{
|
|
311
|
-
logger.log_message(LogLevel::Print, &res);
|
|
312
|
-
} else if let Some(val) = variable_table.get(s) {
|
|
313
|
-
logger.log_message(LogLevel::Print, &format!("{:?}", val));
|
|
314
|
-
} else if s.contains("$env")
|
|
315
|
-
|| s.contains("$math")
|
|
316
|
-
|| s.parse::<f32>().is_ok()
|
|
317
|
-
{
|
|
318
|
-
let v = crate::core::audio::evaluator::evaluate_rhs_into_value(
|
|
319
|
-
s,
|
|
320
|
-
&variable_table,
|
|
321
|
-
bpm,
|
|
322
|
-
beat,
|
|
323
|
-
);
|
|
324
|
-
match v {
|
|
325
|
-
Value::Number(n) => {
|
|
326
|
-
logger.log_message(LogLevel::Print, &format!("{}", n));
|
|
327
|
-
}
|
|
328
|
-
_ => logger.log_message(LogLevel::Print, s),
|
|
329
|
-
}
|
|
330
|
-
} else {
|
|
331
|
-
logger.log_message(LogLevel::Print, s);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
Value::Number(n) => {
|
|
335
|
-
logger.log_message(LogLevel::Print, &format!("{}", n));
|
|
336
|
-
}
|
|
337
|
-
Value::Identifier(name) => {
|
|
338
|
-
if let Some(val) = variable_table.get(name) {
|
|
339
|
-
match val {
|
|
340
|
-
Value::Number(n) => {
|
|
341
|
-
logger.log_message(LogLevel::Print, &format!("{}", n));
|
|
342
|
-
}
|
|
343
|
-
Value::String(s) => logger.log_message(LogLevel::Print, s),
|
|
344
|
-
Value::Boolean(b) => {
|
|
345
|
-
logger.log_message(LogLevel::Print, &format!("{}", b));
|
|
346
|
-
}
|
|
347
|
-
other => {
|
|
348
|
-
logger
|
|
349
|
-
.log_message(LogLevel::Print, &format!("{:?}", other));
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
} else {
|
|
353
|
-
logger.log_message(LogLevel::Print, name);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
v => logger.log_message(LogLevel::Print, &format!("{:?}", v)),
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
_ => {}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// Emit playhead event for UI bindings when building real-time playback
|
|
364
|
-
// Only emit for statements that are "playable" (i.e., schedule audio)
|
|
365
|
-
#[cfg(target_arch = "wasm32")]
|
|
366
|
-
{
|
|
367
|
-
if matches!(
|
|
368
|
-
stmt.kind,
|
|
369
|
-
StatementKind::Trigger { .. }
|
|
370
|
-
| StatementKind::Call { .. }
|
|
371
|
-
| StatementKind::Spawn { .. }
|
|
372
|
-
| StatementKind::Loop
|
|
373
|
-
) {
|
|
374
|
-
emit_playhead(cursor_time, stmt.line, stmt.column);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// Execute parallel spawns (collect results)
|
|
380
|
-
let spawn_results: Vec<(AudioEngine, f32)> = spawns
|
|
381
|
-
.par_iter()
|
|
382
|
-
.map(|stmt| {
|
|
383
|
-
let mut local_engine = AudioEngine::new(audio_engine.module_name.clone());
|
|
384
|
-
let (spawn_max, _) = interprete_spawn_statement(
|
|
385
|
-
stmt,
|
|
386
|
-
&mut local_engine,
|
|
387
|
-
&variable_table,
|
|
388
|
-
&functions_table,
|
|
389
|
-
global_store,
|
|
390
|
-
base_bpm,
|
|
391
|
-
base_duration,
|
|
392
|
-
0.0,
|
|
393
|
-
0.0,
|
|
394
|
-
);
|
|
395
|
-
(local_engine, spawn_max)
|
|
396
|
-
})
|
|
397
|
-
.collect();
|
|
398
|
-
|
|
399
|
-
// Finally, merge results from all spawns
|
|
400
|
-
for (local_engine, spawn_max) in spawn_results {
|
|
401
|
-
audio_engine.merge_with(local_engine);
|
|
402
|
-
if spawn_max > max_end_time {
|
|
403
|
-
max_end_time = spawn_max;
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// Built-in periodic events (e.g., on beat(n), on bar(n))
|
|
408
|
-
// Emit handlers across the timeline up to max_end_time.
|
|
409
|
-
// If no audio was scheduled (max_end_time == 0.0), skip.
|
|
410
|
-
// Don't schedule periodic events if we're already inside an event handler
|
|
411
|
-
let in_event = matches!(variable_table.get("__in_event"), Some(Value::Boolean(true)));
|
|
412
|
-
let depth_is_root = matches!(variable_table.get("__depth"), Some(Value::Number(n)) if (*n - 1.0).abs() < f32::EPSILON);
|
|
413
|
-
if max_end_time > 0.0 && !in_event && depth_is_root && !global_store.events.is_empty() {
|
|
414
|
-
// Beat-based handlers (support "beat" and "$beat")
|
|
415
|
-
for ev_key in ["beat", "$beat"] {
|
|
416
|
-
if let Some(handlers) = global_store.get_event_handlers(ev_key) {
|
|
417
|
-
let mut seen: std::collections::HashSet<(usize, usize, usize)> =
|
|
418
|
-
std::collections::HashSet::new();
|
|
419
|
-
// Default every 1 beat if args missing
|
|
420
|
-
for h in handlers {
|
|
421
|
-
let key = (h.line, h.column, h.indent);
|
|
422
|
-
if !seen.insert(key) {
|
|
423
|
-
continue;
|
|
424
|
-
}
|
|
425
|
-
if let StatementKind::On { event, args, body } = &h.kind {
|
|
426
|
-
let every: f32 = args
|
|
427
|
-
.as_ref()
|
|
428
|
-
.and_then(|v| v.first())
|
|
429
|
-
.and_then(|x| {
|
|
430
|
-
match x {
|
|
431
|
-
Value::Number(n) => Some(*n),
|
|
432
|
-
Value::Identifier(s) => {
|
|
433
|
-
// Try to resolve from variables first, fallback to parsing the literal
|
|
434
|
-
match variable_table.get(s) {
|
|
435
|
-
Some(Value::Number(n)) => Some(*n),
|
|
436
|
-
_ => s.parse::<f32>().ok(),
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
_ => None,
|
|
440
|
-
}
|
|
441
|
-
})
|
|
442
|
-
.unwrap_or(1.0)
|
|
443
|
-
.max(0.0001);
|
|
444
|
-
let step = base_duration * every;
|
|
445
|
-
// Start from first full bar boundary after t=0
|
|
446
|
-
let mut t = step;
|
|
447
|
-
while t <= max_end_time {
|
|
448
|
-
// Prepare event context
|
|
449
|
-
let mut vt = variable_table.clone();
|
|
450
|
-
let mut ctx = std::collections::HashMap::new();
|
|
451
|
-
ctx.insert("name".to_string(), Value::String(event.clone()));
|
|
452
|
-
if let Some(a) = args.clone() {
|
|
453
|
-
ctx.insert("args".to_string(), Value::Array(a));
|
|
454
|
-
}
|
|
455
|
-
vt.set("event".to_string(), Value::Map(ctx));
|
|
456
|
-
vt.set("beat".to_string(), Value::Number(t / base_duration));
|
|
457
|
-
// Prevent nested scheduling
|
|
458
|
-
vt.set("__in_event".to_string(), Value::Boolean(true));
|
|
459
|
-
|
|
460
|
-
let (_m, _c) = execute_audio_block(
|
|
461
|
-
audio_engine,
|
|
462
|
-
global_store,
|
|
463
|
-
vt,
|
|
464
|
-
functions_table.clone(),
|
|
465
|
-
body,
|
|
466
|
-
base_bpm,
|
|
467
|
-
base_duration,
|
|
468
|
-
max_end_time,
|
|
469
|
-
t,
|
|
470
|
-
);
|
|
471
|
-
|
|
472
|
-
t += step;
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// Bar-based handlers (default 4/4 time => 4 beats per bar); support "bar" and "$bar"
|
|
480
|
-
for ev_key in ["bar", "$bar"] {
|
|
481
|
-
if let Some(handlers) = global_store.get_event_handlers(ev_key) {
|
|
482
|
-
let mut seen: std::collections::HashSet<(usize, usize, usize)> =
|
|
483
|
-
std::collections::HashSet::new();
|
|
484
|
-
for h in handlers {
|
|
485
|
-
let key = (h.line, h.column, h.indent);
|
|
486
|
-
if !seen.insert(key) {
|
|
487
|
-
continue;
|
|
488
|
-
}
|
|
489
|
-
if let StatementKind::On { event, args, body } = &h.kind {
|
|
490
|
-
let bar_beats = 4.0f32; // TODO: time signature support
|
|
491
|
-
let first_only = args.as_ref().and_then(|v| v.first()).is_none();
|
|
492
|
-
|
|
493
|
-
let every_bar: f32 = if first_only {
|
|
494
|
-
1.0
|
|
495
|
-
} else {
|
|
496
|
-
args.as_ref()
|
|
497
|
-
.and_then(|v| v.first())
|
|
498
|
-
.and_then(|x| match x {
|
|
499
|
-
Value::Number(n) => Some(*n),
|
|
500
|
-
Value::Identifier(s) => match variable_table.get(s) {
|
|
501
|
-
Some(Value::Number(n)) => Some(*n),
|
|
502
|
-
_ => s.parse::<f32>().ok(),
|
|
503
|
-
},
|
|
504
|
-
_ => None,
|
|
505
|
-
})
|
|
506
|
-
.unwrap_or(1.0)
|
|
507
|
-
.max(0.0001)
|
|
508
|
-
};
|
|
509
|
-
|
|
510
|
-
let step = base_duration * bar_beats * every_bar;
|
|
511
|
-
|
|
512
|
-
if first_only {
|
|
513
|
-
let t = step; // first full bar after t=0
|
|
514
|
-
if t <= max_end_time {
|
|
515
|
-
let mut vt = variable_table.clone();
|
|
516
|
-
let mut ctx = std::collections::HashMap::new();
|
|
517
|
-
ctx.insert("name".to_string(), Value::String(event.clone()));
|
|
518
|
-
if let Some(a) = args.clone() {
|
|
519
|
-
ctx.insert("args".to_string(), Value::Array(a));
|
|
520
|
-
}
|
|
521
|
-
vt.set("event".to_string(), Value::Map(ctx));
|
|
522
|
-
vt.set("beat".to_string(), Value::Number(t / base_duration));
|
|
523
|
-
// Prevent nested scheduling
|
|
524
|
-
vt.set("__in_event".to_string(), Value::Boolean(true));
|
|
525
|
-
|
|
526
|
-
let (_m, _c) = execute_audio_block(
|
|
527
|
-
audio_engine,
|
|
528
|
-
global_store,
|
|
529
|
-
vt,
|
|
530
|
-
functions_table.clone(),
|
|
531
|
-
body,
|
|
532
|
-
base_bpm,
|
|
533
|
-
base_duration,
|
|
534
|
-
max_end_time,
|
|
535
|
-
t,
|
|
536
|
-
);
|
|
537
|
-
}
|
|
538
|
-
} else {
|
|
539
|
-
let mut t = step; // start from first full bar after t=0
|
|
540
|
-
while t <= max_end_time {
|
|
541
|
-
let mut vt = variable_table.clone();
|
|
542
|
-
let mut ctx = std::collections::HashMap::new();
|
|
543
|
-
ctx.insert("name".to_string(), Value::String(event.clone()));
|
|
544
|
-
if let Some(a) = args.clone() {
|
|
545
|
-
ctx.insert("args".to_string(), Value::Array(a));
|
|
546
|
-
}
|
|
547
|
-
vt.set("event".to_string(), Value::Map(ctx));
|
|
548
|
-
vt.set("beat".to_string(), Value::Number(t / base_duration));
|
|
549
|
-
// Prevent nested scheduling
|
|
550
|
-
vt.set("__in_event".to_string(), Value::Boolean(true));
|
|
551
|
-
|
|
552
|
-
let (_m, _c) = execute_audio_block(
|
|
553
|
-
audio_engine,
|
|
554
|
-
global_store,
|
|
555
|
-
vt,
|
|
556
|
-
functions_table.clone(),
|
|
557
|
-
body,
|
|
558
|
-
base_bpm,
|
|
559
|
-
base_duration,
|
|
560
|
-
max_end_time,
|
|
561
|
-
t,
|
|
562
|
-
);
|
|
563
|
-
|
|
564
|
-
t += step;
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
(max_end_time.max(cursor_time), cursor_time)
|
|
574
|
-
}
|
|
1
|
+
use devalang_types::Value;
|
|
2
|
+
use devalang_utils::logger::{LogLevel, Logger};
|
|
3
|
+
use rayon::prelude::*;
|
|
4
|
+
|
|
5
|
+
use crate::core::{
|
|
6
|
+
audio::{
|
|
7
|
+
engine::AudioEngine,
|
|
8
|
+
interpreter::statements::{
|
|
9
|
+
arrow_call::interprete::interprete_arrow_call_statement,
|
|
10
|
+
call::interprete_call_statement, function::interprete_function_statement,
|
|
11
|
+
let_::interprete_let_statement, load::interprete_load_statement,
|
|
12
|
+
loop_::interprete_loop_statement, sleep::interprete_sleep_statement,
|
|
13
|
+
spawn::interprete_spawn_statement, tempo::interprete_tempo_statement,
|
|
14
|
+
trigger::interprete_trigger_statement,
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
parser::statement::{Statement, StatementKind},
|
|
18
|
+
store::global::GlobalStore,
|
|
19
|
+
};
|
|
20
|
+
use devalang_types::{FunctionTable, VariableTable};
|
|
21
|
+
|
|
22
|
+
// WASM playhead callback support (only compiled for wasm32 target)
|
|
23
|
+
#[cfg(target_arch = "wasm32")]
|
|
24
|
+
use serde::Serialize;
|
|
25
|
+
|
|
26
|
+
#[cfg(target_arch = "wasm32")]
|
|
27
|
+
#[derive(Serialize, Clone)]
|
|
28
|
+
pub struct PlayheadEvent {
|
|
29
|
+
time: f32,
|
|
30
|
+
line: usize,
|
|
31
|
+
column: usize,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#[cfg(target_arch = "wasm32")]
|
|
35
|
+
use wasm_bindgen::prelude::JsValue;
|
|
36
|
+
|
|
37
|
+
#[cfg(target_arch = "wasm32")]
|
|
38
|
+
use std::cell::RefCell;
|
|
39
|
+
|
|
40
|
+
#[cfg(target_arch = "wasm32")]
|
|
41
|
+
thread_local! {
|
|
42
|
+
static PLAYHEAD_CB: RefCell<Option<js_sys::Function>> = RefCell::new(None);
|
|
43
|
+
static PLAYHEAD_EVENTS: RefCell<Vec<PlayheadEvent>> = RefCell::new(Vec::new());
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
#[cfg(target_arch = "wasm32")]
|
|
47
|
+
pub fn register_playhead_callback(cb: js_sys::Function) {
|
|
48
|
+
PLAYHEAD_CB.with(|c| {
|
|
49
|
+
*c.borrow_mut() = Some(cb);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#[cfg(target_arch = "wasm32")]
|
|
54
|
+
pub fn unregister_playhead_callback() {
|
|
55
|
+
PLAYHEAD_CB.with(|c| {
|
|
56
|
+
*c.borrow_mut() = None;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#[cfg(target_arch = "wasm32")]
|
|
61
|
+
pub fn collect_playhead_events() -> Vec<PlayheadEvent> {
|
|
62
|
+
PLAYHEAD_EVENTS.with(|c| c.borrow().clone())
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#[cfg(target_arch = "wasm32")]
|
|
66
|
+
fn emit_playhead(time: f32, line: usize, column: usize) {
|
|
67
|
+
let ev = PlayheadEvent { time, line, column };
|
|
68
|
+
PLAYHEAD_EVENTS.with(|c| c.borrow_mut().push(ev.clone()));
|
|
69
|
+
|
|
70
|
+
if let Ok(v) = serde_wasm_bindgen::to_value(&ev) {
|
|
71
|
+
PLAYHEAD_CB.with(|c| {
|
|
72
|
+
if let Some(f) = c.borrow().as_ref() {
|
|
73
|
+
let _ = f.call1(&JsValue::NULL, &v);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
pub fn run_audio_program(
|
|
80
|
+
statements: &[Statement],
|
|
81
|
+
audio_engine: &mut AudioEngine,
|
|
82
|
+
_entry: String,
|
|
83
|
+
_output: String,
|
|
84
|
+
_module_variables: VariableTable,
|
|
85
|
+
_module_functions: FunctionTable,
|
|
86
|
+
global_store: &mut GlobalStore,
|
|
87
|
+
) -> (f32, f32) {
|
|
88
|
+
// Clear any previously collected playhead events for wasm target so each
|
|
89
|
+
// run starts with an empty events buffer.
|
|
90
|
+
#[cfg(target_arch = "wasm32")]
|
|
91
|
+
{
|
|
92
|
+
PLAYHEAD_EVENTS.with(|c| c.borrow_mut().clear());
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let base_bpm = 120.0;
|
|
96
|
+
let base_duration = 60.0 / base_bpm;
|
|
97
|
+
|
|
98
|
+
let (max_end_time, cursor_time) = execute_audio_block(
|
|
99
|
+
audio_engine,
|
|
100
|
+
global_store,
|
|
101
|
+
global_store.variables.clone(),
|
|
102
|
+
global_store.functions.clone(),
|
|
103
|
+
statements,
|
|
104
|
+
base_bpm,
|
|
105
|
+
base_duration,
|
|
106
|
+
0.0,
|
|
107
|
+
0.0,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
(max_end_time, cursor_time)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// Execute a block of statements and schedule audio into the provided
|
|
114
|
+
/// AudioEngine. This function is the core of offline rendering and
|
|
115
|
+
/// performs the following responsibilities:
|
|
116
|
+
/// - sequential evaluation of statements (load, let, trigger, loop, etc.)
|
|
117
|
+
/// - parallel execution of `spawn` blocks (using rayon) and merging results
|
|
118
|
+
/// - scheduling of periodic events (beat/bar) once at the root depth
|
|
119
|
+
/// - emitting playhead events (when compiled for wasm32) after each sequential statement
|
|
120
|
+
pub fn execute_audio_block(
|
|
121
|
+
audio_engine: &mut AudioEngine,
|
|
122
|
+
global_store: &GlobalStore,
|
|
123
|
+
mut variable_table: VariableTable,
|
|
124
|
+
mut functions_table: FunctionTable,
|
|
125
|
+
statements: &[Statement],
|
|
126
|
+
mut base_bpm: f32,
|
|
127
|
+
mut base_duration: f32,
|
|
128
|
+
mut max_end_time: f32,
|
|
129
|
+
mut cursor_time: f32,
|
|
130
|
+
) -> (f32, f32) {
|
|
131
|
+
// Track nested depth of execute_audio_block to avoid scheduling periodic events multiple times
|
|
132
|
+
let current_depth = match variable_table.get("__depth") {
|
|
133
|
+
Some(Value::Number(n)) => *n,
|
|
134
|
+
_ => 0.0,
|
|
135
|
+
};
|
|
136
|
+
variable_table.set("__depth".to_string(), Value::Number(current_depth + 1.0));
|
|
137
|
+
let (spawns, others): (Vec<_>, Vec<_>) = statements
|
|
138
|
+
.iter()
|
|
139
|
+
.partition(|stmt| matches!(stmt.kind, StatementKind::Spawn { .. }));
|
|
140
|
+
|
|
141
|
+
// Execute sequential statements first
|
|
142
|
+
for stmt in others {
|
|
143
|
+
match &stmt.kind {
|
|
144
|
+
StatementKind::Load { .. } => {
|
|
145
|
+
if let Some(new_table) = interprete_load_statement(stmt, &mut variable_table) {
|
|
146
|
+
variable_table = new_table;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
StatementKind::On { .. } => {
|
|
150
|
+
// already registered in global store during parsing; nothing to do at runtime
|
|
151
|
+
}
|
|
152
|
+
StatementKind::Emit { event, payload: _ } => {
|
|
153
|
+
if let Some(handlers) = global_store.get_event_handlers(event) {
|
|
154
|
+
for h in handlers {
|
|
155
|
+
if let StatementKind::On {
|
|
156
|
+
event: _,
|
|
157
|
+
args,
|
|
158
|
+
body,
|
|
159
|
+
} = &h.kind
|
|
160
|
+
{
|
|
161
|
+
// Create a derived variable table with event context
|
|
162
|
+
let mut vt = variable_table.clone();
|
|
163
|
+
let mut ctx = std::collections::HashMap::new();
|
|
164
|
+
ctx.insert("name".to_string(), Value::String(event.clone()));
|
|
165
|
+
if let Some(arg_list) = args.clone() {
|
|
166
|
+
ctx.insert("args".to_string(), Value::Array(arg_list));
|
|
167
|
+
}
|
|
168
|
+
// Attach payload if any on the Emit statement value
|
|
169
|
+
ctx.insert("payload".to_string(), stmt.value.clone());
|
|
170
|
+
vt.set("event".to_string(), Value::Map(ctx));
|
|
171
|
+
// Mark we're inside an event handler to avoid re-scheduling periodic events recursively
|
|
172
|
+
vt.set("__in_event".to_string(), Value::Boolean(true));
|
|
173
|
+
|
|
174
|
+
let (_max, _cursor) = execute_audio_block(
|
|
175
|
+
audio_engine,
|
|
176
|
+
global_store,
|
|
177
|
+
vt,
|
|
178
|
+
functions_table.clone(),
|
|
179
|
+
body,
|
|
180
|
+
base_bpm,
|
|
181
|
+
base_duration,
|
|
182
|
+
max_end_time,
|
|
183
|
+
cursor_time,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
StatementKind::Let { .. } => {
|
|
190
|
+
if let Some(new_table) = interprete_let_statement(stmt, &mut variable_table) {
|
|
191
|
+
variable_table = new_table;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
StatementKind::Function { .. } => {
|
|
195
|
+
if let Some(new_functions) =
|
|
196
|
+
interprete_function_statement(stmt, &mut functions_table)
|
|
197
|
+
{
|
|
198
|
+
functions_table = new_functions;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
StatementKind::Tempo => {
|
|
202
|
+
if let Some((new_bpm, new_duration)) = interprete_tempo_statement(stmt) {
|
|
203
|
+
base_bpm = new_bpm;
|
|
204
|
+
base_duration = new_duration;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
StatementKind::Trigger { .. } => {
|
|
208
|
+
if let Some((new_cursor, new_max, _)) = interprete_trigger_statement(
|
|
209
|
+
stmt,
|
|
210
|
+
audio_engine,
|
|
211
|
+
&variable_table,
|
|
212
|
+
base_duration,
|
|
213
|
+
cursor_time,
|
|
214
|
+
max_end_time,
|
|
215
|
+
) {
|
|
216
|
+
cursor_time = new_cursor;
|
|
217
|
+
max_end_time = new_max;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
StatementKind::Sleep => {
|
|
221
|
+
let (new_cursor, new_max) =
|
|
222
|
+
interprete_sleep_statement(stmt, cursor_time, max_end_time);
|
|
223
|
+
cursor_time = new_cursor;
|
|
224
|
+
max_end_time = new_max;
|
|
225
|
+
}
|
|
226
|
+
StatementKind::Loop => {
|
|
227
|
+
let (new_max, new_cursor) = interprete_loop_statement(
|
|
228
|
+
stmt,
|
|
229
|
+
audio_engine,
|
|
230
|
+
global_store,
|
|
231
|
+
&variable_table,
|
|
232
|
+
&functions_table,
|
|
233
|
+
base_bpm,
|
|
234
|
+
base_duration,
|
|
235
|
+
max_end_time,
|
|
236
|
+
cursor_time,
|
|
237
|
+
);
|
|
238
|
+
cursor_time = new_cursor;
|
|
239
|
+
max_end_time = new_max;
|
|
240
|
+
}
|
|
241
|
+
StatementKind::Call { .. } => {
|
|
242
|
+
let (new_max, _) = interprete_call_statement(
|
|
243
|
+
stmt,
|
|
244
|
+
audio_engine,
|
|
245
|
+
&variable_table,
|
|
246
|
+
&functions_table,
|
|
247
|
+
global_store,
|
|
248
|
+
base_bpm,
|
|
249
|
+
base_duration,
|
|
250
|
+
max_end_time,
|
|
251
|
+
cursor_time,
|
|
252
|
+
);
|
|
253
|
+
cursor_time = new_max;
|
|
254
|
+
max_end_time = new_max;
|
|
255
|
+
}
|
|
256
|
+
StatementKind::ArrowCall { .. } => {
|
|
257
|
+
let (new_max, new_cursor) = interprete_arrow_call_statement(
|
|
258
|
+
stmt,
|
|
259
|
+
audio_engine,
|
|
260
|
+
&variable_table,
|
|
261
|
+
global_store,
|
|
262
|
+
base_bpm,
|
|
263
|
+
base_duration,
|
|
264
|
+
&mut max_end_time,
|
|
265
|
+
Some(&mut cursor_time),
|
|
266
|
+
true,
|
|
267
|
+
);
|
|
268
|
+
cursor_time = new_cursor;
|
|
269
|
+
|
|
270
|
+
if new_max > max_end_time {
|
|
271
|
+
max_end_time = new_max;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
StatementKind::Automate { .. } => {
|
|
275
|
+
if
|
|
276
|
+
let Some(new_table) =
|
|
277
|
+
crate::core::audio::interpreter::statements::automate::interprete_automate_statement(
|
|
278
|
+
stmt,
|
|
279
|
+
&mut variable_table
|
|
280
|
+
)
|
|
281
|
+
{
|
|
282
|
+
variable_table = new_table;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
StatementKind::Print => {
|
|
286
|
+
// Only print in real-time mode (during playback), not during offline render.
|
|
287
|
+
let is_realtime = matches!(variable_table.get("__rt"), Some(Value::Boolean(true)));
|
|
288
|
+
if is_realtime {
|
|
289
|
+
let logger = Logger::new();
|
|
290
|
+
match &stmt.value {
|
|
291
|
+
Value::String(s) => {
|
|
292
|
+
let bpm = if let Some(Value::Number(n)) = variable_table.get("bpm") {
|
|
293
|
+
*n
|
|
294
|
+
} else {
|
|
295
|
+
120.0
|
|
296
|
+
};
|
|
297
|
+
let beat = if let Some(Value::Number(n)) = variable_table.get("beat") {
|
|
298
|
+
*n
|
|
299
|
+
} else {
|
|
300
|
+
0.0
|
|
301
|
+
};
|
|
302
|
+
// First try JS-like string concatenation: "str " + var + 1 + $env.*
|
|
303
|
+
if let Some(res) =
|
|
304
|
+
crate::core::audio::evaluator::evaluate_string_expression(
|
|
305
|
+
s,
|
|
306
|
+
&variable_table,
|
|
307
|
+
bpm,
|
|
308
|
+
beat,
|
|
309
|
+
)
|
|
310
|
+
{
|
|
311
|
+
logger.log_message(LogLevel::Print, &res);
|
|
312
|
+
} else if let Some(val) = variable_table.get(s) {
|
|
313
|
+
logger.log_message(LogLevel::Print, &format!("{:?}", val));
|
|
314
|
+
} else if s.contains("$env")
|
|
315
|
+
|| s.contains("$math")
|
|
316
|
+
|| s.parse::<f32>().is_ok()
|
|
317
|
+
{
|
|
318
|
+
let v = crate::core::audio::evaluator::evaluate_rhs_into_value(
|
|
319
|
+
s,
|
|
320
|
+
&variable_table,
|
|
321
|
+
bpm,
|
|
322
|
+
beat,
|
|
323
|
+
);
|
|
324
|
+
match v {
|
|
325
|
+
Value::Number(n) => {
|
|
326
|
+
logger.log_message(LogLevel::Print, &format!("{}", n));
|
|
327
|
+
}
|
|
328
|
+
_ => logger.log_message(LogLevel::Print, s),
|
|
329
|
+
}
|
|
330
|
+
} else {
|
|
331
|
+
logger.log_message(LogLevel::Print, s);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
Value::Number(n) => {
|
|
335
|
+
logger.log_message(LogLevel::Print, &format!("{}", n));
|
|
336
|
+
}
|
|
337
|
+
Value::Identifier(name) => {
|
|
338
|
+
if let Some(val) = variable_table.get(name) {
|
|
339
|
+
match val {
|
|
340
|
+
Value::Number(n) => {
|
|
341
|
+
logger.log_message(LogLevel::Print, &format!("{}", n));
|
|
342
|
+
}
|
|
343
|
+
Value::String(s) => logger.log_message(LogLevel::Print, s),
|
|
344
|
+
Value::Boolean(b) => {
|
|
345
|
+
logger.log_message(LogLevel::Print, &format!("{}", b));
|
|
346
|
+
}
|
|
347
|
+
other => {
|
|
348
|
+
logger
|
|
349
|
+
.log_message(LogLevel::Print, &format!("{:?}", other));
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
} else {
|
|
353
|
+
logger.log_message(LogLevel::Print, name);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
v => logger.log_message(LogLevel::Print, &format!("{:?}", v)),
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
_ => {}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Emit playhead event for UI bindings when building real-time playback
|
|
364
|
+
// Only emit for statements that are "playable" (i.e., schedule audio)
|
|
365
|
+
#[cfg(target_arch = "wasm32")]
|
|
366
|
+
{
|
|
367
|
+
if matches!(
|
|
368
|
+
stmt.kind,
|
|
369
|
+
StatementKind::Trigger { .. }
|
|
370
|
+
| StatementKind::Call { .. }
|
|
371
|
+
| StatementKind::Spawn { .. }
|
|
372
|
+
| StatementKind::Loop
|
|
373
|
+
) {
|
|
374
|
+
emit_playhead(cursor_time, stmt.line, stmt.column);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Execute parallel spawns (collect results)
|
|
380
|
+
let spawn_results: Vec<(AudioEngine, f32)> = spawns
|
|
381
|
+
.par_iter()
|
|
382
|
+
.map(|stmt| {
|
|
383
|
+
let mut local_engine = AudioEngine::new(audio_engine.module_name.clone());
|
|
384
|
+
let (spawn_max, _) = interprete_spawn_statement(
|
|
385
|
+
stmt,
|
|
386
|
+
&mut local_engine,
|
|
387
|
+
&variable_table,
|
|
388
|
+
&functions_table,
|
|
389
|
+
global_store,
|
|
390
|
+
base_bpm,
|
|
391
|
+
base_duration,
|
|
392
|
+
0.0,
|
|
393
|
+
0.0,
|
|
394
|
+
);
|
|
395
|
+
(local_engine, spawn_max)
|
|
396
|
+
})
|
|
397
|
+
.collect();
|
|
398
|
+
|
|
399
|
+
// Finally, merge results from all spawns
|
|
400
|
+
for (local_engine, spawn_max) in spawn_results {
|
|
401
|
+
audio_engine.merge_with(local_engine);
|
|
402
|
+
if spawn_max > max_end_time {
|
|
403
|
+
max_end_time = spawn_max;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Built-in periodic events (e.g., on beat(n), on bar(n))
|
|
408
|
+
// Emit handlers across the timeline up to max_end_time.
|
|
409
|
+
// If no audio was scheduled (max_end_time == 0.0), skip.
|
|
410
|
+
// Don't schedule periodic events if we're already inside an event handler
|
|
411
|
+
let in_event = matches!(variable_table.get("__in_event"), Some(Value::Boolean(true)));
|
|
412
|
+
let depth_is_root = matches!(variable_table.get("__depth"), Some(Value::Number(n)) if (*n - 1.0).abs() < f32::EPSILON);
|
|
413
|
+
if max_end_time > 0.0 && !in_event && depth_is_root && !global_store.events.is_empty() {
|
|
414
|
+
// Beat-based handlers (support "beat" and "$beat")
|
|
415
|
+
for ev_key in ["beat", "$beat"] {
|
|
416
|
+
if let Some(handlers) = global_store.get_event_handlers(ev_key) {
|
|
417
|
+
let mut seen: std::collections::HashSet<(usize, usize, usize)> =
|
|
418
|
+
std::collections::HashSet::new();
|
|
419
|
+
// Default every 1 beat if args missing
|
|
420
|
+
for h in handlers {
|
|
421
|
+
let key = (h.line, h.column, h.indent);
|
|
422
|
+
if !seen.insert(key) {
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
if let StatementKind::On { event, args, body } = &h.kind {
|
|
426
|
+
let every: f32 = args
|
|
427
|
+
.as_ref()
|
|
428
|
+
.and_then(|v| v.first())
|
|
429
|
+
.and_then(|x| {
|
|
430
|
+
match x {
|
|
431
|
+
Value::Number(n) => Some(*n),
|
|
432
|
+
Value::Identifier(s) => {
|
|
433
|
+
// Try to resolve from variables first, fallback to parsing the literal
|
|
434
|
+
match variable_table.get(s) {
|
|
435
|
+
Some(Value::Number(n)) => Some(*n),
|
|
436
|
+
_ => s.parse::<f32>().ok(),
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
_ => None,
|
|
440
|
+
}
|
|
441
|
+
})
|
|
442
|
+
.unwrap_or(1.0)
|
|
443
|
+
.max(0.0001);
|
|
444
|
+
let step = base_duration * every;
|
|
445
|
+
// Start from first full bar boundary after t=0
|
|
446
|
+
let mut t = step;
|
|
447
|
+
while t <= max_end_time {
|
|
448
|
+
// Prepare event context
|
|
449
|
+
let mut vt = variable_table.clone();
|
|
450
|
+
let mut ctx = std::collections::HashMap::new();
|
|
451
|
+
ctx.insert("name".to_string(), Value::String(event.clone()));
|
|
452
|
+
if let Some(a) = args.clone() {
|
|
453
|
+
ctx.insert("args".to_string(), Value::Array(a));
|
|
454
|
+
}
|
|
455
|
+
vt.set("event".to_string(), Value::Map(ctx));
|
|
456
|
+
vt.set("beat".to_string(), Value::Number(t / base_duration));
|
|
457
|
+
// Prevent nested scheduling
|
|
458
|
+
vt.set("__in_event".to_string(), Value::Boolean(true));
|
|
459
|
+
|
|
460
|
+
let (_m, _c) = execute_audio_block(
|
|
461
|
+
audio_engine,
|
|
462
|
+
global_store,
|
|
463
|
+
vt,
|
|
464
|
+
functions_table.clone(),
|
|
465
|
+
body,
|
|
466
|
+
base_bpm,
|
|
467
|
+
base_duration,
|
|
468
|
+
max_end_time,
|
|
469
|
+
t,
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
t += step;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Bar-based handlers (default 4/4 time => 4 beats per bar); support "bar" and "$bar"
|
|
480
|
+
for ev_key in ["bar", "$bar"] {
|
|
481
|
+
if let Some(handlers) = global_store.get_event_handlers(ev_key) {
|
|
482
|
+
let mut seen: std::collections::HashSet<(usize, usize, usize)> =
|
|
483
|
+
std::collections::HashSet::new();
|
|
484
|
+
for h in handlers {
|
|
485
|
+
let key = (h.line, h.column, h.indent);
|
|
486
|
+
if !seen.insert(key) {
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
if let StatementKind::On { event, args, body } = &h.kind {
|
|
490
|
+
let bar_beats = 4.0f32; // TODO: time signature support
|
|
491
|
+
let first_only = args.as_ref().and_then(|v| v.first()).is_none();
|
|
492
|
+
|
|
493
|
+
let every_bar: f32 = if first_only {
|
|
494
|
+
1.0
|
|
495
|
+
} else {
|
|
496
|
+
args.as_ref()
|
|
497
|
+
.and_then(|v| v.first())
|
|
498
|
+
.and_then(|x| match x {
|
|
499
|
+
Value::Number(n) => Some(*n),
|
|
500
|
+
Value::Identifier(s) => match variable_table.get(s) {
|
|
501
|
+
Some(Value::Number(n)) => Some(*n),
|
|
502
|
+
_ => s.parse::<f32>().ok(),
|
|
503
|
+
},
|
|
504
|
+
_ => None,
|
|
505
|
+
})
|
|
506
|
+
.unwrap_or(1.0)
|
|
507
|
+
.max(0.0001)
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
let step = base_duration * bar_beats * every_bar;
|
|
511
|
+
|
|
512
|
+
if first_only {
|
|
513
|
+
let t = step; // first full bar after t=0
|
|
514
|
+
if t <= max_end_time {
|
|
515
|
+
let mut vt = variable_table.clone();
|
|
516
|
+
let mut ctx = std::collections::HashMap::new();
|
|
517
|
+
ctx.insert("name".to_string(), Value::String(event.clone()));
|
|
518
|
+
if let Some(a) = args.clone() {
|
|
519
|
+
ctx.insert("args".to_string(), Value::Array(a));
|
|
520
|
+
}
|
|
521
|
+
vt.set("event".to_string(), Value::Map(ctx));
|
|
522
|
+
vt.set("beat".to_string(), Value::Number(t / base_duration));
|
|
523
|
+
// Prevent nested scheduling
|
|
524
|
+
vt.set("__in_event".to_string(), Value::Boolean(true));
|
|
525
|
+
|
|
526
|
+
let (_m, _c) = execute_audio_block(
|
|
527
|
+
audio_engine,
|
|
528
|
+
global_store,
|
|
529
|
+
vt,
|
|
530
|
+
functions_table.clone(),
|
|
531
|
+
body,
|
|
532
|
+
base_bpm,
|
|
533
|
+
base_duration,
|
|
534
|
+
max_end_time,
|
|
535
|
+
t,
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
} else {
|
|
539
|
+
let mut t = step; // start from first full bar after t=0
|
|
540
|
+
while t <= max_end_time {
|
|
541
|
+
let mut vt = variable_table.clone();
|
|
542
|
+
let mut ctx = std::collections::HashMap::new();
|
|
543
|
+
ctx.insert("name".to_string(), Value::String(event.clone()));
|
|
544
|
+
if let Some(a) = args.clone() {
|
|
545
|
+
ctx.insert("args".to_string(), Value::Array(a));
|
|
546
|
+
}
|
|
547
|
+
vt.set("event".to_string(), Value::Map(ctx));
|
|
548
|
+
vt.set("beat".to_string(), Value::Number(t / base_duration));
|
|
549
|
+
// Prevent nested scheduling
|
|
550
|
+
vt.set("__in_event".to_string(), Value::Boolean(true));
|
|
551
|
+
|
|
552
|
+
let (_m, _c) = execute_audio_block(
|
|
553
|
+
audio_engine,
|
|
554
|
+
global_store,
|
|
555
|
+
vt,
|
|
556
|
+
functions_table.clone(),
|
|
557
|
+
body,
|
|
558
|
+
base_bpm,
|
|
559
|
+
base_duration,
|
|
560
|
+
max_end_time,
|
|
561
|
+
t,
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
t += step;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
(max_end_time.max(cursor_time), cursor_time)
|
|
574
|
+
}
|