File
Blob: archive/fft-benchmark/firmware/crates/radio-core/src/music.rs
| 1 | //! Checked reader for the version 1 pack produced by `prepare-music.py`. |
| 2 | use std::{fmt, num::NonZeroU32}; |
| 3 | |
| 4 | pub const FRAME_MS: u32 = 20; |
| 5 | pub const BANDS: usize = 32; |
| 6 | pub const MAX_OPUS_BYTES: usize = 1275; |
| 7 | const HEADER_BYTES: usize = 64; |
| 8 | const MAX_FRAMES: u32 = 30_000; |
| 9 | |
| 10 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 11 | pub enum Error { |
| 12 | Header, |
| 13 | Format, |
| 14 | FrameCount, |
| 15 | Truncated, |
| 16 | PacketSize, |
| 17 | Checksum, |
| 18 | Memory, |
| 19 | } |
| 20 | |
| 21 | impl 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 | } |
| 34 | impl std::error::Error for Error {} |
| 35 | |
| 36 | /// A validated pack borrowing immutable storage, including any erased flash tail. |
| 37 | #[derive(Debug)] |
| 38 | pub struct Pack<'a> { |
| 39 | bytes: &'a [u8], |
| 40 | offsets: Vec<usize>, |
| 41 | frames: NonZeroU32, |
| 42 | } |
| 43 | |
| 44 | #[derive(Debug, Clone, Copy)] |
| 45 | pub struct Frame<'a> { |
| 46 | pub opus: &'a [u8], |
| 47 | pub bands: &'a [u8; BANDS], |
| 48 | } |
| 49 | |
| 50 | impl<'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. |
| 107 | fn 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 | } |