File
Blob: firmware/crates/esp32-radio/src/platform/music.rs
| 1 | //! One bounded read cache. Native reads copy synchronously into Rust-owned bytes. |
| 2 | use super::ffi; |
| 3 | use crate::error::{Error, Result}; |
| 4 | use radio_core::music::{Error as ReadError, MAX_MUSIC_BYTES, ReadAt}; |
| 5 | |
| 6 | const CACHE_BYTES: usize = 32 * 1024; |
| 7 | |
| 8 | pub(crate) struct MusicStorage { |
| 9 | length: usize, |
| 10 | cache: Vec<u8>, |
| 11 | start: usize, |
| 12 | used: usize, |
| 13 | validating: bool, |
| 14 | pub(crate) reads: u32, |
| 15 | pub(crate) max_read_us: u32, |
| 16 | } |
| 17 | |
| 18 | impl std::fmt::Debug for MusicStorage { |
| 19 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 20 | f.debug_struct("MusicStorage") |
| 21 | .field("length", &self.length) |
| 22 | .field("cache_bytes", &self.cache.len()) |
| 23 | .field("reads", &self.reads) |
| 24 | .field("max_read_us", &self.max_read_us) |
| 25 | .finish_non_exhaustive() |
| 26 | } |
| 27 | } |
| 28 | |
| 29 | impl MusicStorage { |
| 30 | pub(crate) fn open() -> Result<Self> { |
| 31 | // SAFETY: IDF owns the immutable partition descriptor for the process lifetime. |
| 32 | let length = unsafe { ffi::radio_music_size() }; |
| 33 | if length == 0 || length > MAX_MUSIC_BYTES { |
| 34 | return Err(Error::new("music partition is missing or too large")); |
| 35 | } |
| 36 | Ok(Self { |
| 37 | length, |
| 38 | cache: vec![0; CACHE_BYTES], |
| 39 | start: 0, |
| 40 | used: 0, |
| 41 | validating: true, |
| 42 | reads: 0, |
| 43 | max_read_us: 0, |
| 44 | }) |
| 45 | } |
| 46 | pub(crate) fn finish_validation(&mut self) { |
| 47 | self.validating = false; |
| 48 | self.reads = 0; |
| 49 | self.max_read_us = 0; |
| 50 | } |
| 51 | pub(crate) fn cache_bytes(&self) -> usize { |
| 52 | self.cache.len() |
| 53 | } |
| 54 | } |
| 55 | |
| 56 | impl ReadAt for MusicStorage { |
| 57 | fn size(&self) -> usize { |
| 58 | self.length |
| 59 | } |
| 60 | fn read_exact(&mut self, offset: usize, out: &mut [u8]) -> std::result::Result<(), ReadError> { |
| 61 | if offset |
| 62 | .checked_add(out.len()) |
| 63 | .is_none_or(|end| end > self.length) |
| 64 | { |
| 65 | return Err(ReadError::Truncated); |
| 66 | } |
| 67 | let mut cursor = offset; |
| 68 | let mut copied = 0; |
| 69 | while copied < out.len() { |
| 70 | if cursor < self.start || cursor >= self.start + self.used { |
| 71 | let start = cursor / CACHE_BYTES * CACHE_BYTES; |
| 72 | let length = CACHE_BYTES.min(self.length - start); |
| 73 | let began = super::now_us(); |
| 74 | // SAFETY: cache is initialized, exclusively borrowed and large enough. |
| 75 | // The native function validates partition bounds, copies synchronously, |
| 76 | // and retains no pointer. No flash writes occur while this reader lives. |
| 77 | let result = |
| 78 | unsafe { ffi::radio_music_read(start, self.cache.as_mut_ptr(), length) }; |
| 79 | if result != 0 { |
| 80 | return Err(ReadError::Read); |
| 81 | } |
| 82 | self.reads = self.reads.saturating_add(1); |
| 83 | self.max_read_us = self |
| 84 | .max_read_us |
| 85 | .max(super::now_us().saturating_sub(began).min(u32::MAX as u64) as u32); |
| 86 | self.start = start; |
| 87 | self.used = length; |
| 88 | // Full-catalog CRC validation can keep core 0 busy for seconds. |
| 89 | // Let its idle task run between cache fills; playback reads do |
| 90 | // not take this extra sleep. |
| 91 | if self.validating { |
| 92 | std::thread::sleep(std::time::Duration::from_millis(1)); |
| 93 | } |
| 94 | } |
| 95 | let within = cursor - self.start; |
| 96 | let count = (self.used - within).min(out.len() - copied); |
| 97 | out[copied..copied + count].copy_from_slice(&self.cache[within..within + count]); |
| 98 | cursor += count; |
| 99 | copied += count; |
| 100 | } |
| 101 | Ok(()) |
| 102 | } |
| 103 | } |