@devaloop/devalang 0.0.1-alpha.14 → 0.0.1-alpha.15

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 (90) hide show
  1. package/.devalang +8 -8
  2. package/Cargo.toml +2 -2
  3. package/README.md +31 -14
  4. package/docs/CHANGELOG.md +59 -0
  5. package/docs/ROADMAP.md +1 -1
  6. package/examples/automation.deva +44 -0
  7. package/examples/index.deva +41 -25
  8. package/examples/plugin.deva +15 -0
  9. package/out-tsc/bin/devalang.exe +0 -0
  10. package/package.json +1 -1
  11. package/project-version.json +3 -3
  12. package/rust/cli/bank.rs +14 -15
  13. package/rust/cli/check.rs +1 -1
  14. package/rust/cli/play.rs +1 -1
  15. package/rust/cli/update.rs +1 -1
  16. package/rust/common/api.rs +3 -6
  17. package/rust/common/cdn.rs +3 -6
  18. package/rust/common/sso.rs +3 -6
  19. package/rust/core/audio/engine.rs +215 -74
  20. package/rust/core/audio/evaluator.rs +101 -0
  21. package/rust/core/audio/interpreter/arrow_call.rs +27 -1
  22. package/rust/core/audio/interpreter/automate.rs +18 -0
  23. package/rust/core/audio/interpreter/call.rs +2 -2
  24. package/rust/core/audio/interpreter/condition.rs +3 -3
  25. package/rust/core/audio/interpreter/driver.rs +49 -16
  26. package/rust/core/audio/interpreter/let_.rs +14 -7
  27. package/rust/core/audio/interpreter/loop_.rs +39 -6
  28. package/rust/core/audio/interpreter/mod.rs +2 -1
  29. package/rust/core/audio/interpreter/sleep.rs +2 -4
  30. package/rust/core/audio/interpreter/spawn.rs +2 -2
  31. package/rust/core/audio/loader/trigger.rs +2 -5
  32. package/rust/core/audio/mod.rs +2 -1
  33. package/rust/core/audio/renderer.rs +1 -1
  34. package/rust/core/audio/special/easing.rs +120 -0
  35. package/rust/core/audio/special/env.rs +41 -0
  36. package/rust/core/audio/special/math.rs +92 -0
  37. package/rust/core/audio/special/mod.rs +9 -0
  38. package/rust/core/audio/special/modulator.rs +120 -0
  39. package/rust/core/debugger/store.rs +1 -1
  40. package/rust/core/error/mod.rs +4 -1
  41. package/rust/core/lexer/handler/arrow.rs +60 -9
  42. package/rust/core/lexer/handler/at.rs +4 -4
  43. package/rust/core/lexer/handler/brace.rs +8 -8
  44. package/rust/core/lexer/handler/colon.rs +4 -4
  45. package/rust/core/lexer/handler/comment.rs +2 -2
  46. package/rust/core/lexer/handler/dot.rs +4 -4
  47. package/rust/core/lexer/handler/driver.rs +42 -13
  48. package/rust/core/lexer/handler/identifier.rs +5 -4
  49. package/rust/core/lexer/handler/newline.rs +1 -1
  50. package/rust/core/lexer/handler/number.rs +3 -3
  51. package/rust/core/lexer/handler/operator.rs +3 -1
  52. package/rust/core/lexer/handler/parenthesis.rs +8 -8
  53. package/rust/core/lexer/handler/slash.rs +5 -5
  54. package/rust/core/lexer/handler/string.rs +1 -1
  55. package/rust/core/lexer/mod.rs +1 -1
  56. package/rust/core/lexer/token.rs +3 -0
  57. package/rust/core/parser/driver.rs +94 -12
  58. package/rust/core/parser/handler/arrow_call.rs +105 -89
  59. package/rust/core/parser/handler/at.rs +1 -1
  60. package/rust/core/parser/handler/dot.rs +3 -3
  61. package/rust/core/parser/handler/identifier/automate.rs +194 -0
  62. package/rust/core/parser/handler/identifier/function.rs +2 -3
  63. package/rust/core/parser/handler/identifier/let_.rs +16 -0
  64. package/rust/core/parser/handler/identifier/mod.rs +14 -10
  65. package/rust/core/parser/handler/identifier/print.rs +29 -0
  66. package/rust/core/parser/handler/identifier/sleep.rs +1 -1
  67. package/rust/core/parser/handler/identifier/synth.rs +7 -9
  68. package/rust/core/parser/handler/loop_.rs +60 -43
  69. package/rust/core/parser/statement.rs +5 -0
  70. package/rust/core/preprocessor/loader.rs +1 -1
  71. package/rust/core/preprocessor/processor.rs +4 -4
  72. package/rust/core/preprocessor/resolver/bank.rs +1 -2
  73. package/rust/core/preprocessor/resolver/call.rs +19 -18
  74. package/rust/core/preprocessor/resolver/driver.rs +7 -5
  75. package/rust/core/preprocessor/resolver/function.rs +3 -13
  76. package/rust/core/preprocessor/resolver/loop_.rs +31 -1
  77. package/rust/core/preprocessor/resolver/spawn.rs +3 -22
  78. package/rust/core/preprocessor/resolver/tempo.rs +1 -1
  79. package/rust/core/preprocessor/resolver/trigger.rs +2 -3
  80. package/rust/core/preprocessor/resolver/value.rs +6 -12
  81. package/rust/core/shared/bank.rs +1 -1
  82. package/rust/core/utils/path.rs +1 -1
  83. package/rust/core/utils/validation.rs +0 -1
  84. package/rust/installer/bank.rs +1 -1
  85. package/rust/installer/plugin.rs +2 -2
  86. package/rust/main.rs +0 -1
  87. package/rust/utils/error.rs +51 -0
  88. package/rust/utils/logger.rs +4 -0
  89. package/rust/utils/mod.rs +1 -44
  90. package/rust/utils/spinner.rs +1 -1
