File
Blob: firmware/crates/radio-core/src/music.rs
| 1 | //! Checked reader for Opus packs produced by `prepare_music.py`. |
| 2 | mod catalog; |
| 3 | pub use catalog::{Catalog, MAX_MUSIC_BYTES, MAX_TOTAL_FRAMES, MAX_TRACKS, ReadAt, Song}; |
| 4 | |
| 5 | use serde::Serialize; |
| 6 | use std::fmt; |
| 7 | |
| 8 | pub const FRAME_MS: u32 = 20; |
| 9 | pub const BANDS: usize = 32; |
| 10 | pub const MAX_OPUS_BYTES: usize = 1275; |
| 11 | pub const MAX_TAG_BYTES: usize = 256; |
| 12 | const HEADER_BYTES: usize = 64; |
| 13 | const MAX_FRAMES: u32 = 30_000; |
| 14 | |
| 15 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 16 | pub enum Error { |
| 17 | Header, |
| 18 | Format, |
| 19 | FrameCount, |
| 20 | Truncated, |
| 21 | PacketSize, |
| 22 | Checksum, |
| 23 | Metadata, |
| 24 | Memory, |
| 25 | TrackCount, |
| 26 | Read, |
| 27 | } |
| 28 | |
| 29 | impl fmt::Display for Error { |
| 30 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| 31 | f.write_str(match self { |
| 32 | Self::Header => "invalid music pack header", |
| 33 | Self::Format => "music must be stereo 48 kHz Opus with 20 ms frames", |
| 34 | Self::FrameCount => "music frame count is outside the supported range", |
| 35 | Self::Truncated => "music pack ends inside a frame", |
| 36 | Self::PacketSize => "invalid Opus packet size", |
| 37 | Self::Checksum => "music pack checksum mismatch", |
| 38 | Self::Metadata => "invalid music title or artist", |
| 39 | Self::Memory => "cannot allocate music frame index", |
| 40 | Self::TrackCount => "music track count or index is outside the supported range", |
| 41 | Self::Read => "music storage read failed", |
| 42 | }) |
| 43 | } |
| 44 | } |
| 45 | impl std::error::Error for Error {} |
| 46 | |
| 47 | /// Metadata from a validated song in the music catalog. |
| 48 | #[derive(Debug, Clone, PartialEq, Eq, Serialize)] |
| 49 | #[serde(rename_all = "camelCase")] |
| 50 | pub struct Track { |
| 51 | pub title: String, |
| 52 | pub artist: String, |
| 53 | pub duration_ms: u32, |
| 54 | } |
| 55 | |
| 56 | fn tag(bytes: &[u8]) -> Result<&str, Error> { |
| 57 | let text = std::str::from_utf8(bytes).map_err(|_| Error::Metadata)?; |
| 58 | if text.chars().any(char::is_control) { |
| 59 | return Err(Error::Metadata); |
| 60 | } |
| 61 | Ok(text) |
| 62 | } |
| 63 | |
| 64 | // Only called after validating the complete fixed-size header. |
| 65 | fn le32(bytes: &[u8], start: usize) -> u32 { |
| 66 | u32::from_le_bytes( |
| 67 | bytes[start..start + 4] |
| 68 | .try_into() |
| 69 | .expect("validated header"), |
| 70 | ) |
| 71 | } |