use std::path::Path;
use super::{Categorization, FileCategorizer};
use limnifs_core::codec::CODEC_FLAC;
const FLAC_ENABLED: bool = true;
const MIN_PCM_AUDIO_SIZE: usize = 64;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PcmParams {
pub sample_rate: u32,
pub channels: u8,
pub bits_per_sample: u8,
pub endianness: Endianness,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Endianness {
Little,
Big,
}
impl PcmParams {
#[must_use]
pub fn encode(&self) -> [u8; 6] {
let mut out = [0u8; 6];
out[0..4].copy_from_slice(&self.sample_rate.to_le_bytes());
out[4] = self.channels;
out[5] = (self.bits_per_sample << 1)
| match self.endianness {
Endianness::Little => 0,
Endianness::Big => 1,
};
out
}
}
pub struct PcmAudioCategorizer;
impl FileCategorizer for PcmAudioCategorizer {
fn name(&self) -> &'static str {
"pcm-audio"
}
fn categories(&self) -> &'static [&'static str] {
&["pcmaudio/waveform"]
}
fn first_byte_hint(&self) -> Option<&'static [u8]> {
Some(b"R")
}
fn categorize(&self, _path: &Path, data: &[u8]) -> Option<Categorization> {
if !FLAC_ENABLED {
return None;
}
if data.len() < MIN_PCM_AUDIO_SIZE {
return None;
}
let params = omnizip_flac::pcm_header::parse_wav(data)
.or_else(|| omnizip_flac::pcm_header::parse_aiff(data))?;
Some(Categorization {
codec_id: CODEC_FLAC,
codec_params: encode_pcm_params(params).to_vec(),
category: "pcmaudio/waveform",
})
}
}
fn encode_pcm_params(p: omnizip_flac::PcmParams) -> [u8; 6] {
let mut out = [0u8; 6];
out[0..4].copy_from_slice(&p.sample_rate.to_le_bytes());
out[4] = p.channels;
out[5] = (p.bits_per_sample << 1)
| match p.endianness {
omnizip_flac::Endianness::LittleEndian => 0,
omnizip_flac::Endianness::BigEndian => 1,
};
out
}
#[must_use]
#[allow(dead_code)]
fn parse_wav(data: &[u8]) -> Option<PcmParams> {
if data.len() < 12 || &data[0..4] != b"RIFF" || &data[8..12] != b"WAVE" {
return None;
}
let mut off = 12;
while off + 8 <= data.len() {
let chunk_id = &data[off..off + 4];
let chunk_size =
u32::from_le_bytes([data[off + 4], data[off + 5], data[off + 6], data[off + 7]])
as usize;
let body_off = off + 8;
if body_off + chunk_size > data.len() {
return None;
}
if chunk_id == b"fmt " {
if chunk_size < 16 {
return None;
}
let body = &data[body_off..body_off + 16];
let tag = u16::from_le_bytes([body[0], body[1]]);
if tag != 1 {
return None; }
let channels = u16::from_le_bytes([body[2], body[3]]);
let sample_rate = u32::from_le_bytes([body[4], body[5], body[6], body[7]]);
let bits_per_sample = u16::from_le_bytes([body[14], body[15]]);
return Some(PcmParams {
sample_rate,
channels: u8::try_from(channels).ok()?,
bits_per_sample: u8::try_from(bits_per_sample).ok()?,
endianness: Endianness::Little,
});
}
off = body_off + chunk_size + (chunk_size & 1); }
None
}
#[must_use]
#[allow(dead_code)]
fn parse_aiff(data: &[u8]) -> Option<PcmParams> {
if data.len() < 12 || &data[0..4] != b"FORM" || &data[8..12] != b"AIFF" {
return None;
}
let mut off = 12;
while off + 8 <= data.len() {
let chunk_id = &data[off..off + 4];
let chunk_size =
u32::from_be_bytes([data[off + 4], data[off + 5], data[off + 6], data[off + 7]])
as usize;
let body_off = off + 8;
if body_off + chunk_size > data.len() {
return None;
}
if chunk_id == b"COMM" {
if chunk_size < 18 {
return None;
}
let body = &data[body_off..body_off + 18];
let channels = u16::from_be_bytes([body[0], body[1]]);
let bits_per_sample = u16::from_be_bytes([body[6], body[7]]);
return Some(PcmParams {
sample_rate: 0,
channels: u8::try_from(channels).ok()?,
bits_per_sample: u8::try_from(bits_per_sample).ok()?,
endianness: Endianness::Big,
});
}
off = body_off + chunk_size + (chunk_size & 1);
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn disabled_by_default() {
let c = PcmAudioCategorizer;
let wav = make_minimal_wav(44100, 2, 16);
assert!(c.categorize(Path::new("/x.wav"), &wav).is_none());
}
#[test]
fn wav_header_parsed_correctly() {
let wav = make_minimal_wav(48000, 1, 24);
let params = parse_wav(&wav).expect("wav parses");
assert_eq!(params.sample_rate, 48000);
assert_eq!(params.channels, 1);
assert_eq!(params.bits_per_sample, 24);
assert_eq!(params.endianness, Endianness::Little);
}
#[test]
fn rejects_non_pcm_wav() {
let mut wav = make_minimal_wav(44100, 2, 16);
wav[20] = 0x02;
wav[21] = 0x00;
assert!(parse_wav(&wav).is_none());
}
#[test]
fn rejects_non_wav_magic() {
assert!(parse_wav(b"NOTRIFF____WAVE____").is_none());
assert!(parse_wav(b"RIFF\x00\x00\x00\x00NOPE____").is_none());
}
fn make_minimal_wav(sample_rate: u32, channels: u8, bits: u8) -> Vec<u8> {
let mut wav = Vec::new();
wav.extend_from_slice(b"RIFF");
wav.extend_from_slice(&0u32.to_le_bytes()); wav.extend_from_slice(b"WAVE");
wav.extend_from_slice(b"fmt ");
wav.extend_from_slice(&16u32.to_le_bytes()); wav.extend_from_slice(&1u16.to_le_bytes()); wav.extend_from_slice(&[channels, 0]);
wav.extend_from_slice(&sample_rate.to_le_bytes());
wav.extend_from_slice(
&(sample_rate * u32::from(channels) * u32::from(bits) / 8).to_le_bytes(),
);
wav.extend_from_slice(&[(channels * bits / 8), 0]); wav.extend_from_slice(&[bits, 0]);
wav.extend_from_slice(b"data");
wav.extend_from_slice(&0u32.to_le_bytes());
let total = u32::try_from(wav.len()).unwrap_or(0) - 8;
wav[4..8].copy_from_slice(&total.to_le_bytes());
wav
}
}