@@ -1,13 +1,11 @@
1
1
  use rayon::prelude::*;
2
- use std::sync::{ Arc, Mutex };
3
2
 
4
- use crate::core::{
3
+ use crate::{core::{
5
4
  audio::{
6
5
  engine::AudioEngine,
7
6
  interpreter::{
8
7
  arrow_call::interprete_call_arrow_statement,
9
8
  call::interprete_call_statement,
10
- condition::interprete_condition_statement,
11
9
  function::interprete_function_statement,
12
10
  let_::interprete_let_statement,
13
11
  load::interprete_load_statement,
@@ -17,29 +15,27 @@ use crate::core::{
17
15
  tempo::interprete_tempo_statement,
18
16
  trigger::interprete_trigger_statement,
19
17
  },
20
- },
21
- parser::statement::{ Statement, StatementKind },
22
- store::{ function::FunctionTable, global::GlobalStore, variable::VariableTable },
23
- };
18
+ }, parser::statement::{ Statement, StatementKind }, shared::value::Value, store::{ function::FunctionTable, global::GlobalStore, variable::VariableTable }
19
+ }, utils::logger::{LogLevel, Logger}};
24
20
 
25
21
  pub fn run_audio_program(
26
22
  statements: &Vec<Statement>,
27
23
  audio_engine: &mut AudioEngine,
28
- entry: String,
29
- output: String,
30
- mut module_variables: VariableTable,
31
- mut module_functions: FunctionTable,
24
+ _entry: String,
25
+ _output: String,
26
+ _module_variables: VariableTable,
27
+ _module_functions: FunctionTable,
32
28
  global_store: &mut GlobalStore
33
29
  ) -> (f32, f32) {
34
- let mut base_bpm = 120.0;
35
- let mut base_duration = 60.0 / base_bpm;
30
+ let base_bpm = 120.0;
31
+ let base_duration = 60.0 / base_bpm;
36
32
 
37
33
  let (max_end_time, cursor_time) = execute_audio_block(
38
34
  audio_engine,
39
35
  global_store,
40
36
  global_store.variables.clone(),
41
37
  global_store.functions.clone(),
42
- statements.clone(),
38
+ &statements,
43
39
  base_bpm,
44
40
  base_duration,
45
41
  0.0,
@@ -54,14 +50,14 @@ pub fn execute_audio_block(
54
50
  global_store: &GlobalStore,
55
51
  mut variable_table: VariableTable,
56
52
  mut functions_table: FunctionTable,
57
- statements: Vec<Statement>,
53
+ statements: &[Statement],
58
54
  mut base_bpm: f32,
59
55
  mut base_duration: f32,
60
56
  mut max_end_time: f32,
61
57
  mut cursor_time: f32
62
58
  ) -> (f32, f32) {
63
59
  let (spawns, others): (Vec<_>, Vec<_>) = statements
64
- .into_iter()
60
+ .iter()
65
61
  .partition(|stmt| matches!(stmt.kind, StatementKind::Spawn { .. }));
66
62
 
67
63
  // Execute sequential statements first
@@ -162,6 +158,43 @@ pub fn execute_audio_block(
162
158
  max_end_time = new_max;
163
159
  }
164
160
  }
161
+ StatementKind::Automate { .. } => {
162
+ if let Some(new_table) = crate::core::audio::interpreter::automate::interprete_automate_statement(&stmt, &mut variable_table) {
163
+ variable_table = new_table;
164
+ }
165
+ }
166
+ StatementKind::Print => {
167
+ // Print debug output; if the string contains special expressions, evaluate them.
168
+ let logger = Logger::new();
169
+ match &stmt.value {
170
+ Value::String(s) => {
171
+ let bpm = if let Some(Value::Number(n)) = variable_table.get("bpm") { *n } else { 120.0 };
172
+ let beat = if let Some(Value::Number(n)) = variable_table.get("beat") { *n } else { 0.0 };
173
+ // If the string is exactly a variable name, print its value
174
+ if let Some(val) = variable_table.get(&s) {
175
+ logger.log_message(LogLevel::Print, &format!("{:?}", val));
176
+ } else if s.contains("$env") || s.contains("$math") || s.parse::<f32>().is_ok() {
177
+ let v = crate::core::audio::evaluator::evaluate_rhs_into_value(s, &variable_table, bpm, beat);
178
+ match v { Value::Number(n) => logger.log_message(LogLevel::Print, &format!("{}", n)), _ => logger.log_message(LogLevel::Print, s) }
179
+ } else {
180
+ logger.log_message(LogLevel::Print, s)
181
+ }
182
+ }
183
+ Value::Identifier(name) => {
184
+ if let Some(val) = variable_table.get(name) {
185
+ match val {
186
+ Value::Number(n) => logger.log_message(LogLevel::Print, &format!("{}", n)),
187
+ Value::String(s) => logger.log_message(LogLevel::Print, s),
188
+ Value::Boolean(b) => logger.log_message(LogLevel::Print, &format!("{}", b)),
189
+ other => logger.log_message(LogLevel::Print, &format!("{:?}", other)),
190
+ }
191
+ } else {
192
+ logger.log_message(LogLevel::Print, name)
193
+ }
194
+ }
195
+ v => logger.log_message(LogLevel::Print, &format!("{:?}", v)),
196
+ }
197
+ }
165
198
  _ => {}
166
199
  }
167
200
  }
