Skip to content
22 changes: 17 additions & 5 deletions locale/circuitpython.pot
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ msgstr ""
msgid "%q must be array of type 'h'"
msgstr ""

#: shared-bindings/audiobusio/PDMIn.c
#: shared-bindings/audio_i2sin/I2SIn.c shared-bindings/audiobusio/PDMIn.c
msgid "%q must be multiple of 8."
msgstr ""

Expand Down Expand Up @@ -659,6 +659,7 @@ msgstr ""
msgid "Below minimum frame rate"
msgstr ""

#: ports/raspberrypi/common-hal/audio_i2sin/I2SIn.c
#: ports/raspberrypi/common-hal/audiobusio/I2SOut.c
msgid "Bit clock and word select must be sequential GPIO pins"
msgstr ""
Expand Down Expand Up @@ -804,7 +805,7 @@ msgstr ""
msgid "Cannot pull on input-only pin."
msgstr ""

#: shared-bindings/audiobusio/PDMIn.c
#: shared-bindings/audio_i2sin/I2SIn.c shared-bindings/audiobusio/PDMIn.c
msgid "Cannot record to a file"
msgstr ""

Expand Down Expand Up @@ -926,7 +927,7 @@ msgstr ""
msgid "Deep sleep pins must use a rising edge with pulldown"
msgstr ""

#: shared-bindings/audiobusio/PDMIn.c
#: shared-bindings/audio_i2sin/I2SIn.c shared-bindings/audiobusio/PDMIn.c
msgid "Destination capacity is smaller than destination_length."
msgstr ""

Expand Down Expand Up @@ -1694,6 +1695,10 @@ msgstr ""
msgid "Ok"
msgstr ""

#: ports/raspberrypi/common-hal/audio_i2sin/I2SIn.c
msgid "Only 16, 24, or 32 bit depth supported."
msgstr ""

#: ports/atmel-samd/common-hal/audiobusio/PDMIn.c
#: ports/raspberrypi/common-hal/audiobusio/PDMIn.c
#, c-format
Expand Down Expand Up @@ -1806,6 +1811,7 @@ msgstr ""
msgid "Parameter error"
msgstr ""

#: ports/espressif/common-hal/audio_i2sin/I2SIn.c
#: ports/espressif/common-hal/audiobusio/__init__.c
msgid "Peripheral in use"
msgstr ""
Expand Down Expand Up @@ -2696,6 +2702,7 @@ msgstr ""
msgid "binary op %q not implemented"
msgstr ""

#: ports/espressif/common-hal/audio_i2sin/I2SIn.c
#: ports/espressif/common-hal/audiobusio/PDMIn.c
msgid "bit_depth must be 8, 16, 24, or 32."
Comment thread
FoamyGuy marked this conversation as resolved.
Outdated
msgstr ""
Expand Down Expand Up @@ -3088,15 +3095,20 @@ msgstr ""
msgid "default is not a function"
msgstr ""

#: shared-bindings/audiobusio/PDMIn.c
#: shared-bindings/audio_i2sin/I2SIn.c shared-bindings/audiobusio/PDMIn.c
msgid ""
"destination buffer must be a bytearray or array of type 'B' for bit_depth = 8"
msgstr ""

#: shared-bindings/audiobusio/PDMIn.c
#: shared-bindings/audio_i2sin/I2SIn.c shared-bindings/audiobusio/PDMIn.c
msgid "destination buffer must be an array of type 'H' for bit_depth = 16"
msgstr ""

#: shared-bindings/audio_i2sin/I2SIn.c
msgid ""
"destination buffer must be an array of type 'I' for bit_depth = 24 or 32"
msgstr ""

