Skip to content
File

Blob: archive/fft-benchmark/firmware/crates/radio-core/src/music.rs

rust114 lines
1//! Checked reader for the version 1 pack produced by `prepare-music.py`.
2use std::{fmt, num::NonZeroU32};
3 
4pub const FRAME_MS: u32 = 20;
5pub const BANDS: usize = 32;
6pub const MAX_OPUS_BYTES: usize = 1275;
7const HEADER_BYTES: usize = 64;
8const MAX_FRAMES: u32 = 30_000;
9 
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum Error {
12 Header,
13 Format,
14 FrameCount,
15 Truncated,
16 PacketSize,
17 Checksum,
18 Memory,
19}
20 
21impl fmt::Display for Error {
22 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23 f.write_str(match self {
24 Self::Header => "invalid music pack header",
25 Self::Format => "music must be stereo 48 kHz Opus with 20 ms frames",
26 Self::FrameCount => "music frame count is outside the supported range",
27 Self::Truncated => "music pack ends inside a frame",
28 Self::PacketSize => "invalid Opus packet size",
29 Self::Checksum => "music pack checksum mismatch",
30 Self::Memory => "cannot allocate music frame index",
31 })
32 }
33}
34impl std::error::Error for Error {}
35 
36/// A validated pack borrowing immutable storage, including any erased flash tail.
37#[derive(Debug)]
38pub struct Pack<'a> {
39 bytes: &'a [u8],
40 offsets: Vec<usize>,
41 frames: NonZeroU32,
42}
43 
44#[derive(Debug, Clone, Copy)]
45pub struct Frame<'a> {
46 pub opus: &'a [u8],
47 pub bands: &'a [u8; BANDS],
48}
49 
50impl<'a> Pack<'a> {
51 /// Checks every record boundary and the CRC before exposing any frame.
52 pub fn parse(bytes: &'a [u8]) -> Result<Self, Error> {
53 if bytes.len() < HEADER_BYTES || &bytes[..8] != b"S3MUSIC\0" || le32(bytes, 8) != 1 {
54 return Err(Error::Header);
55 }
56 if le32(bytes, 16) != 48_000 || le32(bytes, 20) != 2 || le32(bytes, 24) != FRAME_MS {
57 return Err(Error::Format);
58 }
59 let frames = NonZeroU32::new(le32(bytes, 12))
60 .filter(|v| v.get() <= MAX_FRAMES)
61 .ok_or(Error::FrameCount)?;
62 let mut offsets = Vec::new();
63 offsets
64 .try_reserve_exact(frames.get() as usize)
65 .map_err(|_| Error::Memory)?;
66 let mut cursor = HEADER_BYTES;
67 for _ in 0..frames.get() {
68 let record = bytes
69 .get(cursor..cursor + 2 + BANDS)
70 .ok_or(Error::Truncated)?;
71 let size = u16::from_le_bytes([record[0], record[1]]) as usize;
72 if !(1..=MAX_OPUS_BYTES).contains(&size) {
73 return Err(Error::PacketSize);
74 }
75 let end = cursor + 2 + BANDS + size;
76 if end > bytes.len() {
77 return Err(Error::Truncated);
78 }
79 offsets.push(cursor);
80 cursor = end;
81 }
82 if crc32fast::hash(&bytes[HEADER_BYTES..cursor]) != le32(bytes, 28) {
83 return Err(Error::Checksum);
84 }
85 Ok(Self {
86 bytes: &bytes[..cursor],
87 offsets,
88 frames,
89 })
90 }
91 
92 pub fn frame_count(&self) -> NonZeroU32 {
93 self.frames
94 }
95 
96 pub fn frame(&self, index: u32) -> Option<Frame<'a>> {
97 let start = *self.offsets.get(index as usize)?;
98 let size = u16::from_le_bytes([self.bytes[start], self.bytes[start + 1]]) as usize;
99 Some(Frame {
100 opus: &self.bytes[start + 2 + BANDS..start + 2 + BANDS + size],
101 bands: self.bytes[start + 2..start + 2 + BANDS].try_into().ok()?,
102 })
103 }
104}
105 
106// Only called after validating the complete fixed-size header.
107fn le32(bytes: &[u8], start: usize) -> u32 {
108 u32::from_le_bytes(
109 bytes[start..start + 4]
110 .try_into()
111 .expect("validated header"),
112 )
113}