@devaloop/devalang 0.0.1-alpha.13 → 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 (110) hide show
  1. package/.devalang +2 -3
  2. package/Cargo.toml +58 -54
  3. package/README.md +59 -27
  4. package/docs/CHANGELOG.md +99 -2
  5. package/docs/CONTRIBUTING.md +1 -0
  6. package/docs/ROADMAP.md +3 -3
  7. package/docs/TODO.md +5 -4
  8. package/examples/automation.deva +44 -0
  9. package/examples/bank.deva +2 -4
  10. package/examples/function.deva +15 -0
  11. package/examples/index.deva +41 -11
  12. package/examples/plugin.deva +15 -0
  13. package/out-tsc/bin/devalang.exe +0 -0
  14. package/package.json +6 -6
  15. package/project-version.json +3 -3
  16. package/rust/cli/bank.rs +16 -16
  17. package/rust/cli/build.rs +69 -30
  18. package/rust/cli/check.rs +46 -6
  19. package/rust/cli/driver.rs +40 -28
  20. package/rust/cli/install.rs +22 -7
  21. package/rust/cli/login.rs +134 -0
  22. package/rust/cli/mod.rs +2 -1
  23. package/rust/cli/play.rs +44 -19
  24. package/rust/cli/update.rs +1 -1
  25. package/rust/common/api.rs +5 -0
  26. package/rust/common/cdn.rs +3 -9
  27. package/rust/common/mod.rs +3 -1
  28. package/rust/common/sso.rs +5 -0
  29. package/rust/config/driver.rs +19 -1
  30. package/rust/config/loader.rs +56 -10
  31. package/rust/core/audio/engine.rs +314 -63
  32. package/rust/core/audio/evaluator.rs +101 -0
  33. package/rust/core/audio/interpreter/arrow_call.rs +60 -15
  34. package/rust/core/audio/interpreter/automate.rs +18 -0
  35. package/rust/core/audio/interpreter/call.rs +4 -4
  36. package/rust/core/audio/interpreter/condition.rs +3 -3
  37. package/rust/core/audio/interpreter/driver.rs +68 -30
  38. package/rust/core/audio/interpreter/let_.rs +14 -7
  39. package/rust/core/audio/interpreter/loop_.rs +39 -6
  40. package/rust/core/audio/interpreter/mod.rs +2 -1
  41. package/rust/core/audio/interpreter/sleep.rs +2 -4
  42. package/rust/core/audio/interpreter/spawn.rs +4 -4
  43. package/rust/core/audio/loader/trigger.rs +2 -5
  44. package/rust/core/audio/mod.rs +2 -1
  45. package/rust/core/audio/renderer.rs +1 -1
  46. package/rust/core/audio/special/easing.rs +120 -0
  47. package/rust/core/audio/special/env.rs +41 -0
  48. package/rust/core/audio/special/math.rs +92 -0
  49. package/rust/core/audio/special/mod.rs +9 -0
  50. package/rust/core/audio/special/modulator.rs +120 -0
  51. package/rust/core/builder/mod.rs +11 -6
  52. package/rust/core/debugger/store.rs +1 -1
  53. package/rust/core/error/mod.rs +4 -1
  54. package/rust/core/lexer/handler/arrow.rs +60 -9
  55. package/rust/core/lexer/handler/at.rs +4 -4
  56. package/rust/core/lexer/handler/brace.rs +8 -8
  57. package/rust/core/lexer/handler/colon.rs +4 -4
  58. package/rust/core/lexer/handler/comment.rs +2 -2
  59. package/rust/core/lexer/handler/dot.rs +4 -4
  60. package/rust/core/lexer/handler/driver.rs +42 -13
  61. package/rust/core/lexer/handler/identifier.rs +5 -4
  62. package/rust/core/lexer/handler/indent.rs +16 -2
  63. package/rust/core/lexer/handler/newline.rs +1 -1
  64. package/rust/core/lexer/handler/number.rs +3 -3
  65. package/rust/core/lexer/handler/operator.rs +3 -1
  66. package/rust/core/lexer/handler/parenthesis.rs +8 -8
  67. package/rust/core/lexer/handler/slash.rs +5 -5
  68. package/rust/core/lexer/handler/string.rs +1 -1
  69. package/rust/core/lexer/mod.rs +1 -1
  70. package/rust/core/lexer/token.rs +4 -0
  71. package/rust/core/mod.rs +2 -1
  72. package/rust/core/parser/driver.rs +134 -11
  73. package/rust/core/parser/handler/arrow_call.rs +141 -65
  74. package/rust/core/parser/handler/at.rs +1 -1
  75. package/rust/core/parser/handler/bank.rs +35 -7
  76. package/rust/core/parser/handler/dot.rs +43 -22
  77. package/rust/core/parser/handler/identifier/automate.rs +194 -0
  78. package/rust/core/parser/handler/identifier/function.rs +2 -3
  79. package/rust/core/parser/handler/identifier/let_.rs +16 -0
  80. package/rust/core/parser/handler/identifier/mod.rs +14 -10
  81. package/rust/core/parser/handler/identifier/print.rs +29 -0
  82. package/rust/core/parser/handler/identifier/sleep.rs +1 -1
  83. package/rust/core/parser/handler/identifier/synth.rs +7 -9
  84. package/rust/core/parser/handler/loop_.rs +60 -43
  85. package/rust/core/parser/statement.rs +5 -0
  86. package/rust/core/plugin/loader.rs +48 -0
  87. package/rust/core/plugin/mod.rs +1 -0
  88. package/rust/core/preprocessor/loader.rs +7 -5
  89. package/rust/core/preprocessor/processor.rs +4 -4
  90. package/rust/core/preprocessor/resolver/bank.rs +1 -2
  91. package/rust/core/preprocessor/resolver/call.rs +19 -18
  92. package/rust/core/preprocessor/resolver/driver.rs +7 -5
  93. package/rust/core/preprocessor/resolver/function.rs +3 -13
  94. package/rust/core/preprocessor/resolver/loop_.rs +31 -1
  95. package/rust/core/preprocessor/resolver/spawn.rs +3 -22
  96. package/rust/core/preprocessor/resolver/tempo.rs +1 -1
  97. package/rust/core/preprocessor/resolver/trigger.rs +2 -3
  98. package/rust/core/preprocessor/resolver/value.rs +6 -12
  99. package/rust/core/shared/bank.rs +1 -1
  100. package/rust/core/utils/path.rs +1 -1
  101. package/rust/core/utils/validation.rs +0 -1
  102. package/rust/installer/addon.rs +80 -0
  103. package/rust/installer/bank.rs +25 -15
  104. package/rust/installer/mod.rs +4 -1
  105. package/rust/installer/plugin.rs +55 -0
  106. package/rust/main.rs +32 -10
  107. package/rust/utils/error.rs +51 -0
  108. package/rust/utils/logger.rs +20 -0
  109. package/rust/utils/mod.rs +1 -44
  110. package/rust/utils/spinner.rs +3 -5