#: py/objdict.c
msgid "dict update sequence has wrong length"
msgstr ""
Expand Down
4 changes: 4 additions & 0 deletions ports/espressif/boards/adafruit_sparkle_motion/pins.c
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ static const mp_rom_map_elem_t board_module_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR_SIG4), MP_ROM_PTR(&pin_GPIO23) },
{ MP_ROM_QSTR(MP_QSTR_D23), MP_ROM_PTR(&pin_GPIO23) },

{ MP_ROM_QSTR(MP_QSTR_D25), MP_ROM_PTR(&pin_GPIO25) },
{ MP_ROM_QSTR(MP_QSTR_D26), MP_ROM_PTR(&pin_GPIO26) },
{ MP_ROM_QSTR(MP_QSTR_D33), MP_ROM_PTR(&pin_GPIO33) },

{ MP_ROM_QSTR(MP_QSTR_TX), MP_ROM_PTR(&pin_GPIO9) },
{ MP_ROM_QSTR(MP_QSTR_D9), MP_ROM_PTR(&pin_GPIO9) },

Expand Down
205 changes: 205 additions & 0 deletions ports/espressif/common-hal/audioi2sin/I2SIn.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
// This file is part of the CircuitPython project: https://circuitpython.org
//
// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries
//
// SPDX-License-Identifier: MIT

#include <string.h>

#include "bindings/espidf/__init__.h"

#include "common-hal/audioi2sin/I2SIn.h"
#include "py/runtime.h"
#include "shared-bindings/audioi2sin/I2SIn.h"
#include "shared-bindings/microcontroller/Pin.h"

#include "driver/i2s_std.h"

#if CIRCUITPY_AUDIOI2SIN

void common_hal_audioi2sin_i2sin_construct(audioi2sin_i2sin_obj_t *self,
const mcu_pin_obj_t *bit_clock, const mcu_pin_obj_t *word_select,
const mcu_pin_obj_t *data, const mcu_pin_obj_t *main_clock,
uint32_t sample_rate, uint8_t bit_depth, bool mono, bool left_justified,
bool samples_signed) {

if (bit_depth != 8 && bit_depth != 16 && bit_depth != 24 && bit_depth != 32) {
mp_raise_ValueError(MP_ERROR_TEXT("bit_depth must be 8, 16, 24, or 32."));
Comment thread
FoamyGuy marked this conversation as resolved.
Outdated
}

i2s_data_bit_width_t bit_width = (i2s_data_bit_width_t)bit_depth;

i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_AUTO, I2S_ROLE_MASTER);
esp_err_t err = i2s_new_channel(&chan_cfg, NULL, &self->rx_chan);
if (err == ESP_ERR_NOT_FOUND) {
mp_raise_RuntimeError(MP_ERROR_TEXT("Peripheral in use"));
}
CHECK_ESP_RESULT(err);

// Always configure the bus as stereo. The newer-family I2S peripherals
// (S2/S3/C-series) ignore I2S_SLOT_MODE_MONO on RX and write both slots
// into the DMA buffer regardless, which yields buffers that fill at 2x
// the WS rate and produces half-speed audio. By configuring stereo and
// dropping one slot ourselves in record_to_buffer, behavior is uniform
// across chips.
i2s_std_slot_config_t slot_cfg = left_justified
? (i2s_std_slot_config_t)I2S_STD_MSB_SLOT_DEFAULT_CONFIG(bit_width, I2S_SLOT_MODE_STEREO)
: (i2s_std_slot_config_t)I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(bit_width, I2S_SLOT_MODE_STEREO);

i2s_std_config_t std_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(sample_rate),
.slot_cfg = slot_cfg,
.gpio_cfg = {
.mclk = main_clock != NULL ? main_clock->number : I2S_GPIO_UNUSED,
.bclk = bit_clock->number,
.ws = word_select->number,
.dout = I2S_GPIO_UNUSED,
.din = data->number,
},
};
CHECK_ESP_RESULT(i2s_channel_init_std_mode(self->rx_chan, &std_cfg));
CHECK_ESP_RESULT(i2s_channel_enable(self->rx_chan));

