devalang_wasm/services/build/outputs/audio/
builder.rs1#![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 let formats = if requested_formats.is_empty() {
65 vec![AudioFormat::Wav]
66 } else {
67 requested_formats.to_vec()
68 };
69
70 let mut summaries = Vec::new();
71 for format in formats {
72 summaries.push(self.render(
73 statements,
74 _entry_path,
75 output_root,
76 module_name,
77 format,
78 requested_bit_depth,
79 channels,
80 sample_rate,
81 ResampleQuality::Sinc24,
82 bpm,
83 )?);
84 }
85
86 let total_time = start.elapsed();
87 let primary = summaries
88 .first()
89 .cloned()
90 .context("no audio formats were rendered")?;
91 let exported = summaries
92 .iter()
93 .map(|summary| (summary.format, summary.path.clone()))
94 .collect();
95
96 Ok(MultiFormatRenderSummary {
97 primary_path: primary.path,
98 primary_format: primary.format,
99 exported_formats: exported,
100 bit_depth: primary.bit_depth,
101 rms: primary.rms,
102 render_time: total_time,
103 audio_length: primary.audio_length,
104 })
105 }
106
107 pub fn render(
108 &self,
109 statements: &[Statement],
110 _entry_path: &Path,
111 output_root: impl AsRef<Path>,
112 module_name: &str,
113 requested_format: AudioFormat,
114 requested_bit_depth: AudioBitDepth,
115 channels: AudioChannels,
116 sample_rate: u32,
117 _resample: ResampleQuality,
118 bpm: f32,
119 ) -> Result<AudioRenderSummary> {
120 use crate::engine::audio::interpreter::driver::AudioInterpreter;
121
122 let mut interpreter = AudioInterpreter::new(sample_rate);
123 interpreter.suppress_print = true;
128
129 let buffer = interpreter.interpret(statements)?;
136
137 let output_root = output_root.as_ref();
138 let audio_dir = output_root.join("audio");
139
140 let module_path = Path::new(module_name);
143 let (output_subdir, file_name) = if let Some(parent) = module_path.parent() {
144 let parent_str = parent.to_string_lossy();
145 if parent_str.is_empty() || parent_str == "." {
146 (String::new(), module_name.to_string())
147 } else {
148 let file_name = module_path
149 .file_name()
150 .map(|s| s.to_string_lossy().to_string())
151 .unwrap_or_else(|| module_name.to_string());
152 (parent_str.to_string(), file_name)
153 }
154 } else {
155 (String::new(), module_name.to_string())
156 };
157
158 let full_audio_dir = if output_subdir.is_empty() {
159 audio_dir.clone()
160 } else {
161 audio_dir.join(&output_subdir)
162 };
163
164 std::fs::create_dir_all(&full_audio_dir).with_context(|| {
165 format!(
166 "failed to create audio output directory: {}",
167 full_audio_dir.display()
168 )
169 })?;
170
171 let output_path = full_audio_dir.join(format!(
172 "{}.{}",
173 file_name,
174 requested_format.file_extension()
175 ));
176
177 let mut rms = 0.0f32;
178 let audio_length = if buffer.is_empty() {
179 Duration::from_secs(0)
180 } else {
181 rms = calculate_rms(&buffer);
182 let channel_count = channels.count() as usize;
183 let frames = if channel_count == 0 {
184 0
185 } else {
186 buffer.len() / channel_count
187 };
188 if sample_rate == 0 {
189 Duration::from_secs(0)
190 } else {
191 Duration::from_secs_f64(frames as f64 / sample_rate as f64)
192 }
193 };
194
195 let applied = match requested_format {
196 AudioFormat::Wav => {
197 if !buffer.is_empty() {
198 write_wav(
199 &output_path,
200 &buffer,
201 sample_rate,
202 requested_bit_depth,
203 channels,
204 )?
205 } else {
206 write_wav(
208 &output_path,
209 &[],
210 sample_rate,
211 requested_bit_depth,
212 channels,
213 )?
214 }
215 }
216 AudioFormat::Mid => {
217 let midi_bpm = if interpreter.bpm.is_finite() && interpreter.bpm > 0.0 {
218 interpreter.bpm
219 } else {
220 bpm
221 };
222 crate::engine::audio::midi::export_midi_file(
223 &interpreter.events.events,
224 &output_path,
225 midi_bpm,
226 )?;
227 requested_bit_depth
228 }
229 AudioFormat::Mp3 | AudioFormat::Flac => {
230 anyhow::bail!(
231 "{:?} export is not implemented in the CLI build pipeline yet",
232 requested_format
233 );
234 }
235 };
236
237 let log_path = output_path.with_file_name(format!("{}.printlog", file_name));
239 if !interpreter.events.logs.is_empty() {
240 if let Ok(mut f) = std::fs::File::create(&log_path) {
241 use std::io::Write;
242 for (t, msg) in &interpreter.events.logs {
243 let _ = writeln!(f, "{:.6}\t{}", t, msg);
245 }
246 }
247 }
248
249 Ok(AudioRenderSummary {
250 path: output_path,
251 format: requested_format,
252 bit_depth: applied,
253 rms,
254 render_time: Duration::from_secs(0),
255 audio_length,
256 })
257 }
258}