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::{Result, anyhow};
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 => {
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/// Encode to WAV format (using hound)
152#[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    // Convert mono f32 to stereo with specified bit depth
173    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/// Encode to MP3 format using LAME encoder
231#[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    // Convert mono f32 samples to stereo i16 for LAME
237    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); // Left channel
242        stereo_samples.push(i16_sample); // Right channel (duplicate for stereo)
243    }
244
245    // Create MP3 encoder with specified settings
246    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    // Set bitrate - convert from kbps to the bitrate enum
256    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, // Default to 192 if not standard
262    };
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    // Allocate output buffer for MP3 data
276    // MP3 compression typically reduces size by ~10x, but we allocate more to be safe
277    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    // Encode the audio data
281    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    // Convert the written portion to initialized bytes
287    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    // Flush the encoder to get remaining data
297    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
309/// Encode to OGG Vorbis format
310/// TODO: Implement using vorbis encoder
311fn 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
324/// Encode to FLAC format
325/// TODO: Implement using FLAC encoder
326fn 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
338/// Encode to Opus format
339/// TODO: Implement using opus encoder
340fn 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); // WAV header + data
376    }
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        // MP3 files should have proper headers
387        assert!(bytes.len() > 100, "MP3 file should have reasonable size");
388    }
389}