self->bit_clock = bit_clock;
self->word_select = word_select;
self->data = data;
self->mclk = main_clock;
self->sample_rate = sample_rate;
self->bit_depth = bit_depth;
self->mono = mono;
self->samples_signed = samples_signed;

claim_pin(bit_clock);
claim_pin(word_select);
claim_pin(data);
if (main_clock) {
claim_pin(main_clock);
}
}

bool common_hal_audioi2sin_i2sin_deinited(audioi2sin_i2sin_obj_t *self) {
return self->data == NULL;
}

void common_hal_audioi2sin_i2sin_deinit(audioi2sin_i2sin_obj_t *self) {
if (common_hal_audioi2sin_i2sin_deinited(self)) {
return;
}

if (self->rx_chan) {
i2s_channel_disable(self->rx_chan);
i2s_del_channel(self->rx_chan);
self->rx_chan = NULL;
}

if (self->bit_clock) {
reset_pin_number(self->bit_clock->number);
}
self->bit_clock = NULL;

if (self->word_select) {
reset_pin_number(self->word_select->number);
}
self->word_select = NULL;

if (self->data) {
reset_pin_number(self->data->number);
}
self->data = NULL;

if (self->mclk) {
reset_pin_number(self->mclk->number);
}
self->mclk = NULL;
}

// I2S delivers signed PCM. When samples_signed is false, XOR each sample with
// the sign bit for its width to convert to unsigned PCM (WAV convention).
static void i2sin_convert_to_unsigned(void *buffer, uint32_t samples,
Comment thread
FoamyGuy marked this conversation as resolved.
uint8_t bit_depth, size_t element_size) {
if (bit_depth == 8) {
uint8_t *p = (uint8_t *)buffer;
for (uint32_t i = 0; i < samples; i++) {
p[i] ^= 0x80u;
}
} else if (bit_depth == 16) {
uint16_t *p = (uint16_t *)buffer;
for (uint32_t i = 0; i < samples; i++) {
p[i] ^= 0x8000u;
}
} else {
// 24- or 32-bit; both stored in 32-bit slots (element_size == 4).
(void)element_size;
uint32_t mask = (bit_depth == 24) ? 0x800000u : 0x80000000u;
uint32_t *p = (uint32_t *)buffer;
for (uint32_t i = 0; i < samples; i++) {
p[i] ^= mask;
}
}
}

uint32_t common_hal_audioi2sin_i2sin_record_to_buffer(audioi2sin_i2sin_obj_t *self,
void *buffer, uint32_t length) {
size_t element_size = self->bit_depth / 8;
// 24-bit samples occupy a 32-bit slot on the I2S bus.
if (self->bit_depth == 24) {
element_size = 4;
}

uint32_t produced;
if (!self->mono) {
size_t result = 0;
esp_err_t err = i2s_channel_read(self->rx_chan, buffer, length * element_size,
&result, portMAX_DELAY);
CHECK_ESP_RESULT(err);
produced = result / element_size;
} else {
// Mono: bus is configured stereo, so each WS frame yields two slots in
// the DMA buffer. Read in chunks into a scratch and keep only the left
// slot of each frame.
uint8_t scratch[256];
const size_t frame_bytes = 2 * element_size;
const size_t scratch_frames = sizeof(scratch) / frame_bytes;
uint8_t *out = (uint8_t *)buffer;
produced = 0;
while (produced < length) {
size_t want_frames = length - produced;
if (want_frames > scratch_frames) {
want_frames = scratch_frames;
}
size_t got_bytes = 0;
esp_err_t err = i2s_channel_read(self->rx_chan, scratch,
want_frames * frame_bytes, &got_bytes, portMAX_DELAY);
CHECK_ESP_RESULT(err);
size_t got_frames = got_bytes / frame_bytes;
for (size_t i = 0; i < got_frames; i++) {
memcpy(out + produced * element_size,
scratch + i * frame_bytes,
element_size);
produced++;
}
if (got_frames < want_frames) {
break;
}
}
}

if (!self->samples_signed && produced > 0) {
i2sin_convert_to_unsigned(buffer, produced, self->bit_depth, element_size);
}
return produced;
}

