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 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 interpreter.suppress_print = true;
118
119 let buffer = interpreter.interpret(statements)?;
126
127 let output_root = output_root.as_ref();
128 let audio_dir = output_root.join("audio");
129
130 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 write_wav(
192 &output_path,
193 &[],
194 sample_rate,
195 requested_bit_depth,
196 channels,
197 )?
198 };
199
200 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 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}