@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
@@ -102,7 +102,8 @@ impl AudioEngine {
102
102
  start_time_ms: f32,
103
103
  duration_ms: f32,
104
104
  synth_params: HashMap<String, Value>,
105
- note_params: HashMap<String, Value>
105
+ note_params: HashMap<String, Value>,
106
+ automation: Option<HashMap<String, Value>>
106
107
  ) {
107
108
  let valid_synth_params = vec!["attack", "decay", "sustain", "release"];
108
109
  let valid_note_params = vec![
@@ -114,7 +115,9 @@ impl AudioEngine {
114
115
  "target_freq",
115
116
  "target_amp",
116
117
  "modulation",
117
- "expression"
118
+ "expression",
119
+ // allow per-note automation map
120
+ "automate"
118
121
  ];
119
122
 
120
123
  // Synth params validation
@@ -131,35 +134,39 @@ impl AudioEngine {
131
134
  }
132
135
  }
133
136
 
134
- // Synth parameters
135
- let attack = self.extract_f32(&synth_params, "attack").unwrap_or(0.0);
136
- let decay = self.extract_f32(&synth_params, "decay").unwrap_or(0.0);
137
- let sustain = self.extract_f32(&synth_params, "sustain").unwrap_or(0.0);
138
- let release = self.extract_f32(&synth_params, "release").unwrap_or(0.0);
139
- let attack_s = if attack > 10.0 { attack / 1000.0 } else { attack };
140
- let decay_s = if decay > 10.0 { decay / 1000.0 } else { decay };
141
- let release_s = if release > 10.0 { release / 1000.0 } else { release };
142
- let sustain_level = if sustain > 1.0 { (sustain / 100.0).clamp(0.0, 1.0) } else { sustain.clamp(0.0, 1.0) };
137
+ // Synth parameters
138
+ let attack = self.extract_f32(&synth_params, "attack").unwrap_or(0.0);
139
+ let decay = self.extract_f32(&synth_params, "decay").unwrap_or(0.0);
140
+ let sustain = self.extract_f32(&synth_params, "sustain").unwrap_or(1.0);
141
+ let release = self.extract_f32(&synth_params, "release").unwrap_or(0.0);
142
+ let attack_s = if attack > 10.0 { attack / 1000.0 } else { attack };
143
+ let decay_s = if decay > 10.0 { decay / 1000.0 } else { decay };
144
+ let release_s = if release > 10.0 { release / 1000.0 } else { release };
145
+ let sustain_level = if sustain > 1.0 {
146
+ (sustain / 100.0).clamp(0.0, 1.0)
147
+ } else {
148
+ sustain.clamp(0.0, 1.0)
149
+ };
143
150
 
144
151
  // Note parameters
145
- let duration_ms = self.extract_f32(&note_params, "duration").unwrap_or(duration_ms);
146
- let velocity = self.extract_f32(&note_params, "velocity").unwrap_or(1.0);
152
+ let duration_ms = self.extract_f32(&note_params, "duration").unwrap_or(duration_ms);
153
+ let velocity = self.extract_f32(&note_params, "velocity").unwrap_or(1.0);
147
154
  let glide = self.extract_boolean(&note_params, "glide").unwrap_or(false);
148
155
  let slide = self.extract_boolean(&note_params, "slide").unwrap_or(false);
149
156
 
150
157
  let _amplitude = (i16::MAX as f32) * amp.clamp(0.0, 1.0) * velocity.clamp(0.0, 1.0);
151
158
 
152
159
  // Logic for glide and slide
153
- let freq_start = freq;
160
+ let freq_start = freq;
154
161
  let mut freq_end = freq;
155
- let amp_start = amp * velocity.clamp(0.0, 1.0);
162
+ let amp_start = amp * velocity.clamp(0.0, 1.0);
156
163
  let mut amp_end = amp_start;
157
164
 
158
165
  if glide {
159
166
  if let Some(Value::Number(target_freq)) = note_params.get("target_freq") {
160
167
  freq_end = *target_freq;
161
168
  } else {
162
- freq_end = freq * 1.5; // Par défaut, glide vers une quinte
169
+ freq_end = freq * 1.5; // By default, glide to a perfect fifth
163
170
  }
164
171
  }
165
172
 
@@ -167,22 +174,24 @@ impl AudioEngine {
167
174
  if let Some(Value::Number(target_amp)) = note_params.get("target_amp") {
168
175
  amp_end = *target_amp * velocity.clamp(0.0, 1.0);
169
176
  } else {
170
- amp_end = amp_start * 0.5; // Par défaut, slide vers la moitié
177
+ amp_end = amp_start * 0.5; // By default, slide to half the amplitude
171
178
  }
172
179
  }
173
-
174
180
  let sample_rate = SAMPLE_RATE as f32;
175
181
  let channels = CHANNELS as usize;
176
182
 
177
183
  let total_samples = ((duration_ms / 1000.0) * sample_rate) as usize;
178
184
  let start_sample = ((start_time_ms / 1000.0) * sample_rate) as usize;
179
185
 
180
- let mut samples = Vec::with_capacity(total_samples);
181
- let fade_len = (sample_rate * 0.01) as usize; // 10 ms fade
186
+ // Precompute automation envelopes
187
+ let (volume_env, pan_env, pitch_env) = Self::env_maps_from_automation(&automation);
182
188
 
183
- let attack_samples = (attack_s * sample_rate) as usize;
184
- let decay_samples = (decay_s * sample_rate) as usize;
185
- let release_samples = (release_s * sample_rate) as usize;
189
+ let mut stereo_samples: Vec<i16> = Vec::with_capacity(total_samples * 2);
190
+ let fade_len = (sample_rate * 0.01) as usize; // 10 ms fade
191
+
192
+ let attack_samples = (attack_s * sample_rate) as usize;
193
+ let decay_samples = (decay_s * sample_rate) as usize;
194
+ let release_samples = (release_s * sample_rate) as usize;
186
195
  let sustain_samples = if total_samples > attack_samples + decay_samples + release_samples {
187
196
  total_samples - attack_samples - decay_samples - release_samples
188
197
  } else {
@@ -199,6 +208,10 @@ impl AudioEngine {
199
208
  freq
200
209
  };
201
210
 
211
+ // Pitch automation (in semitones), applied as frequency multiplier
212
+ let pitch_semi = Self::eval_env_map(&pitch_env, (i as f32) / (total_samples as f32), 0.0);
213
+ let current_freq = current_freq * (2.0_f32).powf(pitch_semi / 12.0);
214
+
202
215
  // Slide
203
216
  let current_amp = if slide {
204
217
  amp_start + ((amp_end - amp_start) * (i as f32)) / (total_samples as f32)
@@ -206,34 +219,17 @@ impl AudioEngine {
206
219
  amp_start
207
220
  };
208
221
 
209
- let phase = 2.0 * std::f32::consts::PI * current_freq * t;
210
-
211
- let mut value = match waveform.as_str() {
212
- "sine" => phase.sin(),
213
- "square" => if phase.sin() >= 0.0 { 1.0 } else { -1.0 },
214
- "saw" => 2.0 * (current_freq * t - (current_freq * t + 0.5).floor()),
215
- "triangle" => (2.0 * (2.0 * (current_freq * t).fract() - 1.0)).abs() * 2.0 - 1.0,
216
- _ => 0.0,
217
- };
222
+ let mut value = Self::oscillator_sample(&waveform, current_freq, t);
218
223
 
219
224
  // ADSR envelope
220
- let envelope = if i < attack_samples {
221
- (i as f32) / (attack_samples as f32)
222
- } else if i < attack_samples + decay_samples {
223
- 1.0 -
224
- (1.0 - sustain_level) * (((i - attack_samples) as f32) / (decay_samples as f32))
225
- } else if i < attack_samples + decay_samples + sustain_samples {
226
- sustain_level
227
- } else {
228
- if release_samples > 0 {
229
- sustain_level *
230
- (1.0 -
231
- ((i - attack_samples - decay_samples - sustain_samples) as f32) /
232
- (release_samples as f32))
233
- } else {
234
- 0.0
235
- }
236
- };
225
+ let envelope = Self::adsr_envelope_value(
226
+ i,
227
+ attack_samples,
228
+ decay_samples,
229
+ sustain_samples,
230
+ release_samples,
231
+ sustain_level,
232
+ );
237
233
 
238
234
  // Fade in/out
239
235
  if i < fade_len {
@@ -243,29 +239,31 @@ impl AudioEngine {
243
239
  }
244
240
 
245
241
  value *= envelope;
246
- // Application de l'amplitude dynamique (slide + velocity)
247
- samples.push((value * (i16::MAX as f32) * current_amp) as i16);
248
- }
249
-
250
- // Convert to stereo
251
- let stereo_samples: Vec<i16> = samples
252
- .iter()
253
- .flat_map(|s| vec![*s, *s])
254
- .collect();
255
-
256
- let offset = start_sample * channels;
257
- let required_len = offset + stereo_samples.len();
258
-
259
- if self.buffer.len() < required_len {
260
- self.buffer.resize(required_len, 0);
242
+ // Apply dynamic amplitude (slide + velocity)
243
+ let mut sample_val = value * (i16::MAX as f32) * current_amp;
244
+
245
+ // Volume automation multiplier
246
+ let vol_mul = Self::eval_env_map(&volume_env, (i as f32) / (total_samples as f32), 1.0)
247
+ .clamp(0.0, 10.0);
248
+ sample_val *= vol_mul;
249
+
250
+ // Pan automation [-1..1]; consistency with pad_samples method
251
+ let pan_val = Self::eval_env_map(&pan_env, (i as f32) / (total_samples as f32), 0.0)
252
+ .clamp(-1.0, 1.0);
253
+ let (left_gain, right_gain) = Self::pan_gains(pan_val);
254
+
255
+ let left = (sample_val * left_gain)
256
+ .round()
257
+ .clamp(i16::MIN as f32, i16::MAX as f32) as i16;
258
+ let right = (sample_val * right_gain)
259
+ .round()
260
+ .clamp(i16::MIN as f32, i16::MAX as f32) as i16;
261
+
262
+ stereo_samples.push(left);
263
+ stereo_samples.push(right);
261
264
  }
262
265
 
263
- for (i, sample) in stereo_samples.iter().enumerate() {
264
- // Debug: note si on rencontre des samples non nuls
265
- // (pour traquer les buffers silencieux)
266
- // if i == 0 { eprintln!("[debug] first stereo sample: {}", sample); }
267
- self.buffer[offset + i] = self.buffer[offset + i].saturating_add(*sample);
268
- }
266
+ self.mix_stereo_samples_into_buffer(start_sample, channels, &stereo_samples);
269
267
  }
270
268
 
271
269
  pub fn insert_sample(
@@ -282,8 +280,8 @@ impl AudioEngine {
282
280
  }
283
281
 
284
282
  let root = Path::new(env!("CARGO_MANIFEST_DIR"));
285
- let module_root = Path::new(&self.module_name);
286
- let resolved_path: String;
283
+ let module_root = Path::new(&self.module_name);
284
+ let resolved_path: String;
287
285
 
288
286
  // Get the variable path from the variable table
289
287
  let mut var_path = filepath.to_string();
@@ -477,8 +475,7 @@ impl AudioEngine {
477
475
  let adjusted_sample = adjusted.round().clamp(i16::MIN as f32, i16::MAX as f32) as i16;
478
476
 
479
477
  // PAN
480
- let left_gain = 1.0 - pan.max(0.0); // Pan > 0 => reduce left
481
- let right_gain = 1.0 + pan.min(0.0); // Pan < 0 => reduce right
478
+ let (left_gain, right_gain) = Self::pan_gains(pan);
482
479
 
483
480
  let left = ((adjusted_sample as f32) * left_gain) as i16;
484
481
  let right = ((adjusted_sample as f32) * right_gain) as i16;
@@ -493,6 +490,150 @@ impl AudioEngine {
493
490
  }
494
491
  }
495
492
 
493
+ // ===== Helper methods to keep long functions modular and readable =====
494
+
495
+ fn env_maps_from_automation(
496
+ automation: &Option<HashMap<String, Value>>
497
+ ) -> (
498
+ Option<HashMap<String, Value>>,
499
+ Option<HashMap<String, Value>>,
500
+ Option<HashMap<String, Value>>,
501
+ ) {
502
+ if let Some(auto) = automation {
503
+ let vol = match auto.get("volume") {
504
+ Some(Value::Map(m)) => Some(m.clone()),
505
+ _ => None,
506
+ };
507
+ let pan = match auto.get("pan") {
508
+ Some(Value::Map(m)) => Some(m.clone()),
509
+ _ => None,
510
+ };
511
+ let pit = match auto.get("pitch") {
512
+ Some(Value::Map(m)) => Some(m.clone()),
513
+ _ => None,
514
+ };
515
+ (vol, pan, pit)
516
+ } else {
517
+ (None, None, None)
518
+ }
519
+ }
520
+
521
+ // Evaluate envelope map at progress [0,1]
522
+ fn eval_env_map(
523
+ env_opt: &Option<HashMap<String, Value>>,
524
+ progress: f32,
525
+ default_val: f32,
526
+ ) -> f32 {
527
+ let env = match env_opt {
528
+ Some(m) => m,
529
+ None => {
530
+ return default_val;
531
+ }
532
+ };
533
+ let mut points: Vec<(f32, f32)> = Vec::with_capacity(env.len());
534
+ for (k, v) in env.iter() {
535
+ // accept keys like "0" or "0%"
536
+ let key = if k.ends_with('%') { &k[..k.len() - 1] } else { &k[..] };
537
+ if let Ok(mut p) = key.parse::<f32>() {
538
+ p = (p / 100.0).clamp(0.0, 1.0);
539
+ let val = match v {
540
+ Value::Number(n) => *n,
541
+ Value::String(s) => s.parse::<f32>().unwrap_or(default_val),
542
+ Value::Identifier(s) => s.parse::<f32>().unwrap_or(default_val),
543
+ _ => default_val,
544
+ };
545
+ points.push((p, val));
546
+ }
547
+ }
548
+ if points.is_empty() {
549
+ return default_val;
550
+ }
551
+ points.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
552
+ let t = progress.clamp(0.0, 1.0);
553
+ if t <= points[0].0 {
554
+ return points[0].1;
555
+ }
556
+ if t >= points[points.len() - 1].0 {
557
+ return points[points.len() - 1].1;
558
+ }
559
+ for w in points.windows(2) {
560
+ let (p0, v0) = w[0];
561
+ let (p1, v1) = w[1];
562
+ if t >= p0 && t <= p1 {
563
+ let ratio = if (p1 - p0).abs() < std::f32::EPSILON {
564
+ 0.0
565
+ } else {
566
+ (t - p0) / (p1 - p0)
567
+ };
568
+ return v0 + (v1 - v0) * ratio;
569
+ }
570
+ }
571
+ default_val
572
+ }
573
+
574
+ fn oscillator_sample(waveform: &str, current_freq: f32, t: f32) -> f32 {
575
+ let phase = 2.0 * std::f32::consts::PI * current_freq * t;
576
+ match waveform {
577
+ "sine" => phase.sin(),
578
+ "square" => {
579
+ if phase.sin() >= 0.0 { 1.0 } else { -1.0 }
580
+ }
581
+ "saw" => 2.0 * (current_freq * t - (current_freq * t + 0.5).floor()),
582
+ "triangle" => (2.0 * (2.0 * (current_freq * t).fract() - 1.0)).abs() * 2.0 - 1.0,
583
+ _ => 0.0,
584
+ }
585
+ }
586
+
587
+ fn adsr_envelope_value(
588
+ i: usize,
589
+ attack_samples: usize,
590
+ decay_samples: usize,
591
+ sustain_samples: usize,
592
+ release_samples: usize,
593
+ sustain_level: f32,
594
+ ) -> f32 {
595
+ if i < attack_samples {
596
+ (i as f32) / (attack_samples as f32)
597
+ } else if i < attack_samples + decay_samples {
598
+ 1.0 - (1.0 - sustain_level) * (((i - attack_samples) as f32) / (decay_samples as f32))
599
+ } else if i < attack_samples + decay_samples + sustain_samples {
600
+ sustain_level
601
+ } else if release_samples > 0 {
602
+ sustain_level
603
+ * (1.0
604
+ - ((i - attack_samples - decay_samples - sustain_samples) as f32)
605
+ / (release_samples as f32))
606
+ } else {
607
+ 0.0
608
+ }
609
+ }
610
+
611
+ fn pan_gains(pan_val: f32) -> (f32, f32) {
612
+ let left_gain = 1.0 - pan_val.max(0.0);
613
+ let right_gain = 1.0 + pan_val.min(0.0);
614
+ (left_gain, right_gain)
615
+ }
616
+
617
+ fn mix_stereo_samples_into_buffer(
618
+ &mut self,
619
+ start_sample: usize,
620
+ channels: usize,
621
+ stereo_samples: &[i16],
622
+ ) {
623
+ let offset = start_sample * channels;
624
+ let required_len = offset + stereo_samples.len();
625
+
626
+ if self.buffer.len() < required_len {
627
+ self.buffer.resize(required_len, 0);
628
+ }
629
+
630
+ for (i, sample) in stereo_samples.iter().enumerate() {
631
+ // Debug: track if we hit non-zero samples (to trace silent buffers)
632
+ // if i == 0 { eprintln!("[debug] first stereo sample: {}", sample); }
633
+ self.buffer[offset + i] = self.buffer[offset + i].saturating_add(*sample);
634
+ }
635
+ }
636
+
496
637
  fn extract_f32(&self, map: &HashMap<String, Value>, key: &str) -> Option<f32> {
497
638
  match map.get(key) {
498
639
  Some(Value::Number(n)) => Some(*n),
@@ -1,4 +1,10 @@
1
1
  use crate::core::{ shared::value::Value, store::variable::VariableTable };
2
+ use crate::core::audio::special::{
3
+ resolve_env_atom,
4
+ find_and_eval_first_math_call,
5
+ find_and_eval_first_easing_call,
6
+ find_and_eval_first_mod_call,
7
+ };
2
8
 
3
9
  pub fn evaluate_condition_string(expr: &str, vars: &VariableTable) -> bool {
4
10
  let tokens: Vec<&str> = expr.split_whitespace().collect();
@@ -29,3 +35,98 @@ pub fn evaluate_condition_string(expr: &str, vars: &VariableTable) -> bool {
29
35
  _ => false,
30
36
  }
31
37
  }
38
+
39
+ // Very small expression evaluator for `$env.*`, `$math.*` and variables.
40
+ // Supports: +, -, *, / and simple parentheses, left-to-right (no precedence), and $math.(sin|cos)(expr)
41
+ pub fn evaluate_numeric_expression(expr: &str, vars: &VariableTable, env_bpm: f32, env_beat: f32) -> Option<f32> {
42
+ let expr = expr.replace(" ", "");
43
+
44
+ // Helper to resolve an atom to a number
45
+ fn resolve_atom(atom: &str, vars: &VariableTable, bpm: f32, beat: f32) -> Option<f32> {
46
+ if let Some(v) = resolve_env_atom(atom, bpm, beat) { return Some(v); }
47
+ if let Ok(n) = atom.parse::<f32>() { return Some(n); }
48
+ if let Some(Value::Number(n)) = vars.get(atom) { return Some(*n); }
49
+ None
50
+ }
51
+
52
+ // Shunting-like, simplified: first evaluate any $math.func(...) calls anywhere in the expression,
53
+ // then fold remaining parentheses and evaluate left-to-right.
54
+ fn eval(expr: &str, vars: &VariableTable, bpm: f32, beat: f32) -> Option<f32> {
55
+ // 1) Replace $math.* calls progressively
56
+ let mut s = expr.to_string();
57
+ // Evaluate modulators first (they may feed easing/math)
58
+ while let Some(next) = find_and_eval_first_mod_call(&s, evaluate_numeric_expression, vars, bpm, beat) { s = next; }
59
+ // Then easing functions
60
+ while let Some(next) = find_and_eval_first_easing_call(&s, evaluate_numeric_expression, vars, bpm, beat) { s = next; }
61
+ // Finally math transforms
62
+ while let Some(next) = find_and_eval_first_math_call(&s, evaluate_numeric_expression, vars, bpm, beat) { s = next; }
63
+
64
+ // 2) Evaluate remaining (pure) parentheses starting from innermost
65
+ if let Some(open) = s.rfind('(') {
66
+ if let Some(close_rel) = s[open..].find(')') { // index relatif
67
+ let close = open + close_rel;
68
+ let inner = &s[open + 1..close];
69
+ let val = eval(inner, vars, bpm, beat)?;
70
+ let mut replaced = String::new();
71
+ replaced.push_str(&s[..open]);
72
+ replaced.push_str(&val.to_string());
73
+ replaced.push_str(&s[close + 1..]);
74
+ return eval(&replaced, vars, bpm, beat);
75
+ }
76
+ }
77
+
78
+ // Tokenize by operators left-to-right
79
+ let mut parts: Vec<String> = Vec::new();
80
+ let mut cur = String::new();
81
+ for ch in s.chars() {
82
+ if "+-*/".contains(ch) {
83
+ if !cur.is_empty() { parts.push(cur.clone()); cur.clear(); }
84
+ parts.push(ch.to_string());
85
+ } else {
86
+ cur.push(ch);
87
+ }
88
+ }
89
+ if !cur.is_empty() { parts.push(cur); }
90
+ if parts.is_empty() { return None; }
91
+
92
+ // Resolve atoms and compute
93
+ let mut acc: Option<f32> = None;
94
+ let mut op: Option<char> = None;
95
+ for part in parts {
96
+ if part.len() == 1 && "+-*/".contains(part.chars().next().unwrap()) {
97
+ op = part.chars().next();
98
+ continue;
99
+ }
100
+ let val = if let Some(v) = resolve_atom(&part, vars, bpm, beat) {
101
+ v
102
+ } else if part.starts_with("$env.") {
103
+ // $env atom not handled by resolve_atom (when composed), try recursive eval
104
+ eval(&part, vars, bpm, beat)?
105
+ } else {
106
+ return None;
107
+ };
108
+
109
+ acc = Some(match (acc, op) {
110
+ (None, _) => val,
111
+ (Some(a), Some('+')) => a + val,
112
+ (Some(a), Some('-')) => a - val,
113
+ (Some(a), Some('*')) => a * val,
114
+ (Some(a), Some('/')) => if val != 0.0 { a / val } else { return Some(f32::INFINITY); },
115
+ (Some(_), None) => val,
116
+ _ => return None,
117
+ });
118
+ }
119
+
120
+ acc
121
+ }
122
+
123
+ eval(&expr, vars, env_bpm, env_beat)
124
+ }
125
+
126
+ pub fn evaluate_rhs_into_value(raw: &str, vars: &VariableTable, env_bpm: f32, env_beat: f32) -> Value {
127
+ if let Some(num) = evaluate_numeric_expression(raw, vars, env_bpm, env_beat) {
128
+ Value::Number(num)
129
+ } else {
130
+ Value::String(raw.to_string())
131
+ }
132
+ }
@@ -95,6 +95,31 @@ pub fn interprete_call_arrow_statement(
95
95
  let start_time = cursor_copy;
96
96
  let end_time = start_time + duration_secs;
97
97
 
98
+ // Fetch automation map if present:
99
+ // - Global (per-synth): key "<target>__automation" => map with key "params"
100
+ // - Per-note: note parameter "automate" => map
101
+ let auto_key = format!("{}__automation", target);
102
+ let synth_automation = match variable_table.get(&auto_key) {
103
+ Some(Value::Map(map)) => match map.get("params") {
104
+ Some(Value::Map(p)) => Some(p.clone()),
105
+ _ => None,
106
+ },
107
+ _ => None,
108
+ };
109
+
110
+ let note_automation = match note_params.get("automate") {
111
+ Some(Value::Map(m)) => Some(m.clone()),
112
+ _ => None,
113
+ };
114
+
115
+ // Merge: per-note overrides synth automation per key (volume/pan/pitch)
116
+ let automation = match (synth_automation, note_automation) {
117
+ (Some(mut a), Some(n)) => { for (k, v) in n { a.insert(k, v); } Some(a) },
118
+ (None, Some(n)) => Some(n),
119
+ (Some(a), None) => Some(a),
120
+ _ => None,
121
+ };
122
+
98
123
  audio_engine.insert_note(
99
124
  waveform.clone(),
100
125
  final_freq,
@@ -102,7 +127,8 @@ pub fn interprete_call_arrow_statement(
102
127
  start_time * 1000.0,
103
128
  duration_ms,
104
129
  synth_params,
105
- note_params
130
+ note_params,
131
+ automation
106
132
  );
107
133
 
108
134
  *max_end_time = (*max_end_time).max(end_time);
@@ -0,0 +1,18 @@
1
+ use crate::core::{
2
+ parser::statement::{Statement, StatementKind},
3
+ store::variable::VariableTable,
4
+ };
5
+
6
+ // Store automation configuration into the variable table under a namespaced key
7
+ // Key: "<target>__automation" => Value::Map({ target, params })
8
+ pub fn interprete_automate_statement(
9
+ stmt: &Statement,
10
+ variable_table: &mut VariableTable,
11
+ ) -> Option<VariableTable> {
12
+ if let StatementKind::Automate { target } = &stmt.kind {
13
+ let key = format!("{}__automation", target);
14
+ variable_table.set(key, stmt.value.clone());
15
+ return Some(variable_table.clone());
16
+ }
17
+ None
18
+ }
@@ -40,7 +40,7 @@ pub fn interprete_call_statement(
40
40
  global_store,
41
41
  local_vars,
42
42
  functions.clone(),
43
- func.body.clone(),
43
+ &func.body,
44
44
  base_bpm,
45
45
  base_duration,
46
46
  max_end_time,
@@ -57,7 +57,7 @@ pub fn interprete_call_statement(
57
57
  global_store,
58
58
  variable_table.clone(),
59
59
  functions.clone(),
60
- body.clone(),
60
+ &body,
61
61
  base_bpm,
62
62
  base_duration,
63
63
  max_end_time,
@@ -20,8 +20,8 @@ pub fn interprete_condition_statement(
20
20
  max_end_time: f32,
21
21
  cursor_time: f32
22
22
  ) -> (f32, f32) {
23
- let mut cur_time = cursor_time;
24
- let mut max_time = max_end_time;
23
+ let cur_time = cursor_time;
24
+ let max_time = max_end_time;
25
25
 
26
26
  let mut current = stmt.value.clone();
27
27
 
@@ -44,7 +44,7 @@ pub fn interprete_condition_statement(
44
44
  global_store,
45
45
  variable_table.clone(),
46
46
  functions_table.clone(),
47
- block.clone(),
47
+ &block,
48
48
  base_bpm,
49
49
  base_duration,
50
50
  max_time,