Skip to content
File

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

rust72 lines
1//! Checked reader for Opus packs produced by `prepare_music.py`.
2mod catalog;
3pub use catalog::{Catalog, MAX_MUSIC_BYTES, MAX_TOTAL_FRAMES, MAX_TRACKS, ReadAt, Song};
4 
5use serde::Serialize;
6use std::fmt;
7 
8pub const FRAME_MS: u32 = 20;
9pub const BANDS: usize = 32;
10pub const MAX_OPUS_BYTES: usize = 1275;
11pub const MAX_TAG_BYTES: usize = 256;
12const HEADER_BYTES: usize = 64;
13const MAX_FRAMES: u32 = 30_000;
14 
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum Error {
17 Header,
18 Format,
19 FrameCount,
20 Truncated,
21 PacketSize,
22 Checksum,
23 Metadata,
24 Memory,
25 TrackCount,
26 Read,
27}
28 
29impl 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}
45impl 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")]
50pub struct Track {
51 pub title: String,
52 pub artist: String,
53 pub duration_ms: u32,
54}
55 
56fn 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.
65fn 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}