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")))]
225fn encode_wav(_pcm_samples: &[f32], _options: &EncoderOptions) -> Result<Vec<u8>> {
226 Err(anyhow!(
227 "WAV export not available in this build: missing 'hound' dependency. Enable the 'cli' or 'wasm' feature to include WAV support."
228 ))
229}
230
231#[cfg(feature = "cli")]
233fn encode_mp3(pcm_samples: &[f32], options: &EncoderOptions) -> Result<Vec<u8>> {
234 use mp3lame_encoder::{Builder, FlushNoGap, InterleavedPcm};
235 use std::mem::MaybeUninit;
236
237 let mut stereo_samples: Vec<i16> = Vec::with_capacity(pcm_samples.len() * 2);
239 for &sample in pcm_samples {
240 let clamped = sample.clamp(-1.0, 1.0);
241 let i16_sample = (clamped * 32767.0) as i16;
242 stereo_samples.push(i16_sample); stereo_samples.push(i16_sample); }
245
246 let mut builder = Builder::new().ok_or_else(|| anyhow!("Failed to create MP3 encoder"))?;
248
249 builder
250 .set_num_channels(2)
251 .map_err(|_| anyhow!("Failed to set channels"))?;
252 builder
253 .set_sample_rate(options.sample_rate)
254 .map_err(|_| anyhow!("Failed to set sample rate"))?;
255
256 let bitrate = match options.bitrate_kbps {
258 128 => mp3lame_encoder::Bitrate::Kbps128,
259 192 => mp3lame_encoder::Bitrate::Kbps192,
260 256 => mp3lame_encoder::Bitrate::Kbps256,
261 320 => mp3lame_encoder::Bitrate::Kbps320,
262 _ => mp3lame_encoder::Bitrate::Kbps192, };
264 builder
265 .set_brate(bitrate)
266 .map_err(|_| anyhow!("Failed to set bitrate"))?;
267
268 builder
269 .set_quality(mp3lame_encoder::Quality::Best)
270 .map_err(|_| anyhow!("Failed to set quality"))?;
271
272 let mut encoder = builder
273 .build()
274 .map_err(|_| anyhow!("Failed to build MP3 encoder"))?;
275
276 let max_output_size = (stereo_samples.len() * 5 / 4) + 7200;
279 let mut output_buffer: Vec<MaybeUninit<u8>> = vec![MaybeUninit::uninit(); max_output_size];
280
281 let input = InterleavedPcm(&stereo_samples);
283 let encoded_size = encoder
284 .encode(input, &mut output_buffer)
285 .map_err(|_| anyhow!("Failed to encode MP3"))?;
286
287 let mut mp3_buffer = Vec::with_capacity(encoded_size + 7200);
289 unsafe {
290 mp3_buffer.extend(
291 output_buffer[..encoded_size]
292 .iter()
293 .map(|b| b.assume_init()),
294 );
295 }
296
297 let mut flush_buffer: Vec<MaybeUninit<u8>> = vec![MaybeUninit::uninit(); 7200];
299 let flushed_size = encoder
300 .flush::<FlushNoGap>(&mut flush_buffer)
301 .map_err(|_| anyhow!("Failed to flush MP3 encoder"))?;
302
303 unsafe {
304 mp3_buffer.extend(flush_buffer[..flushed_size].iter().map(|b| b.assume_init()));
305 }
306
307 Ok(mp3_buffer)
308}
309
310fn encode_ogg(_pcm_samples: &[f32], options: &EncoderOptions) -> Result<Vec<u8>> {
313 Err(anyhow!(
314 "OGG Vorbis export not yet implemented. \n\
315 OGG encoding is planned for v2.1.\n\
316 Workaround: Export to WAV and convert with ffmpeg:\n\
317 - ffmpeg -i output.wav -c:a libvorbis -q:a {} output.ogg\n\
318 \n\
319 Supported formats: WAV (16/24/32-bit)\n\
320 Coming soon: OGG Vorbis, FLAC, Opus",
321 options.quality
322 ))
323}
324
325fn encode_flac(_pcm_samples: &[f32], _options: &EncoderOptions) -> Result<Vec<u8>> {
328 Err(anyhow!(
329 "FLAC export not yet implemented. \n\
330 FLAC encoding is planned for v2.1.\n\
331 Workaround: Export to WAV and convert with ffmpeg:\n\
332 - ffmpeg -i output.wav -c:a flac output.flac\n\
333 \n\
334 Supported formats: WAV (16/24/32-bit)\n\
335 Coming soon: OGG Vorbis, FLAC, Opus"
336 ))
337}
338
339fn encode_opus(_pcm_samples: &[f32], options: &EncoderOptions) -> Result<Vec<u8>> {
342 Err(anyhow!(
343 "Opus export not yet implemented. \n\
344 Opus encoding is planned for v2.1.\n\
345 Workaround: Export to WAV and convert with ffmpeg:\n\
346 - ffmpeg -i output.wav -c:a libopus -b:a {}k output.opus\n\
347 \n\
348 Supported formats: WAV (16/24/32-bit)\n\
349 Coming soon: OGG Vorbis, FLAC, Opus",
350 options.bitrate_kbps
351 ))
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357
358 #[test]
359 fn test_format_parsing() {
360 assert_eq!(AudioFormat::from_str("wav"), Some(AudioFormat::Wav));
361 assert_eq!(AudioFormat::from_str("MP3"), Some(AudioFormat::Mp3));
362 assert_eq!(AudioFormat::from_str("ogg"), Some(AudioFormat::Ogg));
363 assert_eq!(AudioFormat::from_str("vorbis"), Some(AudioFormat::Ogg));
364 assert_eq!(AudioFormat::from_str("flac"), Some(AudioFormat::Flac));
365 assert_eq!(AudioFormat::from_str("opus"), Some(AudioFormat::Opus));
366 assert_eq!(AudioFormat::from_str("unknown"), None);
367 }
368
369 #[test]
370 fn test_wav_encoding() {
371 let samples = vec![0.0, 0.5, -0.5, 1.0, -1.0];
372 let options = EncoderOptions::wav(44100, 16);
373 let result = encode_audio(&samples, &options);
374 assert!(result.is_ok());
375 let bytes = result.unwrap();
376 assert!(bytes.len() > 44); }
378
379 #[test]
380 fn test_mp3_encoding() {
381 let samples = vec![0.0, 0.5, -0.5, 1.0, -1.0, 0.25, -0.75, 0.8];
382 let options = EncoderOptions::mp3(44100, 192);
383 let result = encode_audio(&samples, &options);
384 assert!(result.is_ok(), "MP3 encoding should succeed");
385 let bytes = result.unwrap();
386 assert!(bytes.len() > 0, "MP3 output should not be empty");
387 assert!(bytes.len() > 100, "MP3 file should have reasonable size");
389 }
390}