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/// Fallback stub when `hound` isn't enabled (e.g., plugin-only build without features)
224#[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/// Encode to MP3 format using LAME encoder
232#[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    // Convert mono f32 samples to stereo i16 for LAME
238    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); // Left channel
243        stereo_samples.push(i16_sample); // Right channel (duplicate for stereo)
244    }
245
246    // Create MP3 encoder with specified settings
247    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    // Set bitrate - convert from kbps to the bitrate enum
257    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, // Default to 192 if not standard
263    };
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    // Allocate output buffer for MP3 data
277    // MP3 compression typically reduces size by ~10x, but we allocate more to be safe
278    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    // Encode the audio data
282    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    // Convert the written portion to initialized bytes
288    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    // Flush the encoder to get remaining data
298    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
310/// Encode to OGG Vorbis format
311/// TODO: Implement using vorbis encoder
312fn 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
325/// Encode to FLAC format
326/// TODO: Implement using FLAC encoder
327fn 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
339/// Encode to Opus format
340/// TODO: Implement using opus encoder
341fn 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); // WAV header + data
377    }
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        // MP3 files should have proper headers
388        assert!(bytes.len() > 100, "MP3 file should have reasonable size");
389    }
390}