devalang_wasm/engine/audio/
encoders.rs

1//! Audio format encoders for export
2//!
3//! This module provides encoding functionality for various audio formats.
4//! Currently supported:
5//! - WAV (via hound) - 16/24/32-bit
6//! - MP3 (via mp3lame-encoder) - 128/192/256/320 kbps
7//! 
8//! Planned: OGG Vorbis, FLAC, Opus
9
10use anyhow::{anyhow, Result};
11
12/// Supported export formats
13#[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/// Encoder options for each format
70#[derive(Debug, Clone)]
71pub struct EncoderOptions {
72    pub format: AudioFormat,
73    pub sample_rate: u32,
74    pub bit_depth: u8,      // For WAV/FLAC: 16, 24, 32
75    pub bitrate_kbps: u32,  // For MP3/OGG/Opus: 128, 192, 256, 320
76    pub quality: f32,       // For OGG/Opus: 0.0-10.0 (quality scale)
77}
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
129/// Encode PCM samples (f32, normalized -1.0 to 1.0) to the specified format
130pub 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 => encode_mp3(pcm_samples, options),
134        AudioFormat::Ogg => encode_ogg(pcm_samples, options),
135        AudioFormat::Flac => encode_flac(pcm_samples, options),
136        AudioFormat::Opus => encode_opus(pcm_samples, options),
137    }
138}
139
140/// Encode to WAV format (using hound)
141fn encode_wav(pcm_samples: &[f32], options: &EncoderOptions) -> Result<Vec<u8>> {
142    use hound::{WavSpec, WavWriter, SampleFormat};
143    use std::io::Cursor;
144
145    let spec = WavSpec {
146        channels: 2,
147        sample_rate: options.sample_rate,
148        bits_per_sample: options.bit_depth as u16,
149        sample_format: if options.bit_depth == 32 {
150            SampleFormat::Float
151        } else {
152            SampleFormat::Int
153        },
154    };
155
156    let mut cursor = Cursor::new(Vec::new());
157    let mut writer = WavWriter::new(&mut cursor, spec)
158        .map_err(|e| anyhow!("Failed to create WAV writer: {}", e))?;
159
160    // Convert mono f32 to stereo with specified bit depth
161    match options.bit_depth {
162        16 => {
163            for &sample in pcm_samples {
164                let clamped = sample.clamp(-1.0, 1.0);
165                let i16_sample = (clamped * 32767.0) as i16;
166                writer.write_sample(i16_sample)
167                    .map_err(|e| anyhow!("Failed to write sample: {}", e))?;
168                writer.write_sample(i16_sample)
169                    .map_err(|e| anyhow!("Failed to write sample: {}", e))?;
170            }
171        }
172        24 => {
173            for &sample in pcm_samples {
174                let clamped = sample.clamp(-1.0, 1.0);
175                let i24_sample = (clamped * 8388607.0) as i32;
176                writer.write_sample(i24_sample)
177                    .map_err(|e| anyhow!("Failed to write sample: {}", e))?;
178                writer.write_sample(i24_sample)
179                    .map_err(|e| anyhow!("Failed to write sample: {}", e))?;
180            }
181        }
182        32 => {
183            for &sample in pcm_samples {
184                writer.write_sample(sample)
185                    .map_err(|e| anyhow!("Failed to write sample: {}", e))?;
186                writer.write_sample(sample)
187                    .map_err(|e| anyhow!("Failed to write sample: {}", e))?;
188            }
189        }
190        _ => {
191            return Err(anyhow!("Unsupported bit depth: {} (expected 16, 24, or 32)", options.bit_depth));
192        }
193    }
194
195    writer.finalize()
196        .map_err(|e| anyhow!("Failed to finalize WAV: {}", e))?;
197
198    Ok(cursor.into_inner())
199}
200
201/// Encode to MP3 format using LAME encoder
202fn encode_mp3(pcm_samples: &[f32], options: &EncoderOptions) -> Result<Vec<u8>> {
203    use mp3lame_encoder::{Builder, FlushNoGap, InterleavedPcm};
204    use std::mem::MaybeUninit;
205    
206    // Convert mono f32 samples to stereo i16 for LAME
207    let mut stereo_samples: Vec<i16> = Vec::with_capacity(pcm_samples.len() * 2);
208    for &sample in pcm_samples {
209        let clamped = sample.clamp(-1.0, 1.0);
210        let i16_sample = (clamped * 32767.0) as i16;
211        stereo_samples.push(i16_sample); // Left channel
212        stereo_samples.push(i16_sample); // Right channel (duplicate for stereo)
213    }
214    
215    // Create MP3 encoder with specified settings
216    let mut builder = Builder::new().ok_or_else(|| anyhow!("Failed to create MP3 encoder"))?;
217    
218    builder.set_num_channels(2)
219        .map_err(|_| anyhow!("Failed to set channels"))?;
220    builder.set_sample_rate(options.sample_rate)
221        .map_err(|_| anyhow!("Failed to set sample rate"))?;
222    
223    // Set bitrate - convert from kbps to the bitrate enum
224    let bitrate = match options.bitrate_kbps {
225        128 => mp3lame_encoder::Bitrate::Kbps128,
226        192 => mp3lame_encoder::Bitrate::Kbps192,
227        256 => mp3lame_encoder::Bitrate::Kbps256,
228        320 => mp3lame_encoder::Bitrate::Kbps320,
229        _ => mp3lame_encoder::Bitrate::Kbps192, // Default to 192 if not standard
230    };
231    builder.set_brate(bitrate)
232        .map_err(|_| anyhow!("Failed to set bitrate"))?;
233    
234    builder.set_quality(mp3lame_encoder::Quality::Best)
235        .map_err(|_| anyhow!("Failed to set quality"))?;
236    
237    let mut encoder = builder.build()
238        .map_err(|_| anyhow!("Failed to build MP3 encoder"))?;
239    
240    // Allocate output buffer for MP3 data
241    // MP3 compression typically reduces size by ~10x, but we allocate more to be safe
242    let max_output_size = (stereo_samples.len() * 5 / 4) + 7200;
243    let mut output_buffer: Vec<MaybeUninit<u8>> = vec![MaybeUninit::uninit(); max_output_size];
244    
245    // Encode the audio data
246    let input = InterleavedPcm(&stereo_samples);
247    let encoded_size = encoder.encode(input, &mut output_buffer)
248        .map_err(|_| anyhow!("Failed to encode MP3"))?;
249    
250    // Convert the written portion to initialized bytes
251    let mut mp3_buffer = Vec::with_capacity(encoded_size + 7200);
252    for i in 0..encoded_size {
253        unsafe {
254            mp3_buffer.push(output_buffer[i].assume_init());
255        }
256    }
257    
258    // Flush the encoder to get remaining data
259    let mut flush_buffer: Vec<MaybeUninit<u8>> = vec![MaybeUninit::uninit(); 7200];
260    let flushed_size = encoder.flush::<FlushNoGap>(&mut flush_buffer)
261        .map_err(|_| anyhow!("Failed to flush MP3 encoder"))?;
262    
263    for i in 0..flushed_size {
264        unsafe {
265            mp3_buffer.push(flush_buffer[i].assume_init());
266        }
267    }
268    
269    Ok(mp3_buffer)
270}
271
272/// Encode to OGG Vorbis format
273/// TODO: Implement using vorbis encoder
274fn encode_ogg(_pcm_samples: &[f32], options: &EncoderOptions) -> Result<Vec<u8>> {
275    Err(anyhow!(
276        "OGG Vorbis export not yet implemented. \n\
277        OGG encoding is planned for v2.1.\n\
278        Workaround: Export to WAV and convert with ffmpeg:\n\
279        - ffmpeg -i output.wav -c:a libvorbis -q:a {} output.ogg\n\
280        \n\
281        Supported formats: WAV (16/24/32-bit)\n\
282        Coming soon: OGG Vorbis, FLAC, Opus",
283        options.quality
284    ))
285}
286
287/// Encode to FLAC format
288/// TODO: Implement using FLAC encoder
289fn encode_flac(_pcm_samples: &[f32], _options: &EncoderOptions) -> Result<Vec<u8>> {
290    Err(anyhow!(
291        "FLAC export not yet implemented. \n\
292        FLAC encoding is planned for v2.1.\n\
293        Workaround: Export to WAV and convert with ffmpeg:\n\
294        - ffmpeg -i output.wav -c:a flac output.flac\n\
295        \n\
296        Supported formats: WAV (16/24/32-bit)\n\
297        Coming soon: OGG Vorbis, FLAC, Opus"
298    ))
299}
300
301/// Encode to Opus format
302/// TODO: Implement using opus encoder
303fn encode_opus(_pcm_samples: &[f32], options: &EncoderOptions) -> Result<Vec<u8>> {
304    Err(anyhow!(
305        "Opus export not yet implemented. \n\
306        Opus encoding is planned for v2.1.\n\
307        Workaround: Export to WAV and convert with ffmpeg:\n\
308        - ffmpeg -i output.wav -c:a libopus -b:a {}k output.opus\n\
309        \n\
310        Supported formats: WAV (16/24/32-bit)\n\
311        Coming soon: OGG Vorbis, FLAC, Opus",
312        options.bitrate_kbps
313    ))
314}
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319
320    #[test]
321    fn test_format_parsing() {
322        assert_eq!(AudioFormat::from_str("wav"), Some(AudioFormat::Wav));
323        assert_eq!(AudioFormat::from_str("MP3"), Some(AudioFormat::Mp3));
324        assert_eq!(AudioFormat::from_str("ogg"), Some(AudioFormat::Ogg));
325        assert_eq!(AudioFormat::from_str("vorbis"), Some(AudioFormat::Ogg));
326        assert_eq!(AudioFormat::from_str("flac"), Some(AudioFormat::Flac));
327        assert_eq!(AudioFormat::from_str("opus"), Some(AudioFormat::Opus));
328        assert_eq!(AudioFormat::from_str("unknown"), None);
329    }
330
331    #[test]
332    fn test_wav_encoding() {
333        let samples = vec![0.0, 0.5, -0.5, 1.0, -1.0];
334        let options = EncoderOptions::wav(44100, 16);
335        let result = encode_audio(&samples, &options);
336        assert!(result.is_ok());
337        let bytes = result.unwrap();
338        assert!(bytes.len() > 44); // WAV header + data
339    }
340
341    #[test]
342    fn test_mp3_encoding() {
343        let samples = vec![0.0, 0.5, -0.5, 1.0, -1.0, 0.25, -0.75, 0.8];
344        let options = EncoderOptions::mp3(44100, 192);
345        let result = encode_audio(&samples, &options);
346        assert!(result.is_ok(), "MP3 encoding should succeed");
347        let bytes = result.unwrap();
348        assert!(bytes.len() > 0, "MP3 output should not be empty");
349        // MP3 files should have proper headers
350        assert!(bytes.len() > 100, "MP3 file should have reasonable size");
351    }
352}