devalang_wasm/services/build/outputs/
audio.rs

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 NEW AUDIO INTERPRETER FOR SYNTH RENDERING
95        use crate::engine::audio::interpreter::driver::AudioInterpreter;
96        let mut interpreter = AudioInterpreter::new(sample_rate);
97        let buffer = interpreter.interpret(statements)?;
98
99        // If interpreter produced audio, use it
100        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        // FALLBACK TO OLD SYSTEM IF NO AUDIO GENERATED
143        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    /// Render audio and export to multiple formats
253    #[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 NEW AUDIO INTERPRETER FOR SYNTH RENDERING
278        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        // Export to all requested formats
298        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                    // Export MIDI if we have audio events
367                    let output_path = audio_dir.join(format!("{}.mid", module_name));
368
369                    // Get events from interpreter
370                    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        // Store groups for spawn execution
424        let mut groups = HashMap::<String, Vec<Statement>>::new();
425
426        // Store patterns for spawn execution
427        let mut patterns = HashMap::<String, (String, String)>::new(); // name -> (target, pattern_string)
428
429        // Variable table with scope management
430        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        // First pass: register groups and patterns
439        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        // Second pass: process statements
460        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                    // Resolve entity through variables (e.g., if entity is "kick" and kick = drums.kick)
555                    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                        // Apply effects if present
617                        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                    // Resolve value through variables first
708                    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, // Convert ms to seconds
714                        _ => beats_to_seconds(0.25, tempo),
715                    };
716                    cursor += duration;
717                    max_time = max_time.max(cursor);
718                }
719                StatementKind::Group { .. } => {
720                    // Groups are registered in first pass, skip execution here
721                }
722                StatementKind::Spawn { name, .. } => {
723                    // Try to execute group first
724                    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                        // Create child scope for group
730                        use crate::language::scope::VariableTable;
731                        let child_vars = VariableTable::with_parent(variables.clone());
732
733                        // Process group statements recursively
734                        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                        // Merge jobs from spawn
750                        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                        // Execute pattern
763                        self.logger
764                            .debug(format!("Spawning pattern '{}' at {}s", name, cursor));
765                        let spawn_start = cursor;
766
767                        // Parse pattern string (e.g. "x--- x--- x--- x---")
768                        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                            // Calculate step duration: 4 beats divided by number of steps
774                            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                                    // Resolve the trigger
782                                    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                    // Execute for loop: for i in [1, 2, 3]:
849                    let loop_start = cursor;
850
851                    // Resolve iterable
852                    let resolved_iterable = self.resolve_value(iterable, &variables);
853                    let items = match resolved_iterable {
854                        Value::Array(arr) => arr,
855                        Value::Number(n) => {
856                            // If number, create range [0, 1, 2, ..., n-1]
857                            (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                        // Create child scope with iterator variable
874                        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                        // Execute body
879                        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                    // Execute loop: loop 3:
906                    let loop_start = cursor;
907
908                    // Resolve count
909                    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                        // Create child scope
923                        use crate::language::scope::VariableTable;
924                        let child_vars = VariableTable::with_parent(variables.clone());
925
926                        // Execute body
927                        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                    // Evaluate condition
958                    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                    // Create child scope
974                    use crate::language::scope::VariableTable;
975                    let child_vars = VariableTable::with_parent(variables.clone());
976
977                    // Execute branch
978                    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                    // Resolve entity through variables (e.g., if entity is "kick" and kick = drums.kick)
1062                    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                        // Apply effects if present
1109                        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                    // Resolve value through variables first
1165                    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, // Convert ms to seconds
1171                        _ => beats_to_seconds(0.25, tempo),
1172                    };
1173                    cursor += duration;
1174                    max_time = max_time.max(cursor);
1175                }
1176                StatementKind::Spawn { name, .. } => {
1177                    // Nested spawn support
1178                    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                    // Resolve iterable
1207                    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    /// Resolve a value, replacing identifiers with their variable values
1317    fn resolve_value(
1318        &self,
1319        value: &Value,
1320        variables: &crate::language::scope::VariableTable,
1321    ) -> Value {
1322        match value {
1323            Value::Identifier(name) => {
1324                // Try to resolve as variable, then recursively resolve the result
1325                if let Some(var_value) = variables.get(name) {
1326                    return self.resolve_value(&var_value, variables);
1327                }
1328                // Return as-is if not found (might be a reference to something else)
1329                value.clone()
1330            }
1331            Value::String(s) => {
1332                // Try to parse as number first
1333                if let Ok(num) = s.parse::<f32>() {
1334                    return Value::Number(num);
1335                }
1336                // Check if it looks like an identifier (no spaces, quotes)
1337                if !s.contains(' ') && !s.contains('"') {
1338                    // Check if there's a variable with this name
1339                    if let Some(var_value) = variables.get(s) {
1340                        // Recursively resolve
1341                        return self.resolve_value(&var_value, variables);
1342                    }
1343                    // Otherwise, return as Identifier for entity resolution (drums.kick)
1344                    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    /// Evaluate a condition to a boolean
1364    fn evaluate_condition(
1365        &self,
1366        condition: &Value,
1367        variables: &crate::language::scope::VariableTable,
1368    ) -> bool {
1369        match condition {
1370            Value::Map(map) => {
1371                // Condition is a comparison: { operator: ">", left: "tempo", right: 120 }
1372                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                    // Extract numbers from values
1385                    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                    // Evaluate comparison
1395                    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                // Try to resolve as variable
1411                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    /// Write MP3 file from PCM samples
1553    /// Note: Currently exports as WAV with .mp3 extension as a placeholder
1554    /// Full MP3 encoding requires additional dependencies (e.g., lame, minimp3)
1555    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        // For now, log a message and create a WAV file
1564        // In production, this would use mp3lame-encoder or similar
1565        self.logger.warn(format!(
1566            "MP3 encoding not yet fully implemented. Exporting as WAV to: {}",
1567            path.display()
1568        ));
1569
1570        // TODO: Implement actual MP3 encoding
1571        // Options:
1572        // 1. Use mp3lame-encoder crate (requires lame library)
1573        // 2. Use minimp3 encoder (pure Rust)
1574        // 3. Shell out to ffmpeg/lame binary
1575
1576        // For now, write as WAV for testing
1577        self.write_wav(path, pcm, sample_rate, requested_bit_depth, channels)
1578    }
1579
1580    /// Write FLAC file from PCM samples
1581    /// Note: Currently exports as WAV with .flac extension as a placeholder
1582    /// Full FLAC encoding requires additional dependencies (e.g., claxon)
1583    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        // For now, log a message and create a WAV file
1592        // In production, this would use claxon or flac crate
1593        self.logger.warn(format!(
1594            "FLAC encoding not yet fully implemented. Exporting as WAV to: {}",
1595            path.display()
1596        ));
1597
1598        // TODO: Implement actual FLAC encoding
1599        // Options:
1600        // 1. Use claxon crate (pure Rust encoder)
1601        // 2. Use flac-sys (bindings to libFLAC)
1602        // 3. Shell out to flac binary
1603
1604        // For now, write as WAV for testing
1605        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
1772// ============================================================================
1773// EFFECT PROCESSING FUNCTIONS
1774// ============================================================================
1775
1776/// Apply effects defined in trigger metadata to sample buffer
1777fn 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    // 1. Reverse - flip buffer backwards
1788    if let Some(Value::Boolean(true)) = effect_map.get("reverse") {
1789        buffer.reverse();
1790    }
1791
1792    // 2. Pitch shift (via simple resampling)
1793    if let Some(Value::Number(pitch)) = effect_map.get("pitch") {
1794        // pitch: -1.0 = down octave, 0.0 = no change, +1.0 = up octave
1795        let rate_multiplier = 2.0_f32.powf(*pitch);
1796        *buffer = resample_buffer(buffer, rate_multiplier, channels);
1797    }
1798
1799    // 3. Velocity (volume adjustment)
1800    if let Some(Value::Number(velocity)) = effect_map.get("velocity") {
1801        let gain = (velocity / 127.0).clamp(0.0, 2.0); // MIDI velocity to gain
1802        for sample in buffer.iter_mut() {
1803            *sample *= gain;
1804        }
1805    }
1806
1807    // 4. Delay effect
1808    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    // 5. Reverb effect
1823    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
1830/// Simple resampling for pitch shift effect
1831fn 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            // Pad with zeros if we've run out
1846            for _ in 0..channels {
1847                result.push(0.0);
1848            }
1849            continue;
1850        }
1851
1852        // Linear interpolation between frames
1853        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
1867/// Apply delay effect (echo)
1868fn 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    // Extend buffer to accommodate delay tail (3 echoes)
1886    let extended_frames = frames + delay_frames * 3;
1887    buffer.resize(extended_frames * channels, 0.0);
1888
1889    // Apply feedback delay
1890    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
1898/// Apply simple reverb (multi-tap delay)
1899fn 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    // Prime number delays for natural reverb (in milliseconds)
1912    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    // Find longest delay to extend buffer
1918    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    // Apply each delay tap
1927    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        // Decreasing gain for each tap
1936        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}