//! I2S audio output via PIO + DMA, using the embassy-rp `PioI2sOut` driver. //! //! Targets a PCM5102A (or compatible) I2S DAC wired as: //! GPIO 9 → BCK (bit clock) //! GPIO 10 → LRCK (word select) //! GPIO 11 → DATA //! //! # Buffer format //! `PioI2sOut` expects a flat `&[u32]` of interleaved stereo words: //! `[L0, R0, L1, R1, …]`. Each word holds a 24-bit signed sample //! left-justified in bits 31..8, which is the format expected by PCM5102A in //! 32-bit I2S mode. //! //! # Double-buffering //! Two static DMA buffers (A and B) alternate: //! 1. DMA drains buffer A → PIO TX FIFO. //! 2. CPU renders the next block into buffer B. //! 3. Swap and repeat. //! //! At 48 kHz with 256 frames per block (5.33 ms/block) the Cortex-M33 at //! 150 MHz has ~10× headroom for the DSP render. use embassy_executor::task; use embassy_rp::{ peripherals::{DMA_CH0, PIN_9, PIN_10, PIN_11, PIO0}, pio::Pio, pio_programs::i2s::{PioI2sOut, PioI2sOutProgram}, Peri, }; use synth_core::{ config::SR_48000, envelope::Adsr, filter::{FilterMode, Svf}, math::midi_note_to_hz, oscillator::Vco, vca::Vca, AudioProcessor, }; use crate::{Irqs, PARAMS}; /// Stereo frames per render block (one frame = left sample + right sample). const BLOCK_FRAMES: usize = 256; /// DMA buffer length in u32 words (two words per frame: L and R). const BLOCK_WORDS: usize = BLOCK_FRAMES * 2; const SAMPLE_RATE: u32 = 48_000; const BIT_DEPTH: u32 = 24; // Ping-pong DMA buffers. Placed in .bss (zero-initialised RAM) at link time. // SAFETY: only `audio_task` ever accesses these after `main` has run. static mut DMA_BUF_A: [u32; BLOCK_WORDS] = [0; BLOCK_WORDS]; static mut DMA_BUF_B: [u32; BLOCK_WORDS] = [0; BLOCK_WORDS]; /// Convert a synth-core f32 sample (−1.0 to +1.0) to a 24-bit signed integer /// left-justified in a u32, as expected by PCM5102A in 32-bit I2S mode. #[inline] fn f32_to_i2s(s: f32) -> u32 { let clamped = if s > 1.0 { 1.0 } else if s < -1.0 { -1.0 } else { s }; let i24 = (clamped * 8_388_607.0) as i32; // Left-justify: move to bits 31..8 (i24 << 8) as u32 } /// Render one `BLOCK_FRAMES`-frame stereo block into `buf` using synth-core DSP. fn render( buf: &mut [u32; BLOCK_WORDS], vco: &mut Vco, adsr: &mut Adsr, filt: &mut Svf, vca: &mut Vca, snap: &crate::params::SynthParams, ) { let hz = midi_note_to_hz(snap.note) * libm::powf(2.0_f32, snap.pitch_bend / 12.0); vco.freq_hz = hz; vco.waveform = snap.waveform; filt.cutoff_hz = snap.cutoff_hz; filt.resonance = snap.resonance; vca.gain = snap.volume; if snap.gate { adsr.gate_on(); } else { adsr.gate_off(); } let mut audio: [f32; BLOCK_FRAMES] = [0.0; BLOCK_FRAMES]; let mut env: [f32; BLOCK_FRAMES] = [0.0; BLOCK_FRAMES]; vco.process(&mut audio); adsr.process(&mut env); filt.process(&mut audio); vca.apply_envelope(&mut audio, &env); for (i, &s) in audio.iter().enumerate() { let word = f32_to_i2s(s); buf[i * 2] = word; // Left buf[i * 2 + 1] = word; // Right (mono → stereo) } } #[task] pub async fn audio_task( pio0: Peri<'static, PIO0>, bck: Peri<'static, PIN_9>, lrck: Peri<'static, PIN_10>, data: Peri<'static, PIN_11>, dma: Peri<'static, DMA_CH0>, ) { let Pio { mut common, sm0, .. } = Pio::new(pio0, Irqs); let program = PioI2sOutProgram::new(&mut common); let mut i2s = PioI2sOut::new( &mut common, sm0, dma, Irqs, data, bck, lrck, SAMPLE_RATE, BIT_DEPTH, &program, ); i2s.start(); defmt::info!("I2S PIO running: {}Hz, {}-bit", SAMPLE_RATE, BIT_DEPTH); // Initialise DSP modules. let init = { PARAMS.lock().await.clone() }; let sr = SR_48000; let mut vco = Vco::new(sr, midi_note_to_hz(init.note), init.waveform); let mut adsr = Adsr::new(sr); let mut filt = Svf::new(sr, init.cutoff_hz, init.resonance, FilterMode::LowPass); let mut vca = Vca::new(init.volume); // SAFETY: we are the sole owner of these statics from this point on. let buf_a = unsafe { &mut *core::ptr::addr_of_mut!(DMA_BUF_A) }; let buf_b = unsafe { &mut *core::ptr::addr_of_mut!(DMA_BUF_B) }; // Pre-fill both buffers before starting the DMA loop. { let snap = PARAMS.lock().await.clone(); render(buf_a, &mut vco, &mut adsr, &mut filt, &mut vca, &snap); render(buf_b, &mut vco, &mut adsr, &mut filt, &mut vca, &snap); } // Double-buffered audio loop: // 1. Send the active buffer via DMA. // 2. Render the next block into the inactive buffer. // 3. Await DMA completion and swap roles. loop { i2s.write(buf_a).await; { let snap = PARAMS.lock().await.clone(); render(buf_b, &mut vco, &mut adsr, &mut filt, &mut vca, &snap); } i2s.write(buf_b).await; { let snap = PARAMS.lock().await.clone(); render(buf_a, &mut vco, &mut adsr, &mut filt, &mut vca, &snap); } } }