diff --git a/Cargo.lock b/Cargo.lock index d5cafc5..8b76c5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -828,6 +828,14 @@ dependencies = [ "embedded-io-async", ] +[[package]] +name = "embedded-audio" +version = "0.1.0" +source = "git+https://github.com/LegitCamper/embedded-audio#087784644d810b94dd659a03dbed4795dfb0bd24" +dependencies = [ + "heapless", +] + [[package]] name = "embedded-graphics" version = "0.8.1" @@ -1343,6 +1351,7 @@ dependencies = [ "goblin", "heapless", "kolibri-embedded-gui", + "micromath", "num_enum 0.7.4", "once_cell", "panic-probe", @@ -2516,6 +2525,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wav_player" +version = "0.1.0" +dependencies = [ + "abi", + "embedded-audio", + "embedded-graphics", + "rand", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 3080f3d..3e2e0ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "user-apps/calculator", "user-apps/snake", "user-apps/gallery", + "user-apps/wav_player", ] [profile.release] diff --git a/abi/src/lib.rs b/abi/src/lib.rs index e24f88c..2f4b96a 100644 --- a/abi/src/lib.rs +++ b/abi/src/lib.rs @@ -1,7 +1,10 @@ #![no_std] +pub use abi_sys::{ + AUDIO_BUFFER_LEN, audio_buffer_ready, file_len, get_key, list_dir, lock_display, print, + read_file, send_audio_buffer, sleep, +}; use abi_sys::{RngRequest, draw_iter, gen_rand}; -pub use abi_sys::{file_len, get_key, list_dir, lock_display, print, read_file, sleep}; use rand_core::RngCore; pub use shared::keyboard::{KeyCode, KeyEvent, KeyState, Modifiers}; use talc::*; diff --git a/abi_sys/src/lib.rs b/abi_sys/src/lib.rs index 2384c0a..f40a74f 100644 --- a/abi_sys/src/lib.rs +++ b/abi_sys/src/lib.rs @@ -31,6 +31,8 @@ pub enum CallAbiTable { ListDir = 6, ReadFile = 7, FileLen = 8, + AudioBufferReady = 9, + SendAudioBuffer = 10, } pub type PrintAbi = extern "C" fn(ptr: *const u8, len: usize); @@ -145,3 +147,27 @@ pub fn file_len(file: &str) -> usize { f(file.as_ptr(), file.len()) } } + +pub type AudioBufferReady = extern "C" fn() -> bool; + +#[allow(unused)] +pub fn audio_buffer_ready() -> bool { + unsafe { + let ptr = CALL_ABI_TABLE[CallAbiTable::AudioBufferReady as usize]; + let f: AudioBufferReady = core::mem::transmute(ptr); + f() + } +} + +pub const AUDIO_BUFFER_LEN: usize = 1024; + +pub type SendAudioBuffer = extern "C" fn(ptr: *const u8, len: usize); + +#[allow(unused)] +pub fn send_audio_buffer(buf: &[u8; AUDIO_BUFFER_LEN]) { + unsafe { + let ptr = CALL_ABI_TABLE[CallAbiTable::SendAudioBuffer as usize]; + let f: SendAudioBuffer = core::mem::transmute(ptr); + f(buf.as_ptr(), buf.len()) + } +} diff --git a/justfile b/justfile index d087935..6aa314c 100644 --- a/justfile +++ b/justfile @@ -13,3 +13,4 @@ userapps: just userapp calculator just userapp snake just userapp gallery + just userapp wav_player diff --git a/kernel/Cargo.toml b/kernel/Cargo.toml index 4c582d3..d8ccc74 100644 --- a/kernel/Cargo.toml +++ b/kernel/Cargo.toml @@ -94,3 +94,4 @@ bumpalo = "3.19.0" shared = { path = "../shared" } abi_sys = { path = "../abi_sys" } +micromath = "2.1.0" diff --git a/kernel/src/abi.rs b/kernel/src/abi.rs index 3c31cf2..b88a998 100644 --- a/kernel/src/abi.rs +++ b/kernel/src/abi.rs @@ -1,16 +1,17 @@ use abi_sys::{ - DrawIterAbi, FileLen, GenRand, GetKeyAbi, ListDir, LockDisplay, Modifiers, PrintAbi, ReadFile, - RngRequest, SleepAbi, + AUDIO_BUFFER_LEN, AudioBufferReady, DrawIterAbi, FileLen, GenRand, GetKeyAbi, ListDir, + LockDisplay, Modifiers, PrintAbi, ReadFile, RngRequest, SendAudioBuffer, SleepAbi, }; use alloc::{string::ToString, vec::Vec}; use core::sync::atomic::Ordering; use embassy_rp::clocks::{RoscRng, clk_sys_freq}; use embedded_graphics::{Pixel, draw_target::DrawTarget, pixelcolor::Rgb565}; -use embedded_sdmmc::{DirEntry, LfnBuffer, ShortFileName}; +use embedded_sdmmc::{DirEntry, LfnBuffer}; use heapless::spsc::Queue; use shared::keyboard::KeyEvent; use crate::{ + audio::{AUDIO_BUFFER, AUDIO_BUFFER_READY}, display::{FB_PAUSED, FRAMEBUFFER}, storage::{Dir, File, SDCARD}, }; @@ -181,7 +182,9 @@ pub extern "C" fn read_file( if !file.is_empty() { sd.access_root_dir(|root| { if let Ok(result) = recurse_file(&root, &file[1..], |file| { - file.seek_from_start(start_from as u32).unwrap(); + if file.offset() as usize != start_from { + file.seek_from_start(start_from as u32).unwrap(); + } file.read(&mut buf).unwrap() }) { read = result @@ -210,3 +213,21 @@ pub extern "C" fn file_len(str: *const u8, len: usize) -> usize { } len as usize } + +const _: AudioBufferReady = audio_buffer_ready; +pub extern "C" fn audio_buffer_ready() -> bool { + AUDIO_BUFFER_READY.load(Ordering::Acquire) +} + +const _: SendAudioBuffer = send_audio_buffer; +pub extern "C" fn send_audio_buffer(ptr: *const u8, len: usize) { + // SAFETY: caller guarantees `ptr` is valid for `len` bytes + let buf = unsafe { core::slice::from_raw_parts(ptr, len) }; + + while !AUDIO_BUFFER_READY.load(Ordering::Acquire) {} + + if buf.len() == AUDIO_BUFFER_LEN { + unsafe { AUDIO_BUFFER.copy_from_slice(buf) }; + AUDIO_BUFFER_READY.store(false, Ordering::Release) + } +} diff --git a/kernel/src/audio.rs b/kernel/src/audio.rs new file mode 100644 index 0000000..30c75e0 --- /dev/null +++ b/kernel/src/audio.rs @@ -0,0 +1,52 @@ +use core::{ + sync::atomic::{AtomicBool, Ordering}, + time::Duration, +}; +use embassy_rp::{ + Peri, + pio::Pio, + pio_programs::pwm::{PioPwm, PioPwmProgram}, + pwm::{Config, Pwm, SetDutyCycle}, +}; +use embassy_time::Timer; + +use crate::{Audio, Irqs}; + +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 static AUDIO_BUFFER_READY: AtomicBool = AtomicBool::new(true); + +pub const SAMPLE_RATE_HZ: u32 = 22_050; + +#[embassy_executor::task] +pub async fn audio_handler(audio: Audio) { + let var_name = Pio::new(audio.pio_left, Irqs); + let Pio { + mut common, sm0, .. + } = var_name; + + let prg = PioPwmProgram::new(&mut common); + let mut pwm_pio = PioPwm::new(&mut common, sm0, audio.left, &prg); + + let period = Duration::from_nanos(1_000_000_000 / SAMPLE_RATE_HZ as u64); + pwm_pio.set_period(period); + + pwm_pio.start(); + + let sample_interval = 1_000_000 / SAMPLE_RATE_HZ as u64; // in µs ≈ 45 µs + + loop { + for &sample in unsafe { &AUDIO_BUFFER }.iter() { + let period_ns = period.as_nanos() as u32; + let duty_ns = period_ns * (sample as u32) / 255; + pwm_pio.write(Duration::from_nanos(duty_ns as u64)); + Timer::after_micros(sample_interval).await; // sample interval = 1 / sample rate + } + + unsafe { core::mem::swap(&mut AUDIO_BUFFER, &mut AUDIO_BUFFER_1) }; + AUDIO_BUFFER_READY.store(true, Ordering::Release) + } +} diff --git a/kernel/src/elf.rs b/kernel/src/elf.rs index 488e11e..397baca 100644 --- a/kernel/src/elf.rs +++ b/kernel/src/elf.rs @@ -204,6 +204,8 @@ fn patch_abi( CallAbiTable::ListDir => abi::list_dir as usize, CallAbiTable::ReadFile => abi::read_file as usize, CallAbiTable::FileLen => abi::file_len as usize, + CallAbiTable::AudioBufferReady => abi::audio_buffer_ready as usize, + CallAbiTable::SendAudioBuffer => abi::send_audio_buffer as usize, }; unsafe { table_base.add(idx as usize).write(ptr); diff --git a/kernel/src/main.rs b/kernel/src/main.rs index 431fc50..48fb3d6 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -7,6 +7,7 @@ extern crate alloc; mod abi; +mod audio; mod display; mod elf; mod framebuffer; @@ -19,6 +20,7 @@ mod utils; use crate::{ abi::KEY_CACHE, + audio::audio_handler, display::{FRAMEBUFFER, display_handler, init_display}, peripherals::{ conf_peripherals, @@ -48,8 +50,10 @@ use embassy_rp::{ multicore::{Stack, spawn_core1}, peripherals::{ DMA_CH0, DMA_CH1, I2C1, PIN_6, PIN_7, PIN_10, PIN_11, PIN_12, PIN_13, PIN_14, PIN_15, - PIN_16, PIN_17, PIN_18, PIN_19, PIN_22, SPI0, SPI1, USB, + PIN_16, PIN_17, PIN_18, PIN_19, PIN_22, PIN_26, PIN_27, PIO0, PIO1, PWM_SLICE5, SPI0, SPI1, + USB, }, + pio, spi::{self, Spi}, usb as embassy_rp_usb, }; @@ -65,6 +69,7 @@ use talc::*; embassy_rp::bind_interrupts!(struct Irqs { I2C1_IRQ => i2c::InterruptHandler; USBCTRL_IRQ => embassy_rp_usb::InterruptHandler; + PIO0_IRQ_0 => pio::InterruptHandler; }); static mut CORE1_STACK: Stack<16384> = Stack::new(); @@ -105,6 +110,12 @@ async fn main(_spawner: Spawner) { data: p.PIN_14, reset: p.PIN_15, }; + let audio = Audio { + pio_left: p.PIO0, + pio_right: p.PIO1, + left: p.PIN_26, + right: p.PIN_27, + }; let sd = Sd { spi: p.SPI0, clk: p.PIN_18, @@ -119,7 +130,9 @@ async fn main(_spawner: Spawner) { data: p.PIN_6, }; let executor0 = EXECUTOR0.init(Executor::new()); - executor0.run(|spawner| unwrap!(spawner.spawn(kernel_task(spawner, display, sd, mcu, p.USB)))); + executor0.run(|spawner| { + unwrap!(spawner.spawn(kernel_task(spawner, display, audio, sd, mcu, p.USB))) + }); } // One-slot channel to pass EntryFn from core1 @@ -168,6 +181,12 @@ struct Display { data: Peri<'static, PIN_14>, reset: Peri<'static, PIN_15>, } +struct Audio { + pio_left: Peri<'static, PIO0>, + pio_right: Peri<'static, PIO1>, + left: Peri<'static, PIN_26>, + right: Peri<'static, PIN_27>, +} struct Sd { spi: Peri<'static, SPI0>, clk: Peri<'static, PIN_18>, @@ -225,6 +244,7 @@ async fn setup_sd(sd: Sd) { async fn kernel_task( spawner: Spawner, display: Display, + audio: Audio, sd: Sd, mcu: Mcu, usb: Peri<'static, USB>, @@ -234,6 +254,8 @@ async fn kernel_task( setup_display(display, spawner).await; setup_sd(sd).await; + spawner.spawn(audio_handler(audio)).unwrap(); + let _usb = embassy_rp_usb::Driver::new(usb, Irqs); // spawner.spawn(usb_handler(usb)).unwrap(); diff --git a/user-apps/wav_player/Cargo.toml b/user-apps/wav_player/Cargo.toml new file mode 100644 index 0000000..85c67ac --- /dev/null +++ b/user-apps/wav_player/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "wav_player" +version = "0.1.0" +edition = "2024" + +[dependencies] +abi = { path = "../../abi" } +embedded-graphics = "0.8.1" +rand = { version = "0.9.0", default-features = false } +embedded-audio = { git = "https://github.com/LegitCamper/embedded-audio" } diff --git a/user-apps/wav_player/build.rs b/user-apps/wav_player/build.rs new file mode 100644 index 0000000..332a55b --- /dev/null +++ b/user-apps/wav_player/build.rs @@ -0,0 +1,28 @@ +//! This build script copies the `memory.x` file from the crate root into +//! a directory where the linker can always find it at build time. +//! For many projects this is optional, as the linker always searches the +//! project root directory -- wherever `Cargo.toml` is. However, if you +//! are using a workspace or have a more complicated build setup, this +//! build script becomes required. Additionally, by requesting that +//! Cargo re-run the build script whenever `memory.x` is changed, +//! updating `memory.x` ensures a rebuild of the application with the +//! new memory settings. + +use std::env; +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; + +fn main() { + // Put `memory.x` in our output directory and ensure it's + // on the linker search path. + let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); + File::create(out.join("memory.x")) + .unwrap() + .write_all(include_bytes!("../memory.x")) + .unwrap(); + println!("cargo:rustc-link-search={}", out.display()); + + println!("cargo:rerun-if-changed=memory.x"); + println!("cargo:rustc-link-arg-bins=-Tmemory.x"); +} diff --git a/user-apps/wav_player/src/main.rs b/user-apps/wav_player/src/main.rs new file mode 100644 index 0000000..3a970b5 --- /dev/null +++ b/user-apps/wav_player/src/main.rs @@ -0,0 +1,92 @@ +#![no_std] +#![no_main] + +extern crate alloc; +use abi::{ + AUDIO_BUFFER_LEN, KeyCode, KeyState, Rng, audio_buffer_ready, + display::{Display, SCREEN_HEIGHT, SCREEN_WIDTH}, + file_len, get_key, lock_display, print, read_file, send_audio_buffer, sleep, +}; +use alloc::{format, string::String}; +use core::panic::PanicInfo; +use embedded_audio::{AudioFile, PlatformFile, PlatformFileError, wav::Wav}; +use embedded_graphics::{pixelcolor::Rgb565, prelude::RgbColor}; + +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + print(&format!( + "user panic: {} @ {:?}", + info.message(), + info.location(), + )); + loop {} +} + +#[unsafe(no_mangle)] +pub extern "Rust" fn _start() { + main() +} + +pub fn main() { + print("Starting Wav player app"); + let mut display = Display; + + let mut buf = [0_u8; AUDIO_BUFFER_LEN]; + + let file = File::new(String::from("/music/test.wav")); + let mut wav = Wav::new(file).unwrap(); + loop { + if audio_buffer_ready() { + if wav.is_eof() { + wav.restart().unwrap() + } + + wav.read(&mut buf).unwrap(); + send_audio_buffer(&buf); + } + } +} + +struct File { + current_pos: usize, + file: String, +} + +impl File { + fn new(file: String) -> Self { + Self { + current_pos: 0, + file, + } + } +} + +impl PlatformFile for File { + fn read(&mut self, buf: &mut [u8]) -> Result { + let read = read_file(&self.file, self.current_pos, buf); + Ok(read) + } + + fn seek_from_current(&mut self, offset: i64) -> Result<(), PlatformFileError> { + if offset.is_positive() { + self.current_pos += offset as usize; + } else { + self.current_pos -= offset as usize; + } + Ok(()) + } + + fn seek_from_start(&mut self, offset: usize) -> Result<(), PlatformFileError> { + self.current_pos = offset; + Ok(()) + } + + fn seek_from_end(&mut self, offset: usize) -> Result<(), PlatformFileError> { + self.current_pos = self.length() - offset; + Ok(()) + } + + fn length(&mut self) -> usize { + file_len(&self.file) + } +}