@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.
- package/.devalang +9 -10
- package/Cargo.toml +84 -80
- package/README.md +10 -7
- package/docs/CHANGELOG.md +83 -0
- package/docs/ROADMAP.md +6 -2
- package/docs/TODO.md +3 -14
- package/examples/bus.deva +10 -0
- package/examples/chain.deva +19 -0
- package/examples/effect.deva +2 -0
- package/examples/filter.deva +11 -0
- package/examples/lfo.deva +9 -0
- package/examples/plugin.deva +10 -10
- package/examples/routing.deva +23 -0
- package/examples/synth.deva +11 -1
- package/examples/synth_types.deva +17 -0
- package/out-tsc/bin/project-version.json +6 -0
- package/out-tsc/core/functions/index.d.ts +5 -0
- package/out-tsc/core/functions/index.js +11 -0
- package/out-tsc/pkg/devalang_core.d.ts +2 -0
- package/out-tsc/pkg/devalang_core.js +17 -2
- package/out-tsc/pkg/devalang_core_bg.wasm.d.ts +1 -0
- 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} +34 -43
- package/rust/cli/build/commands.rs +153 -103
- package/rust/cli/build/mod.rs +2 -2
- package/rust/cli/build/process.rs +165 -146
- package/rust/cli/check/mod.rs +208 -208
- package/rust/cli/discover/commands.rs +53 -31
- package/rust/cli/discover/config.rs +2 -4
- package/rust/cli/discover/install.rs +139 -28
- package/rust/cli/discover/metadata.rs +3 -3
- package/rust/cli/login/commands.rs +124 -124
- package/rust/cli/me/commands.rs +52 -0
- package/rust/cli/me/mod.rs +1 -0
- package/rust/cli/mod.rs +2 -2
- package/rust/cli/parser.rs +76 -70
- package/rust/cli/play/commands.rs +375 -324
- package/rust/cli/play/mod.rs +5 -5
- package/rust/cli/play/process.rs +159 -150
- package/rust/cli/play/realtime.rs +91 -91
- package/rust/cli/telemetry/commands.rs +22 -22
- package/rust/cli/telemetry/event_creator.rs +80 -80
- package/rust/cli/telemetry/mod.rs +3 -3
- package/rust/cli/telemetry/send.rs +51 -51
- package/rust/cli/template/commands.rs +69 -69
- package/rust/config/driver.rs +112 -103
- package/rust/config/mod.rs +3 -3
- package/rust/config/ops.rs +26 -26
- package/rust/config/settings.rs +101 -101
- package/rust/core/audio/engine/driver.rs +237 -0
- package/rust/core/audio/engine/export.rs +169 -0
- package/rust/core/audio/engine/helpers.rs +178 -170
- package/rust/core/audio/engine/mod.rs +56 -7
- package/rust/core/audio/engine/notes/dsp.rs +88 -0
- package/rust/core/audio/engine/notes/mod.rs +53 -0
- package/rust/core/audio/engine/notes/params.rs +294 -0
- package/rust/core/audio/engine/sample/insert.rs +300 -0
- package/rust/core/audio/engine/sample/mod.rs +40 -0
- package/rust/core/audio/engine/sample/padding.rs +170 -0
- package/rust/core/audio/evaluator/condition.rs +61 -0
- package/rust/core/audio/evaluator/mod.rs +9 -0
- package/rust/core/audio/{evaluator.rs → evaluator/numeric.rs} +152 -310
- package/rust/core/audio/evaluator/rhs.rs +16 -0
- package/rust/core/audio/evaluator/string_expr.rs +94 -0
- package/rust/core/audio/interpreter/driver.rs +574 -542
- package/rust/core/audio/interpreter/mod.rs +2 -14
- package/rust/core/audio/interpreter/statements/arrow_call/interprete.rs +179 -0
- package/rust/core/audio/interpreter/statements/arrow_call/methods/chord.rs +398 -0
- package/rust/core/audio/interpreter/statements/arrow_call/methods/effects.rs +323 -0
- package/rust/core/audio/interpreter/statements/arrow_call/methods/mod.rs +3 -0
- package/rust/core/audio/interpreter/statements/arrow_call/methods/note.rs +371 -0
- package/rust/core/audio/interpreter/statements/arrow_call/mod.rs +3 -0
- package/rust/core/audio/interpreter/statements/arrow_call/types/arp.rs +192 -0
- package/rust/core/audio/interpreter/statements/arrow_call/types/mod.rs +24 -0
- package/rust/core/audio/interpreter/statements/arrow_call/types/pad.rs +116 -0
- package/rust/core/audio/interpreter/statements/arrow_call/types/pluck.rs +97 -0
- package/rust/core/audio/interpreter/statements/arrow_call/types/sub.rs +100 -0
- package/rust/core/audio/interpreter/{automate.rs → statements/automate.rs} +2 -4
- package/rust/core/audio/interpreter/{call.rs → statements/call.rs} +36 -5
- package/rust/core/audio/interpreter/{condition.rs → statements/condition.rs} +72 -71
- package/rust/core/audio/interpreter/{function.rs → statements/function.rs} +24 -26
- package/rust/core/audio/interpreter/{let_.rs → statements/let_.rs} +36 -38
- package/rust/core/audio/interpreter/{load.rs → statements/load.rs} +17 -19
- package/rust/core/audio/interpreter/{loop_.rs → statements/loop_.rs} +115 -114
- package/rust/core/audio/interpreter/statements/mod.rs +12 -0
- package/rust/core/audio/interpreter/{sleep.rs → statements/sleep.rs} +28 -28
- package/rust/core/audio/interpreter/{spawn.rs → statements/spawn.rs} +54 -4
- package/rust/core/audio/interpreter/{tempo.rs → statements/tempo.rs} +40 -40
- package/rust/core/audio/interpreter/{trigger.rs → statements/trigger.rs} +242 -239
- package/rust/core/audio/loader/trigger.rs +98 -97
- package/rust/core/audio/mod.rs +6 -7
- package/rust/core/audio/special/easing.rs +189 -189
- package/rust/core/audio/special/env.rs +45 -45
- package/rust/core/audio/special/math.rs +134 -134
- package/rust/core/audio/special/modulator.rs +143 -143
- package/rust/core/builder/mod.rs +129 -86
- package/rust/core/debugger/{module.rs → logs.rs} +52 -55
- package/rust/core/debugger/mod.rs +30 -30
- package/rust/core/debugger/store.rs +38 -40
- package/rust/core/error/mod.rs +269 -269
- package/rust/core/lexer/driver.rs +2 -4
- package/rust/core/mod.rs +9 -10
- package/rust/core/parser/driver/block.rs +111 -0
- package/rust/core/parser/driver/cursor.rs +82 -0
- package/rust/core/parser/driver/driver_impl.rs +159 -0
- package/rust/core/parser/driver/mod.rs +6 -0
- package/rust/core/parser/driver/parse_array.rs +120 -0
- package/rust/core/parser/driver/parse_map.rs +247 -0
- package/rust/core/parser/driver/parser.rs +160 -0
- package/rust/core/parser/handler/arrow_call.rs +90 -15
- package/rust/core/parser/handler/at.rs +279 -279
- package/rust/core/parser/handler/bank.rs +104 -104
- package/rust/core/parser/handler/condition.rs +83 -83
- package/rust/core/parser/handler/dot.rs +148 -148
- package/rust/core/parser/handler/identifier/automate.rs +254 -254
- package/rust/core/parser/handler/identifier/call.rs +91 -91
- package/rust/core/parser/handler/identifier/emit.rs +70 -70
- package/rust/core/parser/handler/identifier/function.rs +113 -113
- package/rust/core/parser/handler/identifier/group.rs +89 -89
- package/rust/core/parser/handler/identifier/let_.rs +173 -173
- package/rust/core/parser/handler/identifier/mod.rs +55 -55
- package/rust/core/parser/handler/identifier/on.rs +107 -107
- package/rust/core/parser/handler/identifier/print.rs +49 -49
- package/rust/core/parser/handler/identifier/sleep.rs +96 -43
- package/rust/core/parser/handler/identifier/spawn.rs +91 -91
- package/rust/core/parser/handler/identifier/synth.rs +39 -3
- package/rust/core/parser/handler/loop_.rs +194 -194
- package/rust/core/parser/handler/pattern.rs +25 -2
- package/rust/core/parser/handler/tempo.rs +105 -57
- package/rust/core/parser/statement.rs +10 -11
- package/rust/core/plugin/loader.rs +137 -137
- package/rust/core/plugin/runner/mod.rs +11 -0
- package/rust/core/plugin/{runner.rs → runner/non_wasm.rs} +206 -72
- package/rust/core/plugin/runner/wasm32.rs +44 -0
- package/rust/core/preprocessor/loader/inject.rs +313 -0
- package/rust/core/preprocessor/loader/loader_helpers.rs +110 -0
- package/rust/core/preprocessor/loader/mod.rs +235 -0
- package/rust/core/preprocessor/module.rs +55 -60
- package/rust/core/preprocessor/{processor.rs → processor/handlers.rs} +107 -114
- package/rust/core/preprocessor/processor/mod.rs +1 -0
- package/rust/core/preprocessor/resolver/function.rs +69 -69
- package/rust/core/preprocessor/resolver/group.rs +122 -94
- package/rust/core/preprocessor/resolver/pattern.rs +14 -2
- package/rust/core/store/global.rs +57 -61
- package/rust/core/store/mod.rs +1 -5
- package/rust/lib.rs +323 -308
- package/rust/macros/Cargo.toml +14 -0
- package/rust/macros/src/lib.rs +52 -0
- package/rust/main.rs +336 -143
- package/rust/types/Cargo.toml +1 -1
- package/rust/types/src/addons.rs +57 -55
- package/rust/types/src/config.rs +82 -74
- package/rust/types/src/lib.rs +15 -12
- package/rust/types/src/plugin.rs +20 -0
- package/rust/types/src/store.rs +139 -0
- package/rust/types/src/telemetry.rs +85 -85
- package/rust/utils/Cargo.toml +5 -2
- package/rust/utils/src/file.rs +477 -94
- package/rust/utils/src/first_usage.rs +97 -97
- package/rust/utils/src/lib.rs +9 -9
- package/rust/utils/src/logger.rs +200 -200
- package/rust/utils/src/path.rs +158 -88
- package/rust/utils/src/signature.rs +41 -41
- package/rust/utils/src/spinner.rs +20 -20
- package/rust/utils/src/version.rs +58 -27
- package/rust/utils/src/watcher.rs +46 -46
- package/rust/web/api.rs +5 -5
- package/rust/web/auth.rs +5 -0
- package/rust/web/cdn.rs +34 -34
- package/rust/web/forge.rs +5 -0
- package/rust/web/mod.rs +2 -0
- package/tests/integration.rs +21 -21
- package/typescript/core/functions/index.ts +11 -0
- package/typescript/pkg/devalang_core.ts +20 -4
- 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 -275
- package/rust/cli/bank/mod.rs +0 -29
- package/rust/cli/install/bank.rs +0 -53
- package/rust/cli/install/commands.rs +0 -35
- package/rust/cli/install/mod.rs +0 -4
- package/rust/cli/install/plugin.rs +0 -61
- package/rust/core/audio/engine/sample.rs +0 -366
- package/rust/core/audio/engine/synth.rs +0 -325
- package/rust/core/audio/interpreter/arrow_call.rs +0 -311
- package/rust/core/audio/renderer.rs +0 -54
- package/rust/core/parser/driver.rs +0 -584
- package/rust/core/preprocessor/loader.rs +0 -637
- package/rust/core/store/export.rs +0 -28
- package/rust/core/store/function.rs +0 -40
- package/rust/core/store/import.rs +0 -28
- package/rust/core/store/variable.rs +0 -51
- package/rust/core/utils/mod.rs +0 -1
- package/rust/core/utils/path.rs +0 -37
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
use devalang_types::Value;
|
|
2
|
-
|
|
3
|
-
use
|
|
4
|
-
use std::sync::OnceLock;
|
|
5
|
-
use std::time::{SystemTime, UNIX_EPOCH};
|
|
6
|
-
|
|
7
|
-
static SESSION_SEED: OnceLock<f32> = OnceLock::new();
|
|
8
|
-
|
|
9
|
-
pub fn get_session_seed() -> f32 {
|
|
10
|
-
*SESSION_SEED.get_or_init(|| {
|
|
11
|
-
let now = SystemTime::now()
|
|
12
|
-
.duration_since(UNIX_EPOCH)
|
|
13
|
-
.unwrap_or_default();
|
|
14
|
-
// Build a stable 0..1 seed from nanos
|
|
15
|
-
let nanos = now.subsec_nanos();
|
|
16
|
-
((nanos as f32) / 1_000_000_000.0).clamp(0.0, 1.0)
|
|
17
|
-
})
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Resolve special environment variables like $env.bpm, $env.beat, $env.position
|
|
21
|
-
// For now, $env.position is treated as an alias of beat.
|
|
22
|
-
pub fn resolve_env_atom(atom: &str, bpm: f32, beat: f32) -> Option<f32> {
|
|
23
|
-
match atom {
|
|
24
|
-
"$env.bpm" => Some(bpm),
|
|
25
|
-
"$env.beat" => Some(beat),
|
|
26
|
-
"$env.position" => Some(beat),
|
|
27
|
-
// Optional seed for deterministic randomness
|
|
28
|
-
"$env.seed" => Some(get_session_seed()),
|
|
29
|
-
_ => None,
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Utility: resolve an identifier or numeric literal to f32 using the variable table
|
|
34
|
-
pub fn resolve_atom_or_var(atom: &str, vars: &VariableTable, bpm: f32, beat: f32) -> Option<f32> {
|
|
35
|
-
if let Some(v) = resolve_env_atom(atom, bpm, beat) {
|
|
36
|
-
return Some(v);
|
|
37
|
-
}
|
|
38
|
-
if let Ok(n) = atom.parse::<f32>() {
|
|
39
|
-
return Some(n);
|
|
40
|
-
}
|
|
41
|
-
if let Some(Value::Number(n)) = vars.get(atom) {
|
|
42
|
-
return Some(*n);
|
|
43
|
-
}
|
|
44
|
-
None
|
|
45
|
-
}
|
|
1
|
+
use devalang_types::Value;
|
|
2
|
+
|
|
3
|
+
use devalang_types::VariableTable;
|
|
4
|
+
use std::sync::OnceLock;
|
|
5
|
+
use std::time::{SystemTime, UNIX_EPOCH};
|
|
6
|
+
|
|
7
|
+
static SESSION_SEED: OnceLock<f32> = OnceLock::new();
|
|
8
|
+
|
|
9
|
+
pub fn get_session_seed() -> f32 {
|
|
10
|
+
*SESSION_SEED.get_or_init(|| {
|
|
11
|
+
let now = SystemTime::now()
|
|
12
|
+
.duration_since(UNIX_EPOCH)
|
|
13
|
+
.unwrap_or_default();
|
|
14
|
+
// Build a stable 0..1 seed from nanos
|
|
15
|
+
let nanos = now.subsec_nanos();
|
|
16
|
+
((nanos as f32) / 1_000_000_000.0).clamp(0.0, 1.0)
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Resolve special environment variables like $env.bpm, $env.beat, $env.position
|
|
21
|
+
// For now, $env.position is treated as an alias of beat.
|
|
22
|
+
pub fn resolve_env_atom(atom: &str, bpm: f32, beat: f32) -> Option<f32> {
|
|
23
|
+
match atom {
|
|
24
|
+
"$env.bpm" => Some(bpm),
|
|
25
|
+
"$env.beat" => Some(beat),
|
|
26
|
+
"$env.position" => Some(beat),
|
|
27
|
+
// Optional seed for deterministic randomness
|
|
28
|
+
"$env.seed" => Some(get_session_seed()),
|
|
29
|
+
_ => None,
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Utility: resolve an identifier or numeric literal to f32 using the variable table
|
|
34
|
+
pub fn resolve_atom_or_var(atom: &str, vars: &VariableTable, bpm: f32, beat: f32) -> Option<f32> {
|
|
35
|
+
if let Some(v) = resolve_env_atom(atom, bpm, beat) {
|
|
36
|
+
return Some(v);
|
|
37
|
+
}
|
|
38
|
+
if let Ok(n) = atom.parse::<f32>() {
|
|
39
|
+
return Some(n);
|
|
40
|
+
}
|
|
41
|
+
if let Some(Value::Number(n)) = vars.get(atom) {
|
|
42
|
+
return Some(*n);
|
|
43
|
+
}
|
|
44
|
+
None
|
|
45
|
+
}
|
|
@@ -1,134 +1,134 @@
|
|
|
1
|
-
use
|
|
2
|
-
use devalang_utils::logger::{LogLevel, Logger};
|
|
3
|
-
|
|
4
|
-
// Parse comma-separated arguments at top level (no nested parentheses split)
|
|
5
|
-
fn parse_top_level_args(s: &str) -> Vec<&str> {
|
|
6
|
-
let mut args = Vec::new();
|
|
7
|
-
let mut depth = 0i32;
|
|
8
|
-
let mut start = 0usize;
|
|
9
|
-
for (i, ch) in s.char_indices() {
|
|
10
|
-
match ch {
|
|
11
|
-
'(' => depth += 1,
|
|
12
|
-
')' => depth -= 1,
|
|
13
|
-
',' if depth == 0 => {
|
|
14
|
-
args.push(s[start..i].trim());
|
|
15
|
-
start = i + 1;
|
|
16
|
-
}
|
|
17
|
-
_ => {}
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
let last = s[start..].trim();
|
|
21
|
-
if !last.is_empty() {
|
|
22
|
-
args.push(last);
|
|
23
|
-
}
|
|
24
|
-
args
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
fn eval_math_func(func: &str, args: &[f32], fallback_seed: f32) -> Option<f32> {
|
|
28
|
-
match func {
|
|
29
|
-
"sin" => args.first().copied().map(f32::sin),
|
|
30
|
-
"cos" => args.first().copied().map(f32::cos),
|
|
31
|
-
"random" => {
|
|
32
|
-
// deterministic pseudo-random based on provided seed or a fallback session seed
|
|
33
|
-
let seed = args.first().copied().unwrap_or(fallback_seed);
|
|
34
|
-
let x = (seed * 12.9898).sin() * 43_758.547;
|
|
35
|
-
Some((x.fract() * 2.0 - 1.0).clamp(-1.0, 1.0))
|
|
36
|
-
}
|
|
37
|
-
"lerp" => {
|
|
38
|
-
if args.len() >= 3 {
|
|
39
|
-
Some(args[0] + (args[1] - args[0]) * args[2])
|
|
40
|
-
} else {
|
|
41
|
-
None
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
_ => None,
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Find and evaluate the first $math.<fn>(...) occurrence in the string, replacing it with a number.
|
|
49
|
-
// Supports multi-argument functions by splitting on top-level commas.
|
|
50
|
-
pub fn find_and_eval_first_math_call<EvalFn>(
|
|
51
|
-
s: &str,
|
|
52
|
-
eval: EvalFn,
|
|
53
|
-
vars: &VariableTable,
|
|
54
|
-
bpm: f32,
|
|
55
|
-
beat: f32,
|
|
56
|
-
) -> Option<String>
|
|
57
|
-
where
|
|
58
|
-
EvalFn: Fn(&str, &VariableTable, f32, f32) -> Option<f32>,
|
|
59
|
-
{
|
|
60
|
-
let logger = Logger::new();
|
|
61
|
-
|
|
62
|
-
let start = s.find("$math.")?;
|
|
63
|
-
let open_rel = match s[start..].find('(') {
|
|
64
|
-
Some(i) => i,
|
|
65
|
-
None => {
|
|
66
|
-
logger.log_message(
|
|
67
|
-
LogLevel::Error,
|
|
68
|
-
&format!("Malformed $math call: missing '(' in '{}'", s),
|
|
69
|
-
);
|
|
70
|
-
return None;
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
let open = start + open_rel;
|
|
74
|
-
if open <= start + 6 {
|
|
75
|
-
logger.log_message(
|
|
76
|
-
LogLevel::Error,
|
|
77
|
-
&format!("Malformed $math call: missing function name in '{}'", s),
|
|
78
|
-
);
|
|
79
|
-
return None;
|
|
80
|
-
}
|
|
81
|
-
let func = &s[start + 6..open];
|
|
82
|
-
|
|
83
|
-
// Find matching close parenthesis, handling nesting
|
|
84
|
-
let mut depth: i32 = 0;
|
|
85
|
-
let mut close_abs: Option<usize> = None;
|
|
86
|
-
for (i, ch) in s[open..].char_indices() {
|
|
87
|
-
match ch {
|
|
88
|
-
'(' => depth += 1,
|
|
89
|
-
')' => {
|
|
90
|
-
depth -= 1;
|
|
91
|
-
if depth == 0 {
|
|
92
|
-
close_abs = Some(open + i);
|
|
93
|
-
break;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
_ => {}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
let close = match close_abs {
|
|
100
|
-
Some(c) => c,
|
|
101
|
-
None => {
|
|
102
|
-
logger.log_message(
|
|
103
|
-
LogLevel::Error,
|
|
104
|
-
&format!("Malformed $math call: missing closing ')' in '{}'", s),
|
|
105
|
-
);
|
|
106
|
-
return None;
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
let inner = &s[open + 1..close];
|
|
111
|
-
let raw_args = parse_top_level_args(inner);
|
|
112
|
-
let mut args: Vec<f32> = Vec::with_capacity(raw_args.len());
|
|
113
|
-
for a in raw_args {
|
|
114
|
-
if let Some(v) = eval(a, vars, bpm, beat) {
|
|
115
|
-
args.push(v);
|
|
116
|
-
} else {
|
|
117
|
-
logger.log_message(
|
|
118
|
-
LogLevel::Error,
|
|
119
|
-
&format!("Failed to evaluate argument '{}' for $math.{}", a, func),
|
|
120
|
-
);
|
|
121
|
-
return None;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// If no explicit seed is provided, use $env.seed via fallback
|
|
126
|
-
let fallback_seed = eval("$env.seed", vars, bpm, beat).unwrap_or(0.0);
|
|
127
|
-
let result = eval_math_func(func, &args, fallback_seed)?;
|
|
128
|
-
|
|
129
|
-
let mut replaced = String::new();
|
|
130
|
-
replaced.push_str(&s[..start]);
|
|
131
|
-
replaced.push_str(&result.to_string());
|
|
132
|
-
replaced.push_str(&s[close + 1..]);
|
|
133
|
-
Some(replaced)
|
|
134
|
-
}
|
|
1
|
+
use devalang_types::VariableTable;
|
|
2
|
+
use devalang_utils::logger::{LogLevel, Logger};
|
|
3
|
+
|
|
4
|
+
// Parse comma-separated arguments at top level (no nested parentheses split)
|
|
5
|
+
fn parse_top_level_args(s: &str) -> Vec<&str> {
|
|
6
|
+
let mut args = Vec::new();
|
|
7
|
+
let mut depth = 0i32;
|
|
8
|
+
let mut start = 0usize;
|
|
9
|
+
for (i, ch) in s.char_indices() {
|
|
10
|
+
match ch {
|
|
11
|
+
'(' => depth += 1,
|
|
12
|
+
')' => depth -= 1,
|
|
13
|
+
',' if depth == 0 => {
|
|
14
|
+
args.push(s[start..i].trim());
|
|
15
|
+
start = i + 1;
|
|
16
|
+
}
|
|
17
|
+
_ => {}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
let last = s[start..].trim();
|
|
21
|
+
if !last.is_empty() {
|
|
22
|
+
args.push(last);
|
|
23
|
+
}
|
|
24
|
+
args
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fn eval_math_func(func: &str, args: &[f32], fallback_seed: f32) -> Option<f32> {
|
|
28
|
+
match func {
|
|
29
|
+
"sin" => args.first().copied().map(f32::sin),
|
|
30
|
+
"cos" => args.first().copied().map(f32::cos),
|
|
31
|
+
"random" => {
|
|
32
|
+
// deterministic pseudo-random based on provided seed or a fallback session seed
|
|
33
|
+
let seed = args.first().copied().unwrap_or(fallback_seed);
|
|
34
|
+
let x = (seed * 12.9898).sin() * 43_758.547;
|
|
35
|
+
Some((x.fract() * 2.0 - 1.0).clamp(-1.0, 1.0))
|
|
36
|
+
}
|
|
37
|
+
"lerp" => {
|
|
38
|
+
if args.len() >= 3 {
|
|
39
|
+
Some(args[0] + (args[1] - args[0]) * args[2])
|
|
40
|
+
} else {
|
|
41
|
+
None
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
_ => None,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Find and evaluate the first $math.<fn>(...) occurrence in the string, replacing it with a number.
|
|
49
|
+
// Supports multi-argument functions by splitting on top-level commas.
|
|
50
|
+
pub fn find_and_eval_first_math_call<EvalFn>(
|
|
51
|
+
s: &str,
|
|
52
|
+
eval: EvalFn,
|
|
53
|
+
vars: &VariableTable,
|
|
54
|
+
bpm: f32,
|
|
55
|
+
beat: f32,
|
|
56
|
+
) -> Option<String>
|
|
57
|
+
where
|
|
58
|
+
EvalFn: Fn(&str, &VariableTable, f32, f32) -> Option<f32>,
|
|
59
|
+
{
|
|
60
|
+
let logger = Logger::new();
|
|
61
|
+
|
|
62
|
+
let start = s.find("$math.")?;
|
|
63
|
+
let open_rel = match s[start..].find('(') {
|
|
64
|
+
Some(i) => i,
|
|
65
|
+
None => {
|
|
66
|
+
logger.log_message(
|
|
67
|
+
LogLevel::Error,
|
|
68
|
+
&format!("Malformed $math call: missing '(' in '{}'", s),
|
|
69
|
+
);
|
|
70
|
+
return None;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
let open = start + open_rel;
|
|
74
|
+
if open <= start + 6 {
|
|
75
|
+
logger.log_message(
|
|
76
|
+
LogLevel::Error,
|
|
77
|
+
&format!("Malformed $math call: missing function name in '{}'", s),
|
|
78
|
+
);
|
|
79
|
+
return None;
|
|
80
|
+
}
|
|
81
|
+
let func = &s[start + 6..open];
|
|
82
|
+
|
|
83
|
+
// Find matching close parenthesis, handling nesting
|
|
84
|
+
let mut depth: i32 = 0;
|
|
85
|
+
let mut close_abs: Option<usize> = None;
|
|
86
|
+
for (i, ch) in s[open..].char_indices() {
|
|
87
|
+
match ch {
|
|
88
|
+
'(' => depth += 1,
|
|
89
|
+
')' => {
|
|
90
|
+
depth -= 1;
|
|
91
|
+
if depth == 0 {
|
|
92
|
+
close_abs = Some(open + i);
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
_ => {}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
let close = match close_abs {
|
|
100
|
+
Some(c) => c,
|
|
101
|
+
None => {
|
|
102
|
+
logger.log_message(
|
|
103
|
+
LogLevel::Error,
|
|
104
|
+
&format!("Malformed $math call: missing closing ')' in '{}'", s),
|
|
105
|
+
);
|
|
106
|
+
return None;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
let inner = &s[open + 1..close];
|
|
111
|
+
let raw_args = parse_top_level_args(inner);
|
|
112
|
+
let mut args: Vec<f32> = Vec::with_capacity(raw_args.len());
|
|
113
|
+
for a in raw_args {
|
|
114
|
+
if let Some(v) = eval(a, vars, bpm, beat) {
|
|
115
|
+
args.push(v);
|
|
116
|
+
} else {
|
|
117
|
+
logger.log_message(
|
|
118
|
+
LogLevel::Error,
|
|
119
|
+
&format!("Failed to evaluate argument '{}' for $math.{}", a, func),
|
|
120
|
+
);
|
|
121
|
+
return None;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// If no explicit seed is provided, use $env.seed via fallback
|
|
126
|
+
let fallback_seed = eval("$env.seed", vars, bpm, beat).unwrap_or(0.0);
|
|
127
|
+
let result = eval_math_func(func, &args, fallback_seed)?;
|
|
128
|
+
|
|
129
|
+
let mut replaced = String::new();
|
|
130
|
+
replaced.push_str(&s[..start]);
|
|
131
|
+
replaced.push_str(&result.to_string());
|
|
132
|
+
replaced.push_str(&s[close + 1..]);
|
|
133
|
+
Some(replaced)
|
|
134
|
+
}
|
|
@@ -1,143 +1,143 @@
|
|
|
1
|
-
use
|
|
2
|
-
|
|
3
|
-
fn lfo_sine(rate_per_beat: f32, beat: f32) -> f32 {
|
|
4
|
-
// Output in [-1,1]
|
|
5
|
-
(2.0 * std::f32::consts::PI * rate_per_beat * beat).sin()
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
fn lfo_triangle(rate_per_beat: f32, beat: f32) -> f32 {
|
|
9
|
-
// Triangle in [-1,1]
|
|
10
|
-
let phase = (rate_per_beat * beat).fract();
|
|
11
|
-
// Map [0,1]->[-1,1] tri
|
|
12
|
-
4.0 * (phase - 0.5).abs() - 1.0
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
fn adsr_envelope_value_t(attack: f32, decay: f32, sustain: f32, release: f32, t: f32) -> f32 {
|
|
16
|
-
let a = attack.max(0.0);
|
|
17
|
-
let d = decay.max(0.0);
|
|
18
|
-
let r = release.max(0.0);
|
|
19
|
-
let s = sustain.clamp(0.0, 1.0);
|
|
20
|
-
|
|
21
|
-
// Normalize phases so that the whole ADSR spans t in [0,1]
|
|
22
|
-
let total = (a + d + r).max(1e-6);
|
|
23
|
-
let ap = a / total;
|
|
24
|
-
let dp = d / total;
|
|
25
|
-
let rp = r / total;
|
|
26
|
-
|
|
27
|
-
if t < ap {
|
|
28
|
-
// attack (0->1)
|
|
29
|
-
if ap > 0.0 { t / ap } else { 1.0 }
|
|
30
|
-
} else if t < ap + dp {
|
|
31
|
-
// decay (1->sustain)
|
|
32
|
-
let u = (t - ap) / dp.max(1e-6);
|
|
33
|
-
1.0 - (1.0 - s) * u
|
|
34
|
-
} else if t < 1.0 - rp {
|
|
35
|
-
// sustain
|
|
36
|
-
s
|
|
37
|
-
} else {
|
|
38
|
-
// release (sustain->0)
|
|
39
|
-
let u = (t - (1.0 - rp)) / rp.max(1e-6);
|
|
40
|
-
s * (1.0 - u)
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
fn eval_mod_func(func: &str, args: &[f32], beat: f32) -> Option<f32> {
|
|
45
|
-
match func {
|
|
46
|
-
"lfo.sine" => {
|
|
47
|
-
let rate = args.first().copied().unwrap_or(1.0);
|
|
48
|
-
Some(lfo_sine(rate, beat))
|
|
49
|
-
}
|
|
50
|
-
"lfo.tri" | "lfo.triangle" => {
|
|
51
|
-
let rate = args.first().copied().unwrap_or(1.0);
|
|
52
|
-
Some(lfo_triangle(rate, beat))
|
|
53
|
-
}
|
|
54
|
-
// ADSR envelope normalized over t in [0,1]
|
|
55
|
-
// $mod.envelope(attack, decay, sustain, release, t)
|
|
56
|
-
"envelope" | "mod.envelope" => {
|
|
57
|
-
if args.len() >= 5 {
|
|
58
|
-
Some(adsr_envelope_value_t(
|
|
59
|
-
args[0],
|
|
60
|
-
args[1],
|
|
61
|
-
args[2],
|
|
62
|
-
args[3],
|
|
63
|
-
args[4].clamp(0.0, 1.0),
|
|
64
|
-
))
|
|
65
|
-
} else {
|
|
66
|
-
None
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
_ => None,
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
fn parse_top_level_args(s: &str) -> Vec<&str> {
|
|
74
|
-
let mut args = Vec::new();
|
|
75
|
-
let mut depth = 0i32;
|
|
76
|
-
let mut start = 0usize;
|
|
77
|
-
for (i, ch) in s.char_indices() {
|
|
78
|
-
match ch {
|
|
79
|
-
'(' => depth += 1,
|
|
80
|
-
')' => depth -= 1,
|
|
81
|
-
',' if depth == 0 => {
|
|
82
|
-
args.push(s[start..i].trim());
|
|
83
|
-
start = i + 1;
|
|
84
|
-
}
|
|
85
|
-
_ => {}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
let last = s[start..].trim();
|
|
89
|
-
if !last.is_empty() {
|
|
90
|
-
args.push(last);
|
|
91
|
-
}
|
|
92
|
-
args
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Find and evaluate the first $mod.<fn>(...) occurrence in the string.
|
|
96
|
-
pub fn find_and_eval_first_mod_call<EvalFn>(
|
|
97
|
-
s: &str,
|
|
98
|
-
eval: EvalFn,
|
|
99
|
-
vars: &VariableTable,
|
|
100
|
-
bpm: f32,
|
|
101
|
-
beat: f32,
|
|
102
|
-
) -> Option<String>
|
|
103
|
-
where
|
|
104
|
-
EvalFn: Fn(&str, &VariableTable, f32, f32) -> Option<f32>,
|
|
105
|
-
{
|
|
106
|
-
let start = s.find("$mod.")?;
|
|
107
|
-
let open_rel = s[start..].find('(')?;
|
|
108
|
-
let open = start + open_rel;
|
|
109
|
-
let func = &s[start + 5..open];
|
|
110
|
-
|
|
111
|
-
// matching close
|
|
112
|
-
let mut depth: i32 = 0;
|
|
113
|
-
let mut close_abs: Option<usize> = None;
|
|
114
|
-
for (i, ch) in s[open..].char_indices() {
|
|
115
|
-
match ch {
|
|
116
|
-
'(' => depth += 1,
|
|
117
|
-
')' => {
|
|
118
|
-
depth -= 1;
|
|
119
|
-
if depth == 0 {
|
|
120
|
-
close_abs = Some(open + i);
|
|
121
|
-
break;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
_ => {}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
let close = close_abs?;
|
|
128
|
-
|
|
129
|
-
let inner = &s[open + 1..close];
|
|
130
|
-
let raw_args = parse_top_level_args(inner);
|
|
131
|
-
let mut args: Vec<f32> = Vec::with_capacity(raw_args.len());
|
|
132
|
-
for a in raw_args {
|
|
133
|
-
args.push(eval(a, vars, bpm, beat)?);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
let result = eval_mod_func(func, &args, beat)?;
|
|
137
|
-
|
|
138
|
-
let mut replaced = String::new();
|
|
139
|
-
replaced.push_str(&s[..start]);
|
|
140
|
-
replaced.push_str(&result.to_string());
|
|
141
|
-
replaced.push_str(&s[close + 1..]);
|
|
142
|
-
Some(replaced)
|
|
143
|
-
}
|
|
1
|
+
use devalang_types::VariableTable;
|
|
2
|
+
|
|
3
|
+
fn lfo_sine(rate_per_beat: f32, beat: f32) -> f32 {
|
|
4
|
+
// Output in [-1,1]
|
|
5
|
+
(2.0 * std::f32::consts::PI * rate_per_beat * beat).sin()
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
fn lfo_triangle(rate_per_beat: f32, beat: f32) -> f32 {
|
|
9
|
+
// Triangle in [-1,1]
|
|
10
|
+
let phase = (rate_per_beat * beat).fract();
|
|
11
|
+
// Map [0,1]->[-1,1] tri
|
|
12
|
+
4.0 * (phase - 0.5).abs() - 1.0
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
fn adsr_envelope_value_t(attack: f32, decay: f32, sustain: f32, release: f32, t: f32) -> f32 {
|
|
16
|
+
let a = attack.max(0.0);
|
|
17
|
+
let d = decay.max(0.0);
|
|
18
|
+
let r = release.max(0.0);
|
|
19
|
+
let s = sustain.clamp(0.0, 1.0);
|
|
20
|
+
|
|
21
|
+
// Normalize phases so that the whole ADSR spans t in [0,1]
|
|
22
|
+
let total = (a + d + r).max(1e-6);
|
|
23
|
+
let ap = a / total;
|
|
24
|
+
let dp = d / total;
|
|
25
|
+
let rp = r / total;
|
|
26
|
+
|
|
27
|
+
if t < ap {
|
|
28
|
+
// attack (0->1)
|
|
29
|
+
if ap > 0.0 { t / ap } else { 1.0 }
|
|
30
|
+
} else if t < ap + dp {
|
|
31
|
+
// decay (1->sustain)
|
|
32
|
+
let u = (t - ap) / dp.max(1e-6);
|
|
33
|
+
1.0 - (1.0 - s) * u
|
|
34
|
+
} else if t < 1.0 - rp {
|
|
35
|
+
// sustain
|
|
36
|
+
s
|
|
37
|
+
} else {
|
|
38
|
+
// release (sustain->0)
|
|
39
|
+
let u = (t - (1.0 - rp)) / rp.max(1e-6);
|
|
40
|
+
s * (1.0 - u)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fn eval_mod_func(func: &str, args: &[f32], beat: f32) -> Option<f32> {
|
|
45
|
+
match func {
|
|
46
|
+
"lfo.sine" => {
|
|
47
|
+
let rate = args.first().copied().unwrap_or(1.0);
|
|
48
|
+
Some(lfo_sine(rate, beat))
|
|
49
|
+
}
|
|
50
|
+
"lfo.tri" | "lfo.triangle" => {
|
|
51
|
+
let rate = args.first().copied().unwrap_or(1.0);
|
|
52
|
+
Some(lfo_triangle(rate, beat))
|
|
53
|
+
}
|
|
54
|
+
// ADSR envelope normalized over t in [0,1]
|
|
55
|
+
// $mod.envelope(attack, decay, sustain, release, t)
|
|
56
|
+
"envelope" | "mod.envelope" => {
|
|
57
|
+
if args.len() >= 5 {
|
|
58
|
+
Some(adsr_envelope_value_t(
|
|
59
|
+
args[0],
|
|
60
|
+
args[1],
|
|
61
|
+
args[2],
|
|
62
|
+
args[3],
|
|
63
|
+
args[4].clamp(0.0, 1.0),
|
|
64
|
+
))
|
|
65
|
+
} else {
|
|
66
|
+
None
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
_ => None,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
fn parse_top_level_args(s: &str) -> Vec<&str> {
|
|
74
|
+
let mut args = Vec::new();
|
|
75
|
+
let mut depth = 0i32;
|
|
76
|
+
let mut start = 0usize;
|
|
77
|
+
for (i, ch) in s.char_indices() {
|
|
78
|
+
match ch {
|
|
79
|
+
'(' => depth += 1,
|
|
80
|
+
')' => depth -= 1,
|
|
81
|
+
',' if depth == 0 => {
|
|
82
|
+
args.push(s[start..i].trim());
|
|
83
|
+
start = i + 1;
|
|
84
|
+
}
|
|
85
|
+
_ => {}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
let last = s[start..].trim();
|
|
89
|
+
if !last.is_empty() {
|
|
90
|
+
args.push(last);
|
|
91
|
+
}
|
|
92
|
+
args
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Find and evaluate the first $mod.<fn>(...) occurrence in the string.
|
|
96
|
+
pub fn find_and_eval_first_mod_call<EvalFn>(
|
|
97
|
+
s: &str,
|
|
98
|
+
eval: EvalFn,
|
|
99
|
+
vars: &VariableTable,
|
|
100
|
+
bpm: f32,
|
|
101
|
+
beat: f32,
|
|
102
|
+
) -> Option<String>
|
|
103
|
+
where
|
|
104
|
+
EvalFn: Fn(&str, &VariableTable, f32, f32) -> Option<f32>,
|
|
105
|
+
{
|
|
106
|
+
let start = s.find("$mod.")?;
|
|
107
|
+
let open_rel = s[start..].find('(')?;
|
|
108
|
+
let open = start + open_rel;
|
|
109
|
+
let func = &s[start + 5..open];
|
|
110
|
+
|
|
111
|
+
// matching close
|
|
112
|
+
let mut depth: i32 = 0;
|
|
113
|
+
let mut close_abs: Option<usize> = None;
|
|
114
|
+
for (i, ch) in s[open..].char_indices() {
|
|
115
|
+
match ch {
|
|
116
|
+
'(' => depth += 1,
|
|
117
|
+
')' => {
|
|
118
|
+
depth -= 1;
|
|
119
|
+
if depth == 0 {
|
|
120
|
+
close_abs = Some(open + i);
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
_ => {}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
let close = close_abs?;
|
|
128
|
+
|
|
129
|
+
let inner = &s[open + 1..close];
|
|
130
|
+
let raw_args = parse_top_level_args(inner);
|
|
131
|
+
let mut args: Vec<f32> = Vec::with_capacity(raw_args.len());
|
|
132
|
+
for a in raw_args {
|
|
133
|
+
args.push(eval(a, vars, bpm, beat)?);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let result = eval_mod_func(func, &args, beat)?;
|
|
137
|
+
|
|
138
|
+
let mut replaced = String::new();
|
|
139
|
+
replaced.push_str(&s[..start]);
|
|
140
|
+
replaced.push_str(&result.to_string());
|
|
141
|
+
replaced.push_str(&s[close + 1..]);
|
|
142
|
+
Some(replaced)
|
|
143
|
+
}
|