@@ -100,30 +100,137 @@ impl AudioEngine {
100
100
  freq: f32,
101
101
  amp: f32,
102
102
  start_time_ms: f32,
103
- duration_ms: f32
103
+ duration_ms: f32,
104
+ synth_params: HashMap<String, Value>,
105
+ note_params: HashMap<String, Value>,
106
+ automation: Option<HashMap<String, Value>>
104
107
  ) {
108
+ let valid_synth_params = vec!["attack", "decay", "sustain", "release"];
109
+ let valid_note_params = vec![
110
+ "duration",
111
+ "velocity",
112
+ "glide",
113
+ "slide",
114
+ "amp",
115
+ "target_freq",
116
+ "target_amp",
117
+ "modulation",
118
+ "expression",
119
+ // allow per-note automation map
120
+ "automate"
121
+ ];
122
+
123
+ // Synth params validation
124
+ for key in synth_params.keys() {
125
+ if !valid_synth_params.contains(&key.as_str()) {
126
+ eprintln!("⚠️ Unknown synth parameter: '{}'", key);
127
+ }
128
+ }
129
+
130
+ // Note params validation
131
+ for key in note_params.keys() {
132
+ if !valid_note_params.contains(&key.as_str()) {
133
+ eprintln!("⚠️ Unknown note parameter: '{}'", key);
134
+ }
135
+ }
136
+
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
+ };
150
+
151
+ // Note parameters
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);
154
+ let glide = self.extract_boolean(&note_params, "glide").unwrap_or(false);
155
+ let slide = self.extract_boolean(&note_params, "slide").unwrap_or(false);
156
+
157
+ let _amplitude = (i16::MAX as f32) * amp.clamp(0.0, 1.0) * velocity.clamp(0.0, 1.0);
158
+
159
+ // Logic for glide and slide
160
+ let freq_start = freq;
161
+ let mut freq_end = freq;
162
+ let amp_start = amp * velocity.clamp(0.0, 1.0);
163
+ let mut amp_end = amp_start;
164
+
165
+ if glide {
166
+ if let Some(Value::Number(target_freq)) = note_params.get("target_freq") {
167
+ freq_end = *target_freq;
168
+ } else {
169
+ freq_end = freq * 1.5; // By default, glide to a perfect fifth
170
+ }
171
+ }
172
+
173
+ if slide {
174
+ if let Some(Value::Number(target_amp)) = note_params.get("target_amp") {
175
+ amp_end = *target_amp * velocity.clamp(0.0, 1.0);
176
+ } else {
177
+ amp_end = amp_start * 0.5; // By default, slide to half the amplitude
178
+ }
179
+ }
105
180
  let sample_rate = SAMPLE_RATE as f32;
