diff --git a/Cargo.lock b/Cargo.lock index 24d9dfa..097fc7f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1478,6 +1478,7 @@ dependencies = [ "embedded-layout", "embedded-sdmmc", "embedded-text", + "fixed", "goblin", "heapless", "micromath", diff --git a/abi/src/lib.rs b/abi/src/lib.rs index 7bd1b2d..18f453a 100644 --- a/abi/src/lib.rs +++ b/abi/src/lib.rs @@ -269,7 +269,7 @@ pub mod fs { } pub mod audio { - pub use abi_sys::{AUDIO_BUFFER_LEN, audio_buffer_ready}; + pub use abi_sys::{AUDIO_BUFFER_LEN, AUDIO_BUFFER_SAMPLES, audio_buffer_ready}; pub fn send_audio_buffer(buf: &[u8]) { abi_sys::send_audio_buffer(buf.as_ptr(), buf.len()) diff --git a/abi_sys/src/lib.rs b/abi_sys/src/lib.rs index 82f2f6e..0b26e9f 100644 --- a/abi_sys/src/lib.rs +++ b/abi_sys/src/lib.rs @@ -481,7 +481,8 @@ pub fn audio_buffer_ready() -> bool { } } -pub const AUDIO_BUFFER_LEN: usize = 1024; +pub const AUDIO_BUFFER_SAMPLES: usize = 1024; +pub const AUDIO_BUFFER_LEN: usize = AUDIO_BUFFER_SAMPLES * 2; pub type SendAudioBuffer = extern "C" fn(ptr: *const u8, len: usize); diff --git a/kernel/Cargo.toml b/kernel/Cargo.toml index 8293a53..aae1192 100644 --- a/kernel/Cargo.toml +++ b/kernel/Cargo.toml @@ -82,6 +82,8 @@ embedded-graphics = { version = "0.8.1" } embedded-text = "0.7.2" embedded-layout = "0.4.2" +micromath = "2.1.0" +fixed = "1.29.0" strum = { version = "0.27.2", default-features = false } rand = { version = "0.9.0", default-features = false } once_cell = { version = "1.21.3", default-features = false } @@ -98,4 +100,3 @@ embedded-alloc = { version = "0.6.0", features = [ bumpalo = "3.19.0" abi_sys = { path = "../abi_sys" } -micromath = "2.1.0" diff --git a/kernel/src/abi.rs b/kernel/src/abi.rs index 6254934..8f77bd7 100644 --- a/kernel/src/abi.rs +++ b/kernel/src/abi.rs @@ -1,5 +1,5 @@ use abi_sys::{ - AUDIO_BUFFER_LEN, AllocAbi, AudioBufferReady, CLayout, CPixel, DeallocAbi, DrawIterAbi, + AUDIO_BUFFER_SAMPLES, AllocAbi, AudioBufferReady, CLayout, CPixel, DeallocAbi, DrawIterAbi, FileLen, GenRand, GetMsAbi, ListDir, PrintAbi, ReadFile, RngRequest, SendAudioBuffer, SleepMsAbi, WriteFile, keyboard::*, }; @@ -209,7 +209,6 @@ fn recurse_file( dirs: &[&str], mut access: impl FnMut(&mut File) -> T, ) -> Result { - defmt::info!("dir: {}, dirs: {}", dir, dirs); if dirs.len() == 1 { let mut b = [0_u8; 50]; let mut buf = LfnBuffer::new(&mut b); @@ -346,8 +345,15 @@ pub extern "C" fn send_audio_buffer(ptr: *const u8, len: usize) { while !AUDIO_BUFFER_READY.load(Ordering::Acquire) {} - if buf.len() == AUDIO_BUFFER_LEN { + if buf.len() == AUDIO_BUFFER_SAMPLES * 2 { + AUDIO_BUFFER_READY.store(false, Ordering::Release); unsafe { AUDIO_BUFFER.copy_from_slice(buf) }; - AUDIO_BUFFER_READY.store(false, Ordering::Release) + } else { + #[cfg(feature = "defmt")] + defmt::warn!( + "user audio stream was wrong size: {} should be {}", + buf.len(), + AUDIO_BUFFER_SAMPLES * 2 + ) } } diff --git a/kernel/src/audio.rs b/kernel/src/audio.rs index b5fc298..917f806 100644 --- a/kernel/src/audio.rs +++ b/kernel/src/audio.rs @@ -1,51 +1,92 @@ use crate::Audio; use core::sync::atomic::{AtomicBool, Ordering}; +use embassy_futures::join::join; use embassy_rp::{ Peri, + clocks::clk_sys_freq, dma::{AnyChannel, Channel}, gpio::Level, pio::{ Common, Config, Direction, FifoJoin, Instance, LoadedProgram, PioPin, ShiftConfig, - ShiftDirection, StateMachine, program::pio_asm, + StateMachine, program::pio_asm, }, pio_programs::clock_divider::calculate_pio_clock_divider, }; -use embassy_time::Timer; +use fixed::traits::ToFixed; -const AUDIO_BUFFER_LEN: usize = 1024; -const _: () = assert!(AUDIO_BUFFER_LEN == abi_sys::AUDIO_BUFFER_LEN); -pub static mut AUDIO_BUFFER: [u8; AUDIO_BUFFER_LEN] = [0; AUDIO_BUFFER_LEN]; -static mut AUDIO_BUFFER_1: [u8; AUDIO_BUFFER_LEN] = [0; AUDIO_BUFFER_LEN]; +pub const SAMPLE_RATE_HZ: u32 = 22_050; +const AUDIO_BUFFER_SAMPLES: usize = 1024; +const _: () = assert!(AUDIO_BUFFER_SAMPLES == abi_sys::AUDIO_BUFFER_SAMPLES); + +// 8bit stereo interleaved PCM audio buffers +pub static mut AUDIO_BUFFER: [u8; AUDIO_BUFFER_SAMPLES * 2] = [0; AUDIO_BUFFER_SAMPLES * 2]; +static mut AUDIO_BUFFER_1: [u8; AUDIO_BUFFER_SAMPLES * 2] = [0; AUDIO_BUFFER_SAMPLES * 2]; pub static AUDIO_BUFFER_READY: AtomicBool = AtomicBool::new(true); -pub const SAMPLE_RATE_HZ: u32 = 8_000; - #[embassy_executor::task] pub async fn audio_handler(mut audio: Audio) { + const SILENCE_VALUE: u8 = u8::MAX / 2; + let prg = PioPwmAudioProgram8Bit::new(&mut audio.pio); - defmt::info!("loaded prg"); + let mut pwm_pio_left = + PioPwmAudio::new(audio.dma0, &mut audio.pio, audio.sm0, audio.left, &prg); + let mut pwm_pio_right = + PioPwmAudio::new(audio.dma1, &mut audio.pio, audio.sm1, audio.right, &prg); - let mut pwm_pio = PioPwmAudio::new(audio.dma, &mut audio.pio, audio.sm0, audio.left, &prg); - defmt::info!("cfgd sm"); - - pwm_pio.configure(SAMPLE_RATE_HZ); - pwm_pio.start(); loop { - pwm_pio.write(unsafe { &AUDIO_BUFFER_1 }).await; + write_samples(&mut pwm_pio_left, &mut pwm_pio_right, unsafe { + &AUDIO_BUFFER_1 + }) + .await; + unsafe { &mut AUDIO_BUFFER_1 }.fill(SILENCE_VALUE); unsafe { core::mem::swap(&mut AUDIO_BUFFER, &mut AUDIO_BUFFER_1) }; AUDIO_BUFFER_READY.store(true, Ordering::Release) } } +async fn write_samples( + left: &mut PioPwmAudio<'static, PIO, 0>, + right: &mut PioPwmAudio<'static, PIO, 1>, + buf: &[u8], +) { + // pack two samples per word + let mut packed_buf_left: [u32; AUDIO_BUFFER_SAMPLES / 2] = [0; AUDIO_BUFFER_SAMPLES / 2]; + let mut packed_buf_right: [u32; AUDIO_BUFFER_SAMPLES / 2] = [0; AUDIO_BUFFER_SAMPLES / 2]; + + for ((pl, pr), sample) in packed_buf_left + .iter_mut() + .zip(packed_buf_right.iter_mut()) + .zip(buf.chunks(4)) + { + *pl = pack_u8_samples(sample[0], sample[2]); + *pr = pack_u8_samples(sample[1], sample[3]); + } + + let left_fut = left + .sm + .tx() + .dma_push(left.dma.reborrow(), &packed_buf_left, false); + + let right_fut = right + .sm + .tx() + .dma_push(right.dma.reborrow(), &packed_buf_right, false); + + join(left_fut, right_fut).await; +} + struct PioPwmAudioProgram8Bit<'d, PIO: Instance>(LoadedProgram<'d, PIO>); +/// Writes one sample to pwm as high and low time impl<'d, PIO: Instance> PioPwmAudioProgram8Bit<'d, PIO> { fn new(common: &mut Common<'d, PIO>) -> Self { + // only uses 16 bits top for high, bottom for low + // allows two samples per word let prg = pio_asm!( - "out x, 8", // samples << - "out y, 8", // samples max >> + "out x, 8", // pwm high time + "out y, 8", // pwm low time "loop_high:", "set pins, 1", // keep pin high "jmp x-- loop_high", // decrement X until 0 @@ -62,7 +103,6 @@ impl<'d, PIO: Instance> PioPwmAudioProgram8Bit<'d, PIO> { struct PioPwmAudio<'d, PIO: Instance, const SM: usize> { dma: Peri<'d, AnyChannel>, - cfg: Config<'d, PIO>, sm: StateMachine<'d, PIO, SM>, } @@ -87,51 +127,24 @@ impl<'d, PIO: Instance, const SM: usize> PioPwmAudio<'d, PIO, SM> { cfg.use_program(&prg.0, &[]); sm.set_config(&cfg); + let target_clock = (u8::MAX as u32 + 1) * SAMPLE_RATE_HZ; + let divider = (clk_sys_freq() / (target_clock * 2)).to_fixed(); + sm.set_clock_divider(divider); + + sm.set_enable(true); + Self { dma: dma.into(), - cfg, sm, } } - - fn configure(&mut self, sample_rate: u32) { - let cycles_per_sample = u8::MAX as u32 + 2; // X_max + Y_max + movs - let target_pio_hz = cycles_per_sample * sample_rate; // ~11.3 MHz - - let divider = calculate_pio_clock_divider(target_pio_hz); - self.cfg.clock_divider = divider; - self.sm.set_clock_divider(divider); - } - - async fn write(&mut self, buf: &[u8]) { - let mut packed_buf = [0_u32; AUDIO_BUFFER_LEN / 4]; - - for (packed_sample, sample) in packed_buf.iter_mut().zip(buf.chunks(2)) { - *packed_sample = pack_two_samples(sample[0], sample[1]); - } - - self.sm - .tx() - .dma_push(self.dma.reborrow(), &packed_buf, false) - .await - } - - fn start(&mut self) { - self.sm.set_enable(true); - } - - fn stop(&mut self) { - self.sm.set_enable(false); - } } -fn pack_two_samples(s1: u8, s2: u8) -> u32 { - let x = ((s1 as u16) << 8 | (255 - s1) as u16); // original - let y = ((s2 as u16) << 8 | (255 - s2) as u16); - - // Scale to full 16-bit for higher volume: - let x_scaled = ((x as u32) << 8) | (x as u32 >> 8); - let y_scaled = ((y as u32) << 8) | (y as u32 >> 8); - - (x_scaled << 16) | y_scaled +/// packs two u8 samples into 32bit word +fn pack_u8_samples(sample1: u8, sample2: u8) -> u32 { + (u8_pcm_to_pwm(sample1) as u32) << 16 | u8_pcm_to_pwm(sample2) as u32 +} + +fn u8_pcm_to_pwm(sample: u8) -> u16 { + ((sample as u16) << 8) | ((u8::MAX - sample) as u16) } diff --git a/kernel/src/main.rs b/kernel/src/main.rs index 107e332..b1a6efc 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -121,10 +121,12 @@ async fn watchdog_task(mut watchdog: Watchdog) { static ENABLE_UI: AtomicBool = AtomicBool::new(true); static UI_CHANGE: Signal = Signal::new(); +const OVERCLOCK: u32 = 300_000_000; + #[embassy_executor::main] async fn main(_spawner: Spawner) { let p = if cfg!(feature = "overclock") { - let clocks = ClockConfig::system_freq(300_000_000).unwrap(); + let clocks = ClockConfig::system_freq(OVERCLOCK).unwrap(); let config = Config::new(clocks); embassy_rp::init(config) } else { @@ -156,11 +158,12 @@ async fn main(_spawner: Spawner) { } = Pio::new(p.PIO0, Irqs); let audio = Audio { - dma: p.DMA_CH3, pio: common, sm0, - sm1, + dma0: p.DMA_CH3, left: p.PIN_26, + sm1, + dma1: p.DMA_CH4, right: p.PIN_27, }; let sd = Sd { @@ -244,11 +247,12 @@ struct Display { reset: Peri<'static, PIN_15>, } struct Audio { - dma: Peri<'static, DMA_CH3>, pio: Common<'static, PIO0>, + dma0: Peri<'static, DMA_CH3>, sm0: StateMachine<'static, PIO0, 0>, - sm1: StateMachine<'static, PIO0, 1>, left: Peri<'static, PIN_26>, + dma1: Peri<'static, DMA_CH4>, + sm1: StateMachine<'static, PIO0, 1>, right: Peri<'static, PIN_27>, } struct Sd { diff --git a/user-apps/wav_player/src/main.rs b/user-apps/wav_player/src/main.rs index 46d3277..c662038 100644 --- a/user-apps/wav_player/src/main.rs +++ b/user-apps/wav_player/src/main.rs @@ -33,6 +33,9 @@ pub fn main() { let file = File::new(String::from("/music/test.wav")); let mut wav = Wav::new(file).unwrap(); + println!("sample rate: {}", wav.sample_rate()); + println!("channels: {:?}", wav.channels() as u8); + loop { if audio_buffer_ready() { if wav.is_eof() { @@ -44,7 +47,7 @@ pub fn main() { } let event = get_key(); - if event.state != KeyState::Idle { + if event.state == KeyState::Released { match event.key { KeyCode::Esc => return, _ => (),