mirror of
https://github.com/LegitCamper/picocalc-os-rs.git
synced 2026-02-10 22:15:24 +00:00
audio works
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -1478,6 +1478,7 @@ dependencies = [
|
|||||||
"embedded-layout",
|
"embedded-layout",
|
||||||
"embedded-sdmmc",
|
"embedded-sdmmc",
|
||||||
"embedded-text",
|
"embedded-text",
|
||||||
|
"fixed",
|
||||||
"goblin",
|
"goblin",
|
||||||
"heapless",
|
"heapless",
|
||||||
"micromath",
|
"micromath",
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ pub mod fs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub mod audio {
|
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]) {
|
pub fn send_audio_buffer(buf: &[u8]) {
|
||||||
abi_sys::send_audio_buffer(buf.as_ptr(), buf.len())
|
abi_sys::send_audio_buffer(buf.as_ptr(), buf.len())
|
||||||
|
|||||||
@@ -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);
|
pub type SendAudioBuffer = extern "C" fn(ptr: *const u8, len: usize);
|
||||||
|
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ embedded-graphics = { version = "0.8.1" }
|
|||||||
embedded-text = "0.7.2"
|
embedded-text = "0.7.2"
|
||||||
embedded-layout = "0.4.2"
|
embedded-layout = "0.4.2"
|
||||||
|
|
||||||
|
micromath = "2.1.0"
|
||||||
|
fixed = "1.29.0"
|
||||||
strum = { version = "0.27.2", default-features = false }
|
strum = { version = "0.27.2", default-features = false }
|
||||||
rand = { version = "0.9.0", default-features = false }
|
rand = { version = "0.9.0", default-features = false }
|
||||||
once_cell = { version = "1.21.3", 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"
|
bumpalo = "3.19.0"
|
||||||
|
|
||||||
abi_sys = { path = "../abi_sys" }
|
abi_sys = { path = "../abi_sys" }
|
||||||
micromath = "2.1.0"
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use abi_sys::{
|
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,
|
FileLen, GenRand, GetMsAbi, ListDir, PrintAbi, ReadFile, RngRequest, SendAudioBuffer,
|
||||||
SleepMsAbi, WriteFile, keyboard::*,
|
SleepMsAbi, WriteFile, keyboard::*,
|
||||||
};
|
};
|
||||||
@@ -209,7 +209,6 @@ fn recurse_file<T>(
|
|||||||
dirs: &[&str],
|
dirs: &[&str],
|
||||||
mut access: impl FnMut(&mut File) -> T,
|
mut access: impl FnMut(&mut File) -> T,
|
||||||
) -> Result<T, ()> {
|
) -> Result<T, ()> {
|
||||||
defmt::info!("dir: {}, dirs: {}", dir, dirs);
|
|
||||||
if dirs.len() == 1 {
|
if dirs.len() == 1 {
|
||||||
let mut b = [0_u8; 50];
|
let mut b = [0_u8; 50];
|
||||||
let mut buf = LfnBuffer::new(&mut b);
|
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) {}
|
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) };
|
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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +1,92 @@
|
|||||||
use crate::Audio;
|
use crate::Audio;
|
||||||
use core::sync::atomic::{AtomicBool, Ordering};
|
use core::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use embassy_futures::join::join;
|
||||||
use embassy_rp::{
|
use embassy_rp::{
|
||||||
Peri,
|
Peri,
|
||||||
|
clocks::clk_sys_freq,
|
||||||
dma::{AnyChannel, Channel},
|
dma::{AnyChannel, Channel},
|
||||||
gpio::Level,
|
gpio::Level,
|
||||||
pio::{
|
pio::{
|
||||||
Common, Config, Direction, FifoJoin, Instance, LoadedProgram, PioPin, ShiftConfig,
|
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,
|
pio_programs::clock_divider::calculate_pio_clock_divider,
|
||||||
};
|
};
|
||||||
use embassy_time::Timer;
|
use fixed::traits::ToFixed;
|
||||||
|
|
||||||
const AUDIO_BUFFER_LEN: usize = 1024;
|
pub const SAMPLE_RATE_HZ: u32 = 22_050;
|
||||||
const _: () = assert!(AUDIO_BUFFER_LEN == abi_sys::AUDIO_BUFFER_LEN);
|
const AUDIO_BUFFER_SAMPLES: usize = 1024;
|
||||||
pub static mut AUDIO_BUFFER: [u8; AUDIO_BUFFER_LEN] = [0; AUDIO_BUFFER_LEN];
|
const _: () = assert!(AUDIO_BUFFER_SAMPLES == abi_sys::AUDIO_BUFFER_SAMPLES);
|
||||||
static mut AUDIO_BUFFER_1: [u8; AUDIO_BUFFER_LEN] = [0; AUDIO_BUFFER_LEN];
|
|
||||||
|
// 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 static AUDIO_BUFFER_READY: AtomicBool = AtomicBool::new(true);
|
||||||
|
|
||||||
pub const SAMPLE_RATE_HZ: u32 = 8_000;
|
|
||||||
|
|
||||||
#[embassy_executor::task]
|
#[embassy_executor::task]
|
||||||
pub async fn audio_handler(mut audio: Audio) {
|
pub async fn audio_handler(mut audio: Audio) {
|
||||||
|
const SILENCE_VALUE: u8 = u8::MAX / 2;
|
||||||
|
|
||||||
let prg = PioPwmAudioProgram8Bit::new(&mut audio.pio);
|
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 {
|
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) };
|
unsafe { core::mem::swap(&mut AUDIO_BUFFER, &mut AUDIO_BUFFER_1) };
|
||||||
AUDIO_BUFFER_READY.store(true, Ordering::Release)
|
AUDIO_BUFFER_READY.store(true, Ordering::Release)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn write_samples<PIO: Instance>(
|
||||||
|
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>);
|
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> {
|
impl<'d, PIO: Instance> PioPwmAudioProgram8Bit<'d, PIO> {
|
||||||
fn new(common: &mut Common<'d, PIO>) -> Self {
|
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!(
|
let prg = pio_asm!(
|
||||||
"out x, 8", // samples <<
|
"out x, 8", // pwm high time
|
||||||
"out y, 8", // samples max >>
|
"out y, 8", // pwm low time
|
||||||
"loop_high:",
|
"loop_high:",
|
||||||
"set pins, 1", // keep pin high
|
"set pins, 1", // keep pin high
|
||||||
"jmp x-- loop_high", // decrement X until 0
|
"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> {
|
struct PioPwmAudio<'d, PIO: Instance, const SM: usize> {
|
||||||
dma: Peri<'d, AnyChannel>,
|
dma: Peri<'d, AnyChannel>,
|
||||||
cfg: Config<'d, PIO>,
|
|
||||||
sm: StateMachine<'d, PIO, SM>,
|
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, &[]);
|
cfg.use_program(&prg.0, &[]);
|
||||||
sm.set_config(&cfg);
|
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 {
|
Self {
|
||||||
dma: dma.into(),
|
dma: dma.into(),
|
||||||
cfg,
|
|
||||||
sm,
|
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]) {
|
/// packs two u8 samples into 32bit word
|
||||||
let mut packed_buf = [0_u32; AUDIO_BUFFER_LEN / 4];
|
fn pack_u8_samples(sample1: u8, sample2: u8) -> u32 {
|
||||||
|
(u8_pcm_to_pwm(sample1) as u32) << 16 | u8_pcm_to_pwm(sample2) as u32
|
||||||
for (packed_sample, sample) in packed_buf.iter_mut().zip(buf.chunks(2)) {
|
|
||||||
*packed_sample = pack_two_samples(sample[0], sample[1]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.sm
|
fn u8_pcm_to_pwm(sample: u8) -> u16 {
|
||||||
.tx()
|
((sample as u16) << 8) | ((u8::MAX - sample) as u16)
|
||||||
.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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,10 +121,12 @@ async fn watchdog_task(mut watchdog: Watchdog) {
|
|||||||
static ENABLE_UI: AtomicBool = AtomicBool::new(true);
|
static ENABLE_UI: AtomicBool = AtomicBool::new(true);
|
||||||
static UI_CHANGE: Signal<CriticalSectionRawMutex, ()> = Signal::new();
|
static UI_CHANGE: Signal<CriticalSectionRawMutex, ()> = Signal::new();
|
||||||
|
|
||||||
|
const OVERCLOCK: u32 = 300_000_000;
|
||||||
|
|
||||||
#[embassy_executor::main]
|
#[embassy_executor::main]
|
||||||
async fn main(_spawner: Spawner) {
|
async fn main(_spawner: Spawner) {
|
||||||
let p = if cfg!(feature = "overclock") {
|
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);
|
let config = Config::new(clocks);
|
||||||
embassy_rp::init(config)
|
embassy_rp::init(config)
|
||||||
} else {
|
} else {
|
||||||
@@ -156,11 +158,12 @@ async fn main(_spawner: Spawner) {
|
|||||||
} = Pio::new(p.PIO0, Irqs);
|
} = Pio::new(p.PIO0, Irqs);
|
||||||
|
|
||||||
let audio = Audio {
|
let audio = Audio {
|
||||||
dma: p.DMA_CH3,
|
|
||||||
pio: common,
|
pio: common,
|
||||||
sm0,
|
sm0,
|
||||||
sm1,
|
dma0: p.DMA_CH3,
|
||||||
left: p.PIN_26,
|
left: p.PIN_26,
|
||||||
|
sm1,
|
||||||
|
dma1: p.DMA_CH4,
|
||||||
right: p.PIN_27,
|
right: p.PIN_27,
|
||||||
};
|
};
|
||||||
let sd = Sd {
|
let sd = Sd {
|
||||||
@@ -244,11 +247,12 @@ struct Display {
|
|||||||
reset: Peri<'static, PIN_15>,
|
reset: Peri<'static, PIN_15>,
|
||||||
}
|
}
|
||||||
struct Audio {
|
struct Audio {
|
||||||
dma: Peri<'static, DMA_CH3>,
|
|
||||||
pio: Common<'static, PIO0>,
|
pio: Common<'static, PIO0>,
|
||||||
|
dma0: Peri<'static, DMA_CH3>,
|
||||||
sm0: StateMachine<'static, PIO0, 0>,
|
sm0: StateMachine<'static, PIO0, 0>,
|
||||||
sm1: StateMachine<'static, PIO0, 1>,
|
|
||||||
left: Peri<'static, PIN_26>,
|
left: Peri<'static, PIN_26>,
|
||||||
|
dma1: Peri<'static, DMA_CH4>,
|
||||||
|
sm1: StateMachine<'static, PIO0, 1>,
|
||||||
right: Peri<'static, PIN_27>,
|
right: Peri<'static, PIN_27>,
|
||||||
}
|
}
|
||||||
struct Sd {
|
struct Sd {
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ pub fn main() {
|
|||||||
|
|
||||||
let file = File::new(String::from("/music/test.wav"));
|
let file = File::new(String::from("/music/test.wav"));
|
||||||
let mut wav = Wav::new(file).unwrap();
|
let mut wav = Wav::new(file).unwrap();
|
||||||
|
println!("sample rate: {}", wav.sample_rate());
|
||||||
|
println!("channels: {:?}", wav.channels() as u8);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if audio_buffer_ready() {
|
if audio_buffer_ready() {
|
||||||
if wav.is_eof() {
|
if wav.is_eof() {
|
||||||
@@ -44,7 +47,7 @@ pub fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let event = get_key();
|
let event = get_key();
|
||||||
if event.state != KeyState::Idle {
|
if event.state == KeyState::Released {
|
||||||
match event.key {
|
match event.key {
|
||||||
KeyCode::Esc => return,
|
KeyCode::Esc => return,
|
||||||
_ => (),
|
_ => (),
|
||||||
|
|||||||
Reference in New Issue
Block a user