106
181
  let channels = CHANNELS as usize;
107
182
 
108
183
  let total_samples = ((duration_ms / 1000.0) * sample_rate) as usize;
109
184
  let start_sample = ((start_time_ms / 1000.0) * sample_rate) as usize;
110
- let amplitude = (i16::MAX as f32) * amp.clamp(0.0, 1.0);
111
185
 
112
- let mut samples = Vec::with_capacity(total_samples);
186
+ // Precompute automation envelopes
187
+ let (volume_env, pan_env, pitch_env) = Self::env_maps_from_automation(&automation);
188
+
189
+ let mut stereo_samples: Vec<i16> = Vec::with_capacity(total_samples * 2);
113
190
  let fade_len = (sample_rate * 0.01) as usize; // 10 ms fade
114
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;
195
+ let sustain_samples = if total_samples > attack_samples + decay_samples + release_samples {
196
+ total_samples - attack_samples - decay_samples - release_samples
197
+ } else {
198
+ 0
199
+ };
200
+
115
201
  for i in 0..total_samples {
116
202
  let t = ((start_sample + i) as f32) / sample_rate;
117
- let phase = 2.0 * std::f32::consts::PI * freq * t;
118
-
119
- let mut value = match waveform.as_str() {
120
- "sine" => phase.sin(),
121
- "square" => if phase.sin() >= 0.0 { 1.0 } else { -1.0 }
122
- "saw" => 2.0 * (freq * t - (freq * t + 0.5).floor()),
123
- "triangle" => (2.0 * (2.0 * (freq * t).fract() - 1.0)).abs() * 2.0 - 1.0,
124
- _ => 0.0,
203
+
204
+ // Glide
205
+ let current_freq = if glide {
206
+ freq_start + ((freq_end - freq_start) * (i as f32)) / (total_samples as f32)
207
+ } else {
208
+ freq
125
209
  };
126
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
+
215
+ // Slide
216
+ let current_amp = if slide {
217
+ amp_start + ((amp_end - amp_start) * (i as f32)) / (total_samples as f32)
218
+ } else {
219
+ amp_start
220
+ };
221
+
222
+ let mut value = Self::oscillator_sample(&waveform, current_freq, t);
223
+
224
+ // ADSR envelope
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
+ );
233
+
127
234
  // Fade in/out
128
235
  if i < fade_len {
129
236
  value *= (i as f32) / (fade_len as f32);
@@ -131,27 +238,32 @@ impl AudioEngine {
131
238
  value *= ((total_samples - i) as f32) / (fade_len as f32);
132
239
  }
133
240
 
134
- samples.push((value * amplitude) as i16);
241
+ value *= envelope;
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);
135
264
  }
136
265
 
