@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.
- package/.devalang +8 -8
- package/Cargo.toml +2 -2
- package/README.md +31 -14
- package/docs/CHANGELOG.md +59 -0
- package/docs/ROADMAP.md +1 -1
- package/examples/automation.deva +44 -0
- package/examples/index.deva +41 -25
- package/examples/plugin.deva +15 -0
- package/out-tsc/bin/devalang.exe +0 -0
- package/package.json +1 -1
- package/project-version.json +3 -3
- package/rust/cli/bank.rs +14 -15
- package/rust/cli/check.rs +1 -1
- package/rust/cli/play.rs +1 -1
- package/rust/cli/update.rs +1 -1
- package/rust/common/api.rs +3 -6
- package/rust/common/cdn.rs +3 -6
- package/rust/common/sso.rs +3 -6
- package/rust/core/audio/engine.rs +215 -74
- package/rust/core/audio/evaluator.rs +101 -0
- package/rust/core/audio/interpreter/arrow_call.rs +27 -1
- package/rust/core/audio/interpreter/automate.rs +18 -0
- package/rust/core/audio/interpreter/call.rs +2 -2
- package/rust/core/audio/interpreter/condition.rs +3 -3
- package/rust/core/audio/interpreter/driver.rs +49 -16
- package/rust/core/audio/interpreter/let_.rs +14 -7
- package/rust/core/audio/interpreter/loop_.rs +39 -6
- package/rust/core/audio/interpreter/mod.rs +2 -1
- package/rust/core/audio/interpreter/sleep.rs +2 -4
- package/rust/core/audio/interpreter/spawn.rs +2 -2
- package/rust/core/audio/loader/trigger.rs +2 -5
- package/rust/core/audio/mod.rs +2 -1
- package/rust/core/audio/renderer.rs +1 -1
- package/rust/core/audio/special/easing.rs +120 -0
- package/rust/core/audio/special/env.rs +41 -0
- package/rust/core/audio/special/math.rs +92 -0
- package/rust/core/audio/special/mod.rs +9 -0
- package/rust/core/audio/special/modulator.rs +120 -0
- package/rust/core/debugger/store.rs +1 -1
- package/rust/core/error/mod.rs +4 -1
- package/rust/core/lexer/handler/arrow.rs +60 -9
- package/rust/core/lexer/handler/at.rs +4 -4
- package/rust/core/lexer/handler/brace.rs +8 -8
- package/rust/core/lexer/handler/colon.rs +4 -4
- package/rust/core/lexer/handler/comment.rs +2 -2
- package/rust/core/lexer/handler/dot.rs +4 -4
- package/rust/core/lexer/handler/driver.rs +42 -13
- package/rust/core/lexer/handler/identifier.rs +5 -4
- package/rust/core/lexer/handler/newline.rs +1 -1
- package/rust/core/lexer/handler/number.rs +3 -3
- package/rust/core/lexer/handler/operator.rs +3 -1
- package/rust/core/lexer/handler/parenthesis.rs +8 -8
- package/rust/core/lexer/handler/slash.rs +5 -5
- package/rust/core/lexer/handler/string.rs +1 -1
- package/rust/core/lexer/mod.rs +1 -1
- package/rust/core/lexer/token.rs +3 -0
- package/rust/core/parser/driver.rs +94 -12
- package/rust/core/parser/handler/arrow_call.rs +105 -89
- package/rust/core/parser/handler/at.rs +1 -1
- package/rust/core/parser/handler/dot.rs +3 -3
- package/rust/core/parser/handler/identifier/automate.rs +194 -0
- package/rust/core/parser/handler/identifier/function.rs +2 -3
- package/rust/core/parser/handler/identifier/let_.rs +16 -0
- package/rust/core/parser/handler/identifier/mod.rs +14 -10
- package/rust/core/parser/handler/identifier/print.rs +29 -0
- package/rust/core/parser/handler/identifier/sleep.rs +1 -1
- package/rust/core/parser/handler/identifier/synth.rs +7 -9
- package/rust/core/parser/handler/loop_.rs +60 -43
- package/rust/core/parser/statement.rs +5 -0
- package/rust/core/preprocessor/loader.rs +1 -1
- package/rust/core/preprocessor/processor.rs +4 -4
- package/rust/core/preprocessor/resolver/bank.rs +1 -2
- package/rust/core/preprocessor/resolver/call.rs +19 -18
- package/rust/core/preprocessor/resolver/driver.rs +7 -5
- package/rust/core/preprocessor/resolver/function.rs +3 -13
- package/rust/core/preprocessor/resolver/loop_.rs +31 -1
- package/rust/core/preprocessor/resolver/spawn.rs +3 -22
- package/rust/core/preprocessor/resolver/tempo.rs +1 -1
- package/rust/core/preprocessor/resolver/trigger.rs +2 -3
- package/rust/core/preprocessor/resolver/value.rs +6 -12
- package/rust/core/shared/bank.rs +1 -1
- package/rust/core/utils/path.rs +1 -1
- package/rust/core/utils/validation.rs +0 -1
- package/rust/installer/bank.rs +1 -1
- package/rust/installer/plugin.rs +2 -2
- package/rust/main.rs +0 -1
- package/rust/utils/error.rs +51 -0
- package/rust/utils/logger.rs +4 -0
- package/rust/utils/mod.rs +1 -44
- 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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
146
|
-
|
|
152
|
+
let duration_ms = self.extract_f32(¬e_params, "duration").unwrap_or(duration_ms);
|
|
153
|
+
let velocity = self.extract_f32(¬e_params, "velocity").unwrap_or(1.0);
|
|
147
154
|
let glide = self.extract_boolean(¬e_params, "glide").unwrap_or(false);
|
|
148
155
|
let slide = self.extract_boolean(¬e_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
|
-
|
|
160
|
+
let freq_start = freq;
|
|
154
161
|
let mut freq_end = freq;
|
|
155
|
-
|
|
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; //
|
|
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; //
|
|
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
|
-
|
|
181
|
-
|
|
186
|
+
// Precompute automation envelopes
|
|
187
|
+
let (volume_env, pan_env, pitch_env) = Self::env_maps_from_automation(&automation);
|
|
182
188
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|
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 =
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
sustain_level
|
|
227
|
-
|
|
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
|
-
//
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
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
|
-
|
|
286
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
24
|
-
let
|
|
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
|
|
47
|
+
&block,
|
|
48
48
|
base_bpm,
|
|
49
49
|
base_duration,
|
|
50
50
|
max_time,
|