diff --git a/Cargo.lock b/Cargo.lock index 7272162..4aef9d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16,7 +16,7 @@ checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" [[package]] name = "firefly-audio" -version = "0.1.0" +version = "0.2.0" dependencies = [ "embedded-io", "micromath", diff --git a/Cargo.toml b/Cargo.toml index 9de295e..b4d973b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "firefly-audio" -version = "0.1.0" +version = "0.2.0" +rust-version = "1.82.0" edition = "2021" authors = ["Firefly Zero team"] description = "Tree-based generator and processor for sound. Powers audio in Firefly Zero." diff --git a/src/basic_types.rs b/src/basic_types.rs index 184341d..3ac349d 100644 --- a/src/basic_types.rs +++ b/src/basic_types.rs @@ -13,7 +13,7 @@ pub struct Frame { } impl Frame { - pub(crate) fn zero() -> Self { + pub(crate) const fn zero() -> Self { Self { left: Sample::ZERO, right: None, @@ -21,7 +21,7 @@ impl Frame { } #[must_use] - pub fn mono(s: Sample) -> Self { + pub const fn mono(s: Sample) -> Self { Self { left: s, right: None, @@ -29,7 +29,7 @@ impl Frame { } #[must_use] - pub fn stereo(l: Sample, r: Sample) -> Self { + pub const fn stereo(l: Sample, r: Sample) -> Self { Self { left: l, right: Some(r), @@ -37,10 +37,10 @@ impl Frame { } } -impl Add<&Frame> for Frame { +impl Add<&Self> for Frame { type Output = Self; - fn add(self, rhs: &Frame) -> Self { + fn add(self, rhs: &Self) -> Self { let left = self.left + rhs.left; let right = match (self.right, rhs.right) { (None, None) => None, diff --git a/src/error.rs b/src/error.rs index b960c92..2ccf6fb 100644 --- a/src/error.rs +++ b/src/error.rs @@ -9,9 +9,9 @@ pub enum NodeError { impl fmt::Display for NodeError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - NodeError::TooManyChildren => write!(f, "the node has too many children"), - NodeError::TooManyNodes => write!(f, "the tree has too many nodes"), - NodeError::UnknownID(id) => write!(f, "there is no node with id {id}"), + Self::TooManyChildren => write!(f, "the node has too many children"), + Self::TooManyNodes => write!(f, "the tree has too many nodes"), + Self::UnknownID(id) => write!(f, "there is no node with id {id}"), } } } diff --git a/src/lib.rs b/src/lib.rs index a482e03..11d36fd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,12 +1,17 @@ #![cfg_attr(not(test), no_std)] #![forbid(unsafe_code)] -#![deny(clippy::pedantic)] +#![deny( + clippy::all, + clippy::pedantic, + clippy::nursery, + clippy::allow_attributes +)] #![allow(clippy::wildcard_imports)] -// TODO: fix casting warning #![expect( + // TODO: fix casting warning clippy::cast_precision_loss, clippy::module_name_repetitions, - clippy::new_without_default + clippy::new_without_default, )] extern crate alloc; @@ -15,6 +20,7 @@ mod error; mod manager; pub mod modulators; mod node; +mod pcm; mod processor; mod processors; mod sources; @@ -23,6 +29,7 @@ pub use basic_types::*; pub use error::*; pub use manager::*; pub use node::*; +pub use pcm::*; pub use processor::*; pub use processors::*; pub use sources::*; diff --git a/src/manager.rs b/src/manager.rs index de2fa02..6adf689 100644 --- a/src/manager.rs +++ b/src/manager.rs @@ -40,7 +40,6 @@ impl Manager { /// If the parent node is not present, returns [`NodeError::UnknownID`]. /// If there are too many nodes, returns [`NodeError::TooManyChildren`] /// or [`NodeError::TooManyNodes`]. - #[allow(clippy::cast_possible_truncation)] pub fn add_node(&mut self, parent_id: u32, b: Box) -> Result { const MAX_NODES: usize = 32; if self.paths.len() >= MAX_NODES { @@ -51,6 +50,7 @@ impl Manager { }; let parent_node = self.root.get_node(parent_path); let sub_id = parent_node.add(b)?; + #[expect(clippy::cast_possible_truncation)] let id = self.paths.len() as u32; let mut path = Vec::new(); path.extend_from_slice(parent_path); @@ -134,10 +134,7 @@ impl Manager { /// Write the given frame (starting from skip index) into the beginning of the buffer. fn fill_buf(buf: &mut [i16], frame: &Frame, skip: usize) -> usize { // make iterators over left and right channels - let right = match frame.right { - Some(right) => right, - None => frame.left, - }; + let right = frame.right.unwrap_or(frame.left); let mut left = frame.left.as_array_ref().iter(); let mut right = right.as_array_ref().iter(); @@ -153,7 +150,7 @@ fn fill_buf(buf: &mut [i16], frame: &Frame, skip: usize) -> usize { } let mut written = 0; - #[allow(clippy::cast_possible_truncation)] + #[expect(clippy::cast_possible_truncation)] for tar in buf.iter_mut() { let chan = if even { &mut left } else { &mut right }; let Some(s) = chan.next() else { break }; @@ -202,7 +199,7 @@ mod tests { assert_eq!(&buf[16..], &[0, 0, 0, 0]); } - #[allow(clippy::cast_lossless)] + #[expect(clippy::cast_lossless)] fn u2f(u: u16) -> f32 { u as f32 / 100. } diff --git a/src/modulators.rs b/src/modulators.rs index 4b64a15..d2240c3 100644 --- a/src/modulators.rs +++ b/src/modulators.rs @@ -29,7 +29,7 @@ pub struct Hold { impl Hold { #[must_use] - pub fn new(v1: f32, v2: f32, time: u32) -> Self { + pub const fn new(v1: f32, v2: f32, time: u32) -> Self { Self { v1, v2, time } } } @@ -54,7 +54,7 @@ pub struct Linear { impl Linear { #[must_use] - pub fn new(start: f32, end: f32, start_at: u32, end_at: u32) -> Self { + pub const fn new(start: f32, end: f32, start_at: u32, end_at: u32) -> Self { Self { start, end, @@ -78,11 +78,11 @@ impl Modulator for Linear { } let elapsed = now - self.start_at; let ratio = elapsed as f32 / duration as f32; - self.start + (self.end - self.start) * ratio + (self.end - self.start).mul_add(ratio, self.start) } } -// Sine wave low-frequency oscillator. +/// Sine wave low-frequency oscillator. pub struct Sine { s: f32, mid: f32, @@ -103,7 +103,7 @@ impl Sine { impl Modulator for Sine { fn get(&self, now: u32) -> f32 { let s = F32Ext::sin(self.s * now as f32); - self.mid + self.amp * s + self.amp.mul_add(s, self.mid) } } diff --git a/src/node.rs b/src/node.rs index 2f2252f..15ad007 100644 --- a/src/node.rs +++ b/src/node.rs @@ -5,7 +5,7 @@ use alloc::vec::Vec; const MODULATE_EVERY: u32 = SAMPLE_RATE / 60; -// A modulator connected to a parameter of a node. +/// A modulator connected to a parameter of a node. struct WiredModulator { param: u8, modulator: Box, @@ -28,11 +28,11 @@ impl Node { } /// Add a child node. - #[allow(clippy::cast_possible_truncation)] pub(crate) fn add(&mut self, proc: Box) -> Result { if self.children.len() >= 4 { return Err(NodeError::TooManyChildren); } + #[expect(clippy::cast_possible_truncation)] let child_id = self.children.len() as u8; let child = Self { children: Vec::new(), diff --git a/src/pcm.rs b/src/pcm.rs new file mode 100644 index 0000000..c6d2cf8 --- /dev/null +++ b/src/pcm.rs @@ -0,0 +1,184 @@ +use crate::*; +use alloc::vec::Vec; +use core::fmt::Display; + +pub enum PcmError { + TooShort, + BadMagicNumber, + BadSampleRate(u16), +} + +impl Display for PcmError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::TooShort => write!(f, "file is too short"), + Self::BadMagicNumber => write!(f, "bad magic number"), + Self::BadSampleRate(sr) => write!(f, "bad sample rate: expected 44100, got {sr}"), + } + } +} + +/// Play audio from a pulse-code modulated audio file. +pub struct Pcm { + reader: R, + _sample_rate: u16, + is16: bool, + stereo: bool, + _adpcm: bool, +} + +impl Pcm { + /// Create the source from a file in the Firefly Zero format. + /// + /// # Errors + /// + /// Returns an error if the file header is invalid. + pub fn from_file(mut reader: R) -> Result { + let mut header = [0u8; 4]; + let res = reader.read_exact(&mut header); + if res.is_err() { + return Err(PcmError::TooShort); + } + if header[0] != 0x31 { + return Err(PcmError::BadMagicNumber); + } + let sample_rate = u16::from_le_bytes([header[2], header[3]]); + if sample_rate != 44100 { + return Err(PcmError::BadSampleRate(sample_rate)); + } + Ok(Self { + reader, + _sample_rate: sample_rate, + stereo: header[1] & 0b_100 != 0, + is16: header[1] & 0b_010 != 0, + _adpcm: header[1] & 0b_001 != 0, + }) + } +} + +impl Processor for Pcm { + fn process_children(&mut self, _cn: &mut Vec) -> Option { + let f = match (self.is16, self.stereo) { + // 8 bit mono + (false, false) => { + let mut buf = [0u8; 8]; + self.reader.read_exact(&mut buf).ok()?; + let s = Sample::new(i8s_to_f32s(buf)); + Frame::mono(s) + } + // 8 bit stereo + (false, true) => { + let mut buf = [0u8; 16]; + self.reader.read_exact(&mut buf).ok()?; + let left = Sample::new(i8s_to_f32s_left(buf)); + let right = Sample::new(i8s_to_f32s_right(buf)); + Frame::stereo(left, right) + } + // 16 bit mono + (true, false) => { + let mut buf = [0u8; 16]; + self.reader.read_exact(&mut buf).ok()?; + let s = Sample::new(i16s_to_f32s(buf)); + Frame::mono(s) + } + // 16 bit stereo + (true, true) => { + let mut buf = [0u8; 32]; + self.reader.read_exact(&mut buf).ok()?; + let left = Sample::new(i16s_to_f32s_left(buf)); + let right = Sample::new(i16s_to_f32s_right(buf)); + Frame::stereo(left, right) + } + }; + Some(f) + } +} + +fn i8s_to_f32s(us: [u8; 8]) -> [f32; 8] { + [ + i8_to_f32(us[0]), + i8_to_f32(us[1]), + i8_to_f32(us[2]), + i8_to_f32(us[3]), + i8_to_f32(us[4]), + i8_to_f32(us[5]), + i8_to_f32(us[6]), + i8_to_f32(us[7]), + ] +} + +fn i8s_to_f32s_left(us: [u8; 16]) -> [f32; 8] { + [ + i8_to_f32(us[0]), + i8_to_f32(us[2]), + i8_to_f32(us[4]), + i8_to_f32(us[6]), + i8_to_f32(us[8]), + i8_to_f32(us[10]), + i8_to_f32(us[12]), + i8_to_f32(us[14]), + ] +} + +fn i8s_to_f32s_right(us: [u8; 16]) -> [f32; 8] { + [ + i8_to_f32(us[1]), + i8_to_f32(us[3]), + i8_to_f32(us[5]), + i8_to_f32(us[7]), + i8_to_f32(us[9]), + i8_to_f32(us[11]), + i8_to_f32(us[13]), + i8_to_f32(us[15]), + ] +} + +fn i16s_to_f32s(us: [u8; 16]) -> [f32; 8] { + [ + i16_to_f32(us[0], us[1]), + i16_to_f32(us[2], us[3]), + i16_to_f32(us[4], us[5]), + i16_to_f32(us[6], us[7]), + i16_to_f32(us[8], us[9]), + i16_to_f32(us[10], us[11]), + i16_to_f32(us[12], us[13]), + i16_to_f32(us[14], us[15]), + ] +} + +fn i16s_to_f32s_left(us: [u8; 32]) -> [f32; 8] { + [ + i16_to_f32(us[0], us[1]), + i16_to_f32(us[4], us[5]), + i16_to_f32(us[8], us[9]), + i16_to_f32(us[12], us[13]), + i16_to_f32(us[16], us[17]), + i16_to_f32(us[20], us[21]), + i16_to_f32(us[24], us[25]), + i16_to_f32(us[28], us[29]), + ] +} + +fn i16s_to_f32s_right(us: [u8; 32]) -> [f32; 8] { + [ + i16_to_f32(us[2], us[3]), + i16_to_f32(us[6], us[7]), + i16_to_f32(us[10], us[11]), + i16_to_f32(us[14], us[15]), + i16_to_f32(us[18], us[19]), + i16_to_f32(us[22], us[23]), + i16_to_f32(us[26], us[27]), + i16_to_f32(us[30], us[31]), + ] +} + +fn i8_to_f32(u: u8) -> f32 { + #[expect(clippy::cast_possible_wrap)] + let i = u as i8; + f32::from(i) * 2. / f32::from(u8::MAX) - 1. +} + +fn i16_to_f32(l: u8, r: u8) -> f32 { + let i = i16::from_le_bytes([l, r]); + f32::from(i) * 2. / f32::from(u16::MAX) - 1. +} diff --git a/src/processors.rs b/src/processors.rs index 525a516..33e2d8a 100644 --- a/src/processors.rs +++ b/src/processors.rs @@ -6,7 +6,7 @@ pub struct Mix {} impl Mix { #[must_use] - pub fn new() -> Self { + pub const fn new() -> Self { Self {} } } @@ -18,7 +18,7 @@ pub struct AllForOne {} impl AllForOne { #[must_use] - pub fn new() -> Self { + pub const fn new() -> Self { Self {} } } @@ -44,7 +44,7 @@ pub struct Gain { impl Gain { #[must_use] - pub fn new(lvl: f32) -> Self { + pub const fn new(lvl: f32) -> Self { Self { lvl } } } @@ -69,7 +69,7 @@ pub struct Loop {} impl Loop { #[must_use] - pub fn new() -> Self { + pub const fn new() -> Self { Self {} } } @@ -96,7 +96,7 @@ pub struct Concat {} impl Concat { // TODO: support fade-in/fade-out #[must_use] - pub fn new() -> Self { + pub const fn new() -> Self { Self {} } } @@ -157,7 +157,7 @@ pub struct Mute { impl Mute { #[must_use] - pub fn new() -> Self { + pub const fn new() -> Self { Self { muted: false } } @@ -196,7 +196,7 @@ pub struct Pause { impl Pause { #[must_use] - pub fn new() -> Self { + pub const fn new() -> Self { Self { paused: false } } @@ -235,7 +235,7 @@ pub struct TrackPosition { impl TrackPosition { #[must_use] - pub fn new() -> Self { + pub const fn new() -> Self { Self { elapsed: 0 } } } @@ -327,7 +327,7 @@ impl Processor for LowHighPass { } fn set(&mut self, param: u8, val: f32) { - #[allow(clippy::float_cmp)] + #[expect(clippy::float_cmp)] if param == 0 && val != self.freq { self.freq = val; self.update_coefs(); @@ -356,7 +356,7 @@ pub struct TakeLeft {} impl TakeLeft { #[must_use] - pub fn new() -> Self { + pub const fn new() -> Self { Self {} } } @@ -377,7 +377,7 @@ pub struct TakeRight {} impl TakeRight { #[must_use] - pub fn new() -> Self { + pub const fn new() -> Self { Self {} } } @@ -400,7 +400,7 @@ pub struct Swap {} impl Swap { #[must_use] - pub fn new() -> Self { + pub const fn new() -> Self { Self {} } } @@ -415,7 +415,7 @@ impl Processor for Swap { } } -// Clamp the amplitude onto the given interval +/// Clamp the amplitude onto the given interval pub struct Clip { low: f32, high: f32, @@ -423,7 +423,7 @@ pub struct Clip { impl Clip { #[must_use] - pub fn new(low: f32, high: f32) -> Self { + pub const fn new(low: f32, high: f32) -> Self { Self { low, high } } } diff --git a/src/sources.rs b/src/sources.rs index 9397d92..aa75496 100644 --- a/src/sources.rs +++ b/src/sources.rs @@ -10,7 +10,7 @@ pub struct Empty {} impl Empty { #[must_use] - pub fn new() -> Self { + pub const fn new() -> Self { Self {} } } @@ -26,7 +26,7 @@ pub struct Zero {} impl Zero { #[must_use] - pub fn new() -> Self { + pub const fn new() -> Self { Self {} } }