@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.
- package/.devalang +2 -3
- package/Cargo.toml +58 -54
- package/README.md +59 -27
- package/docs/CHANGELOG.md +99 -2
- package/docs/CONTRIBUTING.md +1 -0
- package/docs/ROADMAP.md +3 -3
- package/docs/TODO.md +5 -4
- package/examples/automation.deva +44 -0
- package/examples/bank.deva +2 -4
- package/examples/function.deva +15 -0
- package/examples/index.deva +41 -11
- package/examples/plugin.deva +15 -0
- package/out-tsc/bin/devalang.exe +0 -0
- package/package.json +6 -6
- package/project-version.json +3 -3
- package/rust/cli/bank.rs +16 -16
- package/rust/cli/build.rs +69 -30
- package/rust/cli/check.rs +46 -6
- package/rust/cli/driver.rs +40 -28
- package/rust/cli/install.rs +22 -7
- package/rust/cli/login.rs +134 -0
- package/rust/cli/mod.rs +2 -1
- package/rust/cli/play.rs +44 -19
- package/rust/cli/update.rs +1 -1
- package/rust/common/api.rs +5 -0
- package/rust/common/cdn.rs +3 -9
- package/rust/common/mod.rs +3 -1
- package/rust/common/sso.rs +5 -0
- package/rust/config/driver.rs +19 -1
- package/rust/config/loader.rs +56 -10
- package/rust/core/audio/engine.rs +314 -63
- package/rust/core/audio/evaluator.rs +101 -0
- package/rust/core/audio/interpreter/arrow_call.rs +60 -15
- package/rust/core/audio/interpreter/automate.rs +18 -0
- package/rust/core/audio/interpreter/call.rs +4 -4
- package/rust/core/audio/interpreter/condition.rs +3 -3
- package/rust/core/audio/interpreter/driver.rs +68 -30
- 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 +4 -4
- 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/builder/mod.rs +11 -6
- 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/indent.rs +16 -2
- 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 +4 -0
- package/rust/core/mod.rs +2 -1
- package/rust/core/parser/driver.rs +134 -11
- package/rust/core/parser/handler/arrow_call.rs +141 -65
- package/rust/core/parser/handler/at.rs +1 -1
- package/rust/core/parser/handler/bank.rs +35 -7
- package/rust/core/parser/handler/dot.rs +43 -22
- 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/plugin/loader.rs +48 -0
- package/rust/core/plugin/mod.rs +1 -0
- package/rust/core/preprocessor/loader.rs +7 -5
- 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/addon.rs +80 -0
- package/rust/installer/bank.rs +25 -15
- package/rust/installer/mod.rs +4 -1
- package/rust/installer/plugin.rs +55 -0
- package/rust/main.rs +32 -10
- package/rust/utils/error.rs +51 -0
- package/rust/utils/logger.rs +20 -0
- package/rust/utils/mod.rs +1 -44
- 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(¬e_params, "duration").unwrap_or(duration_ms);
|
|
153
|
+
let velocity = self.extract_f32(¬e_params, "velocity").unwrap_or(1.0);
|
|
154
|
+
let glide = self.extract_boolean(¬e_params, "glide").unwrap_or(false);
|
|
155
|
+
let slide = self.extract_boolean(¬e_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
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
let
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
183
|
-
if var_path.
|
|
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 =
|
|
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::
|
|
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
|
-
|
|
57
|
-
|
|
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
|
|
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
|
|
66
|
-
if let Some(
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
)
|
|
88
|
+
// Note parameters and calculations
|
|
89
|
+
let amp_note = extract_f32(¬e_params, "amp", base_bpm).unwrap_or(amp);
|
|
90
|
+
let duration_ms = extract_f32(¬e_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(¬e_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
|
-
|
|
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
|
+
}
|