137
- // Convert to stereo
138
- let stereo_samples: Vec<i16> = samples
139
- .iter()
140
- .flat_map(|s| vec![*s, *s])
141
- .collect();
142
-
143
- let offset = start_sample * channels;
144
- let required_len = offset + stereo_samples.len();
145
-
146
- if self.buffer.len() < required_len {
147
- self.buffer.resize(required_len, 0);
148
- }
149
-
150
- for (i, sample) in stereo_samples.iter().enumerate() {
151
- if *sample != 0 {
152
- }
153
- self.buffer[offset + i] = self.buffer[offset + i].saturating_add(*sample);
154
- }
266
+ self.mix_stereo_samples_into_buffer(start_sample, channels, &stereo_samples);
155
267
  }
156
268
 
157
269
  pub fn insert_sample(
@@ -169,7 +281,7 @@ impl AudioEngine {
169
281
 
170
282
  let root = Path::new(env!("CARGO_MANIFEST_DIR"));
171
283
  let module_root = Path::new(&self.module_name);
172
- let mut resolved_path = String::new();
284
+ let resolved_path: String;
173
285
 
174
286
  // Get the variable path from the variable table
175
287
  let mut var_path = filepath.to_string();
@@ -179,41 +291,14 @@ impl AudioEngine {
179
291
  var_path = sample_path.clone();
180
292
  }
181
293
 
182
- // If it's a namespace
183
- if var_path.contains(".") {
184
- let parts: Vec<&str> = var_path.trim_start_matches('.').split('.').collect();
185
- if parts.len() == 2 {
186
- let bank_name = parts[0];
187
- let entity_name = parts[1];
188
-
189
- // Verifies if the bank is declared
190
- if !variable_table.variables.contains_key(bank_name) {
191
- eprintln!(
192
- "❌ Bank '{}' not declared. Please declare it first using : 'bank {}'",
193
- bank_name,
194
- bank_name
195
- );
196
- return;
197
- }
198
-
199
- resolved_path = root
200
- .join(".deva")
201
- .join("bank")
202
- .join(bank_name)
203
- .join(format!("{}.wav", entity_name))
204
- .to_string_lossy()
205
- .to_string();
206
- } else {
207
- eprintln!("❌ Invalid namespace format: {}", var_path);
208
- return;
209
- }
210
- } else if var_path.starts_with("devalang://") {
294
+ // Handle devalang:// protocol
295
+ if var_path.starts_with("devalang://") {
211
296
  let path_after_protocol = var_path.replace("devalang://", "");
212
297
  let parts: Vec<&str> = path_after_protocol.split('/').collect();
213
298
 
214
299
  if parts.len() < 3 {
215
300
  eprintln!(
216
- "❌ Invalid devalang:// path format. Expected devalang://<type>/<bank>/<entity>"
301
+ "❌ Invalid devalang:// path format. Expected devalang://<type>/<author>.<bank>/<entity>"
217
302
  );
218
303
  return;
219
304
  }
@@ -390,8 +475,7 @@ impl AudioEngine {
390
475
  let adjusted_sample = adjusted.round().clamp(i16::MIN as f32, i16::MAX as f32) as i16;
391
476
 
392
477
  // PAN
393
- let left_gain = 1.0 - pan.max(0.0); // Pan > 0 => reduce left
394
- let right_gain = 1.0 + pan.min(0.0); // Pan < 0 => reduce right
478
+ let (left_gain, right_gain) = Self::pan_gains(pan);
395
479
 
396
480
  let left = ((adjusted_sample as f32) * left_gain) as i16;
397
481
  let right = ((adjusted_sample as f32) * right_gain) as i16;
@@ -405,4 +489,171 @@ impl AudioEngine {
405
489
  }
406
490
  }
407
491
  }
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
+
637
+ fn extract_f32(&self, map: &HashMap<String, Value>, key: &str) -> Option<f32> {
638
+ match map.get(key) {
639
+ Some(Value::Number(n)) => Some(*n),
640
+ Some(Value::String(s)) => s.parse::<f32>().ok(),
641
+ Some(Value::Boolean(b)) => Some(if *b { 1.0 } else { 0.0 }),
642
+ _ => None,
643
+ }
644
+ }
645
+
646
+ fn extract_boolean(&self, map: &HashMap<String, Value>, key: &str) -> Option<bool> {
647
+ match map.get(key) {
648
+ Some(Value::Boolean(b)) => Some(*b),
649
+ Some(Value::Number(n)) => Some(*n != 0.0),
650
+ Some(Value::Identifier(s)) => {
651
+ if s == "true" { Some(true) } else if s == "false" { Some(false) } else { None }
652
+ }
653
+ Some(Value::String(s)) => {
654
+ if s == "true" { Some(true) } else if s == "false" { Some(false) } else { None }
655
+ }
656
+ _ => None,
657
+ }
658
+ }
408
659
  }
@@ -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
+ }
@@ -23,11 +23,16 @@ pub fn interprete_call_arrow_statement(
23
23
  .unwrap_or(0.0);
24
24
 
25
25
  if let StatementKind::ArrowCall { target, method, args } = &stmt.kind {
26
- let Some(Value::Map(synth_map)) = variable_table.get(target) else {
26
+ let Some(Value::Statement(synth_stmt)) = variable_table.get(target) else {
27
27
  println!("❌ Synth '{}' not found in variable table", target);
28
28
  return (*max_end_time, cursor_copy);
29
29
  };
30
30
 
31
+ let Value::Map(synth_map) = &synth_stmt.value else {
32
+ println!("❌ Invalid synth statement for '{}', expected a map.", target);
33
+ return (*max_end_time, cursor_copy);
34
+ };
35
+
31
36
  let Some(Value::String(entity)) = synth_map.get("entity") else {
32
37
  println!("❌ Missing 'entity' key in synth '{}'.", target);
33
38
  return (*max_end_time, cursor_copy);
@@ -53,37 +58,77 @@ pub fn interprete_call_arrow_statement(
53
58
  return (*max_end_time, cursor_copy);
54
59
  };
55
60
 
56
- let freq = extract_f32(params, "freq", base_bpm).unwrap_or(440.0);
57
- let amp = extract_f32(params, "amp", base_bpm).unwrap_or(1.0);
61
+ // Synth parameters
62
+ let synth_params = params.clone();
63
+ let amp = extract_f32(&synth_params, "amp", base_bpm).unwrap_or(1.0);
58
64
 
59
65
  if method == "note" {
60
- let Some(Value::Identifier(note_name)) = args.get(0) else {
66
+ let filtered_args: Vec<_> = args
67
+ .iter()
68
+ .filter(|arg| !matches!(arg, Value::Unknown))
69
+ .collect();
70
+
71
+ let Some(Value::Identifier(note_name)) = filtered_args.get(0).map(|v| (*v).clone()) else {
61
72
  println!("❌ Invalid or missing argument for 'note' method on '{}'.", target);
62
73
  return (*max_end_time, cursor_copy);
63
74
  };
64
75
 
65
- let mut final_note_params = HashMap::new();
66
- if let Some(Value::Map(note_params)) = args.get(1) {
67
- for (key, value) in note_params {
68
- final_note_params.insert(key.clone(), value.clone());
76
+ let mut note_params = HashMap::new();
77
+ if let Some(arg1) = filtered_args.get(1) {
78
+ match (*arg1).clone() {
79
+ Value::Map(map) => {
80
+ for (key, value) in map {
81
+ note_params.insert(key, value);
82
+ }
83
+ }
84
+ _ => {}
69
85
  }
70
86
  }
71
87
 
72
- let duration_ms = extract_f32(&final_note_params, "duration", base_bpm).unwrap_or(
73
- base_duration
74
- );
88
+ // Note parameters and calculations
89
+ let amp_note = extract_f32(&note_params, "amp", base_bpm).unwrap_or(amp);
90
+ let duration_ms = extract_f32(&note_params, "duration", base_bpm)
91
+ .unwrap_or(base_duration * 1000.0);
92
+
75
93
  let duration_secs = duration_ms / 1000.0;
76
-
77
- let final_freq = note_to_freq(note_name);
94
+ let final_freq = note_to_freq(&note_name);
78
95
  let start_time = cursor_copy;
79
96
  let end_time = start_time + duration_secs;
80
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
+
81
123
  audio_engine.insert_note(
82
124
  waveform.clone(),
83
125
  final_freq,
84
- amp,
126
+ amp_note,
85
127
  start_time * 1000.0,
86
- duration_ms
128
+ duration_ms,
129
+ synth_params,
130
+ note_params,
131
+ automation
87
132
  );
88
133
 
89
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
+ }