devalang_wasm/services/build/outputs/audio/
builder.rs

1#![cfg(feature = "cli")]
2
3use crate::engine::audio::settings::{AudioBitDepth, AudioChannels, AudioFormat, ResampleQuality};
4use crate::language::syntax::ast::Statement;
5use crate::tools::logger::Logger;
6use anyhow::{Context, Result};
7use std::path::{Path, PathBuf};
8use std::sync::Arc;
9use std::time::{Duration, Instant};
10
11use crate::services::build::outputs::audio::helpers::calculate_rms;
12use crate::services::build::outputs::audio::writer::write_wav;
13
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
35#[derive(Clone)]
36pub struct AudioBuilder {
37    _logger: Arc<Logger>,
38}
39
40impl AudioBuilder {
41    pub fn new(
42        _log_writer: crate::services::build::outputs::logs::LogWriter,
43        logger: Arc<Logger>,
44    ) -> Self {
45        Self { _logger: logger }
46    }
47
48    #[allow(clippy::too_many_arguments)]
49    pub fn render_all_formats(
50        &self,
51        statements: &[Statement],
52        _entry_path: &Path,
53        output_root: &Path,
54        module_name: &str,
55        requested_formats: &[AudioFormat],
56        requested_bit_depth: AudioBitDepth,
57        channels: AudioChannels,
58        sample_rate: u32,
59        _resample: ResampleQuality,
60        _bpm: f32,
61    ) -> Result<MultiFormatRenderSummary> {
62        let start = Instant::now();
63
64        // Pick primary format
65        let primary_fmt = if requested_formats.is_empty() {
66            AudioFormat::Wav
67        } else {
68            requested_formats[0]
69        };
70
71        let audio_summary = self.render(
72            statements,
73            _entry_path,
74            output_root,
75            module_name,
76            primary_fmt,
77            requested_bit_depth,
78            channels,
79            sample_rate,
80            ResampleQuality::Sinc24,
81        )?;
82
83        let exported = vec![(audio_summary.format, audio_summary.path.clone())];
84
85        let total_time = start.elapsed();
86
87        Ok(MultiFormatRenderSummary {
88            primary_path: audio_summary.path,
89            primary_format: audio_summary.format,
90            exported_formats: exported,
91            bit_depth: audio_summary.bit_depth,
92            rms: audio_summary.rms,
93            render_time: total_time,
94            audio_length: audio_summary.audio_length,
95        })
96    }
97
98    pub fn render(
99        &self,
100        statements: &[Statement],
101        _entry_path: &Path,
102        output_root: impl AsRef<Path>,
103        module_name: &str,
104        requested_format: AudioFormat,
105        requested_bit_depth: AudioBitDepth,
106        channels: AudioChannels,
107        sample_rate: u32,
108        _resample: ResampleQuality,
109    ) -> Result<AudioRenderSummary> {
110        use crate::engine::audio::interpreter::driver::AudioInterpreter;
111
112        let mut interpreter = AudioInterpreter::new(sample_rate);
113        // During offline rendering we must not emit prints to stdout/stderr immediately.
114        // Schedule prints into the interpreter event list and (optionally) replay them
115        // in realtime during the render so the user can see PRINT messages as if
116        // the audio was playing.
117        interpreter.suppress_print = true;
118
119        // During offline rendering we schedule prints into the interpreter.events.logs
120        // and write them to a sidecar `.printlog` file for later replay by the
121        // live playback engine. We do NOT replay prints in real-time during the
122        // build here to avoid duplicate prints when the live player replays the
123        // same scheduled logs.
124
125        let buffer = interpreter.interpret(statements)?;
126
127        let output_root = output_root.as_ref();
128        let audio_dir = output_root.join("audio");
129
130        // module_name may be a relative path like "demos/hello-sound/index"
131        // Split it into directory part and file part
132        let module_path = Path::new(module_name);
133        let (output_subdir, file_name) = if let Some(parent) = module_path.parent() {
134            let parent_str = parent.to_string_lossy();
135            if parent_str.is_empty() || parent_str == "." {
136                (String::new(), module_name.to_string())
137            } else {
138                let file_name = module_path
139                    .file_name()
140                    .map(|s| s.to_string_lossy().to_string())
141                    .unwrap_or_else(|| module_name.to_string());
142                (parent_str.to_string(), file_name)
143            }
144        } else {
145            (String::new(), module_name.to_string())
146        };
147
148        let full_audio_dir = if output_subdir.is_empty() {
149            audio_dir.clone()
150        } else {
151            audio_dir.join(&output_subdir)
152        };
153
154        std::fs::create_dir_all(&full_audio_dir).with_context(|| {
155            format!(
156                "failed to create audio output directory: {}",
157                full_audio_dir.display()
158            )
159        })?;
160
161        let output_path = full_audio_dir.join(format!("{}.wav", file_name));
162
163        let mut rms = 0.0f32;
164        let audio_length = if buffer.is_empty() {
165            Duration::from_secs(0)
166        } else {
167            rms = calculate_rms(&buffer);
168            let channel_count = channels.count() as usize;
169            let frames = if channel_count == 0 {
170                0
171            } else {
172                buffer.len() / channel_count
173            };
174            if sample_rate == 0 {
175                Duration::from_secs(0)
176            } else {
177                Duration::from_secs_f64(frames as f64 / sample_rate as f64)
178            }
179        };
180
181        let applied = if !buffer.is_empty() {
182            write_wav(
183                &output_path,
184                &buffer,
185                sample_rate,
186                requested_bit_depth,
187                channels,
188            )?
189        } else {
190            // Even if buffer is empty, create an empty WAV file
191            write_wav(
192                &output_path,
193                &[],
194                sample_rate,
195                requested_bit_depth,
196                channels,
197            )?
198        };
199
200        // Write scheduled print events sidecar for live playback to consume.
201        let log_path = output_path.with_file_name(format!("{}.printlog", file_name));
202        if !interpreter.events.logs.is_empty() {
203            if let Ok(mut f) = std::fs::File::create(&log_path) {
204                use std::io::Write;
205                for (t, msg) in &interpreter.events.logs {
206                    // time in seconds (float) TAB message NEWLINE
207                    let _ = writeln!(f, "{:.6}\t{}", t, msg);
208                }
209            }
210        }
211
212        Ok(AudioRenderSummary {
213            path: output_path,
214            format: requested_format,
215            bit_depth: applied,
216            rms,
217            render_time: Duration::from_secs(0),
218            audio_length,
219        })
220    }
221}