1#![cfg(feature = "cli")]
2use super::logs::LogWriter;
3use crate::engine::audio::mixer::{AudioMixer, MASTER_INSERT, SampleBuffer};
4use crate::engine::audio::settings::{AudioBitDepth, AudioChannels, AudioFormat, ResampleQuality};
5use crate::language::addons::registry::BankRegistry;
6use crate::language::syntax::ast::{DurationValue, Statement, StatementKind, Value};
7use crate::tools::logger::Logger;
8use anyhow::{Context, Result, anyhow};
9use hound::{SampleFormat, WavReader, WavSpec, WavWriter};
10use std::collections::{HashMap, hash_map::Entry};
11use std::path::{Path, PathBuf};
12use std::sync::Arc;
13use std::time::{Duration, Instant};
14#[derive(Debug, Clone)]
15pub struct AudioRenderSummary {
16 pub path: PathBuf,
17 pub format: AudioFormat,
18 pub bit_depth: AudioBitDepth,
19 pub rms: f32,
20 pub render_time: Duration,
21 pub audio_length: Duration,
22}
23
24#[derive(Debug, Clone)]
25pub struct MultiFormatRenderSummary {
26 pub primary_path: PathBuf,
27 pub primary_format: AudioFormat,
28 pub exported_formats: Vec<(AudioFormat, PathBuf)>,
29 pub bit_depth: AudioBitDepth,
30 pub rms: f32,
31 pub render_time: Duration,
32 pub audio_length: Duration,
33}
34#[derive(Debug, Clone)]
35enum RenderJob {
36 Sample {
37 start: f32,
38 duration: f32,
39 sample: SampleBuffer,
40 insert: String,
41 },
42}
43
44#[derive(Debug)]
45struct RenderPlan {
46 jobs: Vec<RenderJob>,
47 inserts: HashMap<String, Option<String>>,
48 total_duration: f32,
49}
50
51#[derive(Debug)]
52struct SpawnPlan {
53 jobs: Vec<RenderJob>,
54 cursor: f32,
55 max_time: f32,
56}
57#[derive(Debug, Clone)]
58pub struct AudioBuilder {
59 log_writer: LogWriter,
60 logger: Arc<Logger>,
61 resample_warning_emitted: Arc<std::sync::atomic::AtomicBool>,
62}
63impl AudioBuilder {
64 pub fn new(log_writer: LogWriter, logger: Arc<Logger>) -> Self {
65 Self {
66 log_writer,
67 logger,
68 resample_warning_emitted: Arc::new(std::sync::atomic::AtomicBool::new(false)),
69 }
70 }
71 #[allow(clippy::too_many_arguments)]
72 pub fn render(
73 &self,
74 statements: &[Statement],
75 entry_path: &Path,
76 output_root: impl AsRef<Path>,
77 module_name: &str,
78 requested_format: AudioFormat,
79 requested_bit_depth: AudioBitDepth,
80 channels: AudioChannels,
81 sample_rate: u32,
82 resample: ResampleQuality,
83 ) -> Result<AudioRenderSummary> {
84 let render_start = Instant::now();
85 let output_root = output_root.as_ref();
86 let audio_dir = output_root.join("audio");
87 std::fs::create_dir_all(&audio_dir).with_context(|| {
88 format!(
89 "failed to create audio output directory: {}",
90 audio_dir.display()
91 )
92 })?;
93
94 use crate::engine::audio::interpreter::driver::AudioInterpreter;
96 let mut interpreter = AudioInterpreter::new(sample_rate);
97 let buffer = interpreter.interpret(statements)?;
98
99 if !buffer.is_empty() {
101 let output_path = audio_dir.join(format!("{}.wav", module_name));
102 let rms = calculate_rms(&buffer);
103 self.logger.debug(format!("Audio RMS: {:.4}", rms));
104
105 let applied_bit_depth = self.write_wav(
106 &output_path,
107 &buffer,
108 sample_rate,
109 requested_bit_depth,
110 channels,
111 )?;
112
113 let render_time = render_start.elapsed();
114 let channel_count = channels.count() as usize;
115 let frames = if channel_count == 0 {
116 0
117 } else {
118 buffer.len() / channel_count
119 };
120 let audio_length = if sample_rate == 0 {
121 Duration::from_secs(0)
122 } else {
123 Duration::from_secs_f64(frames as f64 / sample_rate as f64)
124 };
125
126 self.logger.success(format!(
127 "Rendered audio output -> {} ({:.1} ms)",
128 output_path.display(),
129 render_time.as_secs_f64() * 1000.0
130 ));
131
132 return Ok(AudioRenderSummary {
133 path: output_path,
134 format: AudioFormat::Wav,
135 bit_depth: applied_bit_depth,
136 rms,
137 render_time,
138 audio_length,
139 });
140 }
141
142 if !matches!(resample, ResampleQuality::Sinc24)
144 && !self
145 .resample_warning_emitted
146 .swap(true, std::sync::atomic::Ordering::SeqCst)
147 {
148 self.logger.debug(format!(
149 "Resampling quality '{}' is noted but not yet applied (procedural synthesis).",
150 resample
151 ));
152 }
153 let (target_format, extension) = match requested_format {
154 AudioFormat::Wav => (AudioFormat::Wav, "wav"),
155 other => {
156 self.logger.warn(format!(
157 "Audio export {:?} is not implemented yet. Falling back to WAV",
158 other
159 ));
160 (AudioFormat::Wav, "wav")
161 }
162 };
163 let output_path = audio_dir.join(format!("{}.{}", module_name, extension));
164 let channel_count = channels.count() as usize;
165 let base_dir = entry_path
166 .parent()
167 .map(Path::to_path_buf)
168 .unwrap_or_else(|| PathBuf::from("."));
169 let project_root = find_project_root(entry_path);
170 let RenderPlan {
171 jobs,
172 inserts,
173 total_duration,
174 } = self.plan_jobs(statements, &base_dir, &project_root)?;
175 let total_samples = (total_duration * sample_rate as f32).ceil() as usize;
176 let mut mixer = AudioMixer::new(sample_rate, channel_count);
177 for (insert, parent) in inserts.iter() {
178 if insert != MASTER_INSERT {
179 mixer.register_insert(insert.clone(), parent.as_deref());
180 }
181 }
182 mixer.ensure_master_frames(total_samples);
183 for job in jobs {
184 match job {
185 RenderJob::Sample {
186 start,
187 duration,
188 sample,
189 insert,
190 } => {
191 if sample_rate == 0 {
192 continue;
193 }
194 let start_frame = if start <= 0.0 {
195 0
196 } else {
197 (start * sample_rate as f32).floor() as usize
198 };
199 mixer.mix_sample(&insert, start_frame, duration, &sample);
200 }
201 }
202 }
203 let mut buffer = mixer.into_master_buffer(total_samples);
204 normalize(&mut buffer);
205 let rms = calculate_rms(&buffer);
206 self.logger.debug(format!("Audio RMS: {:.4}", rms));
207 let applied_bit_depth = self.write_wav(
208 &output_path,
209 &buffer,
210 sample_rate,
211 requested_bit_depth,
212 channels,
213 )?;
214 self.log_writer.append(
215 output_root,
216 &format!(
217 "Rendered module '{}' to {} ({:?}, {} bits, {} ch, {:?})",
218 module_name,
219 output_path.display(),
220 target_format,
221 applied_bit_depth.bits(),
222 channels.count(),
223 resample
224 ),
225 )?;
226 let render_time = render_start.elapsed();
227 let frames = if channel_count == 0 {
228 0
229 } else {
230 buffer.len() / channel_count
231 };
232 let audio_length = if sample_rate == 0 {
233 Duration::from_secs(0)
234 } else {
235 Duration::from_secs_f64(frames as f64 / sample_rate as f64)
236 };
237 self.logger.success(format!(
238 "Rendered audio output -> {} ({:.1} ms)",
239 output_path.display(),
240 render_time.as_secs_f64() * 1000.0
241 ));
242 Ok(AudioRenderSummary {
243 path: output_path,
244 format: target_format,
245 bit_depth: applied_bit_depth,
246 rms,
247 render_time,
248 audio_length,
249 })
250 }
251
252 #[allow(clippy::too_many_arguments)]
254 pub fn render_all_formats(
255 &self,
256 statements: &[Statement],
257 _entry_path: &Path,
258 output_root: impl AsRef<Path>,
259 module_name: &str,
260 requested_formats: &[AudioFormat],
261 requested_bit_depth: AudioBitDepth,
262 channels: AudioChannels,
263 sample_rate: u32,
264 _resample: ResampleQuality,
265 bpm: f32,
266 ) -> Result<MultiFormatRenderSummary> {
267 let render_start = Instant::now();
268 let output_root = output_root.as_ref();
269 let audio_dir = output_root.join("audio");
270 std::fs::create_dir_all(&audio_dir).with_context(|| {
271 format!(
272 "failed to create audio output directory: {}",
273 audio_dir.display()
274 )
275 })?;
276
277 use crate::engine::audio::interpreter::driver::AudioInterpreter;
279 let mut interpreter = AudioInterpreter::new(sample_rate);
280 let buffer = interpreter.interpret(statements)?;
281
282 let rms = calculate_rms(&buffer);
283 self.logger.debug(format!("Audio RMS: {:.4}", rms));
284
285 let channel_count = channels.count() as usize;
286 let frames = if channel_count == 0 {
287 0
288 } else {
289 buffer.len() / channel_count
290 };
291 let audio_length = if sample_rate == 0 {
292 Duration::from_secs(0)
293 } else {
294 Duration::from_secs_f64(frames as f64 / sample_rate as f64)
295 };
296
297 let mut exported_formats = Vec::new();
299 let mut primary_path = PathBuf::new();
300 let mut primary_format = AudioFormat::Wav;
301 let mut applied_bit_depth = requested_bit_depth;
302
303 for (idx, &format) in requested_formats.iter().enumerate() {
304 match format {
305 AudioFormat::Wav => {
306 let output_path = audio_dir.join(format!("{}.wav", module_name));
307 applied_bit_depth = self.write_wav(
308 &output_path,
309 &buffer,
310 sample_rate,
311 requested_bit_depth,
312 channels,
313 )?;
314 self.logger.success(format!(
315 "✅ Exported WAV: {} ({} bits)",
316 output_path.display(),
317 applied_bit_depth.bits()
318 ));
319 exported_formats.push((AudioFormat::Wav, output_path.clone()));
320 if idx == 0 {
321 primary_path = output_path;
322 primary_format = AudioFormat::Wav;
323 }
324 }
325 AudioFormat::Mp3 => {
326 let output_path = audio_dir.join(format!("{}.mp3", module_name));
327 applied_bit_depth = self.write_mp3(
328 &output_path,
329 &buffer,
330 sample_rate,
331 requested_bit_depth,
332 channels,
333 )?;
334 self.logger.success(format!(
335 "🎵 Exported MP3: {} ({} bits equivalent)",
336 output_path.display(),
337 applied_bit_depth.bits()
338 ));
339 exported_formats.push((AudioFormat::Mp3, output_path.clone()));
340 if idx == 0 {
341 primary_path = output_path;
342 primary_format = AudioFormat::Mp3;
343 }
344 }
345 AudioFormat::Flac => {
346 let output_path = audio_dir.join(format!("{}.flac", module_name));
347 applied_bit_depth = self.write_flac(
348 &output_path,
349 &buffer,
350 sample_rate,
351 requested_bit_depth,
352 channels,
353 )?;
354 self.logger.success(format!(
355 "🎼 Exported FLAC: {} ({} bits)",
356 output_path.display(),
357 applied_bit_depth.bits()
358 ));
359 exported_formats.push((AudioFormat::Flac, output_path.clone()));
360 if idx == 0 {
361 primary_path = output_path;
362 primary_format = AudioFormat::Flac;
363 }
364 }
365 AudioFormat::Mid => {
366 let output_path = audio_dir.join(format!("{}.mid", module_name));
368
369 if !interpreter.events().events.is_empty() {
371 use crate::engine::audio::midi::export_midi_file;
372 if let Err(e) =
373 export_midi_file(&interpreter.events().events, &output_path, bpm)
374 {
375 self.logger.warn(format!("Failed to export MIDI: {}", e));
376 } else {
377 self.logger
378 .success(format!("🎹 Exported MIDI: {}", output_path.display()));
379 exported_formats.push((AudioFormat::Mid, output_path));
380 }
381 } else {
382 self.logger.warn(
383 "No MIDI events to export (no synth play/chord statements)".to_string(),
384 );
385 }
386 }
387 }
388 }
389
390 let render_time = render_start.elapsed();
391
392 self.logger.success(format!(
393 "Rendered {} format(s) in {:.1} ms",
394 exported_formats.len(),
395 render_time.as_secs_f64() * 1000.0
396 ));
397
398 Ok(MultiFormatRenderSummary {
399 primary_path,
400 primary_format,
401 exported_formats,
402 bit_depth: applied_bit_depth,
403 rms,
404 render_time,
405 audio_length,
406 })
407 }
408
409 fn plan_jobs(
410 &self,
411 statements: &[Statement],
412 base_dir: &Path,
413 project_root: &Path,
414 ) -> Result<RenderPlan> {
415 let mut samples = HashMap::<String, SampleBuffer>::new();
416 let mut sample_cache = HashMap::<PathBuf, SampleBuffer>::new();
417 let mut banks = BankRegistry::new();
418 let mut inserts = HashMap::<String, Option<String>>::new();
419 inserts.insert(MASTER_INSERT.to_string(), None);
420 let mut alias_to_insert = HashMap::<String, String>::new();
421 alias_to_insert.insert(MASTER_INSERT.to_string(), MASTER_INSERT.to_string());
422
423 let mut groups = HashMap::<String, Vec<Statement>>::new();
425
426 let mut patterns = HashMap::<String, (String, String)>::new(); use crate::language::scope::{BindingType, VariableTable};
431 let mut variables = VariableTable::new();
432
433 let mut jobs = Vec::new();
434 let mut tempo = 120.0_f32;
435 let mut cursor = 0.0_f32;
436 let mut max_time = 0.0_f32;
437
438 for statement in statements {
440 match &statement.kind {
441 StatementKind::Group { name, body } => {
442 groups.insert(name.clone(), body.clone());
443 }
444 StatementKind::Pattern { name, target } => {
445 if let Value::String(pattern_str) = &statement.value {
446 if let Some(target_entity) = target {
447 patterns
448 .insert(name.clone(), (target_entity.clone(), pattern_str.clone()));
449 } else {
450 self.logger
451 .warn(format!("Pattern '{}' has no target specified", name));
452 }
453 }
454 }
455 _ => {}
456 }
457 }
458
459 for statement in statements {
461 match &statement.kind {
462 StatementKind::Tempo => {
463 if let Value::Number(value) = statement.value {
464 if value > 0.0 {
465 tempo = value;
466 }
467 }
468 }
469 StatementKind::Let { name, value } => {
470 if let Some(val) = value {
471 let resolved = self.resolve_value(val, &variables);
472 variables.set_with_type(name.clone(), resolved, BindingType::Let);
473 self.logger.debug(format!("let {} = {:?}", name, val));
474 }
475 }
476 StatementKind::Var { name, value } => {
477 if let Some(val) = value {
478 let resolved = self.resolve_value(val, &variables);
479 variables.set_with_type(name.clone(), resolved, BindingType::Var);
480 self.logger.debug(format!("var {} = {:?}", name, val));
481 }
482 }
483 StatementKind::Const { name, value } => {
484 if let Some(val) = value {
485 let resolved = self.resolve_value(val, &variables);
486 variables.set_with_type(name.clone(), resolved, BindingType::Const);
487 self.logger.debug(format!("const {} = {:?}", name, val));
488 }
489 }
490 StatementKind::Load { source, alias } => {
491 let resolved_path = resolve_sample_reference(base_dir, source);
492 match self.load_sample_cached(&mut sample_cache, resolved_path.clone()) {
493 Ok(sample) => {
494 let insert_name = register_route_for_alias(
495 &mut inserts,
496 &mut alias_to_insert,
497 alias,
498 "sample",
499 Some(MASTER_INSERT),
500 );
501 samples.insert(alias.to_string(), sample.clone());
502 self.logger.info(format!(
503 "Loaded sample '{}' into '{}' from {}",
504 alias,
505 insert_name,
506 resolved_path.display()
507 ));
508 }
509 Err(err) => {
510 self.logger.error(format!(
511 "Failed to load sample '{}' (alias '{}'): {}",
512 resolved_path.display(),
513 alias,
514 err
515 ));
516 }
517 }
518 }
519 StatementKind::Bank { name, alias } => {
520 let alias_name = alias.clone().unwrap_or_else(|| default_bank_alias(name));
521 let insert_name = register_route_for_alias(
522 &mut inserts,
523 &mut alias_to_insert,
524 &alias_name,
525 "bank",
526 Some(MASTER_INSERT),
527 );
528 alias_to_insert
529 .entry(name.clone())
530 .or_insert_with(|| insert_name.clone());
531 match banks.register_bank(alias_name.clone(), name, project_root, base_dir) {
532 Ok(bank) => {
533 let count = bank.trigger_count();
534 self.logger.info(format!(
535 "Registered bank '{}' as '{}' -> insert '{}' ({} trigger{})",
536 name,
537 alias_name,
538 insert_name,
539 count,
540 if count == 1 { "" } else { "s" }
541 ));
542 }
543 Err(err) => {
544 self.logger
545 .error(format!("Failed to register bank '{}': {}", name, err));
546 }
547 }
548 }
549 StatementKind::Trigger {
550 entity,
551 duration,
552 effects,
553 } => {
554 let resolved_entity = if let Some(var_value) = variables.get(entity) {
556 match self.resolve_value(&var_value, &variables) {
557 Value::Identifier(id) => id,
558 Value::String(s) => s,
559 _ => entity.clone(),
560 }
561 } else {
562 entity.clone()
563 };
564
565 let (target_alias, trigger_name) = split_trigger_entity(&resolved_entity);
566 let insert_name = alias_to_insert
567 .get(target_alias)
568 .cloned()
569 .unwrap_or_else(|| MASTER_INSERT.to_string());
570 let duration_seconds = duration_in_seconds(duration, tempo);
571 let is_auto = matches!(duration, DurationValue::Auto);
572 let start = cursor;
573 let mut resolved_sample = samples.get(target_alias).cloned();
574 if resolved_sample.is_none() {
575 if let Some(trigger) = trigger_name {
576 if let Some(path) = banks.resolve_trigger(target_alias, trigger) {
577 match self.load_sample_cached(&mut sample_cache, path.clone()) {
578 Ok(sample) => {
579 self.logger.debug(format!(
580 "Trigger '{}.{}' resolved to {}",
581 target_alias,
582 trigger,
583 path.display()
584 ));
585 resolved_sample = Some(sample);
586 }
587 Err(err) => {
588 self.logger.error(format!(
589 "Failed to load trigger '{}.{}' from {}: {}",
590 target_alias,
591 trigger,
592 path.display(),
593 err
594 ));
595 }
596 }
597 } else if banks.has_bank(target_alias) {
598 self.logger.error(format!(
599 "Bank '{}' does not define trigger '{}'; rendering silence",
600 target_alias, trigger
601 ));
602 } else {
603 self.logger.warn(format!(
604 "Bank alias '{}' not registered; rendering silence",
605 target_alias
606 ));
607 }
608 } else if !samples.contains_key(target_alias) {
609 self.logger.warn(format!(
610 "Unknown sample alias '{}'; rendering silence",
611 target_alias
612 ));
613 }
614 }
615 if let Some(mut sample) = resolved_sample {
616 if let Some(fx) = effects {
618 let effect_count = if let Value::Map(m) = fx { m.len() } else { 0 };
619 if effect_count > 0 {
620 self.logger.debug(format!(
621 " 🎛️ Applying {} effect(s) to trigger '{}'",
622 effect_count, entity
623 ));
624
625 let mut buffer = sample.data_clone();
626 if let Err(e) = apply_trigger_effects(
627 &mut buffer,
628 fx,
629 sample.sample_rate(),
630 sample.channels(),
631 ) {
632 self.logger.warn(format!(
633 "Failed to apply effects to trigger '{}': {}",
634 entity, e
635 ));
636 } else {
637 sample = sample.with_modified_data(buffer, None);
638 }
639 }
640 }
641
642 let sample_len = if sample.sample_rate() == 0 {
643 0.0
644 } else {
645 sample.frames() as f32 / sample.sample_rate() as f32
646 };
647 let (advance, playback_duration) = if let Some(value) = duration_seconds {
648 if is_auto {
649 (sample_len, sample_len)
650 } else {
651 (value, value.max(sample_len))
652 }
653 } else {
654 (sample_len, sample_len)
655 };
656 jobs.push(RenderJob::Sample {
657 start,
658 duration: playback_duration,
659 sample,
660 insert: insert_name.clone(),
661 });
662 cursor += advance;
663 max_time = max_time.max(start + playback_duration);
664 } else {
665 let duration_value =
666 duration_seconds.unwrap_or(beats_to_seconds(0.25, tempo));
667 cursor += duration_value;
668 max_time = max_time.max(start + duration_value);
669 }
670 }
671 StatementKind::Routing => {
672 if let Value::Map(route) = &statement.value {
673 if let Some(source_alias) = route.get("source").and_then(value_as_string) {
674 let target_alias = route.get("target").and_then(value_as_string);
675 let parent_insert = target_alias.as_ref().and_then(|alias| {
676 resolve_parent_for_alias(&mut inserts, &mut alias_to_insert, alias)
677 });
678 let parent_ref = parent_insert.as_ref().map(|s| s.as_str());
679 let source_insert = alias_to_insert
680 .get(&source_alias)
681 .cloned()
682 .unwrap_or_else(|| {
683 register_route_for_alias(
684 &mut inserts,
685 &mut alias_to_insert,
686 &source_alias,
687 "bus",
688 parent_ref,
689 )
690 });
691 if source_insert == MASTER_INSERT {
692 self.logger.warn(format!(
693 "Ignoring routing directive attempting to modify master: '{}'",
694 source_alias
695 ));
696 continue;
697 }
698 inserts
699 .entry(source_insert.clone())
700 .and_modify(|entry| *entry = parent_insert.clone())
701 .or_insert(parent_insert.clone());
702 alias_to_insert.entry(source_alias).or_insert(source_insert);
703 }
704 }
705 }
706 StatementKind::Sleep => {
707 let resolved = self.resolve_value(&statement.value, &variables);
709 let duration = match resolved {
710 Value::Duration(d) => {
711 duration_in_seconds(&d, tempo).unwrap_or(beats_to_seconds(0.25, tempo))
712 }
713 Value::Number(n) => n / 1_000.0, _ => beats_to_seconds(0.25, tempo),
715 };
716 cursor += duration;
717 max_time = max_time.max(cursor);
718 }
719 StatementKind::Group { .. } => {
720 }
722 StatementKind::Spawn { name, .. } => {
723 if let Some(body) = groups.get(name) {
725 self.logger
726 .debug(format!("Spawning group '{}' at {}s", name, cursor));
727 let spawn_start = cursor;
728
729 use crate::language::scope::VariableTable;
731 let child_vars = VariableTable::with_parent(variables.clone());
732
733 let spawn_plan = self.plan_jobs_internal(
735 body,
736 base_dir,
737 project_root,
738 &mut samples,
739 &mut sample_cache,
740 &mut banks,
741 &mut inserts,
742 &mut alias_to_insert,
743 &groups,
744 child_vars,
745 tempo,
746 cursor,
747 )?;
748
749 jobs.extend(spawn_plan.jobs);
751 cursor = spawn_plan.cursor;
752 max_time = max_time.max(spawn_plan.max_time);
753
754 self.logger.debug(format!(
755 "Group '{}' spawned from {}s to {}s (duration: {}s)",
756 name,
757 spawn_start,
758 cursor,
759 cursor - spawn_start
760 ));
761 } else if let Some((target, pattern_str)) = patterns.get(name) {
762 self.logger
764 .debug(format!("Spawning pattern '{}' at {}s", name, cursor));
765 let spawn_start = cursor;
766
767 let pattern_chars: Vec<char> =
769 pattern_str.chars().filter(|c| !c.is_whitespace()).collect();
770 let step_count = pattern_chars.len() as f32;
771
772 if step_count > 0.0 {
773 let bar_duration = beats_to_seconds(4.0, tempo);
775 let step_duration = bar_duration / step_count;
776
777 for (i, ch) in pattern_chars.iter().enumerate() {
778 if *ch == 'x' || *ch == 'X' {
779 let trigger_time = spawn_start + (i as f32 * step_duration);
780
781 let (target_alias, trigger_name) = split_trigger_entity(target);
783 let insert_name = alias_to_insert
784 .get(target_alias)
785 .cloned()
786 .unwrap_or_else(|| MASTER_INSERT.to_string());
787
788 let mut resolved_sample = None;
789 if let Some(trigger) = trigger_name {
790 if let Some(path) =
791 banks.resolve_trigger(target_alias, trigger)
792 {
793 match self
794 .load_sample_cached(&mut sample_cache, path.clone())
795 {
796 Ok(sample) => {
797 resolved_sample = Some(sample);
798 }
799 Err(err) => {
800 self.logger.error(format!(
801 "Failed to load pattern trigger '{}.{}': {}",
802 target_alias, trigger, err
803 ));
804 }
805 }
806 }
807 }
808
809 if let Some(sample) = resolved_sample {
810 let sample_len = if sample.sample_rate() == 0 {
811 0.0
812 } else {
813 sample.frames() as f32 / sample.sample_rate() as f32
814 };
815
816 jobs.push(RenderJob::Sample {
817 start: trigger_time,
818 duration: sample_len,
819 sample,
820 insert: insert_name,
821 });
822
823 max_time = max_time.max(trigger_time + sample_len);
824 }
825 }
826 }
827
828 cursor = spawn_start + bar_duration;
829 max_time = max_time.max(cursor);
830
831 self.logger.debug(format!(
832 "Pattern '{}' spawned from {}s to {}s ({} steps)",
833 name, spawn_start, cursor, step_count
834 ));
835 }
836 } else {
837 self.logger.warn(format!(
838 "Unknown group or pattern '{}' in spawn statement",
839 name
840 ));
841 }
842 }
843 StatementKind::For {
844 variable,
845 iterable,
846 body,
847 } => {
848 let loop_start = cursor;
850
851 let resolved_iterable = self.resolve_value(iterable, &variables);
853 let items = match resolved_iterable {
854 Value::Array(arr) => arr,
855 Value::Number(n) => {
856 (0..(n as i32)).map(|i| Value::Number(i as f32)).collect()
858 }
859 _ => {
860 self.logger
861 .warn(format!("For loop iterable must be array or number"));
862 continue;
863 }
864 };
865
866 self.logger.debug(format!(
867 "For loop with {} iterations at {}s",
868 items.len(),
869 cursor
870 ));
871
872 for item in items {
873 use crate::language::scope::{BindingType, VariableTable};
875 let mut child_vars = VariableTable::with_parent(variables.clone());
876 child_vars.set_with_type(variable.clone(), item, BindingType::Let);
877
878 let spawn_plan = self.plan_jobs_internal(
880 body,
881 base_dir,
882 project_root,
883 &mut samples,
884 &mut sample_cache,
885 &mut banks,
886 &mut inserts,
887 &mut alias_to_insert,
888 &groups,
889 child_vars,
890 tempo,
891 cursor,
892 )?;
893
894 jobs.extend(spawn_plan.jobs);
895 cursor = spawn_plan.cursor;
896 max_time = max_time.max(spawn_plan.max_time);
897 }
898
899 self.logger.debug(format!(
900 "For loop completed from {}s to {}s",
901 loop_start, cursor
902 ));
903 }
904 StatementKind::Loop { count, body } => {
905 let loop_start = cursor;
907
908 let resolved_count = self.resolve_value(count, &variables);
910 let iterations = match resolved_count {
911 Value::Number(n) => n as usize,
912 _ => {
913 self.logger.warn(format!("Loop count must be a number"));
914 continue;
915 }
916 };
917
918 self.logger
919 .debug(format!("Loop {} times at {}s", iterations, cursor));
920
921 for _ in 0..iterations {
922 use crate::language::scope::VariableTable;
924 let child_vars = VariableTable::with_parent(variables.clone());
925
926 let spawn_plan = self.plan_jobs_internal(
928 body,
929 base_dir,
930 project_root,
931 &mut samples,
932 &mut sample_cache,
933 &mut banks,
934 &mut inserts,
935 &mut alias_to_insert,
936 &groups,
937 child_vars,
938 tempo,
939 cursor,
940 )?;
941
942 jobs.extend(spawn_plan.jobs);
943 cursor = spawn_plan.cursor;
944 max_time = max_time.max(spawn_plan.max_time);
945 }
946
947 self.logger.debug(format!(
948 "Loop completed from {}s to {}s",
949 loop_start, cursor
950 ));
951 }
952 StatementKind::If {
953 condition,
954 body,
955 else_body,
956 } => {
957 let condition_result = self.evaluate_condition(condition, &variables);
959
960 let branch_to_execute = if condition_result {
961 body
962 } else if let Some(else_branch) = else_body {
963 else_branch
964 } else {
965 continue;
966 };
967
968 self.logger.debug(format!(
969 "If condition evaluated to {}, executing branch at {}s",
970 condition_result, cursor
971 ));
972
973 use crate::language::scope::VariableTable;
975 let child_vars = VariableTable::with_parent(variables.clone());
976
977 let spawn_plan = self.plan_jobs_internal(
979 branch_to_execute,
980 base_dir,
981 project_root,
982 &mut samples,
983 &mut sample_cache,
984 &mut banks,
985 &mut inserts,
986 &mut alias_to_insert,
987 &groups,
988 child_vars,
989 tempo,
990 cursor,
991 )?;
992
993 jobs.extend(spawn_plan.jobs);
994 cursor = spawn_plan.cursor;
995 max_time = max_time.max(spawn_plan.max_time);
996 }
997 _ => {}
998 }
999 }
1000 max_time = max_time.max(cursor);
1001 Ok(RenderPlan {
1002 jobs,
1003 inserts,
1004 total_duration: max_time,
1005 })
1006 }
1007
1008 #[allow(clippy::too_many_arguments)]
1009 fn plan_jobs_internal(
1010 &self,
1011 statements: &[Statement],
1012 base_dir: &Path,
1013 project_root: &Path,
1014 samples: &mut HashMap<String, SampleBuffer>,
1015 sample_cache: &mut HashMap<PathBuf, SampleBuffer>,
1016 banks: &mut BankRegistry,
1017 inserts: &mut HashMap<String, Option<String>>,
1018 alias_to_insert: &mut HashMap<String, String>,
1019 groups: &HashMap<String, Vec<Statement>>,
1020 mut variables: crate::language::scope::VariableTable,
1021 mut tempo: f32,
1022 mut cursor: f32,
1023 ) -> Result<SpawnPlan> {
1024 let mut jobs = Vec::new();
1025 let mut max_time = cursor;
1026
1027 use crate::language::scope::BindingType;
1028
1029 for statement in statements {
1030 match &statement.kind {
1031 StatementKind::Tempo => {
1032 if let Value::Number(value) = statement.value {
1033 if value > 0.0 {
1034 tempo = value;
1035 }
1036 }
1037 }
1038 StatementKind::Let { name, value } => {
1039 if let Some(val) = value {
1040 let resolved = self.resolve_value(val, &variables);
1041 variables.set_with_type(name.clone(), resolved, BindingType::Let);
1042 }
1043 }
1044 StatementKind::Var { name, value } => {
1045 if let Some(val) = value {
1046 let resolved = self.resolve_value(val, &variables);
1047 variables.set_with_type(name.clone(), resolved, BindingType::Var);
1048 }
1049 }
1050 StatementKind::Const { name, value } => {
1051 if let Some(val) = value {
1052 let resolved = self.resolve_value(val, &variables);
1053 variables.set_with_type(name.clone(), resolved, BindingType::Const);
1054 }
1055 }
1056 StatementKind::Trigger {
1057 entity,
1058 duration,
1059 effects,
1060 } => {
1061 let resolved_entity = if let Some(var_value) = variables.get(entity) {
1063 match self.resolve_value(&var_value, &variables) {
1064 Value::Identifier(id) => id,
1065 Value::String(s) => s,
1066 _ => entity.clone(),
1067 }
1068 } else {
1069 entity.clone()
1070 };
1071
1072 let (target_alias, trigger_name) = split_trigger_entity(&resolved_entity);
1073 let insert_name = alias_to_insert
1074 .get(target_alias)
1075 .cloned()
1076 .unwrap_or_else(|| MASTER_INSERT.to_string());
1077 let duration_seconds = duration_in_seconds(duration, tempo);
1078 let is_auto = matches!(duration, DurationValue::Auto);
1079 let start = cursor;
1080 let mut resolved_sample = samples.get(target_alias).cloned();
1081 if resolved_sample.is_none() {
1082 if let Some(trigger) = trigger_name {
1083 if let Some(path) = banks.resolve_trigger(target_alias, trigger) {
1084 match self.load_sample_cached(sample_cache, path.clone()) {
1085 Ok(sample) => {
1086 self.logger.debug(format!(
1087 "Trigger '{}.{}' resolved to {}",
1088 target_alias,
1089 trigger,
1090 path.display()
1091 ));
1092 resolved_sample = Some(sample);
1093 }
1094 Err(err) => {
1095 self.logger.error(format!(
1096 "Failed to load trigger '{}.{}' from {}: {}",
1097 target_alias,
1098 trigger,
1099 path.display(),
1100 err
1101 ));
1102 }
1103 }
1104 }
1105 }
1106 }
1107 if let Some(mut sample) = resolved_sample {
1108 if let Some(fx) = effects {
1110 let effect_count = if let Value::Map(m) = fx { m.len() } else { 0 };
1111 if effect_count > 0 {
1112 self.logger.debug(format!(
1113 " 🎛️ Applying {} effect(s) to trigger '{}'",
1114 effect_count, entity
1115 ));
1116
1117 let mut buffer = sample.data_clone();
1118 if let Err(e) = apply_trigger_effects(
1119 &mut buffer,
1120 fx,
1121 sample.sample_rate(),
1122 sample.channels(),
1123 ) {
1124 self.logger.warn(format!(
1125 "Failed to apply effects to trigger '{}': {}",
1126 entity, e
1127 ));
1128 } else {
1129 sample = sample.with_modified_data(buffer, None);
1130 }
1131 }
1132 }
1133
1134 let sample_len = if sample.sample_rate() == 0 {
1135 0.0
1136 } else {
1137 sample.frames() as f32 / sample.sample_rate() as f32
1138 };
1139 let (advance, playback_duration) = if let Some(value) = duration_seconds {
1140 if is_auto {
1141 (sample_len, sample_len)
1142 } else {
1143 (value, value.max(sample_len))
1144 }
1145 } else {
1146 (sample_len, sample_len)
1147 };
1148 jobs.push(RenderJob::Sample {
1149 start,
1150 duration: playback_duration,
1151 sample,
1152 insert: insert_name.clone(),
1153 });
1154 cursor += advance;
1155 max_time = max_time.max(start + playback_duration);
1156 } else {
1157 let duration_value =
1158 duration_seconds.unwrap_or(beats_to_seconds(0.25, tempo));
1159 cursor += duration_value;
1160 max_time = max_time.max(start + duration_value);
1161 }
1162 }
1163 StatementKind::Sleep => {
1164 let resolved = self.resolve_value(&statement.value, &variables);
1166 let duration = match resolved {
1167 Value::Duration(d) => {
1168 duration_in_seconds(&d, tempo).unwrap_or(beats_to_seconds(0.25, tempo))
1169 }
1170 Value::Number(n) => n / 1_000.0, _ => beats_to_seconds(0.25, tempo),
1172 };
1173 cursor += duration;
1174 max_time = max_time.max(cursor);
1175 }
1176 StatementKind::Spawn { name, .. } => {
1177 if let Some(body) = groups.get(name) {
1179 use crate::language::scope::VariableTable;
1180 let child_vars = VariableTable::with_parent(variables.clone());
1181
1182 let spawn_plan = self.plan_jobs_internal(
1183 body,
1184 base_dir,
1185 project_root,
1186 samples,
1187 sample_cache,
1188 banks,
1189 inserts,
1190 alias_to_insert,
1191 groups,
1192 child_vars,
1193 tempo,
1194 cursor,
1195 )?;
1196 jobs.extend(spawn_plan.jobs);
1197 cursor = spawn_plan.cursor;
1198 max_time = max_time.max(spawn_plan.max_time);
1199 }
1200 }
1201 StatementKind::For {
1202 variable,
1203 iterable,
1204 body,
1205 } => {
1206 let resolved_iterable = self.resolve_value(iterable, &variables);
1208 let items = match resolved_iterable {
1209 Value::Array(arr) => arr,
1210 Value::Number(n) => {
1211 (0..(n as i32)).map(|i| Value::Number(i as f32)).collect()
1212 }
1213 _ => continue,
1214 };
1215
1216 for item in items {
1217 use crate::language::scope::{BindingType, VariableTable};
1218 let mut child_vars = VariableTable::with_parent(variables.clone());
1219 child_vars.set_with_type(variable.clone(), item, BindingType::Let);
1220
1221 let spawn_plan = self.plan_jobs_internal(
1222 body,
1223 base_dir,
1224 project_root,
1225 samples,
1226 sample_cache,
1227 banks,
1228 inserts,
1229 alias_to_insert,
1230 groups,
1231 child_vars,
1232 tempo,
1233 cursor,
1234 )?;
1235 jobs.extend(spawn_plan.jobs);
1236 cursor = spawn_plan.cursor;
1237 max_time = max_time.max(spawn_plan.max_time);
1238 }
1239 }
1240 StatementKind::Loop { count, body } => {
1241 let resolved_count = self.resolve_value(count, &variables);
1242 let iterations = match resolved_count {
1243 Value::Number(n) => n as usize,
1244 _ => continue,
1245 };
1246
1247 for _ in 0..iterations {
1248 use crate::language::scope::VariableTable;
1249 let child_vars = VariableTable::with_parent(variables.clone());
1250
1251 let spawn_plan = self.plan_jobs_internal(
1252 body,
1253 base_dir,
1254 project_root,
1255 samples,
1256 sample_cache,
1257 banks,
1258 inserts,
1259 alias_to_insert,
1260 groups,
1261 child_vars,
1262 tempo,
1263 cursor,
1264 )?;
1265 jobs.extend(spawn_plan.jobs);
1266 cursor = spawn_plan.cursor;
1267 max_time = max_time.max(spawn_plan.max_time);
1268 }
1269 }
1270 StatementKind::If {
1271 condition,
1272 body,
1273 else_body,
1274 } => {
1275 let condition_result = self.evaluate_condition(condition, &variables);
1276 let branch_to_execute = if condition_result {
1277 body
1278 } else if let Some(else_branch) = else_body {
1279 else_branch
1280 } else {
1281 continue;
1282 };
1283
1284 use crate::language::scope::VariableTable;
1285 let child_vars = VariableTable::with_parent(variables.clone());
1286
1287 let spawn_plan = self.plan_jobs_internal(
1288 branch_to_execute,
1289 base_dir,
1290 project_root,
1291 samples,
1292 sample_cache,
1293 banks,
1294 inserts,
1295 alias_to_insert,
1296 groups,
1297 child_vars,
1298 tempo,
1299 cursor,
1300 )?;
1301 jobs.extend(spawn_plan.jobs);
1302 cursor = spawn_plan.cursor;
1303 max_time = max_time.max(spawn_plan.max_time);
1304 }
1305 _ => {}
1306 }
1307 }
1308
1309 Ok(SpawnPlan {
1310 jobs,
1311 cursor,
1312 max_time,
1313 })
1314 }
1315
1316 fn resolve_value(
1318 &self,
1319 value: &Value,
1320 variables: &crate::language::scope::VariableTable,
1321 ) -> Value {
1322 match value {
1323 Value::Identifier(name) => {
1324 if let Some(var_value) = variables.get(name) {
1326 return self.resolve_value(&var_value, variables);
1327 }
1328 value.clone()
1330 }
1331 Value::String(s) => {
1332 if let Ok(num) = s.parse::<f32>() {
1334 return Value::Number(num);
1335 }
1336 if !s.contains(' ') && !s.contains('"') {
1338 if let Some(var_value) = variables.get(s) {
1340 return self.resolve_value(&var_value, variables);
1342 }
1343 return Value::Identifier(s.clone());
1345 }
1346 value.clone()
1347 }
1348 Value::Array(items) => Value::Array(
1349 items
1350 .iter()
1351 .map(|v| self.resolve_value(v, variables))
1352 .collect(),
1353 ),
1354 Value::Map(map) => Value::Map(
1355 map.iter()
1356 .map(|(k, v)| (k.clone(), self.resolve_value(v, variables)))
1357 .collect(),
1358 ),
1359 _ => value.clone(),
1360 }
1361 }
1362
1363 fn evaluate_condition(
1365 &self,
1366 condition: &Value,
1367 variables: &crate::language::scope::VariableTable,
1368 ) -> bool {
1369 match condition {
1370 Value::Map(map) => {
1371 let operator = map.get("operator").and_then(|v| {
1373 if let Value::String(s) = v {
1374 Some(s.as_str())
1375 } else {
1376 None
1377 }
1378 });
1379
1380 let left = map.get("left").map(|v| self.resolve_value(v, variables));
1381 let right = map.get("right").map(|v| self.resolve_value(v, variables));
1382
1383 if let (Some(op), Some(left_val), Some(right_val)) = (operator, left, right) {
1384 let left_num = match left_val {
1386 Value::Number(n) => n,
1387 _ => return false,
1388 };
1389 let right_num = match right_val {
1390 Value::Number(n) => n,
1391 _ => return false,
1392 };
1393
1394 match op {
1396 ">" => left_num > right_num,
1397 "<" => left_num < right_num,
1398 ">=" => left_num >= right_num,
1399 "<=" => left_num <= right_num,
1400 "==" => (left_num - right_num).abs() < f32::EPSILON,
1401 "!=" => (left_num - right_num).abs() >= f32::EPSILON,
1402 _ => false,
1403 }
1404 } else {
1405 false
1406 }
1407 }
1408 Value::Boolean(b) => *b,
1409 Value::Identifier(name) => {
1410 if let Some(var_value) = variables.get(name) {
1412 self.evaluate_condition(&var_value, variables)
1413 } else {
1414 false
1415 }
1416 }
1417 _ => false,
1418 }
1419 }
1420
1421 fn load_sample_cached(
1422 &self,
1423 cache: &mut HashMap<PathBuf, SampleBuffer>,
1424 path: PathBuf,
1425 ) -> Result<SampleBuffer> {
1426 if let Some(existing) = cache.get(&path) {
1427 return Ok(existing.clone());
1428 }
1429 if let Ok(canonical) = std::fs::canonicalize(&path) {
1430 if let Some(existing) = cache.get(&canonical) {
1431 let sample = existing.clone();
1432 cache.insert(path.clone(), sample.clone());
1433 return Ok(sample);
1434 }
1435 }
1436 let sample = self.load_sample_from_path(&path)?;
1437 if let Ok(canonical) = std::fs::canonicalize(&path) {
1438 cache.insert(canonical, sample.clone());
1439 }
1440 cache.insert(path, sample.clone());
1441 Ok(sample)
1442 }
1443 fn load_sample_from_path(&self, path: &Path) -> Result<SampleBuffer> {
1444 let mut reader = WavReader::open(path)
1445 .with_context(|| format!("failed to open sample file: {}", path.display()))?;
1446 let spec = reader.spec();
1447 if spec.sample_format != SampleFormat::Int {
1448 return Err(anyhow!(
1449 "only PCM integer WAV samples are supported ({}): format {:?}",
1450 path.display(),
1451 spec.sample_format
1452 ));
1453 }
1454 let channels = spec.channels as usize;
1455 let samples: Vec<f32> = match spec.bits_per_sample {
1456 16 => {
1457 let raw: Result<Vec<f32>, _> = reader
1458 .samples::<i16>()
1459 .map(|sample| sample.map(|v| v as f32 / i16::MAX as f32))
1460 .collect();
1461 raw.with_context(|| {
1462 format!("failed to read samples from file: {}", path.display())
1463 })?
1464 }
1465 24 => {
1466 const I24_MAX: f32 = 8_388_607.0;
1467 let raw: Result<Vec<f32>, _> = reader
1468 .samples::<i32>()
1469 .map(|sample| sample.map(|v| v as f32 / I24_MAX))
1470 .collect();
1471 raw.with_context(|| {
1472 format!("failed to read samples from file: {}", path.display())
1473 })?
1474 }
1475 other => {
1476 return Err(anyhow!(
1477 "unsupported bit depth {} in sample {} (expected 16-bit or 24-bit)",
1478 other,
1479 path.display()
1480 ));
1481 }
1482 };
1483 Ok(SampleBuffer::new(
1484 Arc::new(samples),
1485 channels,
1486 spec.sample_rate,
1487 ))
1488 }
1489 fn write_wav(
1490 &self,
1491 path: &Path,
1492 pcm: &[f32],
1493 sample_rate: u32,
1494 requested_bit_depth: AudioBitDepth,
1495 channels: AudioChannels,
1496 ) -> Result<AudioBitDepth> {
1497 let (bit_depth, sample_format) = match requested_bit_depth {
1498 AudioBitDepth::Bit32 => (AudioBitDepth::Bit32, SampleFormat::Float),
1499 AudioBitDepth::Bit24 => (AudioBitDepth::Bit24, SampleFormat::Int),
1500 AudioBitDepth::Bit16 => (AudioBitDepth::Bit16, SampleFormat::Int),
1501 AudioBitDepth::Bit8 => (AudioBitDepth::Bit8, SampleFormat::Int),
1502 };
1503 let spec = WavSpec {
1504 channels: channels.count(),
1505 sample_rate,
1506 bits_per_sample: bit_depth.bits(),
1507 sample_format,
1508 };
1509 let mut writer = WavWriter::create(path, spec)
1510 .with_context(|| format!("failed to open WAV writer for {}", path.display()))?;
1511 match bit_depth {
1512 AudioBitDepth::Bit32 => {
1513 for sample in pcm {
1514 writer
1515 .write_sample(sample.clamp(-1.0, 1.0))
1516 .with_context(|| {
1517 format!("unable to write audio sample to {}", path.display())
1518 })?;
1519 }
1520 }
1521 AudioBitDepth::Bit24 => {
1522 for sample in pcm {
1523 let scaled = (sample.clamp(-1.0, 1.0) * 8_388_607.0).round() as i32;
1524 writer.write_sample(scaled).with_context(|| {
1525 format!("unable to write audio sample to {}", path.display())
1526 })?;
1527 }
1528 }
1529 AudioBitDepth::Bit16 => {
1530 for sample in pcm {
1531 let scaled = (sample.clamp(-1.0, 1.0) * i16::MAX as f32).round() as i16;
1532 writer.write_sample(scaled).with_context(|| {
1533 format!("unable to write audio sample to {}", path.display())
1534 })?;
1535 }
1536 }
1537 AudioBitDepth::Bit8 => {
1538 for sample in pcm {
1539 let scaled = (sample.clamp(-1.0, 1.0) * i8::MAX as f32).round() as i8;
1540 writer.write_sample(scaled).with_context(|| {
1541 format!("unable to write audio sample to {}", path.display())
1542 })?;
1543 }
1544 }
1545 }
1546 writer.finalize().with_context(|| {
1547 format!("failed to finalize WAV file writer for {}", path.display())
1548 })?;
1549 Ok(bit_depth)
1550 }
1551
1552 fn write_mp3(
1556 &self,
1557 path: &Path,
1558 pcm: &[f32],
1559 sample_rate: u32,
1560 requested_bit_depth: AudioBitDepth,
1561 channels: AudioChannels,
1562 ) -> Result<AudioBitDepth> {
1563 self.logger.warn(format!(
1566 "MP3 encoding not yet fully implemented. Exporting as WAV to: {}",
1567 path.display()
1568 ));
1569
1570 self.write_wav(path, pcm, sample_rate, requested_bit_depth, channels)
1578 }
1579
1580 fn write_flac(
1584 &self,
1585 path: &Path,
1586 pcm: &[f32],
1587 sample_rate: u32,
1588 requested_bit_depth: AudioBitDepth,
1589 channels: AudioChannels,
1590 ) -> Result<AudioBitDepth> {
1591 self.logger.warn(format!(
1594 "FLAC encoding not yet fully implemented. Exporting as WAV to: {}",
1595 path.display()
1596 ));
1597
1598 self.write_wav(path, pcm, sample_rate, requested_bit_depth, channels)
1606 }
1607}
1608
1609fn find_project_root(entry_path: &Path) -> PathBuf {
1610 let mut current = entry_path
1611 .parent()
1612 .map(Path::to_path_buf)
1613 .unwrap_or_else(|| PathBuf::from("."));
1614 loop {
1615 if current.join(".deva").is_dir()
1616 || current.join("devalang.json").is_file()
1617 || current.join("devalang.toml").is_file()
1618 || current.join("Cargo.toml").is_file()
1619 {
1620 return current;
1621 }
1622 if !current.pop() {
1623 break;
1624 }
1625 }
1626 entry_path
1627 .parent()
1628 .map(Path::to_path_buf)
1629 .unwrap_or_else(|| PathBuf::from("."))
1630}
1631
1632fn resolve_sample_reference(base_dir: &Path, path: &str) -> PathBuf {
1633 let candidate = Path::new(path);
1634 if candidate.is_absolute() {
1635 candidate.to_path_buf()
1636 } else {
1637 base_dir.join(candidate)
1638 }
1639}
1640
1641fn split_trigger_entity(entity: &str) -> (&str, Option<&str>) {
1642 if let Some((alias, rest)) = entity.split_once('.') {
1643 if rest.is_empty() {
1644 (alias, None)
1645 } else {
1646 (alias, Some(rest))
1647 }
1648 } else {
1649 (entity, None)
1650 }
1651}
1652
1653fn register_route_for_alias(
1654 inserts: &mut HashMap<String, Option<String>>,
1655 alias_map: &mut HashMap<String, String>,
1656 alias: &str,
1657 prefix: &str,
1658 parent: Option<&str>,
1659) -> String {
1660 let sanitized = AudioMixer::sanitize_label(alias);
1661 let insert_name = format!("{}::{}", prefix, sanitized);
1662 if insert_name != MASTER_INSERT {
1663 let normalized_parent = parent
1664 .filter(|label| !is_master_label(label))
1665 .map(|label| label.to_string());
1666 match inserts.entry(insert_name.clone()) {
1667 Entry::Occupied(mut entry) => {
1668 entry.insert(normalized_parent.clone());
1669 }
1670 Entry::Vacant(slot) => {
1671 slot.insert(normalized_parent.clone());
1672 }
1673 }
1674 }
1675 alias_map
1676 .entry(alias.to_string())
1677 .or_insert_with(|| insert_name.clone());
1678 insert_name
1679}
1680
1681fn is_master_label(label: &str) -> bool {
1682 let lower = label.trim().trim_start_matches('$').to_ascii_lowercase();
1683 lower == MASTER_INSERT
1684}
1685
1686fn resolve_parent_for_alias(
1687 inserts: &mut HashMap<String, Option<String>>,
1688 alias_map: &mut HashMap<String, String>,
1689 target: &str,
1690) -> Option<String> {
1691 if is_master_label(target) {
1692 return None;
1693 }
1694 if let Some(existing) = alias_map.get(target) {
1695 return Some(existing.clone());
1696 }
1697 if target.contains("::") {
1698 inserts.entry(target.to_string()).or_insert(None);
1699 return Some(target.to_string());
1700 }
1701 let insert = register_route_for_alias(inserts, alias_map, target, "bus", None);
1702 Some(insert)
1703}
1704
1705fn value_as_string(value: &Value) -> Option<String> {
1706 match value {
1707 Value::String(v)
1708 | Value::Identifier(v)
1709 | Value::Sample(v)
1710 | Value::Beat(v)
1711 | Value::Midi(v) => Some(v.clone()),
1712 Value::Number(number) => Some(number.to_string()),
1713 _ => None,
1714 }
1715}
1716
1717fn default_bank_alias(identifier: &str) -> String {
1718 let candidate = identifier
1719 .rsplit(|c| c == '.' || c == '/' || c == '\\')
1720 .next()
1721 .unwrap_or(identifier);
1722 candidate.replace('-', "_").replace(' ', "_")
1723}
1724
1725fn duration_in_seconds(duration: &DurationValue, tempo: f32) -> Option<f32> {
1726 match duration {
1727 DurationValue::Milliseconds(ms) => Some(ms / 1_000.0),
1728 DurationValue::Beats(beats) => Some(beats_to_seconds(*beats, tempo)),
1729 DurationValue::Number(value) => Some(value / 1_000.0),
1730 DurationValue::Beat(value) => parse_fraction(value).map(|b| beats_to_seconds(b, tempo)),
1731 DurationValue::Identifier(_) | DurationValue::Auto => None,
1732 }
1733}
1734
1735fn beats_to_seconds(beats: f32, tempo: f32) -> f32 {
1736 if tempo <= 0.0 {
1737 return 0.0;
1738 }
1739 beats * (60.0 / tempo)
1740}
1741
1742fn parse_fraction(token: &str) -> Option<f32> {
1743 let mut split = token.split('/');
1744 let numerator: f32 = split.next()?.trim().parse().ok()?;
1745 let denominator: f32 = split.next()?.trim().parse().ok()?;
1746 if denominator.abs() < f32::EPSILON {
1747 return None;
1748 }
1749 Some(numerator / denominator)
1750}
1751
1752fn normalize(buffer: &mut [f32]) {
1753 let peak = buffer
1754 .iter()
1755 .fold(0.0_f32, |acc, sample| acc.max(sample.abs()));
1756 if peak > 1.0 {
1757 let inv = 1.0 / peak;
1758 for sample in buffer.iter_mut() {
1759 *sample *= inv;
1760 }
1761 }
1762}
1763
1764fn calculate_rms(buffer: &[f32]) -> f32 {
1765 if buffer.is_empty() {
1766 return 0.0;
1767 }
1768 let sum = buffer.iter().map(|sample| sample * sample).sum::<f32>();
1769 (sum / buffer.len() as f32).sqrt()
1770}
1771
1772fn apply_trigger_effects(
1778 buffer: &mut Vec<f32>,
1779 effects: &Value,
1780 sample_rate: u32,
1781 channels: usize,
1782) -> Result<()> {
1783 let Value::Map(effect_map) = effects else {
1784 return Ok(());
1785 };
1786
1787 if let Some(Value::Boolean(true)) = effect_map.get("reverse") {
1789 buffer.reverse();
1790 }
1791
1792 if let Some(Value::Number(pitch)) = effect_map.get("pitch") {
1794 let rate_multiplier = 2.0_f32.powf(*pitch);
1796 *buffer = resample_buffer(buffer, rate_multiplier, channels);
1797 }
1798
1799 if let Some(Value::Number(velocity)) = effect_map.get("velocity") {
1801 let gain = (velocity / 127.0).clamp(0.0, 2.0); for sample in buffer.iter_mut() {
1803 *sample *= gain;
1804 }
1805 }
1806
1807 if let Some(Value::Number(delay_ms)) = effect_map.get("delay") {
1809 let feedback = effect_map
1810 .get("delay_feedback")
1811 .and_then(|v| {
1812 if let Value::Number(f) = v {
1813 Some(*f)
1814 } else {
1815 None
1816 }
1817 })
1818 .unwrap_or(0.5);
1819 apply_delay(buffer, *delay_ms, sample_rate, channels, feedback)?;
1820 }
1821
1822 if let Some(Value::Number(reverb_amount)) = effect_map.get("reverb") {
1824 apply_reverb(buffer, *reverb_amount, sample_rate, channels)?;
1825 }
1826
1827 Ok(())
1828}
1829
1830fn resample_buffer(buffer: &[f32], rate: f32, channels: usize) -> Vec<f32> {
1832 if rate <= 0.0 || (rate - 1.0).abs() < 0.001 {
1833 return buffer.to_vec();
1834 }
1835
1836 let frames = buffer.len() / channels.max(1);
1837 let new_frames = (frames as f32 / rate).max(1.0) as usize;
1838 let mut result = Vec::with_capacity(new_frames * channels);
1839
1840 for i in 0..new_frames {
1841 let src_frame_f = i as f32 * rate;
1842 let src_frame = src_frame_f as usize;
1843
1844 if src_frame >= frames {
1845 for _ in 0..channels {
1847 result.push(0.0);
1848 }
1849 continue;
1850 }
1851
1852 let frac = src_frame_f - src_frame as f32;
1854 let next_frame = (src_frame + 1).min(frames - 1);
1855
1856 for ch in 0..channels {
1857 let sample1 = buffer[src_frame * channels + ch];
1858 let sample2 = buffer[next_frame * channels + ch];
1859 let interpolated = sample1 + (sample2 - sample1) * frac;
1860 result.push(interpolated);
1861 }
1862 }
1863
1864 result
1865}
1866
1867fn apply_delay(
1869 buffer: &mut Vec<f32>,
1870 delay_ms: f32,
1871 sample_rate: u32,
1872 channels: usize,
1873 feedback: f32,
1874) -> Result<()> {
1875 let delay_frames = ((delay_ms / 1000.0) * sample_rate as f32) as usize;
1876 let delay_samples = delay_frames * channels;
1877
1878 if delay_samples == 0 {
1879 return Ok(());
1880 }
1881
1882 let original_len = buffer.len();
1883 let frames = original_len / channels.max(1);
1884
1885 let extended_frames = frames + delay_frames * 3;
1887 buffer.resize(extended_frames * channels, 0.0);
1888
1889 let feedback_clamped = feedback.clamp(0.0, 0.9);
1891 for i in delay_samples..buffer.len() {
1892 buffer[i] += buffer[i - delay_samples] * feedback_clamped;
1893 }
1894
1895 Ok(())
1896}
1897
1898fn apply_reverb(
1900 buffer: &mut Vec<f32>,
1901 amount: f32,
1902 sample_rate: u32,
1903 channels: usize,
1904) -> Result<()> {
1905 if amount <= 0.0 {
1906 return Ok(());
1907 }
1908
1909 let amount_clamped = amount.clamp(0.0, 1.0);
1910
1911 let delay_times = [13.0, 23.0, 37.0, 53.0, 71.0, 97.0];
1913
1914 let original = buffer.clone();
1915 let frames = original.len() / channels.max(1);
1916
1917 let max_delay_frames = delay_times
1919 .iter()
1920 .map(|&ms| ((ms / 1000.0) * sample_rate as f32) as usize)
1921 .max()
1922 .unwrap_or(0);
1923
1924 buffer.resize((frames + max_delay_frames) * channels, 0.0);
1925
1926 for (tap_idx, &delay_ms) in delay_times.iter().enumerate() {
1928 let delay_frames = ((delay_ms / 1000.0) * sample_rate as f32) as usize;
1929 let delay_samples = delay_frames * channels;
1930
1931 if delay_samples >= original.len() {
1932 continue;
1933 }
1934
1935 let tap_gain = amount_clamped * (0.3 / (tap_idx + 1) as f32);
1937
1938 for i in 0..original.len() {
1939 let target_idx = i + delay_samples;
1940 if target_idx < buffer.len() {
1941 buffer[target_idx] += original[i] * tap_gain;
1942 }
1943 }
1944 }
1945
1946 Ok(())
1947}