limnifs_write/file_categorizer/
pcm_audio.rs1use std::path::Path;
18
19use super::{Categorization, FileCategorizer};
20use limnifs_core::codec::CODEC_FLAC;
21
22const FLAC_ENABLED: bool = true;
26
27const MIN_PCM_AUDIO_SIZE: usize = 64;
30
31#[derive(Clone, Copy, Debug, Eq, PartialEq)]
34pub struct PcmParams {
35 pub sample_rate: u32,
36 pub channels: u8,
37 pub bits_per_sample: u8,
38 pub endianness: Endianness,
39}
40
41#[derive(Clone, Copy, Debug, Eq, PartialEq)]
42pub enum Endianness {
43 Little,
44 Big,
45}
46
47impl PcmParams {
48 #[must_use]
51 pub fn encode(&self) -> [u8; 6] {
52 let mut out = [0u8; 6];
53 out[0..4].copy_from_slice(&self.sample_rate.to_le_bytes());
54 out[4] = self.channels;
55 out[5] = (self.bits_per_sample << 1)
56 | match self.endianness {
57 Endianness::Little => 0,
58 Endianness::Big => 1,
59 };
60 out
61 }
62}
63
64pub struct PcmAudioCategorizer;
66
67impl FileCategorizer for PcmAudioCategorizer {
68 fn name(&self) -> &'static str {
69 "pcm-audio"
70 }
71
72 fn categories(&self) -> &'static [&'static str] {
73 &["pcmaudio/waveform"]
74 }
75
76 fn first_byte_hint(&self) -> Option<&'static [u8]> {
77 Some(b"R")
78 }
79
80 fn categorize(&self, _path: &Path, data: &[u8]) -> Option<Categorization> {
81 if !FLAC_ENABLED {
82 return None;
83 }
84 if data.len() < MIN_PCM_AUDIO_SIZE {
85 return None;
86 }
87 let params = omnizip_flac::pcm_header::parse_wav(data)
92 .or_else(|| omnizip_flac::pcm_header::parse_aiff(data))?;
93 Some(Categorization {
94 codec_id: CODEC_FLAC,
95 codec_params: encode_pcm_params(params).to_vec(),
96 category: "pcmaudio/waveform",
97 })
98 }
99}
100
101fn encode_pcm_params(p: omnizip_flac::PcmParams) -> [u8; 6] {
104 let mut out = [0u8; 6];
105 out[0..4].copy_from_slice(&p.sample_rate.to_le_bytes());
106 out[4] = p.channels;
107 out[5] = (p.bits_per_sample << 1)
108 | match p.endianness {
109 omnizip_flac::Endianness::LittleEndian => 0,
110 omnizip_flac::Endianness::BigEndian => 1,
111 };
112 out
113}
114
115#[must_use]
118#[allow(dead_code)]
119fn parse_wav(data: &[u8]) -> Option<PcmParams> {
120 if data.len() < 12 || &data[0..4] != b"RIFF" || &data[8..12] != b"WAVE" {
122 return None;
123 }
124 let mut off = 12;
126 while off + 8 <= data.len() {
127 let chunk_id = &data[off..off + 4];
128 let chunk_size =
129 u32::from_le_bytes([data[off + 4], data[off + 5], data[off + 6], data[off + 7]])
130 as usize;
131 let body_off = off + 8;
132 if body_off + chunk_size > data.len() {
133 return None;
134 }
135 if chunk_id == b"fmt " {
136 if chunk_size < 16 {
139 return None;
140 }
141 let body = &data[body_off..body_off + 16];
142 let tag = u16::from_le_bytes([body[0], body[1]]);
143 if tag != 1 {
144 return None; }
146 let channels = u16::from_le_bytes([body[2], body[3]]);
147 let sample_rate = u32::from_le_bytes([body[4], body[5], body[6], body[7]]);
148 let bits_per_sample = u16::from_le_bytes([body[14], body[15]]);
149 return Some(PcmParams {
150 sample_rate,
151 channels: u8::try_from(channels).ok()?,
152 bits_per_sample: u8::try_from(bits_per_sample).ok()?,
153 endianness: Endianness::Little,
154 });
155 }
156 off = body_off + chunk_size + (chunk_size & 1); }
158 None
159}
160
161#[must_use]
163#[allow(dead_code)]
164fn parse_aiff(data: &[u8]) -> Option<PcmParams> {
165 if data.len() < 12 || &data[0..4] != b"FORM" || &data[8..12] != b"AIFF" {
167 return None;
168 }
169 let mut off = 12;
170 while off + 8 <= data.len() {
171 let chunk_id = &data[off..off + 4];
172 let chunk_size =
173 u32::from_be_bytes([data[off + 4], data[off + 5], data[off + 6], data[off + 7]])
174 as usize;
175 let body_off = off + 8;
176 if body_off + chunk_size > data.len() {
177 return None;
178 }
179 if chunk_id == b"COMM" {
180 if chunk_size < 18 {
183 return None;
184 }
185 let body = &data[body_off..body_off + 18];
186 let channels = u16::from_be_bytes([body[0], body[1]]);
187 let bits_per_sample = u16::from_be_bytes([body[6], body[7]]);
188 return Some(PcmParams {
197 sample_rate: 0,
198 channels: u8::try_from(channels).ok()?,
199 bits_per_sample: u8::try_from(bits_per_sample).ok()?,
200 endianness: Endianness::Big,
201 });
202 }
203 off = body_off + chunk_size + (chunk_size & 1);
204 }
205 None
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn disabled_by_default() {
214 let c = PcmAudioCategorizer;
215 let wav = make_minimal_wav(44100, 2, 16);
217 assert!(c.categorize(Path::new("/x.wav"), &wav).is_none());
218 }
219
220 #[test]
221 fn wav_header_parsed_correctly() {
222 let wav = make_minimal_wav(48000, 1, 24);
223 let params = parse_wav(&wav).expect("wav parses");
224 assert_eq!(params.sample_rate, 48000);
225 assert_eq!(params.channels, 1);
226 assert_eq!(params.bits_per_sample, 24);
227 assert_eq!(params.endianness, Endianness::Little);
228 }
229
230 #[test]
231 fn rejects_non_pcm_wav() {
232 let mut wav = make_minimal_wav(44100, 2, 16);
234 wav[20] = 0x02;
236 wav[21] = 0x00;
237 assert!(parse_wav(&wav).is_none());
238 }
239
240 #[test]
241 fn rejects_non_wav_magic() {
242 assert!(parse_wav(b"NOTRIFF____WAVE____").is_none());
243 assert!(parse_wav(b"RIFF\x00\x00\x00\x00NOPE____").is_none());
244 }
245
246 fn make_minimal_wav(sample_rate: u32, channels: u8, bits: u8) -> Vec<u8> {
248 let mut wav = Vec::new();
249 wav.extend_from_slice(b"RIFF");
250 wav.extend_from_slice(&0u32.to_le_bytes()); wav.extend_from_slice(b"WAVE");
252 wav.extend_from_slice(b"fmt ");
253 wav.extend_from_slice(&16u32.to_le_bytes()); wav.extend_from_slice(&1u16.to_le_bytes()); wav.extend_from_slice(&[channels, 0]);
256 wav.extend_from_slice(&sample_rate.to_le_bytes());
257 wav.extend_from_slice(
258 &(sample_rate * u32::from(channels) * u32::from(bits) / 8).to_le_bytes(),
259 );
260 wav.extend_from_slice(&[(channels * bits / 8), 0]); wav.extend_from_slice(&[bits, 0]);
262 wav.extend_from_slice(b"data");
264 wav.extend_from_slice(&0u32.to_le_bytes());
265 let total = u32::try_from(wav.len()).unwrap_or(0) - 8;
267 wav[4..8].copy_from_slice(&total.to_le_bytes());
268 wav
269 }
270}