lilkaoxide/
i2s.rs

1use esp_hal::{
2    dma_buffers,
3    gpio::interconnect::PeripheralOutput,
4    i2s::master::{Config, I2s},
5    peripherals::{DMA_CH0, I2S0},
6};
7#[cfg(feature = "blocking")]
8type DriveMode = esp_hal::Blocking;
9#[cfg(feature = "async")]
10type DriveMode = esp_hal::Async;
11
12/// Alias for the I2S transmitter handle, parameterised by the blocking/async drive mode.
13///
14/// * In **blocking** builds: `I2sTx<'d>` = `esp_hal::i2s::master::I2sTx<'d, Blocking>`
15/// * In **async** builds: `I2sTx<'d>` = `esp_hal::i2s::master::I2sTx<'d, Async>`
16///
17/// Obtained via [`crate::Lilka::i2s_tx`] after calling [`crate::Lilka::init`].
18pub type I2sTx<'d> = esp_hal::i2s::master::I2sTx<'d, DriveMode>;
19
20/// Default DMA TX buffer size for the I2S transmitter (in bytes).
21///
22/// Equals `4 × 4096 = 16 384` bytes, enough for ~93 ms of 44 100 Hz stereo 16-bit audio.
23/// Tune this value if you need lower latency (smaller buffer) or gapless playback
24/// (larger buffer), keeping in mind that the buffer lives in static memory.
25pub const DEFAULT_DMA_TX_BUF_SIZE: usize = 4 * 4096;
26
27/// Error returned when the I2S0 peripheral fails to initialise.
28///
29/// Propagated as [`crate::InitError::I2s`] from [`crate::Lilka::init`].
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum I2sInitError {
32    /// I2S0 peripheral or DMA channel could not be configured.
33    PeripheralInitFailed,
34}
35
36impl I2sInitError {
37    /// Returns a short, `'static` error description suitable for `defmt` logging.
38    pub const fn as_str(self) -> &'static str {
39        match self {
40            Self::PeripheralInitFailed => "I2S init failed",
41        }
42    }
43}
44
45/// TDM (Time-Division Multiplexing) framing standard for I2S output.
46///
47/// Selects the bit-clock / word-select phase relationship for audio data framing.
48/// The default [`I2sTxConfig`] uses [`TDMStandart::Phillips`].
49///
50/// # Choosing a standard
51///
52/// | Variant | Use case |
53/// |---|---|
54/// | `Phillips` | Most DACs (PCM5102A, MAX98357A, …). **Default.** |
55/// | `MSB` | Some older DACs that expect data on the rising BCLK edge. |
56/// | `PCM` | PCM voice codecs (short sync pulse). |
57/// | `PCM_Long` | PCM codecs requiring a long sync pulse. |
58pub enum TDMStandart {
59    /// I²S Philips standard — WS changes one BCLK cycle before the MSB.
60    Phillips,
61    /// MSB-justified — data starts immediately on the WS edge.
62    MSB,
63    /// PCM short-sync — one-cycle WS pulse at the start of each frame.
64    PCM,
65    /// PCM long-sync — WS stays high for the duration of the first channel slot.
66    PCM_Long,
67}
68
69macro_rules! tdm_config {
70    ($standard:expr, $cfg:ident => $($method:tt)*) => {
71        match $standard {
72            TDMStandart::Phillips => Config::new_tdm_philips()$($method)*,
73            TDMStandart::MSB      => Config::new_tdm_msb()$($method)*,
74            TDMStandart::PCM      => Config::new_tdm_pcm_short()$($method)*,
75            TDMStandart::PCM_Long => Config::new_tdm_pcm_long()$($method)*,
76        }
77    };
78}
79
80/// Configuration for the I2S0 transmitter.
81///
82/// Passed as part of [`crate::LilkaConfiguration`] to [`crate::Lilka::init`].
83///
84/// # Defaults
85///
86/// | Field | Default | Notes |
87/// |---|---|---|
88/// | `sample_rate` | 44 100 Hz | CD-quality audio. |
89/// | `channels` | `STEREO` | Left + right. |
90/// | `data_format` | `Data16Channel16` | 16-bit samples, 16-bit channel slots. |
91/// | `tdm_standard` | `Phillips` | Compatible with most I2S DACs. |
92///
93/// # Example — custom sample rate
94///
95/// ```rust,no_run
96/// use lilkaoxide::{I2sTxConfig, LilkaConfiguration};
97/// use lilkaoxide::i2s::TDMStandart;
98/// use esp_hal::i2s::master::{Channels, DataFormat};
99/// use esp_hal::time::Rate;
100///
101/// let config = LilkaConfiguration {
102///     i2s_tx_config: I2sTxConfig {
103///         sample_rate: Rate::from_hz(22_050),
104///         channels: Channels::STEREO,
105///         data_format: DataFormat::Data16Channel16,
106///         tdm_standard: TDMStandart::Phillips,
107///     },
108///     ..LilkaConfiguration::default()
109/// };
110/// ```
111pub struct I2sTxConfig {
112    /// Audio sample rate in Hz.  Common values: 8 000, 22 050, 44 100, 48 000.
113    pub sample_rate: esp_hal::time::Rate,
114    /// Number of audio channels.  Use `Channels::STEREO` (default) or `Channels::MONO`.
115    pub channels: esp_hal::i2s::master::Channels,
116    /// Bit depth per sample and per channel slot.  `Data16Channel16` is the standard
117    /// choice for 16-bit PCM audio with 16-bit-wide BCLK slots.
118    pub data_format: esp_hal::i2s::master::DataFormat,
119    /// TDM framing standard.  Must match the connected DAC's expectations.
120    pub tdm_standard: TDMStandart,
121}
122
123impl Default for I2sTxConfig {
124    fn default() -> Self {
125        Self {
126            sample_rate: esp_hal::time::Rate::from_hz(44100),
127            channels: esp_hal::i2s::master::Channels::STEREO,
128            data_format: esp_hal::i2s::master::DataFormat::Data16Channel16,
129            tdm_standard: TDMStandart::Phillips,
130        }
131    }
132}
133
134pub fn init_i2s_tx<'d>(
135    config: I2sTxConfig,
136    bclk: impl PeripheralOutput<'d>,
137    dout: impl PeripheralOutput<'d>,
138    lrck: impl PeripheralOutput<'d>,
139    i2s0: I2S0<'d>,
140    dma_channel: DMA_CH0<'d>,
141) -> Result<I2sTx<'d>, I2sInitError> {
142    let i2s = I2s::new(
143        i2s0,
144        dma_channel,
145        tdm_config!(config.tdm_standard, cfg => .with_sample_rate(config.sample_rate).with_channels(config.channels).with_data_format(config.data_format))
146    )
147    .map_err(|_| I2sInitError::PeripheralInitFailed)?;
148    let (_, _, _tx_buf, tx_desc) = dma_buffers!(0, DEFAULT_DMA_TX_BUF_SIZE);
149
150    #[cfg(feature = "async")]
151    let i2s = i2s.into_async();
152    let i2stx = i2s
153        .i2s_tx
154        .with_bclk(bclk)
155        .with_ws(lrck)
156        .with_dout(dout)
157        .build(tx_desc);
158    Ok(i2stx)
159}