Skip to main content

core_s3/
audio.rs

1//! Audio codec configuration helpers for CoreS3.
2//!
3//! CoreS3 uses an ES7210 microphone ADC and AW88298 speaker amplifier. This
4//! module configures the I²C-controlled devices, documents the expected I²S
5//! format, and provides small `no_std` sample-source helpers for raw PCM, WAV
6//! assets, and tones. High-throughput I²S clocks/DMA remain in the
7//! HAL/application layer.
8
9use embedded_hal::i2c::I2c;
10
11use crate::devices;
12
13const SINE_64_Q15: [i16; 64] = [
14    0, 3212, 6393, 9512, 12539, 15446, 18205, 20787, 23170, 25329, 27245, 28898, 30273, 31357,
15    32138, 32610, 32767, 32610, 32138, 31357, 30273, 28898, 27245, 25329, 23170, 20787, 18205,
16    15446, 12539, 9512, 6393, 3212, 0, -3212, -6393, -9512, -12539, -15446, -18205, -20787, -23170,
17    -25329, -27245, -28898, -30273, -31357, -32138, -32610, -32767, -32610, -32138, -31357, -30273,
18    -28898, -27245, -25329, -23170, -20787, -18205, -15446, -12539, -9512, -6393, -3212,
19];
20
21/// Common audio sample rates supported by the CoreS3 audio helpers.
22#[cfg_attr(feature = "defmt", derive(defmt::Format))]
23#[derive(Clone, Copy, Debug, Eq, PartialEq)]
24pub enum SampleRate {
25    /// 8 kHz, suitable for very small voice prompts.
26    Rate8K,
27    /// 16 kHz, the default low-bandwidth voice/microphone rate.
28    Rate16K,
29    /// 24 kHz.
30    Rate24K,
31    /// 32 kHz.
32    Rate32K,
33    /// 44.1 kHz.
34    Rate44K1,
35    /// 48 kHz.
36    Rate48K,
37}
38
39/// I²S sample width used for codec configuration.
40#[cfg_attr(feature = "defmt", derive(defmt::Format))]
41#[derive(Clone, Copy, Debug, Eq, PartialEq)]
42pub enum SampleWidth {
43    /// 16-bit samples.
44    Bits16,
45    /// 24-bit samples packed according to the selected HAL I²S mode.
46    Bits24,
47    /// 32-bit samples packed according to the selected HAL I²S mode.
48    Bits32,
49}
50
51/// I²S frame alignment mode for the codec control path.
52#[cfg_attr(feature = "defmt", derive(defmt::Format))]
53#[derive(Clone, Copy, Debug, Eq, PartialEq)]
54pub enum I2sMode {
55    /// Standard Philips I²S timing.
56    Standard,
57    /// Left-justified timing.
58    LeftJustified,
59}
60
61/// Board-level I²S format expected by the ES7210/AW88298 path.
62#[cfg_attr(feature = "defmt", derive(defmt::Format))]
63#[derive(Clone, Copy, Debug, Eq, PartialEq)]
64pub struct I2sAudioFormat {
65    /// Audio sample rate.
66    pub sample_rate: SampleRate,
67    /// Bits per sample.
68    pub sample_width: SampleWidth,
69    /// I²S frame alignment mode.
70    pub mode: I2sMode,
71    /// Number of microphone slots/channels expected from ES7210.
72    pub microphone_channels: u8,
73    /// Number of speaker slots/channels expected by the playback path.
74    pub speaker_channels: u8,
75}
76
77impl I2sAudioFormat {
78    /// Conservative default format for board examples: 16 kHz, 16-bit standard I²S.
79    pub const DEFAULT: Self = Self {
80        sample_rate: SampleRate::Rate16K,
81        sample_width: SampleWidth::Bits16,
82        mode: I2sMode::Standard,
83        microphone_channels: 2,
84        speaker_channels: 1,
85    };
86
87    /// Return the sample rate in hertz.
88    pub const fn sample_rate_hz(self) -> u32 {
89        match self.sample_rate {
90            SampleRate::Rate8K => 8_000,
91            SampleRate::Rate16K => 16_000,
92            SampleRate::Rate24K => 24_000,
93            SampleRate::Rate32K => 32_000,
94            SampleRate::Rate44K1 => 44_100,
95            SampleRate::Rate48K => 48_000,
96        }
97    }
98}
99
100/// ES7210 microphone ADC configuration.
101#[cfg_attr(feature = "defmt", derive(defmt::Format))]
102#[derive(Clone, Copy, Debug, Eq, PartialEq)]
103pub struct MicrophoneConfig {
104    /// Expected I²S format for microphone samples.
105    pub format: I2sAudioFormat,
106    /// Analog/digital gain request in dB, clamped to the safe supported range.
107    pub gain_db: u8,
108}
109
110impl MicrophoneConfig {
111    /// Default CoreS3 microphone configuration.
112    pub const DEFAULT: Self = Self {
113        format: I2sAudioFormat::DEFAULT,
114        gain_db: 24,
115    };
116}
117
118/// AW88298 speaker amplifier configuration.
119#[cfg_attr(feature = "defmt", derive(defmt::Format))]
120#[derive(Clone, Copy, Debug, Eq, PartialEq)]
121pub struct SpeakerConfig {
122    /// Expected I²S format for speaker playback samples.
123    pub format: I2sAudioFormat,
124    /// Whether the amplifier should be enabled during initialization.
125    pub enabled: bool,
126    /// Speaker volume percentage-like value used by the AW88298 helper.
127    ///
128    /// M5Unified writes `100` for full-volume CoreS3 smoke tests; values above
129    /// `100` are clamped by this helper.
130    pub volume: u8,
131}
132
133impl SpeakerConfig {
134    /// Default CoreS3 speaker amplifier configuration.
135    pub const DEFAULT: Self = Self {
136        format: I2sAudioFormat::DEFAULT,
137        enabled: true,
138        volume: 100,
139    };
140}
141
142/// Pull-based mono PCM source for application-owned I²S streaming.
143///
144/// Samples are signed 16-bit PCM. Applications can duplicate samples to stereo
145/// frames or write mono frames depending on their HAL/I²S configuration.
146pub trait AudioSource {
147    /// Return the next signed 16-bit mono sample, or `None` when a finite source is exhausted.
148    fn next_sample(&mut self) -> Option<i16>;
149}
150
151/// Raw signed 16-bit PCM sample source backed by a borrowed slice.
152pub struct RawPcm<'a> {
153    samples: &'a [i16],
154    position: usize,
155    repeat: bool,
156}
157
158impl<'a> RawPcm<'a> {
159    /// Create a finite PCM source that emits each sample exactly once.
160    pub const fn once(samples: &'a [i16]) -> Self {
161        Self {
162            samples,
163            position: 0,
164            repeat: false,
165        }
166    }
167
168    /// Create a PCM source that loops over `samples` forever.
169    ///
170    /// Empty slices still return `None`.
171    pub const fn repeating(samples: &'a [i16]) -> Self {
172        Self {
173            samples,
174            position: 0,
175            repeat: true,
176        }
177    }
178}
179
180impl AudioSource for RawPcm<'_> {
181    fn next_sample(&mut self) -> Option<i16> {
182        if self.samples.is_empty() {
183            return None;
184        }
185        if self.position >= self.samples.len() {
186            if self.repeat {
187                self.position = 0;
188            } else {
189                return None;
190            }
191        }
192        let sample = self.samples[self.position];
193        self.position += 1;
194        Some(sample)
195    }
196}
197
198/// Error returned while parsing a PCM WAV asset.
199#[cfg_attr(feature = "defmt", derive(defmt::Format))]
200#[derive(Clone, Copy, Debug, Eq, PartialEq)]
201pub enum WavError {
202    /// The byte slice is too short for a WAV header or chunk.
203    Truncated,
204    /// The file is not a RIFF/WAVE file.
205    NotWave,
206    /// The file does not contain a supported `fmt ` chunk.
207    UnsupportedFormat,
208    /// The file has no PCM data chunk.
209    MissingData,
210}
211
212/// Borrowed 16-bit PCM WAV source for app-owned I²S streaming.
213///
214/// This parser intentionally supports the simple format useful for embedded
215/// prompts: RIFF/WAVE, PCM format tag 1, 16-bit little-endian samples, mono or
216/// stereo. Stereo input is downmixed by averaging left and right samples. It does
217/// not allocate and can wrap `include_bytes!()` assets stored in flash.
218pub struct WavPcm16<'a> {
219    data: &'a [u8],
220    cursor: usize,
221    end: usize,
222    channels: u16,
223    sample_rate_hz: u32,
224}
225
226impl<'a> WavPcm16<'a> {
227    /// Parse a borrowed WAV file.
228    pub fn new(wav: &'a [u8]) -> Result<Self, WavError> {
229        if wav.len() < 12 {
230            return Err(WavError::Truncated);
231        }
232        if &wav[0..4] != b"RIFF" || &wav[8..12] != b"WAVE" {
233            return Err(WavError::NotWave);
234        }
235
236        let mut offset = 12;
237        let mut channels = 0u16;
238        let mut sample_rate_hz = 0u32;
239        let mut bits_per_sample = 0u16;
240        let mut format_tag = 0u16;
241        let mut data_range = None;
242
243        while offset + 8 <= wav.len() {
244            let id = &wav[offset..offset + 4];
245            let size = read_le_u32(wav, offset + 4).ok_or(WavError::Truncated)? as usize;
246            let payload = offset + 8;
247            let next = payload.checked_add(size).ok_or(WavError::Truncated)?;
248            if next > wav.len() {
249                return Err(WavError::Truncated);
250            }
251
252            if id == b"fmt " {
253                if size < 16 {
254                    return Err(WavError::UnsupportedFormat);
255                }
256                format_tag = read_le_u16(wav, payload).ok_or(WavError::Truncated)?;
257                channels = read_le_u16(wav, payload + 2).ok_or(WavError::Truncated)?;
258                sample_rate_hz = read_le_u32(wav, payload + 4).ok_or(WavError::Truncated)?;
259                bits_per_sample = read_le_u16(wav, payload + 14).ok_or(WavError::Truncated)?;
260            } else if id == b"data" {
261                data_range = Some((payload, next));
262            }
263
264            offset = next + (size & 1);
265        }
266
267        if format_tag != 1 || bits_per_sample != 16 || !(channels == 1 || channels == 2) {
268            return Err(WavError::UnsupportedFormat);
269        }
270        let (cursor, end) = data_range.ok_or(WavError::MissingData)?;
271        Ok(Self {
272            data: wav,
273            cursor,
274            end,
275            channels,
276            sample_rate_hz,
277        })
278    }
279
280    /// Return the sample rate declared by the WAV file.
281    pub const fn sample_rate_hz(&self) -> u32 {
282        self.sample_rate_hz
283    }
284
285    /// Return the number of source channels: 1 or 2.
286    pub const fn channels(&self) -> u16 {
287        self.channels
288    }
289}
290
291impl AudioSource for WavPcm16<'_> {
292    fn next_sample(&mut self) -> Option<i16> {
293        match self.channels {
294            1 => {
295                if self.cursor + 2 > self.end {
296                    return None;
297                }
298                let sample = read_le_i16(self.data, self.cursor)?;
299                self.cursor += 2;
300                Some(sample)
301            }
302            2 => {
303                if self.cursor + 4 > self.end {
304                    return None;
305                }
306                let left = i32::from(read_le_i16(self.data, self.cursor)?);
307                let right = i32::from(read_le_i16(self.data, self.cursor + 2)?);
308                self.cursor += 4;
309                Some(((left + right) / 2) as i16)
310            }
311            _ => None,
312        }
313    }
314}
315
316fn read_le_u16(data: &[u8], offset: usize) -> Option<u16> {
317    let bytes = data.get(offset..offset + 2)?;
318    Some(u16::from_le_bytes([bytes[0], bytes[1]]))
319}
320
321fn read_le_i16(data: &[u8], offset: usize) -> Option<i16> {
322    let bytes = data.get(offset..offset + 2)?;
323    Some(i16::from_le_bytes([bytes[0], bytes[1]]))
324}
325
326fn read_le_u32(data: &[u8], offset: usize) -> Option<u32> {
327    let bytes = data.get(offset..offset + 4)?;
328    Some(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
329}
330
331/// Small sine-wave tone source for beeps and smoke tests.
332pub struct Tone {
333    phase: u32,
334    phase_step: u32,
335    remaining_samples: u32,
336    amplitude: i16,
337}
338
339impl Tone {
340    /// Create a finite sine tone.
341    ///
342    /// `amplitude` is the signed 16-bit peak amplitude. A zero `sample_rate_hz`
343    /// creates an already-exhausted source instead of panicking.
344    pub const fn new(
345        frequency_hz: u16,
346        duration_ms: u16,
347        sample_rate_hz: u32,
348        amplitude: i16,
349    ) -> Self {
350        let phase_step = match ((frequency_hz as u32) << 16).checked_div(sample_rate_hz) {
351            Some(value) => value,
352            None => 0,
353        };
354        let remaining_samples = (sample_rate_hz / 1000) * duration_ms as u32;
355
356        Self {
357            phase: 0,
358            phase_step,
359            remaining_samples,
360            amplitude,
361        }
362    }
363}
364
365impl AudioSource for Tone {
366    fn next_sample(&mut self) -> Option<i16> {
367        if self.remaining_samples == 0 {
368            return None;
369        }
370        self.remaining_samples -= 1;
371        let index = ((self.phase >> 10) & 0x3F) as usize;
372        self.phase = self.phase.wrapping_add(self.phase_step);
373        Some(((i32::from(SINE_64_Q15[index]) * i32::from(self.amplitude)) / 32767) as i16)
374    }
375}
376
377/// ES7210 microphone ADC I²C control driver.
378pub struct Es7210<I2C> {
379    i2c: I2C,
380    address: u8,
381}
382
383impl<I2C> Es7210<I2C> {
384    /// Create an ES7210 driver using the CoreS3 ES7210 I²C address.
385    pub const fn new(i2c: I2C) -> Self {
386        Self {
387            i2c,
388            address: devices::i2c::ES7210_ADC,
389        }
390    }
391    /// Release the underlying I²C bus/device.
392    pub fn release(self) -> I2C {
393        self.i2c
394    }
395}
396
397impl<I2C, Error> Es7210<I2C>
398where
399    I2C: I2c<Error = Error>,
400{
401    /// Apply a conservative ES7210 initialization sequence for CoreS3 microphones.
402    pub fn init(&mut self, config: MicrophoneConfig) -> Result<(), Error> {
403        // Conservative ES7210 setup for I²S slave mode. Applications may tune
404        // registers further for clock tree and analog gain requirements.
405        self.write_register(0x00, 0xFF)?;
406        self.write_register(0x00, 0x32)?;
407        self.write_register(0x01, 0x30)?;
408        self.write_register(0x02, 0x10)?;
409        self.write_register(0x03, 0x20)?;
410        self.write_register(0x04, sample_width_code(config.format.sample_width))?;
411        self.write_register(0x22, config.gain_db.min(37))
412    }
413
414    /// Set microphone gain, clamped to the supported range used by this helper.
415    pub fn set_gain(&mut self, gain_db: u8) -> Result<(), Error> {
416        self.write_register(0x22, gain_db.min(37))
417    }
418
419    /// Read one ES7210 register.
420    pub fn read_register(&mut self, register: u8) -> Result<u8, Error> {
421        let mut value = [0u8];
422        self.i2c.write_read(self.address, &[register], &mut value)?;
423        Ok(value[0])
424    }
425
426    /// Write one ES7210 register.
427    pub fn write_register(&mut self, register: u8, value: u8) -> Result<(), Error> {
428        self.i2c.write(self.address, &[register, value])
429    }
430}
431
432/// AW88298 speaker amplifier I²C control driver.
433pub struct Aw88298<I2C> {
434    i2c: I2C,
435    address: u8,
436}
437
438impl<I2C> Aw88298<I2C> {
439    /// Create an AW88298 driver using the CoreS3 amplifier I²C address.
440    pub const fn new(i2c: I2C) -> Self {
441        Self {
442            i2c,
443            address: devices::i2c::AW88298_AMPLIFIER,
444        }
445    }
446    /// Release the underlying I²C bus/device.
447    pub fn release(self) -> I2C {
448        self.i2c
449    }
450}
451
452impl<I2C, Error> Aw88298<I2C>
453where
454    I2C: I2c<Error = Error>,
455{
456    /// Apply the CoreS3 AW88298 speaker amplifier sequence used by M5Unified.
457    ///
458    /// The CoreS3 speaker path clocks the amplifier from BCK/WS/DOUT. The helper
459    /// selects the AW88298 sample-rate bucket from [`SpeakerConfig::format`] and
460    /// enables I²S input, unmutes the high-level path, disables boost mode, and
461    /// applies a clamped volume value.
462    pub fn init(&mut self, config: SpeakerConfig) -> Result<(), Error> {
463        if config.enabled {
464            self.enable_core_s3(config)
465        } else {
466            self.set_enabled(false)
467        }
468    }
469
470    /// Enable or disable the CoreS3 amplifier output path.
471    pub fn set_enabled(&mut self, enabled: bool) -> Result<(), Error> {
472        if enabled {
473            self.enable_core_s3(SpeakerConfig::DEFAULT)
474        } else {
475            self.write_register16(0x04, 0x4000)
476        }
477    }
478
479    /// Set the CoreS3 amplifier volume register value.
480    pub fn set_volume(&mut self, volume: u8) -> Result<(), Error> {
481        self.write_register16(0x0C, u16::from(volume.min(100)))
482    }
483
484    fn enable_core_s3(&mut self, config: SpeakerConfig) -> Result<(), Error> {
485        let sample_rate_code = aw88298_sample_rate_code(config.format.sample_rate_hz());
486        self.write_register16(0x61, 0x0673)?;
487        self.write_register16(0x04, 0x4040)?;
488        self.write_register16(0x05, 0x0008)?;
489        self.write_register16(0x06, 0x14C0 | u16::from(sample_rate_code))?;
490        self.set_volume(config.volume)
491    }
492
493    /// Read one 16-bit AW88298 register.
494    pub fn read_register16(&mut self, register: u8) -> Result<u16, Error> {
495        let mut data = [0u8; 2];
496        self.i2c.write_read(self.address, &[register], &mut data)?;
497        Ok(u16::from_be_bytes(data))
498    }
499
500    /// Write one 16-bit AW88298 register.
501    pub fn write_register16(&mut self, register: u8, value: u16) -> Result<(), Error> {
502        let bytes = value.to_be_bytes();
503        self.i2c
504            .write(self.address, &[register, bytes[0], bytes[1]])
505    }
506}
507
508const fn sample_width_code(width: SampleWidth) -> u8 {
509    match width {
510        SampleWidth::Bits16 => 0x60,
511        SampleWidth::Bits24 => 0x00,
512        SampleWidth::Bits32 => 0x10,
513    }
514}
515
516const fn aw88298_sample_rate_code(sample_rate_hz: u32) -> u8 {
517    let rate = (sample_rate_hz + 1_102) / 2_205;
518    if rate <= 4 {
519        0
520    } else if rate <= 5 {
521        1
522    } else if rate <= 6 {
523        2
524    } else if rate <= 8 {
525        3
526    } else if rate <= 10 {
527        4
528    } else if rate <= 11 {
529        5
530    } else if rate <= 15 {
531        6
532    } else if rate <= 20 {
533        7
534    } else if rate <= 22 {
535        8
536    } else {
537        9
538    }
539}
540
541#[cfg(test)]
542mod tests {
543    use super::*;
544
545    #[test]
546    fn raw_pcm_once_stops_after_slice() {
547        let mut pcm = RawPcm::once(&[10, -20]);
548
549        assert_eq!(pcm.next_sample(), Some(10));
550        assert_eq!(pcm.next_sample(), Some(-20));
551        assert_eq!(pcm.next_sample(), None);
552        assert_eq!(pcm.next_sample(), None);
553    }
554
555    #[test]
556    fn raw_pcm_repeating_loops() {
557        let mut pcm = RawPcm::repeating(&[1, 2, 3]);
558
559        assert_eq!(pcm.next_sample(), Some(1));
560        assert_eq!(pcm.next_sample(), Some(2));
561        assert_eq!(pcm.next_sample(), Some(3));
562        assert_eq!(pcm.next_sample(), Some(1));
563    }
564
565    #[test]
566    fn empty_repeating_pcm_stops() {
567        let mut pcm = RawPcm::repeating(&[]);
568
569        assert_eq!(pcm.next_sample(), None);
570    }
571
572    #[test]
573    fn wav_pcm16_parses_mono_prompt() {
574        let mut wav = WavPcm16::new(MONO_WAV).expect("valid mono wav");
575
576        assert_eq!(wav.sample_rate_hz(), 16_000);
577        assert_eq!(wav.channels(), 1);
578        assert_eq!(wav.next_sample(), Some(1_000));
579        assert_eq!(wav.next_sample(), Some(-1_000));
580        assert_eq!(wav.next_sample(), None);
581    }
582
583    #[test]
584    fn wav_pcm16_downmixes_stereo() {
585        let mut wav = WavPcm16::new(STEREO_WAV).expect("valid stereo wav");
586
587        assert_eq!(wav.sample_rate_hz(), 16_000);
588        assert_eq!(wav.channels(), 2);
589        assert_eq!(wav.next_sample(), Some(0));
590        assert_eq!(wav.next_sample(), Some(0));
591        assert_eq!(wav.next_sample(), None);
592    }
593
594    #[test]
595    fn wav_pcm16_rejects_non_wave() {
596        assert!(matches!(
597            WavPcm16::new(b"not wave"),
598            Err(WavError::Truncated)
599        ));
600    }
601
602    #[test]
603    fn tone_yields_finite_nonzero_samples() {
604        let mut tone = Tone::new(440, 10, 16_000, 8_000);
605        let mut nonzero = false;
606        let mut count = 0;
607
608        while let Some(sample) = tone.next_sample() {
609            nonzero |= sample != 0;
610            count += 1;
611        }
612
613        assert!(nonzero);
614        assert_eq!(count, 160);
615        assert_eq!(tone.next_sample(), None);
616    }
617
618    #[test]
619    fn tone_with_zero_sample_rate_is_exhausted() {
620        let mut tone = Tone::new(440, 100, 0, 8_000);
621
622        assert_eq!(tone.next_sample(), None);
623    }
624
625    #[test]
626    fn aw88298_sample_rate_code_matches_m5_rate_buckets() {
627        assert_eq!(aw88298_sample_rate_code(8_000), 0);
628        assert_eq!(aw88298_sample_rate_code(16_000), 3);
629        assert_eq!(aw88298_sample_rate_code(44_100), 7);
630        assert_eq!(aw88298_sample_rate_code(48_000), 8);
631    }
632
633    const MONO_WAV: &[u8] = &[
634        b'R', b'I', b'F', b'F', 40, 0, 0, 0, b'W', b'A', b'V', b'E', b'f', b'm', b't', b' ', 16, 0,
635        0, 0, 1, 0, 1, 0, 0x80, 0x3E, 0, 0, 0x00, 0x7D, 0, 0, 2, 0, 16, 0, b'd', b'a', b't', b'a',
636        4, 0, 0, 0, 0xE8, 0x03, 0x18, 0xFC,
637    ];
638
639    const STEREO_WAV: &[u8] = &[
640        b'R', b'I', b'F', b'F', 44, 0, 0, 0, b'W', b'A', b'V', b'E', b'f', b'm', b't', b' ', 16, 0,
641        0, 0, 1, 0, 2, 0, 0x80, 0x3E, 0, 0, 0x00, 0xFA, 0, 0, 4, 0, 16, 0, b'd', b'a', b't', b'a',
642        8, 0, 0, 0, 0xE8, 0x03, 0x18, 0xFC, 0x18, 0xFC, 0xE8, 0x03,
643    ];
644}