@@ -1,16 +1,23 @@
1
- use crate::core::{
2
- audio::engine::AudioEngine,
3
- parser::statement::{ Statement, StatementKind },
4
- shared::value::Value,
5
- store::variable::VariableTable,
6
- };
1
+ use crate::core::{ parser::statement::{ Statement, StatementKind }, shared::value::Value, store::variable::VariableTable };
7
2
 
8
3
  pub fn interprete_let_statement(
9
4
  stmt: &Statement,
10
5
  variable_table: &mut VariableTable
11
6
  ) -> Option<VariableTable> {
12
7
  if let StatementKind::Let { name } = &stmt.kind {
13
- variable_table.set(name.to_string(), stmt.value.clone());
8
+ // If RHS is a string and looks like an expression, evaluate it
9
+ let evaluated = match &stmt.value {
10
+ Value::String(s) if s.contains("$env") || s.contains("$math") => {
11
+ // We don't have direct env here; use defaults or infer from table
12
+ let bpm = if let Some(Value::Number(n)) = variable_table.get("bpm") { *n } else { 120.0 };
13
+ // Try to infer beat from time-based variables if any, else 0.0
14
+ let beat = if let Some(Value::Number(n)) = variable_table.get("beat") { *n } else { 0.0 };
15
+ crate::core::audio::evaluator::evaluate_rhs_into_value(s, variable_table, bpm, beat)
16
+ }
17
+ other => other.clone(),
18
+ };
19
+
20
+ variable_table.set(name.to_string(), evaluated);
14
21
 
15
22
  return Some(variable_table.clone())
16
23
  }
@@ -1,7 +1,7 @@
1
1
  use crate::core::{
2
2
  audio::{ engine::AudioEngine, interpreter::driver::execute_audio_block },
3
- parser::statement::{ Statement, StatementKind },
4
- shared::{ duration::Duration, value::Value },
3
+ parser::statement::Statement,
4
+ shared::value::Value,
5
5
  store::{function::FunctionTable, global::GlobalStore, variable::VariableTable},
6
6
  };
7
7
 
@@ -17,6 +17,39 @@ pub fn interprete_loop_statement(
17
17
  cursor_time: f32
18
18
  ) -> (f32, f32) {
19
19
  if let Value::Map(loop_value) = &stmt.value {
20
+ // Foreach form: { foreach: Identifier(name), array: Array([...]), body: Block }
21
+ if let (Some(Value::Identifier(var_name)), Some(Value::Array(items)), Some(Value::Block(loop_body))) = (
22
+ loop_value.get("foreach"),
23
+ loop_value.get("array"),
24
+ loop_value.get("body"),
25
+ ) {
26
+ let mut engine = audio_engine;
27
+ let mut cur_time = cursor_time;
28
+ let mut max_time = max_end_time;
29
+
30
+ for item in items {
31
+ let mut scoped_vars = variable_table.clone();
32
+ scoped_vars.set(var_name.clone(), item.clone());
33
+
34
+ let (block_end_time, new_cursor) = execute_audio_block(
35
+ &mut engine,
36
+ global_store,
37
+ scoped_vars,
38
+ functions_table.clone(),
39
+ &loop_body,
40
+ base_bpm,
41
+ base_duration,
42
+ max_time,
43
+ cur_time,
44
+ );
45
+
46
+ cur_time = new_cursor.max(block_end_time);
47
+ max_time = max_time.max(cur_time);
48
+ }
49
+
50
+ return (max_time, cur_time);
51
+ }
52
+
20
53
  let loop_count = match loop_value.get("iterator") {
21
54
  Some(Value::Number(n)) => *n as usize,
22
55
  Some(Value::Identifier(ident)) => {
@@ -48,20 +81,20 @@ pub fn interprete_loop_statement(
48
81
  let mut cur_time = cursor_time;
49
82
  let mut max_time = max_end_time;
50
83
 
51
- for i in 0..loop_count {
52
- let (block_end_time, cursor_time) = execute_audio_block(
84
+ for _ in 0..loop_count {
85
+ let (block_end_time, new_cursor) = execute_audio_block(
53
86
  &mut engine,
54
87
  global_store,
55
88
  variable_table.clone(),
56
89
  functions_table.clone(),
57
- loop_body.clone(),
90
+ &loop_body,
58
91
  base_bpm,
59
92
  base_duration,
60
93
  max_time,
61
94
  cur_time
62
95
  );
63
96
 
64
- cur_time = block_end_time;
97
+ cur_time = new_cursor.max(block_end_time);
65
98
  max_time = max_time.max(cur_time);
66
99
  }
67
100
 
@@ -10,4 +10,5 @@ pub mod loop_;
10
10
  pub mod call;
11
11
  pub mod condition;
12
12
  pub mod arrow_call;
13
- pub mod function;
13
+ pub mod function;
14
+ pub mod automate;
@@ -1,8 +1,6 @@
1
1
  use crate::core::{
2
- audio::{ engine::AudioEngine, loader::trigger::load_trigger },
3
- parser::statement::{ Statement, StatementKind },
4
- shared::{ duration::Duration, value::Value },
5
- store::variable::VariableTable,
2
+ parser::statement::Statement,
3
+ shared::value::Value,
6
4
  };
7
5
 
8
6
  pub fn interprete_sleep_statement(
@@ -42,7 +42,7 @@ pub fn interprete_spawn_statement(
42
42
  global_store,
43
43
  local_vars,
44
44
  functions.clone(),
45
- func.body.clone(),
45
+ &func.body,
46
46
  base_bpm,
47
47
  base_duration,
48
48
  0.0,
@@ -62,7 +62,7 @@ pub fn interprete_spawn_statement(
62
62
  global_store,
63
63
  variable_table.clone(),
64
64
  functions.clone(),
65
- body.clone(),
65
+ &body,
66
66
  base_bpm,
67
67
  base_duration,
68
68
  0.0,
@@ -7,7 +7,7 @@ use crate::core::{
7
7
  pub fn load_trigger(
8
8
  trigger: &Value,
9
9
  duration: &Duration,
10
- effects: &Option<Value>,
10
+ _effects: &Option<Value>,
11
11
  base_duration: f32,
12
12
  variable_table: VariableTable
13
13
  ) -> (String, f32) {
@@ -37,7 +37,7 @@ pub fn load_trigger(
37
37
  trigger_path = src.to_string();
38
38
  }
39
39
  Value::Statement(stmt) => {
40
- if let StatementKind::Trigger { entity, duration, effects } = &stmt.kind {
40
+ if let StatementKind::Trigger { entity, duration: _, effects: _ } = &stmt.kind {
41
41
  trigger_path = entity.clone();
42
42
  } else {
43
43
  eprintln!("❌ Trigger statement must be of type 'Trigger', found: {:?}", stmt.kind);
@@ -88,9 +88,6 @@ pub fn load_trigger(
88
88
  }
89
89
  }
90
90
 
91
- _ => {
92
- eprintln!("❌ Invalid duration type. Expected an identifier.");
93
- }
94
91
  }
95
92
 
96
93
  (trigger_path, duration_as_secs)
@@ -3,4 +3,5 @@ pub mod interpreter;
3
3
  pub mod loader;
4
4
  pub mod player;
5
5
  pub mod renderer;
6
- pub mod evaluator;
6
+ pub mod evaluator;
7
+ pub mod special;
@@ -22,7 +22,7 @@ pub fn render_audio_with_modules(
22
22
  // Apply global variables to the initial engine
23
23
  if let Some(module) = global_store.get_module(&module_name) {
24
24
  // interprete statements to fill the audio buffer
25
- let (module_max_end_time, cursor_time) = run_audio_program(
25
+ let (module_max_end_time, _cursor_time) = run_audio_program(
26
26
  &statements,
27
27
  &mut audio_engine,
28
28
  module_name.clone(),
@@ -0,0 +1,120 @@
1
+ use crate::core::store::variable::VariableTable;
2
+
3
+ // Basic easing functions operating on t in [0,1]
4
+ fn easing_value(func: &str, t: f32) -> Option<f32> {
5
+ let x = t.clamp(0.0, 1.0);
6
+ match func {
7
+ "linear" => Some(x),
8
+ "easeInQuad" => Some(x * x),
9
+ "easeOutQuad" => Some(x * (2.0 - x)),
10
+ "easeInOutQuad" => {
11
+ if x < 0.5 { Some(2.0 * x * x) } else { Some(-1.0 + (4.0 - 2.0 * x) * x) }
12
+ }
13
+ // Cubic
14
+ "easeInCubic" => Some(x * x * x),
15
+ "easeOutCubic" => Some(1.0 - (1.0 - x).powi(3)),
16
+ "easeInOutCubic" => {
17
+ if x < 0.5 { Some(4.0 * x * x * x) } else { Some(1.0 - (-2.0 * x + 2.0).powi(3) / 2.0) }
18
+ }
19
+ // Quartic
20
+ "easeInQuart" => Some(x.powi(4)),
21
+ "easeOutQuart" => Some(1.0 - (1.0 - x).powi(4)),
22
+ "easeInOutQuart" => {
23
+ if x < 0.5 { Some(8.0 * x.powi(4)) } else { Some(1.0 - (-2.0 * x + 2.0).powi(4) / 2.0) }
24
+ }
25
+ // Exponential
26
+ "easeInExpo" => Some(if x <= 0.0 { 0.0 } else { 2.0_f32.powf(10.0 * x - 10.0) }),
27
+ "easeOutExpo" => Some(if x >= 1.0 { 1.0 } else { 1.0 - 2.0_f32.powf(-10.0 * x) }),
28
+ "easeInOutExpo" => Some(if x <= 0.0 { 0.0 } else if x >= 1.0 { 1.0 } else if x < 0.5 { 2.0_f32.powf(20.0 * x - 10.0) / 2.0 } else { (2.0 - 2.0_f32.powf(-20.0 * x + 10.0)) / 2.0 }),
29
+ // Back (overshoot c ~ 1.70158)
30
+ "easeInBack" => { let c = 1.70158; Some((c + 1.0) * x * x * x - c * x * x) }
31
+ "easeOutBack" => { let c = 1.70158; let y = 1.0 - x; Some(1.0 - ((c + 1.0) * y * y * y - c * y * y)) }
32
+ "easeInOutBack" => {
33
+ let c1 = 1.70158; let c2 = c1 * 1.525; let x2 = x * 2.0;
34
+ if x2 < 1.0 { Some((x2 * x2 * ((c2 + 1.0) * x2 - c2)) / 2.0) } else {
35
+ let x2 = x2 - 2.0; Some((x2 * x2 * ((c2 + 1.0) * x2 + c2)) / 2.0 + 1.0)
36
+ }
37
+ }
38
+ // Elastic
39
+ "easeInElastic" => {
40
+ if x == 0.0 { Some(0.0) } else if x == 1.0 { Some(1.0) } else {
41
+ let c = 2.0 * std::f32::consts::PI / 3.0;
42
+ Some(- (2.0_f32.powf(10.0 * x - 10.0)) * ((x * 10.0 - 10.75) * c).sin())
43
+ }
44
+ }
45
+ "easeOutElastic" => {
46
+ if x == 0.0 { Some(0.0) } else if x == 1.0 { Some(1.0) } else {
47
+ let c = 2.0 * std::f32::consts::PI / 3.0;
48
+ Some(2.0_f32.powf(-10.0 * x) * ((x * 10.0 - 0.75) * c).sin() + 1.0)
49
+ }
50
+ }
51
+ "easeInOutElastic" => {
52
+ if x == 0.0 { Some(0.0) } else if x == 1.0 { Some(1.0) } else {
53
+ let c = 2.0 * std::f32::consts::PI / 4.5;
54
+ if x < 0.5 {
55
+ Some(-(2.0_f32.powf(20.0 * x - 10.0)) * ((20.0 * x - 11.125) * c).sin() / 2.0)
56
+ } else {
57
+ Some(2.0_f32.powf(-20.0 * x + 10.0) * ((20.0 * x - 11.125) * c).sin() / 2.0 + 1.0)
58
+ }
59
+ }
60
+ }
61
+ // Bounce helpers
62
+ "easeInBounce" => Some(1.0 - bounce_out(1.0 - x)),
63
+ "easeOutBounce" => Some(bounce_out(x)),
64
+ "easeInOutBounce" => Some(if x < 0.5 { (1.0 - bounce_out(1.0 - 2.0 * x)) / 2.0 } else { (1.0 + bounce_out(2.0 * x - 1.0)) / 2.0 }),
65
+ _ => None,
66
+ }
67
+ }
68
+
69
+ fn bounce_out(x: f32) -> f32 {
70
+ let n1 = 7.5625; let d1 = 2.75;
71
+ if x < 1.0 / d1 {
72
+ n1 * x * x
73
+ } else if x < 2.0 / d1 {
74
+ let x = x - 1.5 / d1; n1 * x * x + 0.75
75
+ } else if x < 2.5 / d1 {
76
+ let x = x - 2.25 / d1; n1 * x * x + 0.9375
77
+ } else {
78
+ let x = x - 2.625 / d1; n1 * x * x + 0.984375
79
+ }
80
+ }
81
+
82
+ // Find and evaluate the first $easing.<fn>(...) occurrence in the string.
83
+ // Accepts a single argument expression producing t in [0,1].
84
+ pub fn find_and_eval_first_easing_call<EvalFn>(
85
+ s: &str,
86
+ eval: EvalFn,
87
+ vars: &VariableTable,
88
+ bpm: f32,
89
+ beat: f32,
90
+ ) -> Option<String>
91
+ where
92
+ EvalFn: Fn(&str, &VariableTable, f32, f32) -> Option<f32>,
93
+ {
94
+ let start = s.find("$easing.")?;
95
+ let open_rel = s[start..].find('(')?;
96
+ let open = start + open_rel;
97
+ let func = &s[start + 9..open];
98
+
99
+ // Find matching close parenthesis
100
+ let mut depth: i32 = 0;
101
+ let mut close_abs: Option<usize> = None;
102
+ for (i, ch) in s[open..].char_indices() {
103
+ match ch {
104
+ '(' => depth += 1,
105
+ ')' => { depth -= 1; if depth == 0 { close_abs = Some(open + i); break; } }
106
+ _ => {}
107
+ }
108
+ }
109
+ let close = close_abs?;
110
+
111
+ let inner = &s[open + 1..close];
112
+ let t = eval(inner, vars, bpm, beat)?;
113
+ let result = easing_value(func, t)?;
114
+
115
+ let mut replaced = String::new();
116
+ replaced.push_str(&s[..start]);
117
+ replaced.push_str(&result.to_string());
118
+ replaced.push_str(&s[close + 1..]);
119
+ Some(replaced)
120
+ }
@@ -0,0 +1,41 @@
1
+ use crate::core::store::variable::VariableTable;
2
+ use std::sync::OnceLock;
3
+ use std::time::{ SystemTime, UNIX_EPOCH };
4
+
5
+ static SESSION_SEED: OnceLock<f32> = OnceLock::new();
6
+
7
+ pub fn get_session_seed() -> f32 {
8
+ *SESSION_SEED.get_or_init(|| {
9
+ let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default();
10
+ // Build a stable 0..1 seed from nanos
11
+ let nanos = now.subsec_nanos();
12
+ ((nanos as f32) / 1_000_000_000.0).clamp(0.0, 1.0)
13
+ })
14
+ }
15
+
16
+ // Resolve special environment variables like $env.bpm, $env.beat, $env.position
17
+ // For now, $env.position is treated as an alias of beat.
18
+ pub fn resolve_env_atom(atom: &str, bpm: f32, beat: f32) -> Option<f32> {
19
+ match atom {
20
+ "$env.bpm" => Some(bpm),
21
+ "$env.beat" => Some(beat),
22
+ "$env.position" => Some(beat),
23
+ // Optional seed for deterministic randomness
24
+ "$env.seed" => Some(get_session_seed()),
25
+ _ => None,
26
+ }
27
+ }
28
+
29
+ // Utility: resolve an identifier or numeric literal to f32 using the variable table
30
+ pub fn resolve_atom_or_var(atom: &str, vars: &VariableTable, bpm: f32, beat: f32) -> Option<f32> {
31
+ if let Some(v) = resolve_env_atom(atom, bpm, beat) {
32
+ return Some(v);
33
+ }
34
+ if let Ok(n) = atom.parse::<f32>() {
35
+ return Some(n);
36
+ }
37
+ if let Some(crate::core::shared::value::Value::Number(n)) = vars.get(atom) {
38
+ return Some(*n);
39
+ }
40
+ None
41
+ }
@@ -0,0 +1,92 @@
1
+ use crate::core::store::variable::VariableTable;
2
+
3
+ // Parse comma-separated arguments at top level (no nested parentheses split)
4
+ fn parse_top_level_args(s: &str) -> Vec<&str> {
5
+ let mut args = Vec::new();
6
+ let mut depth = 0i32;
7
+ let mut start = 0usize;
8
+ for (i, ch) in s.char_indices() {
9
+ match ch {
10
+ '(' => depth += 1,
11
+ ')' => depth -= 1,
12
+ ',' if depth == 0 => {
13
+ args.push(s[start..i].trim());
14
+ start = i + 1;
15
+ }
16
+ _ => {}
17
+ }
18
+ }
19
+ let last = s[start..].trim();
20
+ if !last.is_empty() { args.push(last); }
21
+ args
22
+ }
23
+
24
+ fn eval_math_func(func: &str, args: &[f32], fallback_seed: f32) -> Option<f32> {
25
+ match func {
26
+ "sin" => args.get(0).copied().map(f32::sin),
27
+ "cos" => args.get(0).copied().map(f32::cos),
28
+ "random" => {
29
+ // deterministic pseudo-random based on provided seed or a fallback session seed
30
+ let seed = args.get(0).copied().unwrap_or(fallback_seed);
31
+ let x = (seed * 12.9898).sin() * 43758.5453;
32
+ Some((x.fract() * 2.0 - 1.0).clamp(-1.0, 1.0))
33
+ }
34
+ "lerp" => {
35
+ if args.len() >= 3 { Some(args[0] + (args[1] - args[0]) * args[2]) } else { None }
36
+ }
37
+ _ => None,
38
+ }
39
+ }
40
+
41
+ // Find and evaluate the first $math.<fn>(...) occurrence in the string, replacing it with a number.
42
+ // Supports multi-argument functions by splitting on top-level commas.
43
+ pub fn find_and_eval_first_math_call<EvalFn>(
44
+ s: &str,
45
+ eval: EvalFn,
46
+ vars: &VariableTable,
47
+ bpm: f32,
48
+ beat: f32,
49
+ ) -> Option<String>
50
+ where
51
+ EvalFn: Fn(&str, &VariableTable, f32, f32) -> Option<f32>,
52
+ {
53
+ let start = s.find("$math.")?;
54
+ let open_rel = s[start..].find('(')?;
55
+ let open = start + open_rel;
56
+ let func = &s[start + 6..open];
57
+
58
+ // Find matching close parenthesis, handling nesting
59
+ let mut depth: i32 = 0;
60
+ let mut close_abs: Option<usize> = None;
61
+ for (i, ch) in s[open..].char_indices() {
62
+ match ch {
63
+ '(' => depth += 1,
64
+ ')' => {
65
+ depth -= 1;
66
+ if depth == 0 {
67
+ close_abs = Some(open + i);
68
+ break;
69
+ }
70
+ }
71
+ _ => {}
72
+ }
73
+ }
74
+ let close = close_abs?;
75
+
76
+ let inner = &s[open + 1..close];
77
+ let raw_args = parse_top_level_args(inner);
78
+ let mut args: Vec<f32> = Vec::with_capacity(raw_args.len());
79
+ for a in raw_args {
80
+ if let Some(v) = eval(a, vars, bpm, beat) { args.push(v); } else { return None; }
81
+ }
82
+
83
+ // If no explicit seed is provided, use $env.seed via fallback
84
+ let fallback_seed = eval("$env.seed", vars, bpm, beat).unwrap_or(0.0);
85
+ let result = eval_math_func(func, &args, fallback_seed)?;
86
+
87
+ let mut replaced = String::new();
88
+ replaced.push_str(&s[..start]);
89
+ replaced.push_str(&result.to_string());
90
+ replaced.push_str(&s[close + 1..]);
91
+ Some(replaced)
92
+ }
@@ -0,0 +1,9 @@
1
+ pub mod env;
2
+ pub mod math;
3
+ pub mod easing;
4
+ pub mod modulator;
5
+
6
+ pub use env::resolve_env_atom;
7
+ pub use math::find_and_eval_first_math_call;
8
+ pub use easing::find_and_eval_first_easing_call;
9
+ pub use modulator::find_and_eval_first_mod_call;