devalang_wasm/engine/audio/
encoders.rs1use anyhow::{Result, anyhow};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum AudioFormat {
15 Wav,
16 Mp3,
17 Ogg,
18 Flac,
19 Opus,
20}
21
22impl AudioFormat {
23 pub fn from_str(s: &str) -> Option<Self> {
24 match s.to_lowercase().as_str() {
25 "wav" => Some(Self::Wav),
26 "mp3" => Some(Self::Mp3),
27 "ogg" | "vorbis" => Some(Self::Ogg),
28 "flac" => Some(Self::Flac),
29 "opus" => Some(Self::Opus),
30 _ => None,
31 }
32 }
33
34 pub fn as_str(&self) -> &'static str {
35 match self {
36 Self::Wav => "wav",
37 Self::Mp3 => "mp3",
38 Self::Ogg => "ogg",
39 Self::Flac => "flac",
40 Self::Opus => "opus",
41 }
42 }
43
44 pub fn is_supported(&self) -> bool {
45 matches!(self, Self::Wav | Self::Mp3)
46 }
47
48 pub fn file_extension(&self) -> &'static str {
49 match self {
50 Self::Wav => "wav",
51 Self::Mp3 => "mp3",
52 Self::Ogg => "ogg",
53 Self::Flac => "flac",
54 Self::Opus => "opus",
55 }
56 }
57
58 pub fn mime_type(&self) -> &'static str {
59 match self {
60 Self::Wav => "audio/wav",
61 Self::Mp3 => "audio/mpeg",
62 Self::Ogg => "audio/ogg",
63 Self::Flac => "audio/flac",
64 Self::Opus => "audio/opus",
65 }
66 }
67}
68
69#[derive(Debug, Clone)]
71pub struct EncoderOptions {
72 pub format: AudioFormat,
73 pub sample_rate: u32,
74 pub bit_depth: u8, pub bitrate_kbps: u32, pub quality: f32, }
78
79impl Default for EncoderOptions {
80 fn default() -> Self {
81 Self {
82 format: AudioFormat::Wav,
83 sample_rate: 44100,
84 bit_depth: 16,
85 bitrate_kbps: 192,
86 quality: 5.0,
87 }
88 }
89}
90
91impl EncoderOptions {
92 pub fn wav(sample_rate: u32, bit_depth: u8) -> Self {
93 Self {
94 format: AudioFormat::Wav,
95 sample_rate,
96 bit_depth,
97 ..Default::default()
98 }
99 }
100
101 pub fn mp3(sample_rate: u32, bitrate_kbps: u32) -> Self {
102 Self {
103 format: AudioFormat::Mp3,
104 sample_rate,
105 bitrate_kbps,
106 ..Default::default()
107 }
108 }
109
110 pub fn ogg(sample_rate: u32, quality: f32) -> Self {
111 Self {
112 format: AudioFormat::Ogg,
113 sample_rate,
114 quality,
115 ..Default::default()
116 }
117 }
118
119 pub fn flac(sample_rate: u32, bit_depth: u8) -> Self {
120 Self {
121 format: AudioFormat::Flac,
122 sample_rate,
123 bit_depth,
124 ..Default::default()
125 }
126 }
127}
128
129pub fn encode_audio(pcm_samples: &[f32], options: &EncoderOptions) -> Result<Vec<u8>> {
131 match options.format {
132 AudioFormat::Wav => encode_wav(pcm_samples, options),
133 AudioFormat::Mp3 => {
134 #[cfg(feature = "cli")]
135 {
136 return encode_mp3(pcm_samples, options);
137 }
138 #[cfg(not(feature = "cli"))]
139 {
140 return Err(anyhow!(
141 "MP3 export not available in this build (disabled for WASM)."
142 ));
143 }
144 }
145 AudioFormat::Ogg => encode_ogg(pcm_samples, options),
146 AudioFormat::Flac => encode_flac(pcm_samples, options),
147 AudioFormat::Opus => encode_opus(pcm_samples, options),
148 }
149}
150
151#[cfg(any(feature = "cli", feature = "wasm"))]
153fn encode_wav(pcm_samples: &[f32], options: &EncoderOptions) -> Result<Vec<u8>> {
154 use hound::{SampleFormat, WavSpec, WavWriter};
155 use std::io::Cursor;
156
157 let spec = WavSpec {
158 channels: 2,
159 sample_rate: options.sample_rate,
160 bits_per_sample: options.bit_depth as u16,
161 sample_format: if options.bit_depth == 32 {
162 SampleFormat::Float
163 } else {
164 SampleFormat::Int
165 },
166 };
167
168 let mut cursor = Cursor::new(Vec::new());
169 let mut writer = WavWriter::new(&mut cursor, spec)
170 .map_err(|e| anyhow!("Failed to create WAV writer: {}", e))?;
171
172 match options.bit_depth {
174 16 => {
175 for &sample in pcm_samples {
176 let clamped = sample.clamp(-1.0, 1.0);
177 let i16_sample = (clamped * 32767.0) as i16;
178 writer
179 .write_sample(i16_sample)
180 .map_err(|e| anyhow!("Failed to write sample: {}", e))?;
181 writer
182 .write_sample(i16_sample)
183 .map_err(|e| anyhow!("Failed to write sample: {}", e))?;
184 }
185 }
186 24 => {
187 for &sample in pcm_samples {
188 let clamped = sample.clamp(-1.0, 1.0);
189 let i24_sample = (clamped * 8388607.0) as i32;
190 writer
191 .write_sample(i24_sample)
192 .map_err(|e| anyhow!("Failed to write sample: {}", e))?;
193 writer
194 .write_sample(i24_sample)
195 .map_err(|e| anyhow!("Failed to write sample: {}", e))?;
196 }
197 }
198 32 => {
199 for &sample in pcm_samples {
200 writer
201 .write_sample(sample)
202 .map_err(|e| anyhow!("Failed to write sample: {}", e))?;
203 writer
204 .write_sample(sample)
205 .map_err(|e| anyhow!("Failed to write sample: {}", e))?;
206 }
207 }
208 _ => {
209 return Err(anyhow!(
210 "Unsupported bit depth: {} (expected 16, 24, or 32)",
211 options.bit_depth
212 ));
213 }
214 }
215
216 writer
217 .finalize()
218 .map_err(|e| anyhow!("Failed to finalize WAV: {}", e))?;
219
220 Ok(cursor.into_inner())
221}
222
223#[cfg(not(any(feature = "cli", feature = "wasm")))]
224fn encode_wav(_pcm_samples: &[f32], _options: &EncoderOptions) -> Result<Vec<u8>> {
225 Err(anyhow!(
226 "WAV export not available in this build: missing 'hound' dependency. Enable the 'cli' or 'wasm' feature to include WAV support."
227 ))
228}
229
230#[cfg(feature = "cli")]
232fn encode_mp3(pcm_samples: &[f32], options: &EncoderOptions) -> Result<Vec<u8>> {
233 use mp3lame_encoder::{Builder, FlushNoGap, InterleavedPcm};
234 use std::mem::MaybeUninit;
235
236 let mut stereo_samples: Vec<i16> = Vec::with_capacity(pcm_samples.len() * 2);
238 for &sample in pcm_samples {
239 let clamped = sample.clamp(-1.0, 1.0);
240 let i16_sample = (clamped * 32767.0) as i16;
241 stereo_samples.push(i16_sample); stereo_samples.push(i16_sample); }
244
245 let mut builder = Builder::new().ok_or_else(|| anyhow!("Failed to create MP3 encoder"))?;
247
248 builder
249 .set_num_channels(2)
250 .map_err(|_| anyhow!("Failed to set channels"))?;
251 builder
252 .set_sample_rate(options.sample_rate)
253 .map_err(|_| anyhow!("Failed to set sample rate"))?;
254
255 let bitrate = match options.bitrate_kbps {
257 128 => mp3lame_encoder::Bitrate::Kbps128,
258 192 => mp3lame_encoder::Bitrate::Kbps192,
259 256 => mp3lame_encoder::Bitrate::Kbps256,
260 320 => mp3lame_encoder::Bitrate::Kbps320,
261 _ => mp3lame_encoder::Bitrate::Kbps192, };
263 builder
264 .set_brate(bitrate)
265 .map_err(|_| anyhow!("Failed to set bitrate"))?;
266
267 builder
268 .set_quality(mp3lame_encoder::Quality::Best)
269 .map_err(|_| anyhow!("Failed to set quality"))?;
270
271 let mut encoder = builder
272 .build()
273 .map_err(|_| anyhow!("Failed to build MP3 encoder"))?;
274
275 let max_output_size = (stereo_samples.len() * 5 / 4) + 7200;
278 let mut output_buffer: Vec<MaybeUninit<u8>> = vec![MaybeUninit::uninit(); max_output_size];
279
280 let input = InterleavedPcm(&stereo_samples);
282 let encoded_size = encoder
283 .encode(input, &mut output_buffer)
284 .map_err(|_| anyhow!("Failed to encode MP3"))?;
285
286 let mut mp3_buffer = Vec::with_capacity(encoded_size + 7200);
288 unsafe {
289 mp3_buffer.extend(
290 output_buffer[..encoded_size]
291 .iter()
292 .map(|b| b.assume_init()),
293 );
294 }
295
296 let mut flush_buffer: Vec<MaybeUninit<u8>> = vec![MaybeUninit::uninit(); 7200];
298 let flushed_size = encoder
299 .flush::<FlushNoGap>(&mut flush_buffer)
300 .map_err(|_| anyhow!("Failed to flush MP3 encoder"))?;
301
302 unsafe {
303 mp3_buffer.extend(flush_buffer[..flushed_size].iter().map(|b| b.assume_init()));
304 }
305
306 Ok(mp3_buffer)
307}
308
309fn encode_ogg(_pcm_samples: &[f32], options: &EncoderOptions) -> Result<Vec<u8>> {
312 Err(anyhow!(
313 "OGG Vorbis export not yet implemented. \n\
314 OGG encoding is planned for v2.1.\n\
315 Workaround: Export to WAV and convert with ffmpeg:\n\
316 - ffmpeg -i output.wav -c:a libvorbis -q:a {} output.ogg\n\
317 \n\
318 Supported formats: WAV (16/24/32-bit)\n\
319 Coming soon: OGG Vorbis, FLAC, Opus",
320 options.quality
321 ))
322}
323
324fn encode_flac(_pcm_samples: &[f32], _options: &EncoderOptions) -> Result<Vec<u8>> {
327 Err(anyhow!(
328 "FLAC export not yet implemented. \n\
329 FLAC encoding is planned for v2.1.\n\
330 Workaround: Export to WAV and convert with ffmpeg:\n\
331 - ffmpeg -i output.wav -c:a flac output.flac\n\
332 \n\
333 Supported formats: WAV (16/24/32-bit)\n\
334 Coming soon: OGG Vorbis, FLAC, Opus"
335 ))
336}
337
338fn encode_opus(_pcm_samples: &[f32], options: &EncoderOptions) -> Result<Vec<u8>> {
341 Err(anyhow!(
342 "Opus export not yet implemented. \n\
343 Opus encoding is planned for v2.1.\n\
344 Workaround: Export to WAV and convert with ffmpeg:\n\
345 - ffmpeg -i output.wav -c:a libopus -b:a {}k output.opus\n\
346 \n\
347 Supported formats: WAV (16/24/32-bit)\n\
348 Coming soon: OGG Vorbis, FLAC, Opus",
349 options.bitrate_kbps
350 ))
351}
352
353#[cfg(test)]
354mod tests {
355 use super::*;
356
357 #[test]
358 fn test_format_parsing() {
359 assert_eq!(AudioFormat::from_str("wav"), Some(AudioFormat::Wav));
360 assert_eq!(AudioFormat::from_str("MP3"), Some(AudioFormat::Mp3));
361 assert_eq!(AudioFormat::from_str("ogg"), Some(AudioFormat::Ogg));
362 assert_eq!(AudioFormat::from_str("vorbis"), Some(AudioFormat::Ogg));
363 assert_eq!(AudioFormat::from_str("flac"), Some(AudioFormat::Flac));
364 assert_eq!(AudioFormat::from_str("opus"), Some(AudioFormat::Opus));
365 assert_eq!(AudioFormat::from_str("unknown"), None);
366 }
367
368 #[test]
369 fn test_wav_encoding() {
370 let samples = vec![0.0, 0.5, -0.5, 1.0, -1.0];
371 let options = EncoderOptions::wav(44100, 16);
372 let result = encode_audio(&samples, &options);
373 assert!(result.is_ok());
374 let bytes = result.unwrap();
375 assert!(bytes.len() > 44); }
377
378 #[test]
379 fn test_mp3_encoding() {
380 let samples = vec![0.0, 0.5, -0.5, 1.0, -1.0, 0.25, -0.75, 0.8];
381 let options = EncoderOptions::mp3(44100, 192);
382 let result = encode_audio(&samples, &options);
383 assert!(result.is_ok(), "MP3 encoding should succeed");
384 let bytes = result.unwrap();
385 assert!(bytes.len() > 0, "MP3 output should not be empty");
386 assert!(bytes.len() > 100, "MP3 file should have reasonable size");
388 }
389}