uint8_t common_hal_audioi2sin_i2sin_get_bit_depth(audioi2sin_i2sin_obj_t *self) {
return self->bit_depth;
}

uint32_t common_hal_audioi2sin_i2sin_get_sample_rate(audioi2sin_i2sin_obj_t *self) {
return self->sample_rate;
}

bool common_hal_audioi2sin_i2sin_get_samples_signed(audioi2sin_i2sin_obj_t *self) {
return self->samples_signed;
}

#endif // CIRCUITPY_AUDIOI2SIN
30 changes: 30 additions & 0 deletions ports/espressif/common-hal/audioi2sin/I2SIn.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// This file is part of the CircuitPython project: https://circuitpython.org
//
// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries
//
// SPDX-License-Identifier: MIT

#pragma once

#include "py/obj.h"

#include "common-hal/microcontroller/Pin.h"

#include "driver/i2s_std.h"

#if CIRCUITPY_AUDIOI2SIN

typedef struct {
mp_obj_base_t base;
i2s_chan_handle_t rx_chan;
const mcu_pin_obj_t *bit_clock;
const mcu_pin_obj_t *word_select;
const mcu_pin_obj_t *data;
const mcu_pin_obj_t *mclk;
uint32_t sample_rate;
uint8_t bit_depth;
bool mono;
bool samples_signed;
} audioi2sin_i2sin_obj_t;

#endif
5 changes: 5 additions & 0 deletions ports/espressif/common-hal/audioi2sin/__init__.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// This file is part of the CircuitPython project: https://circuitpython.org
//
// SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries
//
// SPDX-License-Identifier: MIT
1 change: 1 addition & 0 deletions ports/espressif/mpconfigport.mk
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ CIRCUITPY_ALARM_TOUCH ?= 0
CIRCUITPY_ANALOGBUFIO ?= 1
CIRCUITPY_AUDIOBUSIO ?= 1
CIRCUITPY_AUDIOBUSIO_PDMIN ?= 0
CIRCUITPY_AUDIOI2SIN ?= 1
CIRCUITPY_AUDIOIO ?= 1
CIRCUITPY_BLEIO_HCI = 0
CIRCUITPY_BLEIO_NATIVE ?= 1
Expand Down
12 changes: 12 additions & 0 deletions ports/raspberrypi/bindings/rp2pio/StateMachine.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,18 @@ bool common_hal_rp2pio_statemachine_background_read(rp2pio_statemachine_obj_t *s
bool common_hal_rp2pio_statemachine_stop_background_write(rp2pio_statemachine_obj_t *self);
bool common_hal_rp2pio_statemachine_stop_background_read(rp2pio_statemachine_obj_t *self);

// Set the once / loop / loop2 read buffers from raw pointers without going
// through an mp_obj_t wrapper. Pass NULL/0 for unused slots. The caller owns
// the memory; it must remain valid until stop_background_read.
void common_hal_rp2pio_statemachine_set_read_buffers_raw(rp2pio_statemachine_obj_t *self,
void *once, size_t once_len,
void *loop, size_t loop_len,
void *loop2, size_t loop2_len);

// Returns the DMA channel index used by the current background read, or -1
// if no background read is active.
int common_hal_rp2pio_statemachine_get_read_dma_channel(rp2pio_statemachine_obj_t *self);

mp_int_t common_hal_rp2pio_statemachine_get_pending_write(rp2pio_statemachine_obj_t *self);
mp_int_t common_hal_rp2pio_statemachine_get_pending_read(rp2pio_statemachine_obj_t *self);

Expand Down
Loading
Loading