kenwood_thd75/sdcard/
audio.rs1use super::{SdCardError, read_u16_le, read_u32_le};
23
24const EXPECTED_SAMPLE_RATE: u32 = 16_000;
26
27const EXPECTED_BITS_PER_SAMPLE: u16 = 16;
29
30const EXPECTED_CHANNELS: u16 = 1;
32
33const WAV_FORMAT_PCM: u16 = 1;
35
36const MIN_WAV_SIZE: usize = 44;
38
39#[derive(Debug, Clone, PartialEq)]
44pub struct AudioRecording {
45 pub sample_rate: u32,
47 pub bits_per_sample: u16,
49 pub channels: u16,
51 pub data_length: u32,
53 pub duration_secs: f64,
57}
58
59pub fn parse(data: &[u8]) -> Result<AudioRecording, SdCardError> {
76 if data.len() < MIN_WAV_SIZE {
77 return Err(SdCardError::FileTooSmall {
78 expected: MIN_WAV_SIZE,
79 actual: data.len(),
80 });
81 }
82
83 if &data[0..4] != b"RIFF" {
85 return Err(SdCardError::InvalidWavHeader {
86 detail: "missing RIFF magic bytes".to_owned(),
87 });
88 }
89
90 if &data[8..12] != b"WAVE" {
92 return Err(SdCardError::InvalidWavHeader {
93 detail: "missing WAVE format identifier".to_owned(),
94 });
95 }
96
97 let fmt_offset = find_chunk(data, *b"fmt ").ok_or_else(|| SdCardError::InvalidWavHeader {
100 detail: "fmt chunk not found".to_owned(),
101 })?;
102
103 if data.len() < fmt_offset + 24 {
105 return Err(SdCardError::FileTooSmall {
106 expected: fmt_offset + 24,
107 actual: data.len(),
108 });
109 }
110
111 let audio_format = read_u16_le(data, fmt_offset + 8);
112 if audio_format != WAV_FORMAT_PCM {
113 return Err(SdCardError::InvalidWavHeader {
114 detail: format!(
115 "unsupported audio format code {audio_format} (expected {WAV_FORMAT_PCM} for PCM)"
116 ),
117 });
118 }
119
120 let channels = read_u16_le(data, fmt_offset + 10);
121 let sample_rate = read_u32_le(data, fmt_offset + 12);
122 let bits_per_sample = read_u16_le(data, fmt_offset + 22);
123
124 if sample_rate != EXPECTED_SAMPLE_RATE
126 || bits_per_sample != EXPECTED_BITS_PER_SAMPLE
127 || channels != EXPECTED_CHANNELS
128 {
129 return Err(SdCardError::UnexpectedAudioFormat {
130 sample_rate,
131 bits_per_sample,
132 channels,
133 });
134 }
135
136 let data_offset = find_chunk(data, *b"data").ok_or_else(|| SdCardError::InvalidWavHeader {
138 detail: "data chunk not found".to_owned(),
139 })?;
140
141 if data.len() < data_offset + 8 {
142 return Err(SdCardError::FileTooSmall {
143 expected: data_offset + 8,
144 actual: data.len(),
145 });
146 }
147
148 let data_length = read_u32_le(data, data_offset + 4);
149
150 let bytes_per_sample_frame =
151 f64::from(sample_rate) * f64::from(channels) * f64::from(bits_per_sample) / 8.0;
152 let duration_secs = f64::from(data_length) / bytes_per_sample_frame;
153
154 Ok(AudioRecording {
155 sample_rate,
156 bits_per_sample,
157 channels,
158 data_length,
159 duration_secs,
160 })
161}
162
163fn find_chunk(data: &[u8], id: [u8; 4]) -> Option<usize> {
166 let mut offset = 12; while offset + 8 <= data.len() {
169 if data[offset..offset + 4] == id {
170 return Some(offset);
171 }
172
173 let chunk_size = read_u32_le(data, offset + 4) as usize;
175 let padded = (chunk_size + 1) & !1;
177 offset += 8 + padded;
178 }
179
180 None
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 fn build_wav(sample_rate: u32, bits_per_sample: u16, channels: u16, pcm_len: u32) -> Vec<u8> {
189 let mut buf = Vec::new();
190
191 buf.extend_from_slice(b"RIFF");
193 let file_size = 36 + pcm_len;
194 buf.extend_from_slice(&file_size.to_le_bytes());
195 buf.extend_from_slice(b"WAVE");
196
197 buf.extend_from_slice(b"fmt ");
199 buf.extend_from_slice(&16u32.to_le_bytes()); buf.extend_from_slice(&WAV_FORMAT_PCM.to_le_bytes()); buf.extend_from_slice(&channels.to_le_bytes());
202 buf.extend_from_slice(&sample_rate.to_le_bytes());
203 let byte_rate = sample_rate * u32::from(channels) * u32::from(bits_per_sample) / 8;
204 buf.extend_from_slice(&byte_rate.to_le_bytes());
205 let block_align = channels * bits_per_sample / 8;
206 buf.extend_from_slice(&block_align.to_le_bytes());
207 buf.extend_from_slice(&bits_per_sample.to_le_bytes());
208
209 buf.extend_from_slice(b"data");
211 buf.extend_from_slice(&pcm_len.to_le_bytes());
212
213 let fill = pcm_len.min(256);
215 buf.resize(buf.len() + fill as usize, 0);
216
217 buf
218 }
219
220 #[test]
221 fn parse_valid_d75_wav() {
222 let pcm_len: u32 = 32_000;
224 let wav = build_wav(16_000, 16, 1, pcm_len);
225 let rec = parse(&wav).unwrap();
226
227 assert_eq!(rec.sample_rate, 16_000);
228 assert_eq!(rec.bits_per_sample, 16);
229 assert_eq!(rec.channels, 1);
230 assert_eq!(rec.data_length, pcm_len);
231 assert!((rec.duration_secs - 1.0).abs() < 0.001);
232 }
233
234 #[test]
235 fn parse_duration_calculation() {
236 let pcm_len: u32 = 9_600_000;
238 let wav = build_wav(16_000, 16, 1, pcm_len);
239 let rec = parse(&wav).unwrap();
240
241 assert!((rec.duration_secs - 300.0).abs() < 0.001);
242 }
243
244 #[test]
245 fn too_short_returns_error() {
246 let data = b"RIFF";
247 let err = parse(data).unwrap_err();
248 assert!(matches!(err, SdCardError::FileTooSmall { .. }));
249 }
250
251 #[test]
252 fn empty_returns_error() {
253 let err = parse(b"").unwrap_err();
254 assert!(matches!(err, SdCardError::FileTooSmall { .. }));
255 }
256
257 #[test]
258 fn wrong_riff_magic() {
259 let mut wav = build_wav(16_000, 16, 1, 32_000);
260 wav[0..4].copy_from_slice(b"XXXX");
261 let err = parse(&wav).unwrap_err();
262 assert!(matches!(err, SdCardError::InvalidWavHeader { .. }));
263 }
264
265 #[test]
266 fn wrong_wave_format() {
267 let mut wav = build_wav(16_000, 16, 1, 32_000);
268 wav[8..12].copy_from_slice(b"AVI ");
269 let err = parse(&wav).unwrap_err();
270 assert!(matches!(err, SdCardError::InvalidWavHeader { .. }));
271 }
272
273 #[test]
274 fn non_pcm_format_rejected() {
275 let mut wav = build_wav(16_000, 16, 1, 32_000);
276 wav[20..22].copy_from_slice(&3u16.to_le_bytes());
278 let err = parse(&wav).unwrap_err();
279 assert!(matches!(err, SdCardError::InvalidWavHeader { .. }));
280 }
281
282 #[test]
283 fn wrong_sample_rate_rejected() {
284 let wav = build_wav(44_100, 16, 1, 88_200);
285 let err = parse(&wav).unwrap_err();
286 assert!(matches!(err, SdCardError::UnexpectedAudioFormat { .. }));
287 }
288
289 #[test]
290 fn wrong_bit_depth_rejected() {
291 let wav = build_wav(16_000, 8, 1, 16_000);
292 let err = parse(&wav).unwrap_err();
293 assert!(matches!(err, SdCardError::UnexpectedAudioFormat { .. }));
294 }
295
296 #[test]
297 fn stereo_rejected() {
298 let wav = build_wav(16_000, 16, 2, 64_000);
299 let err = parse(&wav).unwrap_err();
300 assert!(matches!(err, SdCardError::UnexpectedAudioFormat { .. }));
301 }
302}