Skip to main content

maolan_engine/
audio_codec.rs

1use std::io::{self, Write};
2use std::path::Path;
3
4/// Decode an audio file to interleaved `f32` samples.
5///
6/// The format is detected from the file extension (case-insensitive):
7/// `.wav`, `.flac`, or `.opus`. Returns `(samples, channels, sample_rate)`.
8/// All decode paths emit samples in the range `[-1.0, 1.0]`.
9pub fn decode_audio_to_f32_interleaved_sync(path: &Path) -> io::Result<(Vec<f32>, usize, u32)> {
10    let ext = file_extension(path)?;
11    match ext.as_str() {
12        "wav" => decode_wav(path),
13        "flac" => decode_flac(path),
14        "opus" => decode_opus(path),
15        _ => Err(io::Error::other(format!(
16            "Unsupported audio extension '{ext}' for '{}'",
17            path.display()
18        ))),
19    }
20}
21
22/// Decode a WAV file preferentially, falling back to the general decoder.
23///
24/// This preserves the old "try the WAV-specific path first" behavior for
25/// callers that want to avoid the format-detection overhead/differences for
26/// `.wav` inputs.
27pub fn decode_audio_to_f32_interleaved_preferring_wav(
28    path: &Path,
29) -> io::Result<(Vec<f32>, usize, u32)> {
30    if file_extension(path)?.eq_ignore_ascii_case("wav")
31        && let Ok(ok) = decode_wav(path)
32    {
33        return Ok(ok);
34    }
35    decode_audio_to_f32_interleaved_sync(path)
36}
37
38fn file_extension(path: &Path) -> io::Result<String> {
39    path.extension()
40        .and_then(|e| e.to_str())
41        .map(|e| e.to_lowercase())
42        .ok_or_else(|| io::Error::other(format!("Missing file extension for '{}'", path.display())))
43}
44
45// ---------------------------------------------------------------------------
46// WAV
47// ---------------------------------------------------------------------------
48
49fn decode_wav(path: &Path) -> io::Result<(Vec<f32>, usize, u32)> {
50    let mut reader = hound::WavReader::open(path)
51        .map_err(|e| io::Error::other(format!("Failed to open WAV '{}': {e}", path.display())))?;
52    let spec = reader.spec();
53    let channels = spec.channels as usize;
54    let sample_rate = spec.sample_rate;
55    let bits_per_sample = spec.bits_per_sample;
56
57    let mut samples = Vec::new();
58    match spec.sample_format {
59        hound::SampleFormat::Float => {
60            for s in reader.samples::<f32>() {
61                let v = s.map_err(|e| {
62                    io::Error::other(format!("WAV decode error for '{}': {e}", path.display()))
63                })?;
64                samples.push(v.clamp(-1.0, 1.0));
65            }
66        }
67        hound::SampleFormat::Int => {
68            for s in reader.samples::<i32>() {
69                let v = s.map_err(|e| {
70                    io::Error::other(format!("WAV decode error for '{}': {e}", path.display()))
71                })?;
72                samples.push(int_sample_to_f32(v, bits_per_sample));
73            }
74        }
75    }
76
77    if samples.is_empty() {
78        return Err(io::Error::other(format!(
79            "WAV '{}' contains no samples",
80            path.display()
81        )));
82    }
83    Ok((samples, channels, sample_rate))
84}
85
86pub fn write_wav_f32(
87    path: &Path,
88    samples: &[f32],
89    channels: usize,
90    sample_rate: u32,
91) -> io::Result<()> {
92    let bytes_per_sample = 4usize;
93    let block_align = (channels.max(1) * bytes_per_sample) as u16;
94    let byte_rate = sample_rate * u32::from(block_align);
95    let data_size = samples
96        .len()
97        .checked_mul(bytes_per_sample)
98        .ok_or_else(|| io::Error::other("WAV data too large"))? as u32;
99    let riff_size = 36u32
100        .checked_add(data_size)
101        .ok_or_else(|| io::Error::other("WAV file too large"))?;
102
103    let mut file = std::fs::File::create(path)?;
104    file.write_all(b"RIFF")?;
105    file.write_all(&riff_size.to_le_bytes())?;
106    file.write_all(b"WAVE")?;
107    file.write_all(b"fmt ")?;
108    file.write_all(&16u32.to_le_bytes())?;
109    file.write_all(&3u16.to_le_bytes())?;
110    file.write_all(&(channels.max(1) as u16).to_le_bytes())?;
111    file.write_all(&sample_rate.to_le_bytes())?;
112    file.write_all(&byte_rate.to_le_bytes())?;
113    file.write_all(&block_align.to_le_bytes())?;
114    file.write_all(&32u16.to_le_bytes())?;
115    file.write_all(b"data")?;
116    file.write_all(&data_size.to_le_bytes())?;
117    for &sample in samples {
118        file.write_all(&sample.clamp(-1.0, 1.0).to_le_bytes())?;
119    }
120    Ok(())
121}
122
123// ---------------------------------------------------------------------------
124// FLAC
125// ---------------------------------------------------------------------------
126
127fn decode_flac(path: &Path) -> io::Result<(Vec<f32>, usize, u32)> {
128    let bytes = std::fs::read(path)?;
129    let decoded = libflac_rs::decode(&bytes)
130        .ok_or_else(|| io::Error::other(format!("Failed to decode FLAC '{}'", path.display())))?;
131
132    let channels = decoded.channels as usize;
133    let sample_rate = decoded.sample_rate;
134    let bits_per_sample = decoded.bits_per_sample as u16;
135
136    let mut samples = Vec::with_capacity(decoded.interleaved.len());
137    for v in decoded.interleaved {
138        samples.push(int_sample_to_f32(v, bits_per_sample));
139    }
140
141    if samples.is_empty() {
142        return Err(io::Error::other(format!(
143            "FLAC '{}' contains no samples",
144            path.display()
145        )));
146    }
147    Ok((samples, channels, sample_rate))
148}
149
150pub fn write_flac(
151    path: &Path,
152    samples: &[f32],
153    channels: usize,
154    sample_rate: u32,
155    bits_per_sample: u16,
156) -> io::Result<()> {
157    if channels == 0 {
158        return Err(io::Error::other("FLAC write: channels must be > 0"));
159    }
160    if !samples.len().is_multiple_of(channels) {
161        return Err(io::Error::other(
162            "FLAC write: sample slice length is not a multiple of channels",
163        ));
164    }
165    if !matches!(bits_per_sample, 16 | 24 | 32) {
166        return Err(io::Error::other(format!(
167            "FLAC write: unsupported bits_per_sample {bits_per_sample} (use 16, 24, or 32)"
168        )));
169    }
170
171    let pcm: Vec<i32> = samples
172        .iter()
173        .map(|&s| f32_sample_to_int(s, bits_per_sample))
174        .collect();
175
176    let config =
177        libflac_rs::EncoderConfig::new(channels as u32, bits_per_sample as u32, sample_rate);
178    let encoder = libflac_rs::Encoder::new(config);
179    let encoded = encoder.encode(&pcm);
180
181    std::fs::write(path, encoded)?;
182    Ok(())
183}
184
185// ---------------------------------------------------------------------------
186// Opus (Ogg Opus container)
187// ---------------------------------------------------------------------------
188
189fn decode_opus(path: &Path) -> io::Result<(Vec<f32>, usize, u32)> {
190    use ogg::PacketReader;
191
192    let file = std::fs::File::open(path)?;
193    let mut reader = PacketReader::new(file);
194
195    // First packet: OpusHead
196    let head_packet = reader
197        .read_packet()
198        .map_err(|e| io::Error::other(format!("Ogg read error for '{}': {e}", path.display())))?
199        .ok_or_else(|| {
200            io::Error::other(format!(
201                "Opus file '{}' contains no packets",
202                path.display()
203            ))
204        })?;
205
206    if !head_packet.data.starts_with(b"OpusHead") {
207        return Err(io::Error::other(format!(
208            "Opus file '{}' missing OpusHead header",
209            path.display()
210        )));
211    }
212    let channels = parse_opus_head_channels(&head_packet.data)?;
213
214    // Second packet: OpusTags (skip)
215    let _tags_packet = reader
216        .read_packet()
217        .map_err(|e| io::Error::other(format!("Ogg read error for '{}': {e}", path.display())))?;
218
219    // Remaining packets are audio.
220    let sample_rate = 48_000;
221    let mut decoder = opus_rs::OpusDecoder::new(sample_rate, channels).map_err(|e| {
222        io::Error::other(format!(
223            "Opus decoder init failed for '{}': {e}",
224            path.display()
225        ))
226    })?;
227
228    let mut output = Vec::new();
229    loop {
230        let packet = match reader.read_packet() {
231            Ok(Some(p)) => p,
232            Ok(None) => break,
233            Err(e) => {
234                return Err(io::Error::other(format!(
235                    "Ogg read error for '{}': {e}",
236                    path.display()
237                )));
238            }
239        };
240
241        let frame_size = opus_packet_frame_size(&packet.data, sample_rate).ok_or_else(|| {
242            io::Error::other(format!(
243                "Invalid Opus packet framing in '{}'",
244                path.display()
245            ))
246        })?;
247
248        let mut frame = vec![0.0f32; frame_size * channels];
249        decoder
250            .decode(&packet.data, frame_size, &mut frame)
251            .map_err(|e| {
252                io::Error::other(format!("Opus decode error for '{}': {e}", path.display()))
253            })?;
254        output.extend_from_slice(&frame);
255    }
256
257    if output.is_empty() {
258        return Err(io::Error::other(format!(
259            "Opus file '{}' contains no audio",
260            path.display()
261        )));
262    }
263    Ok((output, channels, sample_rate as u32))
264}
265
266pub fn write_opus(
267    path: &Path,
268    samples: &[f32],
269    channels: usize,
270    sample_rate: u32,
271    bitrate_bps: i32,
272) -> io::Result<()> {
273    use ogg::{PacketWriteEndInfo, PacketWriter};
274
275    if channels == 0 || channels > 2 {
276        return Err(io::Error::other(
277            "Opus write: only mono and stereo are supported",
278        ));
279    }
280    if !samples.len().is_multiple_of(channels) {
281        return Err(io::Error::other(
282            "Opus write: sample slice length is not a multiple of channels",
283        ));
284    }
285    if !matches!(sample_rate, 8_000 | 12_000 | 16_000 | 24_000 | 48_000) {
286        return Err(io::Error::other(format!(
287            "Opus write: unsupported sample rate {sample_rate} (use 8000, 12000, 16000, 24000, or 48000)"
288        )));
289    }
290
291    let total_samples = samples.len() / channels;
292    // 20 ms frames are the standard Opus frame size and are valid for all
293    // supported sample rates / applications.
294    let frame_size = (sample_rate as usize / 50).max(1);
295
296    let mut encoder =
297        opus_rs::OpusEncoder::new(sample_rate as i32, channels, opus_rs::Application::Audio)
298            .map_err(|e| io::Error::other(format!("Opus encoder init failed: {e}")))?;
299    encoder.bitrate_bps = bitrate_bps;
300
301    let file = std::fs::File::create(path)?;
302    let mut writer = PacketWriter::new(file);
303    let serial = ogg_serial_from_path(path);
304
305    // OpusHead must be the sole packet on the first page.
306    writer.write_packet(
307        opus_head(channels as u8, sample_rate),
308        serial,
309        PacketWriteEndInfo::EndPage,
310        0,
311    )?;
312
313    // OpusTags on its own page.
314    writer.write_packet(opus_tags(), serial, PacketWriteEndInfo::EndPage, 0)?;
315
316    let mut packet_buf = vec![0u8; 4000];
317    let mut encoded_granule: u64 = 0;
318
319    for frame_idx in 0..frame_count(total_samples, frame_size) {
320        let start = frame_idx * frame_size;
321        let end = ((start + frame_size).min(total_samples)).max(start);
322        let actual_len = end - start;
323
324        // Opus requires a full frame; pad the tail with silence.
325        let mut frame_input = vec![0.0f32; frame_size * channels];
326        frame_input[..actual_len * channels]
327            .copy_from_slice(&samples[start * channels..end * channels]);
328
329        let encoded_len = encoder
330            .encode(&frame_input, frame_size, &mut packet_buf)
331            .map_err(|e| io::Error::other(format!("Opus encode failed: {e}")))?;
332
333        encoded_granule = encoded_granule.saturating_add(frame_size as u64);
334        // For the final frame use the real sample count as the granule position
335        // so compliant decoders can trim the padding.
336        let is_last = frame_idx == frame_count(total_samples, frame_size) - 1;
337        let granule = if is_last {
338            total_samples.min(encoded_granule as usize) as u64
339        } else {
340            encoded_granule
341        };
342
343        let end_info = if is_last {
344            PacketWriteEndInfo::EndStream
345        } else {
346            PacketWriteEndInfo::NormalPacket
347        };
348        writer.write_packet(
349            packet_buf[..encoded_len].to_vec(),
350            serial,
351            end_info,
352            granule,
353        )?;
354    }
355
356    Ok(())
357}
358
359fn parse_opus_head_channels(head: &[u8]) -> io::Result<usize> {
360    if head.len() < 10 || head[8] != 1 {
361        return Err(io::Error::other(
362            "OpusHead header is too short or has unsupported version",
363        ));
364    }
365    let channels = head[9] as usize;
366    if channels == 0 {
367        return Err(io::Error::other("OpusHead reports zero channels"));
368    }
369    Ok(channels)
370}
371
372fn opus_head(channels: u8, sample_rate: u32) -> Vec<u8> {
373    let mut head = Vec::with_capacity(19);
374    head.extend_from_slice(b"OpusHead");
375    head.push(1); // version
376    head.push(channels);
377    head.extend_from_slice(&0u16.to_le_bytes()); // pre-skip
378    head.extend_from_slice(&sample_rate.to_le_bytes()); // input sample rate
379    head.extend_from_slice(&0i16.to_le_bytes()); // output gain
380    head.push(0); // channel mapping family (mono/stereo)
381    head
382}
383
384fn opus_tags() -> Vec<u8> {
385    let vendor = b"maolan-engine";
386    let mut tags = Vec::with_capacity(8 + 4 + vendor.len() + 4);
387    tags.extend_from_slice(b"OpusTags");
388    tags.extend_from_slice(&(vendor.len() as u32).to_le_bytes());
389    tags.extend_from_slice(vendor);
390    tags.extend_from_slice(&0u32.to_le_bytes()); // user comment count
391    tags
392}
393
394fn ogg_serial_from_path(path: &Path) -> u32 {
395    use std::collections::hash_map::DefaultHasher;
396    use std::hash::{Hash, Hasher};
397    let mut hasher = DefaultHasher::new();
398    path.as_os_str().hash(&mut hasher);
399    hasher.finish() as u32
400}
401
402/// Compute the total number of fixed-size frames needed for `total_samples`,
403/// padding the final frame if necessary.
404fn frame_count(total_samples: usize, frame_size: usize) -> usize {
405    if total_samples == 0 {
406        return 0;
407    }
408    total_samples.div_ceil(frame_size)
409}
410
411/// Determine the total number of samples encoded by an Opus packet.
412/// The TOC byte describes the mode and the per-sub-frame duration; this
413/// helper multiplies by the number of sub-frames to obtain the value that
414/// the opus-rs decoder expects as `frame_size`.
415fn opus_packet_frame_size(packet: &[u8], sample_rate: i32) -> Option<usize> {
416    if packet.is_empty() {
417        return None;
418    }
419    let toc = packet[0];
420
421    let sub_frame_duration_ms = if toc & 0x80 != 0 {
422        // CELT-only
423        match (toc >> 3) & 0x03 {
424            0 => 2,
425            1 => 5,
426            2 => 10,
427            _ => 20,
428        }
429    } else if toc & 0x60 == 0x60 {
430        // Hybrid
431        if ((toc >> 3) & 0x01) == 0 { 10 } else { 20 }
432    } else {
433        // SILK-only
434        match (toc >> 3) & 0x03 {
435            0 => 10,
436            1 => 20,
437            2 => 40,
438            _ => 60,
439        }
440    };
441
442    let sub_frame_size = (sample_rate as i64 * sub_frame_duration_ms as i64 / 1000).max(1) as usize;
443    let code = toc & 0x03;
444    let frame_count = match code {
445        0 => 1,
446        1 | 2 => 2,
447        3 => {
448            if packet.len() < 2 {
449                return None;
450            }
451            let n = (packet[1] & 0x3F) as usize;
452            if n == 0 {
453                return None;
454            }
455            n
456        }
457        _ => return None,
458    };
459
460    Some(sub_frame_size * frame_count)
461}
462
463// ---------------------------------------------------------------------------
464// Sample format conversions
465// ---------------------------------------------------------------------------
466
467fn int_sample_to_f32(v: i32, bits_per_sample: u16) -> f32 {
468    let divisor = match bits_per_sample {
469        8 => 128.0,
470        16 => 32_768.0,
471        24 => 8_388_608.0,
472        32 => 2_147_483_648.0,
473        _ => 2.0f32.powi(bits_per_sample as i32 - 1),
474    };
475    (v as f32 / divisor).clamp(-1.0, 1.0)
476}
477
478fn f32_sample_to_int(v: f32, bits_per_sample: u16) -> i32 {
479    let max_positive = match bits_per_sample {
480        16 => 32_767i32,
481        24 => 8_388_607i32,
482        32 => 2_147_483_647i32,
483        _ => ((1u64 << (bits_per_sample - 1)) - 1) as i32,
484    };
485    (v.clamp(-1.0, 1.0) * max_positive as f32).round() as i32
486}