devalang_wasm/engine/audio/
encoders.rs1use anyhow::{anyhow, Result};
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 => 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
140fn 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 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
201fn encode_mp3(pcm_samples: &[f32], options: &EncoderOptions) -> Result<Vec<u8>> {
203 use mp3lame_encoder::{Builder, FlushNoGap, InterleavedPcm};
204 use std::mem::MaybeUninit;
205
206 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); stereo_samples.push(i16_sample); }
214
215 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 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, };
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 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 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 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 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
272fn 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
287fn 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
301fn 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); }
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 assert!(bytes.len() > 100, "MP3 file should have reasonable size");
351 }
352}