Skip to main content

oxstack_cardputer/
audio.rs

1//! Audio: ES8311 codec on I²S0 (DMA channel 1) and the internal I²C bus,
2//! driving an NS4150B speaker amplifier and fed by an analog MEMS mic.
3//!
4//! Two drivers share the hardware, one at a time:
5//! - [`AudioResources::recorder`] captures the microphone. **Silent by
6//!   construction**: I²S TX is never set up (DOUT stays unconnected), and
7//!   the codec's DAC stays powered down, muted, at minimum volume and
8//!   unclocked.
9//! - [`AudioResources::player`] plays audio through the speaker (or
10//!   headphones, which mute the amplifier in hardware).
11//!
12//! Both borrow the resources, so a program can record, drop the
13//! [`Recorder`], then play, as often as it likes. They also share one
14//! 32 KB DMA stream buffer, since only one of them can exist at a time.
15//!
16//! The codec takes its master clock from the I²S bit clock, so the clock
17//! must keep running whenever the codec is powered. Both drivers start the
18//! I²S stream *before* powering the codec; [`Recorder::shutdown`] and
19//! [`Player::shutdown`] power it down before stopping the stream. Plain
20//! `drop` stops the stream at once and leaves the codec for the next `init`
21//! to reset.
22//!
23//! Samples are interleaved 16-bit stereo (`L R L R …`). The ES8311 has one
24//! ADC, so a recording carries the microphone in the left slot; the right
25//! slot mirrors it.
26
27use embassy_time::Delay;
28use esp_hal::Async;
29use esp_hal::dma::{DmaError, DmaRxStreamBuf, DmaTxStreamBuf};
30use esp_hal::i2s::master::{
31    Channels, DataFormat, I2s, I2sRx, I2sRxDmaTransfer, I2sTx, I2sTxDmaTransfer, TdmConfig,
32};
33use esp_hal::peripherals::{DMA_CH1, GPIO41, GPIO42, GPIO43, GPIO46, I2S0};
34use esp_hal::time::Rate;
35use oxstack_drivers::es8311::{ADDRESS_CE_LOW, Es8311, Mode};
36pub use oxstack_drivers::es8311::{MicGain, SampleRate, Volume};
37
38use crate::i2c::{SysI2cBus, SysI2cDevice, device};
39
40/// Bytes per stereo 16-bit frame.
41pub const FRAME_BYTES: usize = 4;
42/// DMA chunk size requested from esp-hal (descriptors end up 2046 bytes).
43///
44/// Measured on hardware: with 4092-byte descriptors in a 16 KB buffer the
45/// RX stream stalled permanently after ~8 KB, right where the I²S unit's
46/// 12-bit `RX_EOF_NUM` (which esp-hal sets to its maximum) fires EOF. With
47/// this geometry, the one esp-hal's own `embassy_i2s_read` QA test uses, it
48/// streams continuously. The exact GDMA interaction is not yet understood;
49/// change these values only with an on-device test.
50const CHUNK: usize = 2048;
51/// Stream buffer size: 32 KB, about 170 ms at 48 kHz.
52const STREAM_BYTES: usize = 8 * 4092;
53
54/// The codec on the shared bus.
55pub type Codec = Es8311<SysI2cDevice>;
56
57/// Audio errors.
58#[derive(Debug)]
59#[cfg_attr(feature = "defmt", derive(defmt::Format))]
60pub enum Error {
61    /// Codec control failed.
62    I2c(embassy_embedded_hal::shared_bus::I2cDeviceError<esp_hal::i2c::master::Error>),
63    /// I²S configuration was rejected.
64    Config(esp_hal::i2s::master::ConfigError),
65    /// I²S start failed.
66    I2s(esp_hal::i2s::master::Error),
67    /// DMA failure.
68    Dma(DmaError),
69    /// Samples were not read in time and some were lost. The stream has
70    /// been restarted; the next read continues with fresh audio.
71    Overrun,
72    /// The output ran dry before more samples arrived. The stream has been
73    /// restarted.
74    Underrun,
75}
76
77impl From<embassy_embedded_hal::shared_bus::I2cDeviceError<esp_hal::i2c::master::Error>> for Error {
78    fn from(
79        e: embassy_embedded_hal::shared_bus::I2cDeviceError<esp_hal::i2c::master::Error>,
80    ) -> Self {
81        Error::I2c(e)
82    }
83}
84
85/// I²S0, DMA channel 1 and the codec link pins.
86#[allow(missing_docs)]
87pub struct AudioResources<'d> {
88    pub i2s: I2S0<'d>,
89    pub dma: DMA_CH1<'d>,
90    pub bclk: GPIO41<'d>,
91    pub ws: GPIO43<'d>,
92    pub dout: GPIO42<'d>,
93    pub din: GPIO46<'d>,
94}
95reborrow_group!(AudioResources {
96    i2s,
97    dma,
98    bclk,
99    ws,
100    dout,
101    din
102});
103
104fn i2s_config(rate: SampleRate) -> TdmConfig {
105    TdmConfig::new_tdm_philips()
106        .with_sample_rate(Rate::from_hz(rate.hz()))
107        .with_data_format(DataFormat::Data16Channel16)
108        .with_channels(Channels::STEREO)
109}
110
111/// The one DMA stream buffer, used for capture or playback. The `&mut`
112/// borrow of [`AudioResources`] guarantees a single user at a time.
113macro_rules! stream_memory {
114    () => {
115        static_dma_memory!(STREAM_BYTES, CHUNK)
116    };
117}
118
119impl<'d> AudioResources<'d> {
120    /// Starts capturing the microphone. Never produces sound.
121    pub async fn recorder(
122        &mut self,
123        bus: &'static SysI2cBus,
124        rate: SampleRate,
125        gain: MicGain,
126    ) -> Result<Recorder<'_>, Error> {
127        let i2s = I2s::new(self.i2s.reborrow(), self.dma.reborrow(), i2s_config(rate))
128            .map_err(Error::Config)?
129            .into_async();
130        let rx = i2s
131            .i2s_rx
132            .with_bclk(self.bclk.reborrow())
133            .with_ws(self.ws.reborrow())
134            .with_din(self.din.reborrow())
135            .build();
136        let (buffer, descriptors) = stream_memory!();
137        let buf = DmaRxStreamBuf::new(descriptors, buffer).expect("valid DMA stream buffer");
138        // Clocks first: the codec runs from BCLK.
139        let transfer = rx.read(buf).map_err(|(e, _, _)| Error::I2s(e))?;
140
141        let mut codec = Es8311::new(device(bus), ADDRESS_CE_LOW);
142        codec.init(&mut Delay).await?;
143        codec.set_mic_gain(gain).await?;
144        codec.set_mode(Mode::Record).await?;
145        Ok(Recorder {
146            codec,
147            rx: Stream::Running(transfer),
148            rate,
149        })
150    }
151
152    /// Starts audio output, muted at `volume` until the first
153    /// [`Player::write`].
154    pub async fn player(
155        &mut self,
156        bus: &'static SysI2cBus,
157        rate: SampleRate,
158        volume: Volume,
159    ) -> Result<Player<'_>, Error> {
160        let i2s = I2s::new(self.i2s.reborrow(), self.dma.reborrow(), i2s_config(rate))
161            .map_err(Error::Config)?
162            .into_async();
163        let tx = i2s
164            .i2s_tx
165            .with_bclk(self.bclk.reborrow())
166            .with_ws(self.ws.reborrow())
167            .with_dout(self.dout.reborrow())
168            .build();
169        let (mut buffer, descriptors) = stream_memory!();
170        // A previous recording may have left samples here: open with silence.
171        buffer.fill(0);
172        let buf = DmaTxStreamBuf::new(descriptors, buffer).expect("valid DMA stream buffer");
173        let transfer = tx.write(buf).map_err(|(e, _, _)| Error::I2s(e))?;
174
175        let mut codec = Es8311::new(device(bus), ADDRESS_CE_LOW);
176        codec.init(&mut Delay).await?;
177        codec.set_mode(Mode::Playback).await?;
178        codec.set_dac_volume(volume).await?;
179        Ok(Player {
180            codec,
181            tx: Stream::Running(transfer),
182            rate,
183            muted: true,
184        })
185    }
186}
187
188/// A DMA stream that may need restarting after an overrun or underrun.
189enum Stream<T, U, B> {
190    Running(T),
191    Stopped(U, B),
192    Poisoned,
193}
194
195type RxStream<'d> =
196    Stream<I2sRxDmaTransfer<'d, Async, DmaRxStreamBuf>, I2sRx<'d, Async>, DmaRxStreamBuf>;
197type TxStream<'d> =
198    Stream<I2sTxDmaTransfer<'d, Async, DmaTxStreamBuf>, I2sTx<'d, Async>, DmaTxStreamBuf>;
199
200/// Microphone capture.
201pub struct Recorder<'d> {
202    codec: Codec,
203    rx: RxStream<'d>,
204    rate: SampleRate,
205}
206
207impl Recorder<'_> {
208    /// Waits for audio and fills `out` with as many whole stereo frames as
209    /// are available (at least one). Returns the number of `i16` samples
210    /// written, always even.
211    ///
212    /// Read at least every ~170 ms (at 48 kHz) or the stream overruns; an
213    /// overrun returns [`Error::Overrun`] once and restarts the stream.
214    ///
215    /// **Cancel-safe.** It only waits before copying, so a cancelled call
216    /// has written nothing to `out` and dropped no audio: the samples stay
217    /// in the DMA buffer for the next call.
218    pub async fn read(&mut self, out: &mut [i16]) -> Result<usize, Error> {
219        let want = out.len() / 2 * FRAME_BYTES;
220        if want == 0 {
221            return Ok(0);
222        }
223        loop {
224            let Stream::Running(t) = &mut self.rx else {
225                self.restart()?;
226                continue;
227            };
228            let avail = t.available_bytes();
229            if avail >= FRAME_BYTES {
230                let n = avail.min(want) / FRAME_BYTES * FRAME_BYTES;
231                let popped = t.pop(as_bytes_mut(out, n));
232                return Ok(popped / 2);
233            }
234            if t.wait_for_available_async().await.is_err() {
235                self.restart()?;
236                return Err(Error::Overrun);
237            }
238        }
239    }
240
241    /// Bytes captured but not yet read (0 while the stream is restarting).
242    pub fn buffered_bytes(&mut self) -> usize {
243        match &mut self.rx {
244            Stream::Running(t) => t.available_bytes(),
245            _ => 0,
246        }
247    }
248
249    /// Whether the DMA has stopped (e.g. after an overrun).
250    pub fn is_stopped(&mut self) -> bool {
251        match &mut self.rx {
252            Stream::Running(t) => t.is_done(),
253            _ => true,
254        }
255    }
256
257    /// Changes the microphone pre-amplifier gain.
258    pub async fn set_gain(&mut self, gain: MicGain) -> Result<(), Error> {
259        Ok(self.codec.set_mic_gain(gain).await?)
260    }
261
262    /// The sample rate.
263    pub fn rate(&self) -> SampleRate {
264        self.rate
265    }
266
267    /// The codec, for advanced control.
268    pub fn codec(&mut self) -> &mut Codec {
269        &mut self.codec
270    }
271
272    /// Powers the codec down, then stops the stream. Prefer this over a
273    /// plain `drop` when the device goes quiet for a while.
274    pub async fn shutdown(mut self) -> Result<(), Error> {
275        self.codec.set_mode(Mode::Off).await?;
276        Ok(()) // dropping `self` stops the I²S stream
277    }
278
279    fn restart(&mut self) -> Result<(), Error> {
280        let (rx, buf) = match core::mem::replace(&mut self.rx, Stream::Poisoned) {
281            Stream::Running(t) => t.stop(),
282            Stream::Stopped(rx, buf) => (rx, buf),
283            Stream::Poisoned => return Err(Error::Dma(DmaError::DescriptorError)),
284        };
285        match rx.read(buf) {
286            Ok(t) => {
287                self.rx = Stream::Running(t);
288                Ok(())
289            }
290            Err((e, rx, buf)) => {
291                self.rx = Stream::Stopped(rx, buf);
292                Err(Error::I2s(e))
293            }
294        }
295    }
296}
297
298/// Audio output.
299pub struct Player<'d> {
300    codec: Codec,
301    tx: TxStream<'d>,
302    rate: SampleRate,
303    muted: bool,
304}
305
306impl Player<'_> {
307    /// Queues interleaved stereo samples, waiting for space as needed, and
308    /// returns once all are queued. Unmutes on first use.
309    ///
310    /// Keep the queue fed: if it runs dry the stream stops, returns
311    /// [`Error::Underrun`] once, and is restarted.
312    ///
313    /// **Not cancel-safe**: a cancelled call may have queued any prefix of
314    /// `samples`, and there is no way to learn how much. Use
315    /// [`write_some`](Self::write_some) in a `select`.
316    pub async fn write(&mut self, samples: &[i16]) -> Result<(), Error> {
317        let mut rest = samples;
318        while !rest.is_empty() {
319            let n = self.write_some(rest).await?;
320            rest = &rest[n..];
321        }
322        Ok(())
323    }
324
325    /// Waits for space, queues as many whole frames from `samples` as fit,
326    /// and returns how many `i16` samples were taken (always even, at least
327    /// two unless `samples` has fewer). Unmutes on first use.
328    ///
329    /// **Cancel-safe**: it only waits before queueing, so a cancelled call
330    /// has queued nothing.
331    pub async fn write_some(&mut self, samples: &[i16]) -> Result<usize, Error> {
332        let bytes = as_bytes(samples);
333        let bytes = &bytes[..bytes.len() / FRAME_BYTES * FRAME_BYTES];
334        if bytes.is_empty() {
335            return Ok(0);
336        }
337        loop {
338            let Stream::Running(t) = &mut self.tx else {
339                self.restart()?;
340                continue;
341            };
342            let space = t.available_bytes() / FRAME_BYTES * FRAME_BYTES;
343            if space > 0 {
344                let n = t.push(&bytes[..space.min(bytes.len())]);
345                if self.muted {
346                    // Cancelled here, `muted` stays true and the next write
347                    // unmutes again: harmless.
348                    self.codec.set_dac_mute(false).await?;
349                    self.muted = false;
350                }
351                return Ok(n / 2);
352            }
353            if t.wait_for_available_async().await.is_err() {
354                self.restart()?;
355                return Err(Error::Underrun);
356            }
357        }
358    }
359
360    /// Digital output volume.
361    pub async fn set_volume(&mut self, volume: Volume) -> Result<(), Error> {
362        Ok(self.codec.set_dac_volume(volume).await?)
363    }
364
365    /// Soft mute.
366    pub async fn set_mute(&mut self, mute: bool) -> Result<(), Error> {
367        self.codec.set_dac_mute(mute).await?;
368        self.muted = mute;
369        Ok(())
370    }
371
372    /// The sample rate.
373    pub fn rate(&self) -> SampleRate {
374        self.rate
375    }
376
377    /// The codec, for advanced control.
378    pub fn codec(&mut self) -> &mut Codec {
379        &mut self.codec
380    }
381
382    /// Mutes, powers the codec down, then stops the stream. Prefer this over
383    /// a plain `drop`, which can click.
384    pub async fn shutdown(mut self) -> Result<(), Error> {
385        self.codec.set_dac_mute(true).await?;
386        self.codec.set_mode(Mode::Off).await?;
387        Ok(()) // dropping `self` stops the I²S stream
388    }
389
390    fn restart(&mut self) -> Result<(), Error> {
391        let (tx, buf) = match core::mem::replace(&mut self.tx, Stream::Poisoned) {
392            Stream::Running(t) => t.stop(),
393            Stream::Stopped(tx, buf) => (tx, buf),
394            Stream::Poisoned => return Err(Error::Dma(DmaError::DescriptorError)),
395        };
396        match tx.write(buf) {
397            Ok(t) => {
398                self.tx = Stream::Running(t);
399                Ok(())
400            }
401            Err((e, tx, buf)) => {
402                self.tx = Stream::Stopped(tx, buf);
403                Err(Error::I2s(e))
404            }
405        }
406    }
407}
408
409fn as_bytes(samples: &[i16]) -> &[u8] {
410    // SAFETY: i16 has no padding and any byte pattern is a valid u8; u8 has
411    // alignment 1; the length is exactly the slice's size in bytes.
412    unsafe { core::slice::from_raw_parts(samples.as_ptr().cast(), samples.len() * 2) }
413}
414
415fn as_bytes_mut(samples: &mut [i16], len: usize) -> &mut [u8] {
416    debug_assert!(len <= samples.len() * 2);
417    // SAFETY: as in `as_bytes`, and every byte pattern is also a valid i16,
418    // so writes through the byte view can't create invalid values.
419    unsafe { core::slice::from_raw_parts_mut(samples.as_mut_ptr().cast(), len) }
420}