From 2236879a37b657c8c98c567830bbe49cd5f55750 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Fri, 8 May 2026 15:06:15 -0500 Subject: [PATCH 1/9] audio_i2sin module for espressif and raspberrypi --- locale/circuitpython.pot | 22 +- .../boards/adafruit_sparkle_motion/pins.c | 4 + .../espressif/common-hal/audio_i2sin/I2SIn.c | 169 +++++++ .../espressif/common-hal/audio_i2sin/I2SIn.h | 29 ++ .../common-hal/audio_i2sin/__init__.c | 5 + ports/espressif/mpconfigport.mk | 1 + .../bindings/rp2pio/StateMachine.h | 12 + .../common-hal/audio_i2sin/I2SIn.c | 436 ++++++++++++++++++ .../common-hal/audio_i2sin/I2SIn.h | 29 ++ .../common-hal/audio_i2sin/README.pio | 14 + .../common-hal/audio_i2sin/__init__.c | 5 + .../common-hal/audio_i2sin/i2sin.pio | 27 ++ .../common-hal/audio_i2sin/i2sin_32.pio | 24 + .../common-hal/audio_i2sin/i2sin_left.pio | 23 + .../common-hal/audio_i2sin/i2sin_left_32.pio | 23 + .../common-hal/audio_i2sin/i2sin_swap.pio | 24 + .../common-hal/audio_i2sin/i2sin_swap_32.pio | 24 + .../audio_i2sin/i2sin_swap_left.pio | 24 + .../audio_i2sin/i2sin_swap_left_32.pio | 24 + .../common-hal/rp2pio/StateMachine.c | 30 ++ ports/raspberrypi/mpconfigport.mk | 1 + py/circuitpy_defns.mk | 5 + py/circuitpy_mpconfig.mk | 4 + shared-bindings/audio_i2sin/I2SIn.c | 223 +++++++++ shared-bindings/audio_i2sin/I2SIn.h | 28 ++ shared-bindings/audio_i2sin/__init__.c | 38 ++ shared-bindings/audio_i2sin/__init__.h | 7 + .../audio_i2sin/i2sin_neopixel_reactive.py | 109 +++++ .../audio_i2sin/i2sin_record_sdcard.py | 103 +++++ 29 files changed, 1462 insertions(+), 5 deletions(-) create mode 100644 ports/espressif/common-hal/audio_i2sin/I2SIn.c create mode 100644 ports/espressif/common-hal/audio_i2sin/I2SIn.h create mode 100644 ports/espressif/common-hal/audio_i2sin/__init__.c create mode 100644 ports/raspberrypi/common-hal/audio_i2sin/I2SIn.c create mode 100644 ports/raspberrypi/common-hal/audio_i2sin/I2SIn.h create mode 100644 ports/raspberrypi/common-hal/audio_i2sin/README.pio create mode 100644 ports/raspberrypi/common-hal/audio_i2sin/__init__.c create mode 100644 ports/raspberrypi/common-hal/audio_i2sin/i2sin.pio create mode 100644 ports/raspberrypi/common-hal/audio_i2sin/i2sin_32.pio create mode 100644 ports/raspberrypi/common-hal/audio_i2sin/i2sin_left.pio create mode 100644 ports/raspberrypi/common-hal/audio_i2sin/i2sin_left_32.pio create mode 100644 ports/raspberrypi/common-hal/audio_i2sin/i2sin_swap.pio create mode 100644 ports/raspberrypi/common-hal/audio_i2sin/i2sin_swap_32.pio create mode 100644 ports/raspberrypi/common-hal/audio_i2sin/i2sin_swap_left.pio create mode 100644 ports/raspberrypi/common-hal/audio_i2sin/i2sin_swap_left_32.pio create mode 100644 shared-bindings/audio_i2sin/I2SIn.c create mode 100644 shared-bindings/audio_i2sin/I2SIn.h create mode 100644 shared-bindings/audio_i2sin/__init__.c create mode 100644 shared-bindings/audio_i2sin/__init__.h create mode 100644 tests/circuitpython-manual/audio_i2sin/i2sin_neopixel_reactive.py create mode 100644 tests/circuitpython-manual/audio_i2sin/i2sin_record_sdcard.py diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot index e09df9d17818a..84e9fd7edd6b1 100644 --- a/locale/circuitpython.pot +++ b/locale/circuitpython.pot @@ -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 "" @@ -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 "" @@ -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 "" @@ -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 "" @@ -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 @@ -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 "" @@ -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." msgstr "" @@ -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 "" diff --git a/ports/espressif/boards/adafruit_sparkle_motion/pins.c b/ports/espressif/boards/adafruit_sparkle_motion/pins.c index d78c1e1c4c75c..f22cb50608d1f 100644 --- a/ports/espressif/boards/adafruit_sparkle_motion/pins.c +++ b/ports/espressif/boards/adafruit_sparkle_motion/pins.c @@ -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) }, diff --git a/ports/espressif/common-hal/audio_i2sin/I2SIn.c b/ports/espressif/common-hal/audio_i2sin/I2SIn.c new file mode 100644 index 0000000000000..4362351c68deb --- /dev/null +++ b/ports/espressif/common-hal/audio_i2sin/I2SIn.c @@ -0,0 +1,169 @@ +// 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 + +#include "bindings/espidf/__init__.h" + +#include "common-hal/audio_i2sin/I2SIn.h" +#include "py/runtime.h" +#include "shared-bindings/audio_i2sin/I2SIn.h" +#include "shared-bindings/microcontroller/Pin.h" + +#include "driver/i2s_std.h" + +#if CIRCUITPY_AUDIO_I2SIN + +void common_hal_audio_i2sin_i2sin_construct(audio_i2sin_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) { + + 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.")); + } + + 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; + + claim_pin(bit_clock); + claim_pin(word_select); + claim_pin(data); + if (main_clock) { + claim_pin(main_clock); + } +} + +bool common_hal_audio_i2sin_i2sin_deinited(audio_i2sin_i2sin_obj_t *self) { + return self->data == NULL; +} + +void common_hal_audio_i2sin_i2sin_deinit(audio_i2sin_i2sin_obj_t *self) { + if (common_hal_audio_i2sin_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; +} + +uint32_t common_hal_audio_i2sin_i2sin_record_to_buffer(audio_i2sin_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; + } + + 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); + return result / element_size; + } + + // 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; + uint32_t 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; + } + } + return produced; +} + +uint8_t common_hal_audio_i2sin_i2sin_get_bit_depth(audio_i2sin_i2sin_obj_t *self) { + return self->bit_depth; +} + +uint32_t common_hal_audio_i2sin_i2sin_get_sample_rate(audio_i2sin_i2sin_obj_t *self) { + return self->sample_rate; +} + +#endif // CIRCUITPY_AUDIO_I2SIN diff --git a/ports/espressif/common-hal/audio_i2sin/I2SIn.h b/ports/espressif/common-hal/audio_i2sin/I2SIn.h new file mode 100644 index 0000000000000..917b72c954b74 --- /dev/null +++ b/ports/espressif/common-hal/audio_i2sin/I2SIn.h @@ -0,0 +1,29 @@ +// 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_AUDIO_I2SIN + +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; +} audio_i2sin_i2sin_obj_t; + +#endif diff --git a/ports/espressif/common-hal/audio_i2sin/__init__.c b/ports/espressif/common-hal/audio_i2sin/__init__.c new file mode 100644 index 0000000000000..584c821b99631 --- /dev/null +++ b/ports/espressif/common-hal/audio_i2sin/__init__.c @@ -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 diff --git a/ports/espressif/mpconfigport.mk b/ports/espressif/mpconfigport.mk index 67f8ca2987bd8..e52d47d91139a 100644 --- a/ports/espressif/mpconfigport.mk +++ b/ports/espressif/mpconfigport.mk @@ -68,6 +68,7 @@ CIRCUITPY_ALARM_TOUCH ?= 0 CIRCUITPY_ANALOGBUFIO ?= 1 CIRCUITPY_AUDIOBUSIO ?= 1 CIRCUITPY_AUDIOBUSIO_PDMIN ?= 0 +CIRCUITPY_AUDIO_I2SIN ?= 1 CIRCUITPY_AUDIOIO ?= 1 CIRCUITPY_BLEIO_HCI = 0 CIRCUITPY_BLEIO_NATIVE ?= 1 diff --git a/ports/raspberrypi/bindings/rp2pio/StateMachine.h b/ports/raspberrypi/bindings/rp2pio/StateMachine.h index 16f884bcfca32..3c775a96449f0 100644 --- a/ports/raspberrypi/bindings/rp2pio/StateMachine.h +++ b/ports/raspberrypi/bindings/rp2pio/StateMachine.h @@ -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); diff --git a/ports/raspberrypi/common-hal/audio_i2sin/I2SIn.c b/ports/raspberrypi/common-hal/audio_i2sin/I2SIn.c new file mode 100644 index 0000000000000..54d66df88c472 --- /dev/null +++ b/ports/raspberrypi/common-hal/audio_i2sin/I2SIn.c @@ -0,0 +1,436 @@ +// 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 +#include + +#include "py/mperrno.h" +#include "py/mphal.h" +#include "py/runtime.h" +#include "shared/runtime/interrupt_char.h" +#include "common-hal/audio_i2sin/I2SIn.h" +#include "shared-bindings/audio_i2sin/I2SIn.h" +#include "shared-bindings/microcontroller/Pin.h" +#include "bindings/rp2pio/StateMachine.h" +#include "supervisor/port.h" + +#include "hardware/dma.h" + +#if CIRCUITPY_AUDIO_I2SIN + +// 16-bit programs sample a stereo frame of 16+16 = 32 bits and rely on +// auto-push at 32 to deliver one packed (right<<16 | left) word per frame. +// 32-bit programs sample 32 bits per channel and produce two FIFO words per +// frame (right first, then left). Each bit takes 6 PIO clocks (a [2]-delay +// pair). 24-bit recordings reuse the 32-bit programs because most 24-bit +// MEMS mics (SPH0645LM4H, INMP441, ICS-43434) transmit their 24 valid bits +// left-justified inside a 32-bit slot. +// +// `in pins 1` runs on a cycle where side-set drives BCLK high. The slave +// updates the data line on the BCLK falling edge, so by the rising edge it +// has settled and is safe to sample. Sampling at BCLK low (the previous, +// incorrect arrangement) catches the data mid-transition and the result is +// effectively noise. +#define PIO_CLOCKS_PER_BIT (6) + +// Master-mode RX, regular pin order (BCLK = WS - 1), Philips alignment. +static const uint16_t i2sin_program[] = { + // .wrap_target + 0xf04e, // 0: set y, 14 side 2 + 0x5a01, // 1: in pins, 1 side 3 [2] + 0x1281, // 2: jmp y--, 1 side 2 [2] + 0x4a01, // 3: in pins, 1 side 1 [2] + 0xe24e, // 4: set y, 14 side 0 [2] + 0x4a01, // 5: in pins, 1 side 1 [2] + 0x0285, // 6: jmp y--, 5 side 0 [2] + 0x5a01, // 7: in pins, 1 side 3 [2] + // .wrap +}; + +// Master-mode RX, regular pin order, left-justified. +static const uint16_t i2sin_program_left_justified[] = { + // .wrap_target + 0xe04e, // 0: set y, 14 side 0 + 0x5a01, // 1: in pins, 1 side 3 [2] + 0x1281, // 2: jmp y--, 1 side 2 [2] + 0x5a01, // 3: in pins, 1 side 3 [2] + 0xf24e, // 4: set y, 14 side 2 [2] + 0x4a01, // 5: in pins, 1 side 1 [2] + 0x0285, // 6: jmp y--, 5 side 0 [2] + 0x4a01, // 7: in pins, 1 side 1 [2] + // .wrap +}; + +// Master-mode RX, swapped pin order (BCLK = WS + 1), Philips alignment. +static const uint16_t i2sin_program_swap[] = { + // .wrap_target + 0xe84e, // 0: set y, 14 side 1 + 0x5a01, // 1: in pins, 1 side 3 [2] + 0x0a81, // 2: jmp y--, 1 side 1 [2] + 0x5201, // 3: in pins, 1 side 2 [2] + 0xe24e, // 4: set y, 14 side 0 [2] + 0x5201, // 5: in pins, 1 side 2 [2] + 0x0285, // 6: jmp y--, 5 side 0 [2] + 0x5a01, // 7: in pins, 1 side 3 [2] + // .wrap +}; + +// Master-mode RX, swapped pin order, left-justified. +static const uint16_t i2sin_program_left_justified_swap[] = { + // .wrap_target + 0xe04e, // 0: set y, 14 side 0 + 0x5a01, // 1: in pins, 1 side 3 [2] + 0x0a81, // 2: jmp y--, 1 side 1 [2] + 0x5a01, // 3: in pins, 1 side 3 [2] + 0xea4e, // 4: set y, 14 side 1 [2] + 0x5201, // 5: in pins, 1 side 2 [2] + 0x0285, // 6: jmp y--, 5 side 0 [2] + 0x5201, // 7: in pins, 1 side 2 [2] + // .wrap +}; + +// 32-bit-per-channel variants: identical to the 16-bit programs above except +// the loop counter is set to 30 (so each `bitloop` runs 31 in's, plus one +// outside the loop = 32 in's per channel). +static const uint16_t i2sin_program_32[] = { + // .wrap_target + 0xf05e, // 0: set y, 30 side 2 + 0x5a01, // 1: in pins, 1 side 3 [2] + 0x1281, // 2: jmp y--, 1 side 2 [2] + 0x4a01, // 3: in pins, 1 side 1 [2] + 0xe25e, // 4: set y, 30 side 0 [2] + 0x4a01, // 5: in pins, 1 side 1 [2] + 0x0285, // 6: jmp y--, 5 side 0 [2] + 0x5a01, // 7: in pins, 1 side 3 [2] + // .wrap +}; + +static const uint16_t i2sin_program_left_justified_32[] = { + // .wrap_target + 0xe05e, // 0: set y, 30 side 0 + 0x5a01, // 1: in pins, 1 side 3 [2] + 0x1281, // 2: jmp y--, 1 side 2 [2] + 0x5a01, // 3: in pins, 1 side 3 [2] + 0xf25e, // 4: set y, 30 side 2 [2] + 0x4a01, // 5: in pins, 1 side 1 [2] + 0x0285, // 6: jmp y--, 5 side 0 [2] + 0x4a01, // 7: in pins, 1 side 1 [2] + // .wrap +}; + +static const uint16_t i2sin_program_swap_32[] = { + // .wrap_target + 0xe85e, // 0: set y, 30 side 1 + 0x5a01, // 1: in pins, 1 side 3 [2] + 0x0a81, // 2: jmp y--, 1 side 1 [2] + 0x5201, // 3: in pins, 1 side 2 [2] + 0xe25e, // 4: set y, 30 side 0 [2] + 0x5201, // 5: in pins, 1 side 2 [2] + 0x0285, // 6: jmp y--, 5 side 0 [2] + 0x5a01, // 7: in pins, 1 side 3 [2] + // .wrap +}; + +static const uint16_t i2sin_program_left_justified_swap_32[] = { + // .wrap_target + 0xe05e, // 0: set y, 30 side 0 + 0x5a01, // 1: in pins, 1 side 3 [2] + 0x0a81, // 2: jmp y--, 1 side 1 [2] + 0x5a01, // 3: in pins, 1 side 3 [2] + 0xea5e, // 4: set y, 30 side 1 [2] + 0x5201, // 5: in pins, 1 side 2 [2] + 0x0285, // 6: jmp y--, 5 side 0 [2] + 0x5201, // 7: in pins, 1 side 2 [2] + // .wrap +}; + +// Caller validates that pins are free. +void common_hal_audio_i2sin_i2sin_construct(audio_i2sin_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) { + + if (main_clock != NULL) { + mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_main_clock); + } + if (bit_depth != 16 && bit_depth != 24 && bit_depth != 32) { + mp_raise_NotImplementedError(MP_ERROR_TEXT("Only 16, 24, or 32 bit depth supported.")); + } + + // 24- and 32-bit recordings both clock 32 bits per channel; 24-bit MEMS + // mics deliver their data left-justified inside that 32-bit slot. + bool wide = (bit_depth != 16); + uint32_t bits_per_channel = wide ? 32 : 16; + uint32_t pio_clocks_per_frame = bits_per_channel * 2 * PIO_CLOCKS_PER_BIT; + + const mcu_pin_obj_t *sideset_pin = NULL; + const uint16_t *program = NULL; + size_t program_len = 0; + + if (bit_clock->number == word_select->number - 1) { + sideset_pin = bit_clock; + if (left_justified) { + program = wide ? i2sin_program_left_justified_32 : i2sin_program_left_justified; + program_len = wide ? MP_ARRAY_SIZE(i2sin_program_left_justified_32) + : MP_ARRAY_SIZE(i2sin_program_left_justified); + } else { + program = wide ? i2sin_program_32 : i2sin_program; + program_len = wide ? MP_ARRAY_SIZE(i2sin_program_32) + : MP_ARRAY_SIZE(i2sin_program); + } + } else if (bit_clock->number == word_select->number + 1) { + sideset_pin = word_select; + if (left_justified) { + program = wide ? i2sin_program_left_justified_swap_32 : i2sin_program_left_justified_swap; + program_len = wide ? MP_ARRAY_SIZE(i2sin_program_left_justified_swap_32) + : MP_ARRAY_SIZE(i2sin_program_left_justified_swap); + } else { + program = wide ? i2sin_program_swap_32 : i2sin_program_swap; + program_len = wide ? MP_ARRAY_SIZE(i2sin_program_swap_32) + : MP_ARRAY_SIZE(i2sin_program_swap); + } + } else { + mp_raise_ValueError(MP_ERROR_TEXT("Bit clock and word select must be sequential GPIO pins")); + } + + common_hal_rp2pio_statemachine_construct( + &self->state_machine, + program, program_len, + sample_rate * pio_clocks_per_frame, + NULL, 0, // init + NULL, 0, // may_exec + NULL, 0, PIO_PINMASK32_NONE, PIO_PINMASK32_NONE, // out pin + data, 1, // in pins + PIO_PINMASK32_NONE, PIO_PINMASK32_NONE, // in pulls + NULL, 0, PIO_PINMASK32_NONE, PIO_PINMASK32_FROM_VALUE(0x1f), // set pins + sideset_pin, 2, false, PIO_PINMASK32_NONE, PIO_PINMASK32_FROM_VALUE(0x1f), // sideset pins + false, // No sideset enable + NULL, PULL_NONE, // jump pin + PIO_PINMASK_NONE, // wait gpio pins + true, // exclusive pin use + false, 32, false, // out settings (unused) + false, // Wait for txstall + true, 32, false, // in settings: auto-push at 32 bits, shift left (MSB first) + false, // Not user-interruptible. + 0, -1, // wrap settings + PIO_ANY_OFFSET, + PIO_FIFO_TYPE_DEFAULT, + PIO_MOV_STATUS_DEFAULT, + PIO_MOV_N_DEFAULT); + + uint32_t actual_frequency = common_hal_rp2pio_statemachine_get_frequency(&self->state_machine); + self->sample_rate = actual_frequency / pio_clocks_per_frame; + self->bit_depth = bit_depth; + self->mono = mono; + self->settled = false; + self->ring = NULL; + self->ring_size = 0; + self->half_size = 0; + self->read_pos = 0; + self->dma_channel = -1; + self->overflow = false; + + // Each PIO frame produces 4 bytes in the FIFO regardless of bit depth + // (16-bit auto-pushes one packed stereo word, 24/32-bit pushes two + // separate 32-bit words). One stereo frame is therefore either 4 or + // 8 bytes; size the half-buffer for ~40 ms of audio so a slow consumer + // (SD card flush etc.) can complete without overrunning. + size_t bytes_per_frame = (bit_depth == 16) ? 4 : 8; + size_t target = (size_t)self->sample_rate * bytes_per_frame * 2 / 2; + size_t half_size = (target > 4096) ? target : 4096; + // Round up to a multiple of 8 so 24/32-bit pair reads never straddle + // the half boundary. + half_size = (half_size + 7u) & ~(size_t)7u; + + self->ring = (uint8_t *)port_malloc(2 * half_size, true); + if (self->ring == NULL) { + common_hal_rp2pio_statemachine_deinit(&self->state_machine); + m_malloc_fail(2 * half_size); + } + self->half_size = half_size; + self->ring_size = 2 * half_size; + + common_hal_rp2pio_statemachine_set_read_buffers_raw(&self->state_machine, + NULL, 0, + self->ring, half_size, + self->ring + half_size, half_size); + if (!common_hal_rp2pio_statemachine_background_read(&self->state_machine, 4, false)) { + port_free(self->ring); + self->ring = NULL; + common_hal_rp2pio_statemachine_deinit(&self->state_machine); + mp_raise_OSError(MP_EIO); + } + self->dma_channel = common_hal_rp2pio_statemachine_get_read_dma_channel(&self->state_machine); +} + +bool common_hal_audio_i2sin_i2sin_deinited(audio_i2sin_i2sin_obj_t *self) { + return common_hal_rp2pio_statemachine_deinited(&self->state_machine); +} + +void common_hal_audio_i2sin_i2sin_deinit(audio_i2sin_i2sin_obj_t *self) { + if (common_hal_audio_i2sin_i2sin_deinited(self)) { + return; + } + common_hal_rp2pio_statemachine_stop_background_read(&self->state_machine); + common_hal_rp2pio_statemachine_deinit(&self->state_machine); + if (self->ring != NULL) { + port_free(self->ring); + self->ring = NULL; + } + self->ring_size = 0; + self->half_size = 0; + self->dma_channel = -1; +} + +uint8_t common_hal_audio_i2sin_i2sin_get_bit_depth(audio_i2sin_i2sin_obj_t *self) { + return self->bit_depth; +} + +uint32_t common_hal_audio_i2sin_i2sin_get_sample_rate(audio_i2sin_i2sin_obj_t *self) { + return self->sample_rate; +} + +// In 16-bit mode, each PIO frame produces a single 32-bit FIFO word with bits +// 31..16 = right channel and bits 15..0 = left channel (both MSB-first signed +// 16-bit). In 24/32-bit mode each frame produces two FIFO words: right first, +// then left, each MSB-first in the full 32 bits. For mono we keep the left +// channel; for stereo we emit (left, right) pairs. +// +// `output_buffer_length` is the requested number of samples to write (sample +// width = 2 bytes for bit_depth=16, 4 bytes for bit_depth=24 or 32). Returns +// the number actually written. +// Compute the byte offset inside `ring` that the DMA is currently writing +// (one past the last word it finished). Always lies in [0, ring_size). +static size_t i2sin_write_pos(audio_i2sin_i2sin_obj_t *self) { + uintptr_t addr = (uintptr_t)dma_channel_hw_addr(self->dma_channel)->write_addr; + uintptr_t base = (uintptr_t)self->ring; + if (addr < base || addr >= base + self->ring_size) { + // The ISR retargets write_addr to the start of the next half right + // after a transfer completes; it should always be in-range, but if + // we observe it mid-update just report "no new data". + return self->read_pos; + } + return (size_t)(addr - base); +} + +uint32_t common_hal_audio_i2sin_i2sin_record_to_buffer(audio_i2sin_i2sin_obj_t *self, + void *buffer, uint32_t output_buffer_length) { + uint32_t output_count = 0; + const size_t ring_size = self->ring_size; + const size_t half_size = self->half_size; + + if (self->bit_depth == 16) { + // 16-bit mode auto-pushes one stereo frame per FIFO word. The DMA has + // been streaming since construct time, so the ring already contains + // settled data; drop the first 4 bytes once to discard a single + // pre-record frame (matches the prior synchronous behaviour). + uint16_t *output = (uint16_t *)buffer; + while (output_count < output_buffer_length) { + size_t write_pos = i2sin_write_pos(self); + size_t avail = (write_pos + ring_size - self->read_pos) % ring_size; + if (avail > half_size) { + // DMA has filled more than one half ahead of us -- we lost + // data. Snap to just behind the DMA on a 4-byte boundary. + self->overflow = true; + self->read_pos = write_pos & ~(size_t)3u; + avail = 0; + } + if (!self->settled && avail >= 4) { + self->read_pos = (self->read_pos + 4) % ring_size; + avail -= 4; + self->settled = true; + } + if (avail < 4) { + RUN_BACKGROUND_TASKS; + if (mp_hal_is_interrupted()) { + return output_count; + } + continue; + } + while (avail >= 4 && output_count < output_buffer_length) { + uint32_t frame = *(volatile uint32_t *)(self->ring + self->read_pos); + uint16_t left = (uint16_t)(frame & 0xffff); + uint16_t right = (uint16_t)(frame >> 16); + if (self->mono) { + output[output_count++] = left; + } else { + output[output_count++] = left; + if (output_count >= output_buffer_length) { + self->read_pos = (self->read_pos + 4) % ring_size; + avail -= 4; + break; + } + output[output_count++] = right; + } + self->read_pos = (self->read_pos + 4) % ring_size; + avail -= 4; + } + } + } else { + // 24-/32-bit mode emits two FIFO pushes per stereo frame (right then + // left). The state machine was started at instruction 0 by the + // constructor, so the very first push is the right channel and + // alternation is preserved as long as we never touch the program + // counter and always read an even number of words. half_size is a + // multiple of 8, so reading 8-byte pairs stays aligned across the + // ring wrap. + uint32_t *output = (uint32_t *)buffer; + while (output_count < output_buffer_length) { + size_t write_pos = i2sin_write_pos(self); + size_t avail = (write_pos + ring_size - self->read_pos) % ring_size; + if (avail > half_size) { + // Overflow: snap to a frame-aligned position one half behind + // the DMA's current half so we resume on the right channel. + self->overflow = true; + size_t cur_half = (write_pos < half_size) ? 0 : half_size; + self->read_pos = (cur_half + half_size) % ring_size; + self->settled = false; + avail = (write_pos + ring_size - self->read_pos) % ring_size; + } + if (!self->settled) { + if (avail < 8) { + RUN_BACKGROUND_TASKS; + if (mp_hal_is_interrupted()) { + return output_count; + } + continue; + } + self->read_pos = (self->read_pos + 8) % ring_size; + avail -= 8; + self->settled = true; + } + if (avail < 8) { + RUN_BACKGROUND_TASKS; + if (mp_hal_is_interrupted()) { + return output_count; + } + continue; + } + while (avail >= 8 && output_count < output_buffer_length) { + uint32_t right = *(volatile uint32_t *)(self->ring + self->read_pos); + size_t next_pos = (self->read_pos + 4) % ring_size; + uint32_t left = *(volatile uint32_t *)(self->ring + next_pos); + if (self->mono) { + output[output_count++] = left; + } else { + output[output_count++] = left; + if (output_count >= output_buffer_length) { + self->read_pos = (self->read_pos + 8) % ring_size; + avail -= 8; + break; + } + output[output_count++] = right; + } + self->read_pos = (self->read_pos + 8) % ring_size; + avail -= 8; + } + } + } + + return output_count; +} + +#endif // CIRCUITPY_AUDIO_I2SIN diff --git a/ports/raspberrypi/common-hal/audio_i2sin/I2SIn.h b/ports/raspberrypi/common-hal/audio_i2sin/I2SIn.h new file mode 100644 index 0000000000000..9f825c79f72ca --- /dev/null +++ b/ports/raspberrypi/common-hal/audio_i2sin/I2SIn.h @@ -0,0 +1,29 @@ +// 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 "common-hal/microcontroller/Pin.h" +#include "bindings/rp2pio/StateMachine.h" + +#include "py/obj.h" + +typedef struct { + mp_obj_base_t base; + uint32_t sample_rate; + uint8_t bit_depth; + bool mono; + bool settled; + rp2pio_statemachine_obj_t state_machine; + // Background DMA ring buffer. The state machine alternates DMA writes + // between the two halves so BCLK never stalls between record() calls. + uint8_t *ring; + size_t ring_size; + size_t half_size; + size_t read_pos; + int dma_channel; + bool overflow; +} audio_i2sin_i2sin_obj_t; diff --git a/ports/raspberrypi/common-hal/audio_i2sin/README.pio b/ports/raspberrypi/common-hal/audio_i2sin/README.pio new file mode 100644 index 0000000000000..664c1301a7228 --- /dev/null +++ b/ports/raspberrypi/common-hal/audio_i2sin/README.pio @@ -0,0 +1,14 @@ +.pio files right now are compiled by hand with pico-sdk/tools/pioasm and inserted into I2SIn.c + +i2sin.pio I2S RX, 16 bits/channel, regular pin order, not left_justified +i2sin_left.pio I2S RX, 16 bits/channel, regular pin order, left_justified +i2sin_swap.pio I2S RX, 16 bits/channel, swapped pin order, not left_justified +i2sin_swap_left.pio I2S RX, 16 bits/channel, swapped pin order, left_justified +i2sin_32.pio I2S RX, 32 bits/channel, regular pin order, not left_justified +i2sin_left_32.pio I2S RX, 32 bits/channel, regular pin order, left_justified +i2sin_swap_32.pio I2S RX, 32 bits/channel, swapped pin order, not left_justified +i2sin_swap_left_32.pio I2S RX, 32 bits/channel, swapped pin order, left_justified + +The 32-bit programs are also used for bit_depth=24 because most 24-bit I2S +mics (SPH0645LM4H, INMP441, ICS-43434) place their 24 valid data bits +left-justified inside a 32-bit slot. diff --git a/ports/raspberrypi/common-hal/audio_i2sin/__init__.c b/ports/raspberrypi/common-hal/audio_i2sin/__init__.c new file mode 100644 index 0000000000000..584c821b99631 --- /dev/null +++ b/ports/raspberrypi/common-hal/audio_i2sin/__init__.c @@ -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 diff --git a/ports/raspberrypi/common-hal/audio_i2sin/i2sin.pio b/ports/raspberrypi/common-hal/audio_i2sin/i2sin.pio new file mode 100644 index 0000000000000..109d1ed5ab494 --- /dev/null +++ b/ports/raspberrypi/common-hal/audio_i2sin/i2sin.pio @@ -0,0 +1,27 @@ +; 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 + +.program i2sin +.side_set 2 + +; Master-mode I2S RX. Generates BCLK and LRCLK via side-set and samples the +; data pin. The slave updates `data` on BCLK falling edge, so the master +; samples on the rising edge: every `in pins 1` runs on a side-set value +; with BCLK=1, and the loop/transition instructions hold BCLK=0 so the +; slave has time to settle the next bit. + ; /--- LRCLK + ; |/-- BCLK + ; || + set y 14 side 0b10 +bitloop1: + in pins 1 side 0b11 [2] ; Right channel first + jmp y-- bitloop1 side 0b10 [2] + in pins 1 side 0b01 [2] + set y 14 side 0b00 [2] +bitloop0: + in pins 1 side 0b01 [2] ; Then left channel + jmp y-- bitloop0 side 0b00 [2] + in pins 1 side 0b11 [2] diff --git a/ports/raspberrypi/common-hal/audio_i2sin/i2sin_32.pio b/ports/raspberrypi/common-hal/audio_i2sin/i2sin_32.pio new file mode 100644 index 0000000000000..d3d7fb5aa6f52 --- /dev/null +++ b/ports/raspberrypi/common-hal/audio_i2sin/i2sin_32.pio @@ -0,0 +1,24 @@ +; 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 + +.program i2sin_32 +.side_set 2 + +; Master-mode I2S RX, 32 bits per channel (also used for 24-bit mics that +; transmit data left-justified in a 32-bit slot). + ; /--- LRCLK + ; |/-- BCLK + ; || + set y 30 side 0b10 +bitloop1: + in pins 1 side 0b11 [2] ; Right channel first + jmp y-- bitloop1 side 0b10 [2] + in pins 1 side 0b01 [2] + set y 30 side 0b00 [2] +bitloop0: + in pins 1 side 0b01 [2] ; Then left channel + jmp y-- bitloop0 side 0b00 [2] + in pins 1 side 0b11 [2] diff --git a/ports/raspberrypi/common-hal/audio_i2sin/i2sin_left.pio b/ports/raspberrypi/common-hal/audio_i2sin/i2sin_left.pio new file mode 100644 index 0000000000000..eb2b42a70312e --- /dev/null +++ b/ports/raspberrypi/common-hal/audio_i2sin/i2sin_left.pio @@ -0,0 +1,23 @@ +; 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 + +.program i2sin_left +.side_set 2 + +; Master-mode I2S RX, left-justified. Mirrors the timing of i2s_left.pio. + ; /--- LRCLK + ; |/-- BCLK + ; || + set y 14 side 0b00 +bitloop1: + in pins 1 side 0b11 [2] ; Right channel first + jmp y-- bitloop1 side 0b10 [2] + in pins 1 side 0b11 [2] + set y 14 side 0b10 [2] +bitloop0: + in pins 1 side 0b01 [2] ; Then left channel + jmp y-- bitloop0 side 0b00 [2] + in pins 1 side 0b01 [2] diff --git a/ports/raspberrypi/common-hal/audio_i2sin/i2sin_left_32.pio b/ports/raspberrypi/common-hal/audio_i2sin/i2sin_left_32.pio new file mode 100644 index 0000000000000..d2502a21540d8 --- /dev/null +++ b/ports/raspberrypi/common-hal/audio_i2sin/i2sin_left_32.pio @@ -0,0 +1,23 @@ +; 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 + +.program i2sin_left_32 +.side_set 2 + +; Master-mode I2S RX, 32 bits per channel, left-justified. + ; /--- LRCLK + ; |/-- BCLK + ; || + set y 30 side 0b00 +bitloop1: + in pins 1 side 0b11 [2] ; Right channel first + jmp y-- bitloop1 side 0b10 [2] + in pins 1 side 0b11 [2] + set y 30 side 0b10 [2] +bitloop0: + in pins 1 side 0b01 [2] ; Then left channel + jmp y-- bitloop0 side 0b00 [2] + in pins 1 side 0b01 [2] diff --git a/ports/raspberrypi/common-hal/audio_i2sin/i2sin_swap.pio b/ports/raspberrypi/common-hal/audio_i2sin/i2sin_swap.pio new file mode 100644 index 0000000000000..cbf3905bdbcc3 --- /dev/null +++ b/ports/raspberrypi/common-hal/audio_i2sin/i2sin_swap.pio @@ -0,0 +1,24 @@ +; 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 + +.program i2sin_swap +.side_set 2 + +; Master-mode I2S RX with the LRCLK and BCLK pin order swapped (BCLK is the +; higher-numbered GPIO). + ; /--- BCLK + ; |/-- LRCLK + ; || + set y 14 side 0b01 +bitloop1: + in pins 1 side 0b11 [2] ; Right channel first + jmp y-- bitloop1 side 0b01 [2] + in pins 1 side 0b10 [2] + set y 14 side 0b00 [2] +bitloop0: + in pins 1 side 0b10 [2] ; Then left channel + jmp y-- bitloop0 side 0b00 [2] + in pins 1 side 0b11 [2] diff --git a/ports/raspberrypi/common-hal/audio_i2sin/i2sin_swap_32.pio b/ports/raspberrypi/common-hal/audio_i2sin/i2sin_swap_32.pio new file mode 100644 index 0000000000000..4e00a5a8e545d --- /dev/null +++ b/ports/raspberrypi/common-hal/audio_i2sin/i2sin_swap_32.pio @@ -0,0 +1,24 @@ +; 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 + +.program i2sin_swap_32 +.side_set 2 + +; Master-mode I2S RX, 32 bits per channel, with the LRCLK and BCLK pin order +; swapped (BCLK is the higher-numbered GPIO). + ; /--- BCLK + ; |/-- LRCLK + ; || + set y 30 side 0b01 +bitloop1: + in pins 1 side 0b11 [2] ; Right channel first + jmp y-- bitloop1 side 0b01 [2] + in pins 1 side 0b10 [2] + set y 30 side 0b00 [2] +bitloop0: + in pins 1 side 0b10 [2] ; Then left channel + jmp y-- bitloop0 side 0b00 [2] + in pins 1 side 0b11 [2] diff --git a/ports/raspberrypi/common-hal/audio_i2sin/i2sin_swap_left.pio b/ports/raspberrypi/common-hal/audio_i2sin/i2sin_swap_left.pio new file mode 100644 index 0000000000000..90ae7ea8d0e8e --- /dev/null +++ b/ports/raspberrypi/common-hal/audio_i2sin/i2sin_swap_left.pio @@ -0,0 +1,24 @@ +; 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 + +.program i2sin_swap_left +.side_set 2 + +; Master-mode I2S RX, left-justified, with the LRCLK and BCLK pin order +; swapped (BCLK is the higher-numbered GPIO). + ; /--- BCLK + ; |/-- LRCLK + ; || + set y 14 side 0b00 +bitloop1: + in pins 1 side 0b11 [2] ; Right channel first + jmp y-- bitloop1 side 0b01 [2] + in pins 1 side 0b11 [2] + set y 14 side 0b01 [2] +bitloop0: + in pins 1 side 0b10 [2] ; Then left channel + jmp y-- bitloop0 side 0b00 [2] + in pins 1 side 0b10 [2] diff --git a/ports/raspberrypi/common-hal/audio_i2sin/i2sin_swap_left_32.pio b/ports/raspberrypi/common-hal/audio_i2sin/i2sin_swap_left_32.pio new file mode 100644 index 0000000000000..49e0ec7dccf43 --- /dev/null +++ b/ports/raspberrypi/common-hal/audio_i2sin/i2sin_swap_left_32.pio @@ -0,0 +1,24 @@ +; 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 + +.program i2sin_swap_left_32 +.side_set 2 + +; Master-mode I2S RX, 32 bits per channel, left-justified, with the LRCLK and +; BCLK pin order swapped (BCLK is the higher-numbered GPIO). + ; /--- BCLK + ; |/-- LRCLK + ; || + set y 30 side 0b00 +bitloop1: + in pins 1 side 0b11 [2] ; Right channel first + jmp y-- bitloop1 side 0b01 [2] + in pins 1 side 0b11 [2] + set y 30 side 0b01 [2] +bitloop0: + in pins 1 side 0b10 [2] ; Then left channel + jmp y-- bitloop0 side 0b00 [2] + in pins 1 side 0b10 [2] diff --git a/ports/raspberrypi/common-hal/rp2pio/StateMachine.c b/ports/raspberrypi/common-hal/rp2pio/StateMachine.c index 71050677f225f..af6e98bbc85cc 100644 --- a/ports/raspberrypi/common-hal/rp2pio/StateMachine.c +++ b/ports/raspberrypi/common-hal/rp2pio/StateMachine.c @@ -1482,6 +1482,36 @@ bool common_hal_rp2pio_statemachine_stop_background_read(rp2pio_statemachine_obj return true; } +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) { + memset(&self->once_read_buf_info, 0, sizeof(self->once_read_buf_info)); + memset(&self->loop_read_buf_info, 0, sizeof(self->loop_read_buf_info)); + memset(&self->loop2_read_buf_info, 0, sizeof(self->loop2_read_buf_info)); + if (once && once_len) { + self->once_read_buf_info.info.buf = once; + self->once_read_buf_info.info.len = once_len; + } + if (loop && loop_len) { + self->loop_read_buf_info.info.buf = loop; + self->loop_read_buf_info.info.len = loop_len; + } + if (loop2 && loop2_len) { + self->loop2_read_buf_info.info.buf = loop2; + self->loop2_read_buf_info.info.len = loop2_len; + } +} + +int common_hal_rp2pio_statemachine_get_read_dma_channel(rp2pio_statemachine_obj_t *self) { + uint8_t pio_index = pio_get_index(self->pio); + uint8_t sm = self->state_machine; + if (!SM_DMA_ALLOCATED_READ(pio_index, sm)) { + return -1; + } + return SM_DMA_GET_CHANNEL_READ(pio_index, sm); +} + bool common_hal_rp2pio_statemachine_get_reading(rp2pio_statemachine_obj_t *self) { return !self->dma_completed_read; } diff --git a/ports/raspberrypi/mpconfigport.mk b/ports/raspberrypi/mpconfigport.mk index 90be1afd10922..dd93e21648168 100644 --- a/ports/raspberrypi/mpconfigport.mk +++ b/ports/raspberrypi/mpconfigport.mk @@ -44,6 +44,7 @@ CIRCUITPY_ANALOGBUFIO = 1 # Audio via PWM CIRCUITPY_AUDIOIO = 0 CIRCUITPY_AUDIOBUSIO ?= 1 +CIRCUITPY_AUDIO_I2SIN ?= $(CIRCUITPY_AUDIOBUSIO) CIRCUITPY_AUDIOCORE ?= 1 CIRCUITPY_AUDIOPWMIO ?= 1 diff --git a/py/circuitpy_defns.mk b/py/circuitpy_defns.mk index e0ad8fa81bc10..48fa62bf3c71a 100644 --- a/py/circuitpy_defns.mk +++ b/py/circuitpy_defns.mk @@ -122,6 +122,9 @@ endif ifeq ($(CIRCUITPY_AUDIOBUSIO),1) SRC_PATTERNS += audiobusio/% endif +ifeq ($(CIRCUITPY_AUDIO_I2SIN),1) +SRC_PATTERNS += audio_i2sin/% +endif ifeq ($(CIRCUITPY_AUDIOIO),1) SRC_PATTERNS += audioio/% endif @@ -505,6 +508,8 @@ SRC_COMMON_HAL_ALL = \ audiobusio/I2SOut.c \ audiobusio/PDMIn.c \ audiobusio/__init__.c \ + audio_i2sin/I2SIn.c \ + audio_i2sin/__init__.c \ audioio/AudioOut.c \ audioio/__init__.c \ audiopwmio/PWMAudioOut.c \ diff --git a/py/circuitpy_mpconfig.mk b/py/circuitpy_mpconfig.mk index 170122903286b..a5ccd9273b187 100644 --- a/py/circuitpy_mpconfig.mk +++ b/py/circuitpy_mpconfig.mk @@ -135,6 +135,10 @@ CFLAGS += -DCIRCUITPY_AUDIOBUSIO_I2SOUT=$(CIRCUITPY_AUDIOBUSIO_I2SOUT) CIRCUITPY_AUDIOBUSIO_PDMIN ?= $(CIRCUITPY_AUDIOBUSIO) CFLAGS += -DCIRCUITPY_AUDIOBUSIO_PDMIN=$(CIRCUITPY_AUDIOBUSIO_PDMIN) +# I2S audio input (separate core module `audio_i2sin`). +CIRCUITPY_AUDIO_I2SIN ?= 0 +CFLAGS += -DCIRCUITPY_AUDIO_I2SIN=$(CIRCUITPY_AUDIO_I2SIN) + CIRCUITPY_AUDIOIO ?= $(CIRCUITPY_FULL_BUILD) CFLAGS += -DCIRCUITPY_AUDIOIO=$(CIRCUITPY_AUDIOIO) diff --git a/shared-bindings/audio_i2sin/I2SIn.c b/shared-bindings/audio_i2sin/I2SIn.c new file mode 100644 index 0000000000000..7c43d24bfa10f --- /dev/null +++ b/shared-bindings/audio_i2sin/I2SIn.c @@ -0,0 +1,223 @@ +// 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 + +#include "extmod/vfs_fat.h" +#include "shared/runtime/context_manager_helpers.h" +#include "py/binary.h" +#include "py/mphal.h" +#include "py/objproperty.h" +#include "py/runtime.h" +#include "shared-bindings/microcontroller/Pin.h" +#include "shared-bindings/audio_i2sin/I2SIn.h" +#include "shared-bindings/util.h" + +//| class I2SIn: +//| """Record an input I2S audio stream from an external I2S source such as a MEMS microphone.""" +//| +//| def __init__( +//| self, +//| bit_clock: microcontroller.Pin, +//| word_select: microcontroller.Pin, +//| data: microcontroller.Pin, +//| *, +//| main_clock: Optional[microcontroller.Pin] = None, +//| sample_rate: int = 16000, +//| bit_depth: int = 16, +//| mono: bool = True, +//| left_justified: bool = False, +//| ) -> None: +//| """Create an I2SIn object associated with the given pins. This allows you to +//| record audio signals from an external I2S source (e.g. an I2S MEMS microphone +//| like the SPH0645LM4H or INMP441). +//| +//| The pin signature mirrors `audiobusio.I2SOut` so users can swap classes; +//| recording parameters mirror `audiobusio.PDMIn`. +//| +//| :param ~microcontroller.Pin bit_clock: The bit clock (or serial clock) pin +//| :param ~microcontroller.Pin word_select: The word select (or left/right clock) pin +//| :param ~microcontroller.Pin data: The data input pin +//| :param ~microcontroller.Pin main_clock: The main clock pin. Not all ports support this. +//| :param int sample_rate: Target sample rate of the resulting samples. Check `sample_rate` for actual value. +//| :param int bit_depth: Number of bits per sample. Must be 8, 16, 24, or 32. +//| For 8-bit, pass a ``bytearray`` or ``array.array('B', ...)``; for 16-bit, +//| ``array.array('H', ...)``; for 24- or 32-bit, ``array.array('I', ...)``. +//| Note that 24-bit samples from mics like the SPH0645LM4H / INMP441 are +//| transported in 32-bit slots, so use ``bit_depth=32`` and an ``'I'`` buffer. +//| :param bool mono: True when capturing a single channel of audio, captures two channels otherwise. +//| :param bool left_justified: True when data bits are aligned with the word select clock. False +//| when they are shifted by one to match classic I2S protocol. Set True for mics like the SPH0645LM4H. +//| +//| Example, recording 16-bit mono samples from an INMP441:: +//| +//| import array +//| import audio_i2sin +//| import board +//| +//| buf = array.array("H", [0] * 16000) +//| with audio_i2sin.I2SIn(board.D9, board.D10, board.D11, +//| sample_rate=16000, bit_depth=16) as mic: +//| mic.record(buf, len(buf)) +//| """ +//| ... +//| +static mp_obj_t audio_i2sin_i2sin_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { + #if !CIRCUITPY_AUDIO_I2SIN + mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_I2SIn); + return NULL; // Not reachable. + #else + enum { ARG_bit_clock, ARG_word_select, ARG_data, ARG_main_clock, + ARG_sample_rate, ARG_bit_depth, ARG_mono, ARG_left_justified }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_bit_clock, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_word_select, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_data, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_main_clock, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = mp_const_none} }, + { MP_QSTR_sample_rate, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 16000} }, + { MP_QSTR_bit_depth, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 16} }, + { MP_QSTR_mono, MP_ARG_KW_ONLY | MP_ARG_BOOL, {.u_bool = true} }, + { MP_QSTR_left_justified, MP_ARG_KW_ONLY | MP_ARG_BOOL, {.u_bool = false} }, + }; + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + const mcu_pin_obj_t *bit_clock = validate_obj_is_free_pin(args[ARG_bit_clock].u_obj, MP_QSTR_bit_clock); + const mcu_pin_obj_t *word_select = validate_obj_is_free_pin(args[ARG_word_select].u_obj, MP_QSTR_word_select); + const mcu_pin_obj_t *data = validate_obj_is_free_pin(args[ARG_data].u_obj, MP_QSTR_data); + const mcu_pin_obj_t *main_clock = validate_obj_is_free_pin_or_none(args[ARG_main_clock].u_obj, MP_QSTR_main_clock); + + uint32_t sample_rate = args[ARG_sample_rate].u_int; + uint8_t bit_depth = args[ARG_bit_depth].u_int; + if (bit_depth % 8 != 0) { + mp_raise_ValueError_varg(MP_ERROR_TEXT("%q must be multiple of 8."), MP_QSTR_bit_depth); + } + bool mono = args[ARG_mono].u_bool; + bool left_justified = args[ARG_left_justified].u_bool; + + audio_i2sin_i2sin_obj_t *self = mp_obj_malloc_with_finaliser(audio_i2sin_i2sin_obj_t, &audio_i2sin_i2sin_type); + common_hal_audio_i2sin_i2sin_construct(self, bit_clock, word_select, data, main_clock, + sample_rate, bit_depth, mono, left_justified); + + return MP_OBJ_FROM_PTR(self); + #endif +} + +#if CIRCUITPY_AUDIO_I2SIN +//| def deinit(self) -> None: +//| """Deinitialises the I2SIn and releases any hardware resources for reuse.""" +//| ... +//| +static mp_obj_t audio_i2sin_i2sin_deinit(mp_obj_t self_in) { + audio_i2sin_i2sin_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_audio_i2sin_i2sin_deinit(self); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(audio_i2sin_i2sin_deinit_obj, audio_i2sin_i2sin_deinit); + +static void check_for_deinit(audio_i2sin_i2sin_obj_t *self) { + if (common_hal_audio_i2sin_i2sin_deinited(self)) { + raise_deinited_error(); + } +} + +//| def __enter__(self) -> I2SIn: +//| """No-op used by Context Managers.""" +//| ... +//| +// Provided by context manager helper. + +//| def __exit__(self) -> None: +//| """Automatically deinitializes the hardware when exiting a context. See +//| :ref:`lifetime-and-contextmanagers` for more info.""" +//| ... +//| +// Provided by context manager helper. + +//| def record(self, destination: WriteableBuffer, destination_length: int) -> int: +//| """Records destination_length samples to destination. This is blocking. +//| +//| :return: The number of samples recorded. If this is less than ``destination_length``, +//| some samples were missed due to processing time.""" +//| ... +//| +static mp_obj_t audio_i2sin_i2sin_obj_record(mp_obj_t self_obj, mp_obj_t destination, mp_obj_t destination_length) { + audio_i2sin_i2sin_obj_t *self = MP_OBJ_TO_PTR(self_obj); + check_for_deinit(self); + uint32_t length = mp_arg_validate_type_int(destination_length, MP_QSTR_length); + mp_arg_validate_length_min(length, 0, MP_QSTR_length); + + mp_buffer_info_t bufinfo; + if (mp_obj_is_type(destination, &mp_type_fileio)) { + mp_raise_NotImplementedError(MP_ERROR_TEXT("Cannot record to a file")); + } + mp_get_buffer_raise(destination, &bufinfo, MP_BUFFER_WRITE); + if (bufinfo.len / mp_binary_get_size('@', bufinfo.typecode, NULL) < length) { + mp_raise_ValueError(MP_ERROR_TEXT("Destination capacity is smaller than destination_length.")); + } + uint8_t bit_depth = common_hal_audio_i2sin_i2sin_get_bit_depth(self); + if ((bit_depth == 24 || bit_depth == 32) && bufinfo.typecode != 'I') { + mp_raise_ValueError(MP_ERROR_TEXT("destination buffer must be an array of type 'I' for bit_depth = 24 or 32")); + } else if (bit_depth == 16 && bufinfo.typecode != 'H') { + mp_raise_ValueError(MP_ERROR_TEXT("destination buffer must be an array of type 'H' for bit_depth = 16")); + } else if (bit_depth == 8 && bufinfo.typecode != 'B' && bufinfo.typecode != BYTEARRAY_TYPECODE) { + mp_raise_ValueError(MP_ERROR_TEXT("destination buffer must be a bytearray or array of type 'B' for bit_depth = 8")); + } + uint32_t length_written = + common_hal_audio_i2sin_i2sin_record_to_buffer(self, bufinfo.buf, length); + return MP_OBJ_NEW_SMALL_INT(length_written); +} +MP_DEFINE_CONST_FUN_OBJ_3(audio_i2sin_i2sin_record_obj, audio_i2sin_i2sin_obj_record); + +//| sample_rate: int +//| """The actual sample rate of the recording. This may not match the constructed +//| sample rate due to internal clock limitations.""" +//| +static mp_obj_t audio_i2sin_i2sin_obj_get_sample_rate(mp_obj_t self_in) { + audio_i2sin_i2sin_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return MP_OBJ_NEW_SMALL_INT(common_hal_audio_i2sin_i2sin_get_sample_rate(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(audio_i2sin_i2sin_get_sample_rate_obj, audio_i2sin_i2sin_obj_get_sample_rate); + +MP_PROPERTY_GETTER(audio_i2sin_i2sin_sample_rate_obj, + (mp_obj_t)&audio_i2sin_i2sin_get_sample_rate_obj); + +//| bit_depth: int +//| """The actual bit depth of the recording. (read-only)""" +//| +//| +static mp_obj_t audio_i2sin_i2sin_obj_get_bit_depth(mp_obj_t self_in) { + audio_i2sin_i2sin_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return MP_OBJ_NEW_SMALL_INT(common_hal_audio_i2sin_i2sin_get_bit_depth(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(audio_i2sin_i2sin_get_bit_depth_obj, audio_i2sin_i2sin_obj_get_bit_depth); + +MP_PROPERTY_GETTER(audio_i2sin_i2sin_bit_depth_obj, + (mp_obj_t)&audio_i2sin_i2sin_get_bit_depth_obj); + +static const mp_rom_map_elem_t audio_i2sin_i2sin_locals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&audio_i2sin_i2sin_deinit_obj) }, + { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&audio_i2sin_i2sin_deinit_obj) }, + { MP_ROM_QSTR(MP_QSTR___enter__), MP_ROM_PTR(&default___enter___obj) }, + { MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&default___exit___obj) }, + { MP_ROM_QSTR(MP_QSTR_record), MP_ROM_PTR(&audio_i2sin_i2sin_record_obj) }, + { MP_ROM_QSTR(MP_QSTR_sample_rate), MP_ROM_PTR(&audio_i2sin_i2sin_sample_rate_obj) }, + { MP_ROM_QSTR(MP_QSTR_bit_depth), MP_ROM_PTR(&audio_i2sin_i2sin_bit_depth_obj) }, +}; +static MP_DEFINE_CONST_DICT(audio_i2sin_i2sin_locals_dict, audio_i2sin_i2sin_locals_dict_table); +#endif // CIRCUITPY_AUDIO_I2SIN + +MP_DEFINE_CONST_OBJ_TYPE( + audio_i2sin_i2sin_type, + MP_QSTR_I2SIn, + MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS, + make_new, audio_i2sin_i2sin_make_new + #if CIRCUITPY_AUDIO_I2SIN + , locals_dict, &audio_i2sin_i2sin_locals_dict + #endif + ); diff --git a/shared-bindings/audio_i2sin/I2SIn.h b/shared-bindings/audio_i2sin/I2SIn.h new file mode 100644 index 0000000000000..183b646ecd8b8 --- /dev/null +++ b/shared-bindings/audio_i2sin/I2SIn.h @@ -0,0 +1,28 @@ +// 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 "shared-bindings/microcontroller/Pin.h" + +#if CIRCUITPY_AUDIO_I2SIN +#include "common-hal/audio_i2sin/I2SIn.h" +#endif + +extern const mp_obj_type_t audio_i2sin_i2sin_type; + +#if CIRCUITPY_AUDIO_I2SIN +void common_hal_audio_i2sin_i2sin_construct(audio_i2sin_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); +void common_hal_audio_i2sin_i2sin_deinit(audio_i2sin_i2sin_obj_t *self); +bool common_hal_audio_i2sin_i2sin_deinited(audio_i2sin_i2sin_obj_t *self); +uint32_t common_hal_audio_i2sin_i2sin_record_to_buffer(audio_i2sin_i2sin_obj_t *self, + void *buffer, uint32_t length); +uint8_t common_hal_audio_i2sin_i2sin_get_bit_depth(audio_i2sin_i2sin_obj_t *self); +uint32_t common_hal_audio_i2sin_i2sin_get_sample_rate(audio_i2sin_i2sin_obj_t *self); +#endif diff --git a/shared-bindings/audio_i2sin/__init__.c b/shared-bindings/audio_i2sin/__init__.c new file mode 100644 index 0000000000000..ba163ae75f4f9 --- /dev/null +++ b/shared-bindings/audio_i2sin/__init__.c @@ -0,0 +1,38 @@ +// 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 + +#include "py/obj.h" +#include "py/runtime.h" + +#include "shared-bindings/microcontroller/Pin.h" +#include "shared-bindings/audio_i2sin/__init__.h" +#include "shared-bindings/audio_i2sin/I2SIn.h" + +//| """Support for recording audio from an I2S input source. +//| +//| The `audio_i2sin` module contains the `I2SIn` class for recording audio +//| from an external I2S source such as a MEMS microphone (e.g. SPH0645LM4H +//| or INMP441). +//| +//| All classes change hardware state and should be deinitialized when they +//| are no longer needed. To do so, either call :py:meth:`!deinit` or use a +//| context manager.""" + +static const mp_rom_map_elem_t audio_i2sin_module_globals_table[] = { + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_audio_i2sin) }, + { MP_ROM_QSTR(MP_QSTR_I2SIn), MP_ROM_PTR(&audio_i2sin_i2sin_type) }, +}; + +static MP_DEFINE_CONST_DICT(audio_i2sin_module_globals, audio_i2sin_module_globals_table); + +const mp_obj_module_t audio_i2sin_module = { + .base = { &mp_type_module }, + .globals = (mp_obj_dict_t *)&audio_i2sin_module_globals, +}; + +MP_REGISTER_MODULE(MP_QSTR_audio_i2sin, audio_i2sin_module); diff --git a/shared-bindings/audio_i2sin/__init__.h b/shared-bindings/audio_i2sin/__init__.h new file mode 100644 index 0000000000000..779b49ffd8db3 --- /dev/null +++ b/shared-bindings/audio_i2sin/__init__.h @@ -0,0 +1,7 @@ +// 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 diff --git a/tests/circuitpython-manual/audio_i2sin/i2sin_neopixel_reactive.py b/tests/circuitpython-manual/audio_i2sin/i2sin_neopixel_reactive.py new file mode 100644 index 0000000000000..58a100547f2d6 --- /dev/null +++ b/tests/circuitpython-manual/audio_i2sin/i2sin_neopixel_reactive.py @@ -0,0 +1,109 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +# Audio-reactive NeoPixel effect driven by an I2S MEMS microphone. +# +# Wiring (defaults assume an INMP441 / SPH0645 style mic): +# mic BCLK -> board.D5 +# mic LRCL -> board.D6 +# mic DOUT -> board.D9 +# neopixel -> board.D10 +# +# The mic's 24-bit samples ride in 32-bit slots, so we use bit_depth=32 and +# an array.array("I", ...). For SPH0645 set left_justified=True. + +import array +import math +import time + +import board +import audio_i2sin +import neopixel + +NUM_PIXELS = 30 +PIXEL_PIN = board.D10 + +SAMPLE_RATE = 16000 +SAMPLES_PER_FRAME = 512 # ~32 ms windows + +pixels = neopixel.NeoPixel(PIXEL_PIN, NUM_PIXELS, brightness=0.3, auto_write=False) + +mic = audio_i2sin.I2SIn( + bit_clock=board.D5, + word_select=board.D6, + data=board.D9, + sample_rate=SAMPLE_RATE, + bit_depth=32, + mono=True, + left_justified=False, # set True for SPH0645LM4H +) + +buf = array.array("I", [0] * SAMPLES_PER_FRAME) + + +def to_signed24(u32): + # The mic packs a 24-bit signed sample left-justified in 32 bits. + s = u32 >> 8 + if s & 0x800000: + s -= 0x1000000 + return s + + +def wheel(pos): + pos = pos % 256 + if pos < 85: + return (pos * 3, 255 - pos * 3, 0) + if pos < 170: + pos -= 85 + return (255 - pos * 3, 0, pos * 3) + pos -= 170 + return (0, pos * 3, 255 - pos * 3) + + +# Smoothed noise floor + peak so the effect adapts to the room. +noise_floor = 2000.0 +peak = 20000.0 +hue = 0 +smoothed_level = 0.0 + +while True: + mic.record(buf, len(buf)) + + # Compute RMS of the window. + acc = 0 + for raw in buf: + s = to_signed24(raw) + acc += s * s + rms = math.sqrt(acc / len(buf)) + + # Track a slow noise floor and a decaying peak for auto-gain. + noise_floor = 0.995 * noise_floor + 0.005 * rms + if rms > peak: + peak = rms + else: + peak *= 0.995 + if peak < noise_floor + 1000: + peak = noise_floor + 1000 + + level = (rms - noise_floor) / (peak - noise_floor) + if level < 0: + level = 0.0 + elif level > 1: + level = 1.0 + + # Smooth the bar so it doesn't jitter on every frame. + smoothed_level = 0.6 * smoothed_level + 0.4 * level + + lit = int(smoothed_level * NUM_PIXELS) + hue = (hue + 2) % 256 + + for i in range(NUM_PIXELS): + if i < lit: + r, g, b = wheel((hue + i * (256 // NUM_PIXELS)) % 256) + pixels[i] = (r, g, b) + else: + pixels[i] = (0, 0, 0) + pixels.show() + + time.sleep(0.005) diff --git a/tests/circuitpython-manual/audio_i2sin/i2sin_record_sdcard.py b/tests/circuitpython-manual/audio_i2sin/i2sin_record_sdcard.py new file mode 100644 index 0000000000000..bdb77066f7010 --- /dev/null +++ b/tests/circuitpython-manual/audio_i2sin/i2sin_record_sdcard.py @@ -0,0 +1,103 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +# Record a longer I2S audio capture to a WAV file on an SD card. +# +# Produces /sd/recording.wav. Set left_justified=True for SPH0645LM4H mics. + +import array +import struct +import time + +import ulab.numpy as np +import board +import busio +import sdcardio +import storage +import audio_i2sin + +# ---- Recording config ------------------------------------------------------ +SAMPLE_RATE = 16000 +RECORD_SECONDS = 10 +CHUNK_SAMPLES = 1024 # samples captured per record() call +OUTPUT_PATH = "/sd/talk.wav" + +# ---- Mount SD -------------------------------------------------------------- +spi = board.SPI() +sdcard = sdcardio.SDCard(spi, cs=board.D10, baudrate=24_000_000) +vfs = storage.VfsFat(sdcard) +storage.mount(vfs, "/sd") + +# ---- Mic ------------------------------------------------------------------- +# 24-bit MEMS mics ride in 32-bit slots. Downconvert each slot to a +# signed 16-bit PCM sample before writing. +mic = audio_i2sin.I2SIn( + bit_clock=board.D5, + word_select=board.D6, + data=board.D9, + sample_rate=SAMPLE_RATE, + bit_depth=32, + mono=True, + left_justified=False, # True for SPH0645LM4H +) + +actual_rate = mic.sample_rate +print("Recording at", actual_rate, "Hz for", RECORD_SECONDS, "s ->", OUTPUT_PATH) + +raw = array.array("I", [0] * CHUNK_SAMPLES) +pcm16 = array.array("h", [0] * CHUNK_SAMPLES) + + +def write_wav_header(f, sample_rate, num_samples, bits_per_sample=16, channels=1): + byte_rate = sample_rate * channels * bits_per_sample // 8 + block_align = channels * bits_per_sample // 8 + data_size = num_samples * block_align + f.write(b"RIFF") + f.write(struct.pack("> 16 # take top 16 bits + if s & 0x8000: + s -= 0x10000 # sign-extend + pcm16[i] = s + # Write only the valid portion. + f.write(memoryview(pcm16)[:n]) + written += n + + elapsed = time.monotonic() - start + # Rewrite header now that we know the true sample count. + f.seek(0) + write_wav_header(f, actual_rate, written) + +storage.umount("/sd") + +print("Done. Wrote", written, "samples in", round(elapsed, 2), "s") From da091385848d2ddef462c670569684268e5e6dcd Mon Sep 17 00:00:00 2001 From: foamyguy Date: Fri, 8 May 2026 15:19:00 -0500 Subject: [PATCH 2/9] format sdcard test --- .../audio_i2sin/i2sin_record_sdcard.py | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/tests/circuitpython-manual/audio_i2sin/i2sin_record_sdcard.py b/tests/circuitpython-manual/audio_i2sin/i2sin_record_sdcard.py index bdb77066f7010..3045cac9d5082 100644 --- a/tests/circuitpython-manual/audio_i2sin/i2sin_record_sdcard.py +++ b/tests/circuitpython-manual/audio_i2sin/i2sin_record_sdcard.py @@ -57,14 +57,18 @@ def write_wav_header(f, sample_rate, num_samples, bits_per_sample=16, channels=1 f.write(struct.pack("> 16 # take top 16 bits + s = v >> 16 # take top 16 bits if s & 0x8000: - s -= 0x10000 # sign-extend + s -= 0x10000 # sign-extend pcm16[i] = s # Write only the valid portion. f.write(memoryview(pcm16)[:n]) From 599ef3f84ad7a3eb4005a791ec6f35f1277f2e4f Mon Sep 17 00:00:00 2001 From: foamyguy Date: Tue, 12 May 2026 14:15:54 -0500 Subject: [PATCH 3/9] refactor to audioi2sin --- .../{audio_i2sin => audioi2sin}/I2SIn.c | 22 ++--- .../{audio_i2sin => audioi2sin}/I2SIn.h | 4 +- .../{audio_i2sin => audioi2sin}/__init__.c | 0 ports/espressif/mpconfigport.mk | 2 +- .../{audio_i2sin => audioi2sin}/I2SIn.c | 26 +++--- .../{audio_i2sin => audioi2sin}/I2SIn.h | 2 +- .../{audio_i2sin => audioi2sin}/README.pio | 0 .../{audio_i2sin => audioi2sin}/__init__.c | 0 .../{audio_i2sin => audioi2sin}/i2sin.pio | 0 .../{audio_i2sin => audioi2sin}/i2sin_32.pio | 0 .../i2sin_left.pio | 0 .../i2sin_left_32.pio | 0 .../i2sin_swap.pio | 0 .../i2sin_swap_32.pio | 0 .../i2sin_swap_left.pio | 0 .../i2sin_swap_left_32.pio | 0 ports/raspberrypi/mpconfigport.mk | 2 +- py/circuitpy_defns.mk | 8 +- py/circuitpy_mpconfig.mk | 6 +- shared-bindings/audio_i2sin/I2SIn.h | 28 ------ .../{audio_i2sin => audioi2sin}/I2SIn.c | 86 +++++++++---------- shared-bindings/audioi2sin/I2SIn.h | 28 ++++++ .../{audio_i2sin => audioi2sin}/__init__.c | 20 ++--- .../{audio_i2sin => audioi2sin}/__init__.h | 0 .../i2sin_neopixel_reactive.py | 4 +- .../i2sin_record_sdcard.py | 4 +- 26 files changed, 121 insertions(+), 121 deletions(-) rename ports/espressif/common-hal/{audio_i2sin => audioi2sin}/I2SIn.c (87%) rename ports/espressif/common-hal/{audio_i2sin => audioi2sin}/I2SIn.h (91%) rename ports/espressif/common-hal/{audio_i2sin => audioi2sin}/__init__.c (100%) rename ports/raspberrypi/common-hal/{audio_i2sin => audioi2sin}/I2SIn.c (96%) rename ports/raspberrypi/common-hal/{audio_i2sin => audioi2sin}/I2SIn.h (96%) rename ports/raspberrypi/common-hal/{audio_i2sin => audioi2sin}/README.pio (100%) rename ports/raspberrypi/common-hal/{audio_i2sin => audioi2sin}/__init__.c (100%) rename ports/raspberrypi/common-hal/{audio_i2sin => audioi2sin}/i2sin.pio (100%) rename ports/raspberrypi/common-hal/{audio_i2sin => audioi2sin}/i2sin_32.pio (100%) rename ports/raspberrypi/common-hal/{audio_i2sin => audioi2sin}/i2sin_left.pio (100%) rename ports/raspberrypi/common-hal/{audio_i2sin => audioi2sin}/i2sin_left_32.pio (100%) rename ports/raspberrypi/common-hal/{audio_i2sin => audioi2sin}/i2sin_swap.pio (100%) rename ports/raspberrypi/common-hal/{audio_i2sin => audioi2sin}/i2sin_swap_32.pio (100%) rename ports/raspberrypi/common-hal/{audio_i2sin => audioi2sin}/i2sin_swap_left.pio (100%) rename ports/raspberrypi/common-hal/{audio_i2sin => audioi2sin}/i2sin_swap_left_32.pio (100%) delete mode 100644 shared-bindings/audio_i2sin/I2SIn.h rename shared-bindings/{audio_i2sin => audioi2sin}/I2SIn.c (72%) create mode 100644 shared-bindings/audioi2sin/I2SIn.h rename shared-bindings/{audio_i2sin => audioi2sin}/__init__.c (51%) rename shared-bindings/{audio_i2sin => audioi2sin}/__init__.h (100%) rename tests/circuitpython-manual/{audio_i2sin => audioi2sin}/i2sin_neopixel_reactive.py (98%) rename tests/circuitpython-manual/{audio_i2sin => audioi2sin}/i2sin_record_sdcard.py (98%) diff --git a/ports/espressif/common-hal/audio_i2sin/I2SIn.c b/ports/espressif/common-hal/audioi2sin/I2SIn.c similarity index 87% rename from ports/espressif/common-hal/audio_i2sin/I2SIn.c rename to ports/espressif/common-hal/audioi2sin/I2SIn.c index 4362351c68deb..67395087d1603 100644 --- a/ports/espressif/common-hal/audio_i2sin/I2SIn.c +++ b/ports/espressif/common-hal/audioi2sin/I2SIn.c @@ -8,16 +8,16 @@ #include "bindings/espidf/__init__.h" -#include "common-hal/audio_i2sin/I2SIn.h" +#include "common-hal/audioi2sin/I2SIn.h" #include "py/runtime.h" -#include "shared-bindings/audio_i2sin/I2SIn.h" +#include "shared-bindings/audioi2sin/I2SIn.h" #include "shared-bindings/microcontroller/Pin.h" #include "driver/i2s_std.h" -#if CIRCUITPY_AUDIO_I2SIN +#if CIRCUITPY_AUDIOI2SIN -void common_hal_audio_i2sin_i2sin_construct(audio_i2sin_i2sin_obj_t *self, +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) { @@ -75,12 +75,12 @@ void common_hal_audio_i2sin_i2sin_construct(audio_i2sin_i2sin_obj_t *self, } } -bool common_hal_audio_i2sin_i2sin_deinited(audio_i2sin_i2sin_obj_t *self) { +bool common_hal_audioi2sin_i2sin_deinited(audioi2sin_i2sin_obj_t *self) { return self->data == NULL; } -void common_hal_audio_i2sin_i2sin_deinit(audio_i2sin_i2sin_obj_t *self) { - if (common_hal_audio_i2sin_i2sin_deinited(self)) { +void common_hal_audioi2sin_i2sin_deinit(audioi2sin_i2sin_obj_t *self) { + if (common_hal_audioi2sin_i2sin_deinited(self)) { return; } @@ -111,7 +111,7 @@ void common_hal_audio_i2sin_i2sin_deinit(audio_i2sin_i2sin_obj_t *self) { self->mclk = NULL; } -uint32_t common_hal_audio_i2sin_i2sin_record_to_buffer(audio_i2sin_i2sin_obj_t *self, +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. @@ -158,12 +158,12 @@ uint32_t common_hal_audio_i2sin_i2sin_record_to_buffer(audio_i2sin_i2sin_obj_t * return produced; } -uint8_t common_hal_audio_i2sin_i2sin_get_bit_depth(audio_i2sin_i2sin_obj_t *self) { +uint8_t common_hal_audioi2sin_i2sin_get_bit_depth(audioi2sin_i2sin_obj_t *self) { return self->bit_depth; } -uint32_t common_hal_audio_i2sin_i2sin_get_sample_rate(audio_i2sin_i2sin_obj_t *self) { +uint32_t common_hal_audioi2sin_i2sin_get_sample_rate(audioi2sin_i2sin_obj_t *self) { return self->sample_rate; } -#endif // CIRCUITPY_AUDIO_I2SIN +#endif // CIRCUITPY_AUDIOI2SIN diff --git a/ports/espressif/common-hal/audio_i2sin/I2SIn.h b/ports/espressif/common-hal/audioi2sin/I2SIn.h similarity index 91% rename from ports/espressif/common-hal/audio_i2sin/I2SIn.h rename to ports/espressif/common-hal/audioi2sin/I2SIn.h index 917b72c954b74..e16919b4cbb07 100644 --- a/ports/espressif/common-hal/audio_i2sin/I2SIn.h +++ b/ports/espressif/common-hal/audioi2sin/I2SIn.h @@ -12,7 +12,7 @@ #include "driver/i2s_std.h" -#if CIRCUITPY_AUDIO_I2SIN +#if CIRCUITPY_AUDIOI2SIN typedef struct { mp_obj_base_t base; @@ -24,6 +24,6 @@ typedef struct { uint32_t sample_rate; uint8_t bit_depth; bool mono; -} audio_i2sin_i2sin_obj_t; +} audioi2sin_i2sin_obj_t; #endif diff --git a/ports/espressif/common-hal/audio_i2sin/__init__.c b/ports/espressif/common-hal/audioi2sin/__init__.c similarity index 100% rename from ports/espressif/common-hal/audio_i2sin/__init__.c rename to ports/espressif/common-hal/audioi2sin/__init__.c diff --git a/ports/espressif/mpconfigport.mk b/ports/espressif/mpconfigport.mk index e52d47d91139a..8396b89ee09db 100644 --- a/ports/espressif/mpconfigport.mk +++ b/ports/espressif/mpconfigport.mk @@ -68,7 +68,7 @@ CIRCUITPY_ALARM_TOUCH ?= 0 CIRCUITPY_ANALOGBUFIO ?= 1 CIRCUITPY_AUDIOBUSIO ?= 1 CIRCUITPY_AUDIOBUSIO_PDMIN ?= 0 -CIRCUITPY_AUDIO_I2SIN ?= 1 +CIRCUITPY_AUDIOI2SIN ?= 1 CIRCUITPY_AUDIOIO ?= 1 CIRCUITPY_BLEIO_HCI = 0 CIRCUITPY_BLEIO_NATIVE ?= 1 diff --git a/ports/raspberrypi/common-hal/audio_i2sin/I2SIn.c b/ports/raspberrypi/common-hal/audioi2sin/I2SIn.c similarity index 96% rename from ports/raspberrypi/common-hal/audio_i2sin/I2SIn.c rename to ports/raspberrypi/common-hal/audioi2sin/I2SIn.c index 54d66df88c472..f64bd299bda00 100644 --- a/ports/raspberrypi/common-hal/audio_i2sin/I2SIn.c +++ b/ports/raspberrypi/common-hal/audioi2sin/I2SIn.c @@ -11,15 +11,15 @@ #include "py/mphal.h" #include "py/runtime.h" #include "shared/runtime/interrupt_char.h" -#include "common-hal/audio_i2sin/I2SIn.h" -#include "shared-bindings/audio_i2sin/I2SIn.h" +#include "common-hal/audioi2sin/I2SIn.h" +#include "shared-bindings/audioi2sin/I2SIn.h" #include "shared-bindings/microcontroller/Pin.h" #include "bindings/rp2pio/StateMachine.h" #include "supervisor/port.h" #include "hardware/dma.h" -#if CIRCUITPY_AUDIO_I2SIN +#if CIRCUITPY_AUDIOI2SIN // 16-bit programs sample a stereo frame of 16+16 = 32 bits and rely on // auto-push at 32 to deliver one packed (right<<16 | left) word per frame. @@ -148,7 +148,7 @@ static const uint16_t i2sin_program_left_justified_swap_32[] = { }; // Caller validates that pins are free. -void common_hal_audio_i2sin_i2sin_construct(audio_i2sin_i2sin_obj_t *self, +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) { @@ -239,7 +239,7 @@ void common_hal_audio_i2sin_i2sin_construct(audio_i2sin_i2sin_obj_t *self, // 8 bytes; size the half-buffer for ~40 ms of audio so a slow consumer // (SD card flush etc.) can complete without overrunning. size_t bytes_per_frame = (bit_depth == 16) ? 4 : 8; - size_t target = (size_t)self->sample_rate * bytes_per_frame * 2 / 2; + size_t target = (size_t)self->sample_rate * bytes_per_frame * 40 / 1000; size_t half_size = (target > 4096) ? target : 4096; // Round up to a multiple of 8 so 24/32-bit pair reads never straddle // the half boundary. @@ -266,12 +266,12 @@ void common_hal_audio_i2sin_i2sin_construct(audio_i2sin_i2sin_obj_t *self, self->dma_channel = common_hal_rp2pio_statemachine_get_read_dma_channel(&self->state_machine); } -bool common_hal_audio_i2sin_i2sin_deinited(audio_i2sin_i2sin_obj_t *self) { +bool common_hal_audioi2sin_i2sin_deinited(audioi2sin_i2sin_obj_t *self) { return common_hal_rp2pio_statemachine_deinited(&self->state_machine); } -void common_hal_audio_i2sin_i2sin_deinit(audio_i2sin_i2sin_obj_t *self) { - if (common_hal_audio_i2sin_i2sin_deinited(self)) { +void common_hal_audioi2sin_i2sin_deinit(audioi2sin_i2sin_obj_t *self) { + if (common_hal_audioi2sin_i2sin_deinited(self)) { return; } common_hal_rp2pio_statemachine_stop_background_read(&self->state_machine); @@ -285,11 +285,11 @@ void common_hal_audio_i2sin_i2sin_deinit(audio_i2sin_i2sin_obj_t *self) { self->dma_channel = -1; } -uint8_t common_hal_audio_i2sin_i2sin_get_bit_depth(audio_i2sin_i2sin_obj_t *self) { +uint8_t common_hal_audioi2sin_i2sin_get_bit_depth(audioi2sin_i2sin_obj_t *self) { return self->bit_depth; } -uint32_t common_hal_audio_i2sin_i2sin_get_sample_rate(audio_i2sin_i2sin_obj_t *self) { +uint32_t common_hal_audioi2sin_i2sin_get_sample_rate(audioi2sin_i2sin_obj_t *self) { return self->sample_rate; } @@ -304,7 +304,7 @@ uint32_t common_hal_audio_i2sin_i2sin_get_sample_rate(audio_i2sin_i2sin_obj_t *s // the number actually written. // Compute the byte offset inside `ring` that the DMA is currently writing // (one past the last word it finished). Always lies in [0, ring_size). -static size_t i2sin_write_pos(audio_i2sin_i2sin_obj_t *self) { +static size_t i2sin_write_pos(audioi2sin_i2sin_obj_t *self) { uintptr_t addr = (uintptr_t)dma_channel_hw_addr(self->dma_channel)->write_addr; uintptr_t base = (uintptr_t)self->ring; if (addr < base || addr >= base + self->ring_size) { @@ -316,7 +316,7 @@ static size_t i2sin_write_pos(audio_i2sin_i2sin_obj_t *self) { return (size_t)(addr - base); } -uint32_t common_hal_audio_i2sin_i2sin_record_to_buffer(audio_i2sin_i2sin_obj_t *self, +uint32_t common_hal_audioi2sin_i2sin_record_to_buffer(audioi2sin_i2sin_obj_t *self, void *buffer, uint32_t output_buffer_length) { uint32_t output_count = 0; const size_t ring_size = self->ring_size; @@ -433,4 +433,4 @@ uint32_t common_hal_audio_i2sin_i2sin_record_to_buffer(audio_i2sin_i2sin_obj_t * return output_count; } -#endif // CIRCUITPY_AUDIO_I2SIN +#endif // CIRCUITPY_AUDIOI2SIN diff --git a/ports/raspberrypi/common-hal/audio_i2sin/I2SIn.h b/ports/raspberrypi/common-hal/audioi2sin/I2SIn.h similarity index 96% rename from ports/raspberrypi/common-hal/audio_i2sin/I2SIn.h rename to ports/raspberrypi/common-hal/audioi2sin/I2SIn.h index 9f825c79f72ca..06ae2ca774c4e 100644 --- a/ports/raspberrypi/common-hal/audio_i2sin/I2SIn.h +++ b/ports/raspberrypi/common-hal/audioi2sin/I2SIn.h @@ -26,4 +26,4 @@ typedef struct { size_t read_pos; int dma_channel; bool overflow; -} audio_i2sin_i2sin_obj_t; +} audioi2sin_i2sin_obj_t; diff --git a/ports/raspberrypi/common-hal/audio_i2sin/README.pio b/ports/raspberrypi/common-hal/audioi2sin/README.pio similarity index 100% rename from ports/raspberrypi/common-hal/audio_i2sin/README.pio rename to ports/raspberrypi/common-hal/audioi2sin/README.pio diff --git a/ports/raspberrypi/common-hal/audio_i2sin/__init__.c b/ports/raspberrypi/common-hal/audioi2sin/__init__.c similarity index 100% rename from ports/raspberrypi/common-hal/audio_i2sin/__init__.c rename to ports/raspberrypi/common-hal/audioi2sin/__init__.c diff --git a/ports/raspberrypi/common-hal/audio_i2sin/i2sin.pio b/ports/raspberrypi/common-hal/audioi2sin/i2sin.pio similarity index 100% rename from ports/raspberrypi/common-hal/audio_i2sin/i2sin.pio rename to ports/raspberrypi/common-hal/audioi2sin/i2sin.pio diff --git a/ports/raspberrypi/common-hal/audio_i2sin/i2sin_32.pio b/ports/raspberrypi/common-hal/audioi2sin/i2sin_32.pio similarity index 100% rename from ports/raspberrypi/common-hal/audio_i2sin/i2sin_32.pio rename to ports/raspberrypi/common-hal/audioi2sin/i2sin_32.pio diff --git a/ports/raspberrypi/common-hal/audio_i2sin/i2sin_left.pio b/ports/raspberrypi/common-hal/audioi2sin/i2sin_left.pio similarity index 100% rename from ports/raspberrypi/common-hal/audio_i2sin/i2sin_left.pio rename to ports/raspberrypi/common-hal/audioi2sin/i2sin_left.pio diff --git a/ports/raspberrypi/common-hal/audio_i2sin/i2sin_left_32.pio b/ports/raspberrypi/common-hal/audioi2sin/i2sin_left_32.pio similarity index 100% rename from ports/raspberrypi/common-hal/audio_i2sin/i2sin_left_32.pio rename to ports/raspberrypi/common-hal/audioi2sin/i2sin_left_32.pio diff --git a/ports/raspberrypi/common-hal/audio_i2sin/i2sin_swap.pio b/ports/raspberrypi/common-hal/audioi2sin/i2sin_swap.pio similarity index 100% rename from ports/raspberrypi/common-hal/audio_i2sin/i2sin_swap.pio rename to ports/raspberrypi/common-hal/audioi2sin/i2sin_swap.pio diff --git a/ports/raspberrypi/common-hal/audio_i2sin/i2sin_swap_32.pio b/ports/raspberrypi/common-hal/audioi2sin/i2sin_swap_32.pio similarity index 100% rename from ports/raspberrypi/common-hal/audio_i2sin/i2sin_swap_32.pio rename to ports/raspberrypi/common-hal/audioi2sin/i2sin_swap_32.pio diff --git a/ports/raspberrypi/common-hal/audio_i2sin/i2sin_swap_left.pio b/ports/raspberrypi/common-hal/audioi2sin/i2sin_swap_left.pio similarity index 100% rename from ports/raspberrypi/common-hal/audio_i2sin/i2sin_swap_left.pio rename to ports/raspberrypi/common-hal/audioi2sin/i2sin_swap_left.pio diff --git a/ports/raspberrypi/common-hal/audio_i2sin/i2sin_swap_left_32.pio b/ports/raspberrypi/common-hal/audioi2sin/i2sin_swap_left_32.pio similarity index 100% rename from ports/raspberrypi/common-hal/audio_i2sin/i2sin_swap_left_32.pio rename to ports/raspberrypi/common-hal/audioi2sin/i2sin_swap_left_32.pio diff --git a/ports/raspberrypi/mpconfigport.mk b/ports/raspberrypi/mpconfigport.mk index dd93e21648168..76f468beee65b 100644 --- a/ports/raspberrypi/mpconfigport.mk +++ b/ports/raspberrypi/mpconfigport.mk @@ -44,7 +44,7 @@ CIRCUITPY_ANALOGBUFIO = 1 # Audio via PWM CIRCUITPY_AUDIOIO = 0 CIRCUITPY_AUDIOBUSIO ?= 1 -CIRCUITPY_AUDIO_I2SIN ?= $(CIRCUITPY_AUDIOBUSIO) +CIRCUITPY_AUDIOI2SIN ?= $(CIRCUITPY_AUDIOBUSIO) CIRCUITPY_AUDIOCORE ?= 1 CIRCUITPY_AUDIOPWMIO ?= 1 diff --git a/py/circuitpy_defns.mk b/py/circuitpy_defns.mk index 48fa62bf3c71a..67be45325d565 100644 --- a/py/circuitpy_defns.mk +++ b/py/circuitpy_defns.mk @@ -122,8 +122,8 @@ endif ifeq ($(CIRCUITPY_AUDIOBUSIO),1) SRC_PATTERNS += audiobusio/% endif -ifeq ($(CIRCUITPY_AUDIO_I2SIN),1) -SRC_PATTERNS += audio_i2sin/% +ifeq ($(CIRCUITPY_AUDIOI2SIN),1) +SRC_PATTERNS += audioi2sin/% endif ifeq ($(CIRCUITPY_AUDIOIO),1) SRC_PATTERNS += audioio/% @@ -508,8 +508,8 @@ SRC_COMMON_HAL_ALL = \ audiobusio/I2SOut.c \ audiobusio/PDMIn.c \ audiobusio/__init__.c \ - audio_i2sin/I2SIn.c \ - audio_i2sin/__init__.c \ + audioi2sin/I2SIn.c \ + audioi2sin/__init__.c \ audioio/AudioOut.c \ audioio/__init__.c \ audiopwmio/PWMAudioOut.c \ diff --git a/py/circuitpy_mpconfig.mk b/py/circuitpy_mpconfig.mk index a5ccd9273b187..f56813f3b9174 100644 --- a/py/circuitpy_mpconfig.mk +++ b/py/circuitpy_mpconfig.mk @@ -135,9 +135,9 @@ CFLAGS += -DCIRCUITPY_AUDIOBUSIO_I2SOUT=$(CIRCUITPY_AUDIOBUSIO_I2SOUT) CIRCUITPY_AUDIOBUSIO_PDMIN ?= $(CIRCUITPY_AUDIOBUSIO) CFLAGS += -DCIRCUITPY_AUDIOBUSIO_PDMIN=$(CIRCUITPY_AUDIOBUSIO_PDMIN) -# I2S audio input (separate core module `audio_i2sin`). -CIRCUITPY_AUDIO_I2SIN ?= 0 -CFLAGS += -DCIRCUITPY_AUDIO_I2SIN=$(CIRCUITPY_AUDIO_I2SIN) +# I2S audio input (separate core module `audioi2sin`). +CIRCUITPY_AUDIOI2SIN ?= 0 +CFLAGS += -DCIRCUITPY_AUDIOI2SIN=$(CIRCUITPY_AUDIOI2SIN) CIRCUITPY_AUDIOIO ?= $(CIRCUITPY_FULL_BUILD) CFLAGS += -DCIRCUITPY_AUDIOIO=$(CIRCUITPY_AUDIOIO) diff --git a/shared-bindings/audio_i2sin/I2SIn.h b/shared-bindings/audio_i2sin/I2SIn.h deleted file mode 100644 index 183b646ecd8b8..0000000000000 --- a/shared-bindings/audio_i2sin/I2SIn.h +++ /dev/null @@ -1,28 +0,0 @@ -// 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 "shared-bindings/microcontroller/Pin.h" - -#if CIRCUITPY_AUDIO_I2SIN -#include "common-hal/audio_i2sin/I2SIn.h" -#endif - -extern const mp_obj_type_t audio_i2sin_i2sin_type; - -#if CIRCUITPY_AUDIO_I2SIN -void common_hal_audio_i2sin_i2sin_construct(audio_i2sin_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); -void common_hal_audio_i2sin_i2sin_deinit(audio_i2sin_i2sin_obj_t *self); -bool common_hal_audio_i2sin_i2sin_deinited(audio_i2sin_i2sin_obj_t *self); -uint32_t common_hal_audio_i2sin_i2sin_record_to_buffer(audio_i2sin_i2sin_obj_t *self, - void *buffer, uint32_t length); -uint8_t common_hal_audio_i2sin_i2sin_get_bit_depth(audio_i2sin_i2sin_obj_t *self); -uint32_t common_hal_audio_i2sin_i2sin_get_sample_rate(audio_i2sin_i2sin_obj_t *self); -#endif diff --git a/shared-bindings/audio_i2sin/I2SIn.c b/shared-bindings/audioi2sin/I2SIn.c similarity index 72% rename from shared-bindings/audio_i2sin/I2SIn.c rename to shared-bindings/audioi2sin/I2SIn.c index 7c43d24bfa10f..4c2b221254759 100644 --- a/shared-bindings/audio_i2sin/I2SIn.c +++ b/shared-bindings/audioi2sin/I2SIn.c @@ -13,7 +13,7 @@ #include "py/objproperty.h" #include "py/runtime.h" #include "shared-bindings/microcontroller/Pin.h" -#include "shared-bindings/audio_i2sin/I2SIn.h" +#include "shared-bindings/audioi2sin/I2SIn.h" #include "shared-bindings/util.h" //| class I2SIn: @@ -55,18 +55,18 @@ //| Example, recording 16-bit mono samples from an INMP441:: //| //| import array -//| import audio_i2sin +//| import audioi2sin //| import board //| //| buf = array.array("H", [0] * 16000) -//| with audio_i2sin.I2SIn(board.D9, board.D10, board.D11, +//| with audioi2sin.I2SIn(board.D9, board.D10, board.D11, //| sample_rate=16000, bit_depth=16) as mic: //| mic.record(buf, len(buf)) //| """ //| ... //| -static mp_obj_t audio_i2sin_i2sin_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { - #if !CIRCUITPY_AUDIO_I2SIN +static mp_obj_t audioi2sin_i2sin_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { + #if !CIRCUITPY_AUDIOI2SIN mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_I2SIn); return NULL; // Not reachable. #else @@ -98,28 +98,28 @@ static mp_obj_t audio_i2sin_i2sin_make_new(const mp_obj_type_t *type, size_t n_a bool mono = args[ARG_mono].u_bool; bool left_justified = args[ARG_left_justified].u_bool; - audio_i2sin_i2sin_obj_t *self = mp_obj_malloc_with_finaliser(audio_i2sin_i2sin_obj_t, &audio_i2sin_i2sin_type); - common_hal_audio_i2sin_i2sin_construct(self, bit_clock, word_select, data, main_clock, + audioi2sin_i2sin_obj_t *self = mp_obj_malloc_with_finaliser(audioi2sin_i2sin_obj_t, &audioi2sin_i2sin_type); + common_hal_audioi2sin_i2sin_construct(self, bit_clock, word_select, data, main_clock, sample_rate, bit_depth, mono, left_justified); return MP_OBJ_FROM_PTR(self); #endif } -#if CIRCUITPY_AUDIO_I2SIN +#if CIRCUITPY_AUDIOI2SIN //| def deinit(self) -> None: //| """Deinitialises the I2SIn and releases any hardware resources for reuse.""" //| ... //| -static mp_obj_t audio_i2sin_i2sin_deinit(mp_obj_t self_in) { - audio_i2sin_i2sin_obj_t *self = MP_OBJ_TO_PTR(self_in); - common_hal_audio_i2sin_i2sin_deinit(self); +static mp_obj_t audioi2sin_i2sin_deinit(mp_obj_t self_in) { + audioi2sin_i2sin_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_audioi2sin_i2sin_deinit(self); return mp_const_none; } -static MP_DEFINE_CONST_FUN_OBJ_1(audio_i2sin_i2sin_deinit_obj, audio_i2sin_i2sin_deinit); +static MP_DEFINE_CONST_FUN_OBJ_1(audioi2sin_i2sin_deinit_obj, audioi2sin_i2sin_deinit); -static void check_for_deinit(audio_i2sin_i2sin_obj_t *self) { - if (common_hal_audio_i2sin_i2sin_deinited(self)) { +static void check_for_deinit(audioi2sin_i2sin_obj_t *self) { + if (common_hal_audioi2sin_i2sin_deinited(self)) { raise_deinited_error(); } } @@ -144,8 +144,8 @@ static void check_for_deinit(audio_i2sin_i2sin_obj_t *self) { //| some samples were missed due to processing time.""" //| ... //| -static mp_obj_t audio_i2sin_i2sin_obj_record(mp_obj_t self_obj, mp_obj_t destination, mp_obj_t destination_length) { - audio_i2sin_i2sin_obj_t *self = MP_OBJ_TO_PTR(self_obj); +static mp_obj_t audioi2sin_i2sin_obj_record(mp_obj_t self_obj, mp_obj_t destination, mp_obj_t destination_length) { + audioi2sin_i2sin_obj_t *self = MP_OBJ_TO_PTR(self_obj); check_for_deinit(self); uint32_t length = mp_arg_validate_type_int(destination_length, MP_QSTR_length); mp_arg_validate_length_min(length, 0, MP_QSTR_length); @@ -158,7 +158,7 @@ static mp_obj_t audio_i2sin_i2sin_obj_record(mp_obj_t self_obj, mp_obj_t destina if (bufinfo.len / mp_binary_get_size('@', bufinfo.typecode, NULL) < length) { mp_raise_ValueError(MP_ERROR_TEXT("Destination capacity is smaller than destination_length.")); } - uint8_t bit_depth = common_hal_audio_i2sin_i2sin_get_bit_depth(self); + uint8_t bit_depth = common_hal_audioi2sin_i2sin_get_bit_depth(self); if ((bit_depth == 24 || bit_depth == 32) && bufinfo.typecode != 'I') { mp_raise_ValueError(MP_ERROR_TEXT("destination buffer must be an array of type 'I' for bit_depth = 24 or 32")); } else if (bit_depth == 16 && bufinfo.typecode != 'H') { @@ -167,57 +167,57 @@ static mp_obj_t audio_i2sin_i2sin_obj_record(mp_obj_t self_obj, mp_obj_t destina mp_raise_ValueError(MP_ERROR_TEXT("destination buffer must be a bytearray or array of type 'B' for bit_depth = 8")); } uint32_t length_written = - common_hal_audio_i2sin_i2sin_record_to_buffer(self, bufinfo.buf, length); + common_hal_audioi2sin_i2sin_record_to_buffer(self, bufinfo.buf, length); return MP_OBJ_NEW_SMALL_INT(length_written); } -MP_DEFINE_CONST_FUN_OBJ_3(audio_i2sin_i2sin_record_obj, audio_i2sin_i2sin_obj_record); +MP_DEFINE_CONST_FUN_OBJ_3(audioi2sin_i2sin_record_obj, audioi2sin_i2sin_obj_record); //| sample_rate: int //| """The actual sample rate of the recording. This may not match the constructed //| sample rate due to internal clock limitations.""" //| -static mp_obj_t audio_i2sin_i2sin_obj_get_sample_rate(mp_obj_t self_in) { - audio_i2sin_i2sin_obj_t *self = MP_OBJ_TO_PTR(self_in); +static mp_obj_t audioi2sin_i2sin_obj_get_sample_rate(mp_obj_t self_in) { + audioi2sin_i2sin_obj_t *self = MP_OBJ_TO_PTR(self_in); check_for_deinit(self); - return MP_OBJ_NEW_SMALL_INT(common_hal_audio_i2sin_i2sin_get_sample_rate(self)); + return MP_OBJ_NEW_SMALL_INT(common_hal_audioi2sin_i2sin_get_sample_rate(self)); } -MP_DEFINE_CONST_FUN_OBJ_1(audio_i2sin_i2sin_get_sample_rate_obj, audio_i2sin_i2sin_obj_get_sample_rate); +MP_DEFINE_CONST_FUN_OBJ_1(audioi2sin_i2sin_get_sample_rate_obj, audioi2sin_i2sin_obj_get_sample_rate); -MP_PROPERTY_GETTER(audio_i2sin_i2sin_sample_rate_obj, - (mp_obj_t)&audio_i2sin_i2sin_get_sample_rate_obj); +MP_PROPERTY_GETTER(audioi2sin_i2sin_sample_rate_obj, + (mp_obj_t)&audioi2sin_i2sin_get_sample_rate_obj); //| bit_depth: int //| """The actual bit depth of the recording. (read-only)""" //| //| -static mp_obj_t audio_i2sin_i2sin_obj_get_bit_depth(mp_obj_t self_in) { - audio_i2sin_i2sin_obj_t *self = MP_OBJ_TO_PTR(self_in); +static mp_obj_t audioi2sin_i2sin_obj_get_bit_depth(mp_obj_t self_in) { + audioi2sin_i2sin_obj_t *self = MP_OBJ_TO_PTR(self_in); check_for_deinit(self); - return MP_OBJ_NEW_SMALL_INT(common_hal_audio_i2sin_i2sin_get_bit_depth(self)); + return MP_OBJ_NEW_SMALL_INT(common_hal_audioi2sin_i2sin_get_bit_depth(self)); } -MP_DEFINE_CONST_FUN_OBJ_1(audio_i2sin_i2sin_get_bit_depth_obj, audio_i2sin_i2sin_obj_get_bit_depth); +MP_DEFINE_CONST_FUN_OBJ_1(audioi2sin_i2sin_get_bit_depth_obj, audioi2sin_i2sin_obj_get_bit_depth); -MP_PROPERTY_GETTER(audio_i2sin_i2sin_bit_depth_obj, - (mp_obj_t)&audio_i2sin_i2sin_get_bit_depth_obj); +MP_PROPERTY_GETTER(audioi2sin_i2sin_bit_depth_obj, + (mp_obj_t)&audioi2sin_i2sin_get_bit_depth_obj); -static const mp_rom_map_elem_t audio_i2sin_i2sin_locals_dict_table[] = { - { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&audio_i2sin_i2sin_deinit_obj) }, - { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&audio_i2sin_i2sin_deinit_obj) }, +static const mp_rom_map_elem_t audioi2sin_i2sin_locals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&audioi2sin_i2sin_deinit_obj) }, + { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&audioi2sin_i2sin_deinit_obj) }, { MP_ROM_QSTR(MP_QSTR___enter__), MP_ROM_PTR(&default___enter___obj) }, { MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&default___exit___obj) }, - { MP_ROM_QSTR(MP_QSTR_record), MP_ROM_PTR(&audio_i2sin_i2sin_record_obj) }, - { MP_ROM_QSTR(MP_QSTR_sample_rate), MP_ROM_PTR(&audio_i2sin_i2sin_sample_rate_obj) }, - { MP_ROM_QSTR(MP_QSTR_bit_depth), MP_ROM_PTR(&audio_i2sin_i2sin_bit_depth_obj) }, + { MP_ROM_QSTR(MP_QSTR_record), MP_ROM_PTR(&audioi2sin_i2sin_record_obj) }, + { MP_ROM_QSTR(MP_QSTR_sample_rate), MP_ROM_PTR(&audioi2sin_i2sin_sample_rate_obj) }, + { MP_ROM_QSTR(MP_QSTR_bit_depth), MP_ROM_PTR(&audioi2sin_i2sin_bit_depth_obj) }, }; -static MP_DEFINE_CONST_DICT(audio_i2sin_i2sin_locals_dict, audio_i2sin_i2sin_locals_dict_table); -#endif // CIRCUITPY_AUDIO_I2SIN +static MP_DEFINE_CONST_DICT(audioi2sin_i2sin_locals_dict, audioi2sin_i2sin_locals_dict_table); +#endif // CIRCUITPY_AUDIOI2SIN MP_DEFINE_CONST_OBJ_TYPE( - audio_i2sin_i2sin_type, + audioi2sin_i2sin_type, MP_QSTR_I2SIn, MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS, - make_new, audio_i2sin_i2sin_make_new - #if CIRCUITPY_AUDIO_I2SIN - , locals_dict, &audio_i2sin_i2sin_locals_dict + make_new, audioi2sin_i2sin_make_new + #if CIRCUITPY_AUDIOI2SIN + , locals_dict, &audioi2sin_i2sin_locals_dict #endif ); diff --git a/shared-bindings/audioi2sin/I2SIn.h b/shared-bindings/audioi2sin/I2SIn.h new file mode 100644 index 0000000000000..6e8254d875dd3 --- /dev/null +++ b/shared-bindings/audioi2sin/I2SIn.h @@ -0,0 +1,28 @@ +// 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 "shared-bindings/microcontroller/Pin.h" + +#if CIRCUITPY_AUDIOI2SIN +#include "common-hal/audioi2sin/I2SIn.h" +#endif + +extern const mp_obj_type_t audioi2sin_i2sin_type; + +#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); +void common_hal_audioi2sin_i2sin_deinit(audioi2sin_i2sin_obj_t *self); +bool common_hal_audioi2sin_i2sin_deinited(audioi2sin_i2sin_obj_t *self); +uint32_t common_hal_audioi2sin_i2sin_record_to_buffer(audioi2sin_i2sin_obj_t *self, + void *buffer, uint32_t length); +uint8_t common_hal_audioi2sin_i2sin_get_bit_depth(audioi2sin_i2sin_obj_t *self); +uint32_t common_hal_audioi2sin_i2sin_get_sample_rate(audioi2sin_i2sin_obj_t *self); +#endif diff --git a/shared-bindings/audio_i2sin/__init__.c b/shared-bindings/audioi2sin/__init__.c similarity index 51% rename from shared-bindings/audio_i2sin/__init__.c rename to shared-bindings/audioi2sin/__init__.c index ba163ae75f4f9..45ba16cb01aea 100644 --- a/shared-bindings/audio_i2sin/__init__.c +++ b/shared-bindings/audioi2sin/__init__.c @@ -10,12 +10,12 @@ #include "py/runtime.h" #include "shared-bindings/microcontroller/Pin.h" -#include "shared-bindings/audio_i2sin/__init__.h" -#include "shared-bindings/audio_i2sin/I2SIn.h" +#include "shared-bindings/audioi2sin/__init__.h" +#include "shared-bindings/audioi2sin/I2SIn.h" //| """Support for recording audio from an I2S input source. //| -//| The `audio_i2sin` module contains the `I2SIn` class for recording audio +//| The `audioi2sin` module contains the `I2SIn` class for recording audio //| from an external I2S source such as a MEMS microphone (e.g. SPH0645LM4H //| or INMP441). //| @@ -23,16 +23,16 @@ //| are no longer needed. To do so, either call :py:meth:`!deinit` or use a //| context manager.""" -static const mp_rom_map_elem_t audio_i2sin_module_globals_table[] = { - { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_audio_i2sin) }, - { MP_ROM_QSTR(MP_QSTR_I2SIn), MP_ROM_PTR(&audio_i2sin_i2sin_type) }, +static const mp_rom_map_elem_t audioi2sin_module_globals_table[] = { + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_audioi2sin) }, + { MP_ROM_QSTR(MP_QSTR_I2SIn), MP_ROM_PTR(&audioi2sin_i2sin_type) }, }; -static MP_DEFINE_CONST_DICT(audio_i2sin_module_globals, audio_i2sin_module_globals_table); +static MP_DEFINE_CONST_DICT(audioi2sin_module_globals, audioi2sin_module_globals_table); -const mp_obj_module_t audio_i2sin_module = { +const mp_obj_module_t audioi2sin_module = { .base = { &mp_type_module }, - .globals = (mp_obj_dict_t *)&audio_i2sin_module_globals, + .globals = (mp_obj_dict_t *)&audioi2sin_module_globals, }; -MP_REGISTER_MODULE(MP_QSTR_audio_i2sin, audio_i2sin_module); +MP_REGISTER_MODULE(MP_QSTR_audioi2sin, audioi2sin_module); diff --git a/shared-bindings/audio_i2sin/__init__.h b/shared-bindings/audioi2sin/__init__.h similarity index 100% rename from shared-bindings/audio_i2sin/__init__.h rename to shared-bindings/audioi2sin/__init__.h diff --git a/tests/circuitpython-manual/audio_i2sin/i2sin_neopixel_reactive.py b/tests/circuitpython-manual/audioi2sin/i2sin_neopixel_reactive.py similarity index 98% rename from tests/circuitpython-manual/audio_i2sin/i2sin_neopixel_reactive.py rename to tests/circuitpython-manual/audioi2sin/i2sin_neopixel_reactive.py index 58a100547f2d6..ac9c2c8ba0df5 100644 --- a/tests/circuitpython-manual/audio_i2sin/i2sin_neopixel_reactive.py +++ b/tests/circuitpython-manual/audioi2sin/i2sin_neopixel_reactive.py @@ -18,7 +18,7 @@ import time import board -import audio_i2sin +import audioi2sin import neopixel NUM_PIXELS = 30 @@ -29,7 +29,7 @@ pixels = neopixel.NeoPixel(PIXEL_PIN, NUM_PIXELS, brightness=0.3, auto_write=False) -mic = audio_i2sin.I2SIn( +mic = audioi2sin.I2SIn( bit_clock=board.D5, word_select=board.D6, data=board.D9, diff --git a/tests/circuitpython-manual/audio_i2sin/i2sin_record_sdcard.py b/tests/circuitpython-manual/audioi2sin/i2sin_record_sdcard.py similarity index 98% rename from tests/circuitpython-manual/audio_i2sin/i2sin_record_sdcard.py rename to tests/circuitpython-manual/audioi2sin/i2sin_record_sdcard.py index 3045cac9d5082..060df3dc1b7e2 100644 --- a/tests/circuitpython-manual/audio_i2sin/i2sin_record_sdcard.py +++ b/tests/circuitpython-manual/audioi2sin/i2sin_record_sdcard.py @@ -15,7 +15,7 @@ import busio import sdcardio import storage -import audio_i2sin +import audioi2sin # ---- Recording config ------------------------------------------------------ SAMPLE_RATE = 16000 @@ -32,7 +32,7 @@ # ---- Mic ------------------------------------------------------------------- # 24-bit MEMS mics ride in 32-bit slots. Downconvert each slot to a # signed 16-bit PCM sample before writing. -mic = audio_i2sin.I2SIn( +mic = audioi2sin.I2SIn( bit_clock=board.D5, word_select=board.D6, data=board.D9, From 510307c1e4cd7234a770124ba693527c1d6b89f4 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Tue, 12 May 2026 14:42:13 -0500 Subject: [PATCH 4/9] add samples_signed argument --- ports/espressif/common-hal/audioi2sin/I2SIn.c | 94 +++++++++++++------ ports/espressif/common-hal/audioi2sin/I2SIn.h | 1 + .../raspberrypi/common-hal/audioi2sin/I2SIn.c | 25 ++++- .../raspberrypi/common-hal/audioi2sin/I2SIn.h | 1 + shared-bindings/audioi2sin/I2SIn.c | 25 ++++- shared-bindings/audioi2sin/I2SIn.h | 4 +- 6 files changed, 113 insertions(+), 37 deletions(-) diff --git a/ports/espressif/common-hal/audioi2sin/I2SIn.c b/ports/espressif/common-hal/audioi2sin/I2SIn.c index 67395087d1603..1f458ec79a7cc 100644 --- a/ports/espressif/common-hal/audioi2sin/I2SIn.c +++ b/ports/espressif/common-hal/audioi2sin/I2SIn.c @@ -20,7 +20,8 @@ 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) { + 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.")); @@ -66,6 +67,7 @@ void common_hal_audioi2sin_i2sin_construct(audioi2sin_i2sin_obj_t *self, 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); @@ -111,6 +113,31 @@ void common_hal_audioi2sin_i2sin_deinit(audioi2sin_i2sin_obj_t *self) { 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, + 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; @@ -119,41 +146,46 @@ uint32_t common_hal_audioi2sin_i2sin_record_to_buffer(audioi2sin_i2sin_obj_t *se 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); - return result / element_size; + 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; + } + } } - // 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; - uint32_t 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; } @@ -166,4 +198,8 @@ uint32_t common_hal_audioi2sin_i2sin_get_sample_rate(audioi2sin_i2sin_obj_t *sel return self->sample_rate; } +bool common_hal_audioi2sin_i2sin_get_samples_signed(audioi2sin_i2sin_obj_t *self) { + return self->samples_signed; +} + #endif // CIRCUITPY_AUDIOI2SIN diff --git a/ports/espressif/common-hal/audioi2sin/I2SIn.h b/ports/espressif/common-hal/audioi2sin/I2SIn.h index e16919b4cbb07..74e6eb83d0d3c 100644 --- a/ports/espressif/common-hal/audioi2sin/I2SIn.h +++ b/ports/espressif/common-hal/audioi2sin/I2SIn.h @@ -24,6 +24,7 @@ typedef struct { uint32_t sample_rate; uint8_t bit_depth; bool mono; + bool samples_signed; } audioi2sin_i2sin_obj_t; #endif diff --git a/ports/raspberrypi/common-hal/audioi2sin/I2SIn.c b/ports/raspberrypi/common-hal/audioi2sin/I2SIn.c index f64bd299bda00..181ed219f2da0 100644 --- a/ports/raspberrypi/common-hal/audioi2sin/I2SIn.c +++ b/ports/raspberrypi/common-hal/audioi2sin/I2SIn.c @@ -151,7 +151,8 @@ static const uint16_t i2sin_program_left_justified_swap_32[] = { 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) { + uint32_t sample_rate, uint8_t bit_depth, bool mono, bool left_justified, + bool samples_signed) { if (main_clock != NULL) { mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_main_clock); @@ -225,6 +226,7 @@ void common_hal_audioi2sin_i2sin_construct(audioi2sin_i2sin_obj_t *self, self->sample_rate = actual_frequency / pio_clocks_per_frame; self->bit_depth = bit_depth; self->mono = mono; + self->samples_signed = samples_signed; self->settled = false; self->ring = NULL; self->ring_size = 0; @@ -293,6 +295,10 @@ uint32_t common_hal_audioi2sin_i2sin_get_sample_rate(audioi2sin_i2sin_obj_t *sel return self->sample_rate; } +bool common_hal_audioi2sin_i2sin_get_samples_signed(audioi2sin_i2sin_obj_t *self) { + return self->samples_signed; +} + // In 16-bit mode, each PIO frame produces a single 32-bit FIFO word with bits // 31..16 = right channel and bits 15..0 = left channel (both MSB-first signed // 16-bit). In 24/32-bit mode each frame produces two FIFO words: right first, @@ -322,6 +328,15 @@ uint32_t common_hal_audioi2sin_i2sin_record_to_buffer(audioi2sin_i2sin_obj_t *se const size_t ring_size = self->ring_size; const size_t half_size = self->half_size; + // I2S delivers signed PCM. When the caller asked for unsigned samples, + // flip the sign bit per sample (XOR with 0x8000 for 16-bit, 0x800000 for + // 24-bit data in a 32-bit slot, 0x80000000 for 32-bit), matching the WAV + // convention. + const uint16_t flip16 = self->samples_signed ? 0 : 0x8000u; + const uint32_t flip32 = self->samples_signed + ? 0u + : (self->bit_depth == 24 ? 0x800000u : 0x80000000u); + if (self->bit_depth == 16) { // 16-bit mode auto-pushes one stereo frame per FIFO word. The DMA has // been streaming since construct time, so the ring already contains @@ -352,8 +367,8 @@ uint32_t common_hal_audioi2sin_i2sin_record_to_buffer(audioi2sin_i2sin_obj_t *se } while (avail >= 4 && output_count < output_buffer_length) { uint32_t frame = *(volatile uint32_t *)(self->ring + self->read_pos); - uint16_t left = (uint16_t)(frame & 0xffff); - uint16_t right = (uint16_t)(frame >> 16); + uint16_t left = (uint16_t)(frame & 0xffff) ^ flip16; + uint16_t right = (uint16_t)(frame >> 16) ^ flip16; if (self->mono) { output[output_count++] = left; } else { @@ -410,9 +425,9 @@ uint32_t common_hal_audioi2sin_i2sin_record_to_buffer(audioi2sin_i2sin_obj_t *se continue; } while (avail >= 8 && output_count < output_buffer_length) { - uint32_t right = *(volatile uint32_t *)(self->ring + self->read_pos); + uint32_t right = *(volatile uint32_t *)(self->ring + self->read_pos) ^ flip32; size_t next_pos = (self->read_pos + 4) % ring_size; - uint32_t left = *(volatile uint32_t *)(self->ring + next_pos); + uint32_t left = *(volatile uint32_t *)(self->ring + next_pos) ^ flip32; if (self->mono) { output[output_count++] = left; } else { diff --git a/ports/raspberrypi/common-hal/audioi2sin/I2SIn.h b/ports/raspberrypi/common-hal/audioi2sin/I2SIn.h index 06ae2ca774c4e..399d962afb275 100644 --- a/ports/raspberrypi/common-hal/audioi2sin/I2SIn.h +++ b/ports/raspberrypi/common-hal/audioi2sin/I2SIn.h @@ -16,6 +16,7 @@ typedef struct { uint32_t sample_rate; uint8_t bit_depth; bool mono; + bool samples_signed; bool settled; rp2pio_statemachine_obj_t state_machine; // Background DMA ring buffer. The state machine alternates DMA writes diff --git a/shared-bindings/audioi2sin/I2SIn.c b/shared-bindings/audioi2sin/I2SIn.c index 4c2b221254759..d8d8e3ebcc3b2 100644 --- a/shared-bindings/audioi2sin/I2SIn.c +++ b/shared-bindings/audioi2sin/I2SIn.c @@ -30,6 +30,7 @@ //| bit_depth: int = 16, //| mono: bool = True, //| left_justified: bool = False, +//| samples_signed: bool = True, //| ) -> None: //| """Create an I2SIn object associated with the given pins. This allows you to //| record audio signals from an external I2S source (e.g. an I2S MEMS microphone @@ -51,6 +52,9 @@ //| :param bool mono: True when capturing a single channel of audio, captures two channels otherwise. //| :param bool left_justified: True when data bits are aligned with the word select clock. False //| when they are shifted by one to match classic I2S protocol. Set True for mics like the SPH0645LM4H. +//| :param bool samples_signed: Samples are signed (True) or unsigned (False). I2S mics deliver signed +//| two's-complement PCM natively; set False to have the recorded samples converted to unsigned PCM +//| (the top/sign bit is flipped, matching the WAV convention for unsigned samples). //| //| Example, recording 16-bit mono samples from an INMP441:: //| @@ -71,7 +75,7 @@ static mp_obj_t audioi2sin_i2sin_make_new(const mp_obj_type_t *type, size_t n_ar return NULL; // Not reachable. #else enum { ARG_bit_clock, ARG_word_select, ARG_data, ARG_main_clock, - ARG_sample_rate, ARG_bit_depth, ARG_mono, ARG_left_justified }; + ARG_sample_rate, ARG_bit_depth, ARG_mono, ARG_left_justified, ARG_samples_signed }; static const mp_arg_t allowed_args[] = { { MP_QSTR_bit_clock, MP_ARG_REQUIRED | MP_ARG_OBJ }, { MP_QSTR_word_select, MP_ARG_REQUIRED | MP_ARG_OBJ }, @@ -81,6 +85,7 @@ static mp_obj_t audioi2sin_i2sin_make_new(const mp_obj_type_t *type, size_t n_ar { MP_QSTR_bit_depth, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 16} }, { MP_QSTR_mono, MP_ARG_KW_ONLY | MP_ARG_BOOL, {.u_bool = true} }, { MP_QSTR_left_justified, MP_ARG_KW_ONLY | MP_ARG_BOOL, {.u_bool = false} }, + { MP_QSTR_samples_signed, MP_ARG_KW_ONLY | MP_ARG_BOOL, {.u_bool = true} }, }; mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); @@ -97,10 +102,11 @@ static mp_obj_t audioi2sin_i2sin_make_new(const mp_obj_type_t *type, size_t n_ar } bool mono = args[ARG_mono].u_bool; bool left_justified = args[ARG_left_justified].u_bool; + bool samples_signed = args[ARG_samples_signed].u_bool; audioi2sin_i2sin_obj_t *self = mp_obj_malloc_with_finaliser(audioi2sin_i2sin_obj_t, &audioi2sin_i2sin_type); common_hal_audioi2sin_i2sin_construct(self, bit_clock, word_select, data, main_clock, - sample_rate, bit_depth, mono, left_justified); + sample_rate, bit_depth, mono, left_justified, samples_signed); return MP_OBJ_FROM_PTR(self); #endif @@ -200,6 +206,20 @@ MP_DEFINE_CONST_FUN_OBJ_1(audioi2sin_i2sin_get_bit_depth_obj, audioi2sin_i2sin_o MP_PROPERTY_GETTER(audioi2sin_i2sin_bit_depth_obj, (mp_obj_t)&audioi2sin_i2sin_get_bit_depth_obj); +//| samples_signed: bool +//| """True if recorded samples are signed PCM, False for unsigned. (read-only)""" +//| +//| +static mp_obj_t audioi2sin_i2sin_obj_get_samples_signed(mp_obj_t self_in) { + audioi2sin_i2sin_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return mp_obj_new_bool(common_hal_audioi2sin_i2sin_get_samples_signed(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(audioi2sin_i2sin_get_samples_signed_obj, audioi2sin_i2sin_obj_get_samples_signed); + +MP_PROPERTY_GETTER(audioi2sin_i2sin_samples_signed_obj, + (mp_obj_t)&audioi2sin_i2sin_get_samples_signed_obj); + static const mp_rom_map_elem_t audioi2sin_i2sin_locals_dict_table[] = { { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&audioi2sin_i2sin_deinit_obj) }, { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&audioi2sin_i2sin_deinit_obj) }, @@ -208,6 +228,7 @@ static const mp_rom_map_elem_t audioi2sin_i2sin_locals_dict_table[] = { { MP_ROM_QSTR(MP_QSTR_record), MP_ROM_PTR(&audioi2sin_i2sin_record_obj) }, { MP_ROM_QSTR(MP_QSTR_sample_rate), MP_ROM_PTR(&audioi2sin_i2sin_sample_rate_obj) }, { MP_ROM_QSTR(MP_QSTR_bit_depth), MP_ROM_PTR(&audioi2sin_i2sin_bit_depth_obj) }, + { MP_ROM_QSTR(MP_QSTR_samples_signed), MP_ROM_PTR(&audioi2sin_i2sin_samples_signed_obj) }, }; static MP_DEFINE_CONST_DICT(audioi2sin_i2sin_locals_dict, audioi2sin_i2sin_locals_dict_table); #endif // CIRCUITPY_AUDIOI2SIN diff --git a/shared-bindings/audioi2sin/I2SIn.h b/shared-bindings/audioi2sin/I2SIn.h index 6e8254d875dd3..e75bdf2d34bc0 100644 --- a/shared-bindings/audioi2sin/I2SIn.h +++ b/shared-bindings/audioi2sin/I2SIn.h @@ -18,11 +18,13 @@ extern const mp_obj_type_t audioi2sin_i2sin_type; 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); + uint32_t sample_rate, uint8_t bit_depth, bool mono, bool left_justified, + bool samples_signed); void common_hal_audioi2sin_i2sin_deinit(audioi2sin_i2sin_obj_t *self); bool common_hal_audioi2sin_i2sin_deinited(audioi2sin_i2sin_obj_t *self); uint32_t common_hal_audioi2sin_i2sin_record_to_buffer(audioi2sin_i2sin_obj_t *self, void *buffer, uint32_t length); uint8_t common_hal_audioi2sin_i2sin_get_bit_depth(audioi2sin_i2sin_obj_t *self); uint32_t common_hal_audioi2sin_i2sin_get_sample_rate(audioi2sin_i2sin_obj_t *self); +bool common_hal_audioi2sin_i2sin_get_samples_signed(audioi2sin_i2sin_obj_t *self); #endif From 4bb08635130cd84fda331249aed5c491a10c217b Mon Sep 17 00:00:00 2001 From: foamyguy Date: Thu, 14 May 2026 12:09:10 -0500 Subject: [PATCH 5/9] try to fix c6 builds. implement changes from review --- locale/circuitpython.pot | 28 +++++----- ports/espressif/Makefile | 4 ++ ports/espressif/common-hal/audioi2sin/I2SIn.c | 32 +++++++---- .../raspberrypi/common-hal/audioi2sin/I2SIn.c | 32 ++++++----- .../raspberrypi/common-hal/audioi2sin/I2SIn.h | 1 + shared-bindings/audioi2sin/I2SIn.c | 54 +++++++++++++++---- .../audioi2sin/i2sin_neopixel_reactive.py | 13 +---- .../audioi2sin/i2sin_record_sdcard.py | 4 +- 8 files changed, 108 insertions(+), 60 deletions(-) diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot index 84e9fd7edd6b1..b6af1d43f406a 100644 --- a/locale/circuitpython.pot +++ b/locale/circuitpython.pot @@ -219,7 +219,7 @@ msgstr "" msgid "%q must be array of type 'h'" msgstr "" -#: shared-bindings/audio_i2sin/I2SIn.c shared-bindings/audiobusio/PDMIn.c +#: shared-bindings/audiobusio/PDMIn.c shared-bindings/audioi2sin/I2SIn.c msgid "%q must be multiple of 8." msgstr "" @@ -659,8 +659,8 @@ msgstr "" msgid "Below minimum frame rate" msgstr "" -#: ports/raspberrypi/common-hal/audio_i2sin/I2SIn.c #: ports/raspberrypi/common-hal/audiobusio/I2SOut.c +#: ports/raspberrypi/common-hal/audioi2sin/I2SIn.c msgid "Bit clock and word select must be sequential GPIO pins" msgstr "" @@ -805,7 +805,7 @@ msgstr "" msgid "Cannot pull on input-only pin." msgstr "" -#: shared-bindings/audio_i2sin/I2SIn.c shared-bindings/audiobusio/PDMIn.c +#: shared-bindings/audiobusio/PDMIn.c shared-bindings/audioi2sin/I2SIn.c msgid "Cannot record to a file" msgstr "" @@ -927,7 +927,7 @@ msgstr "" msgid "Deep sleep pins must use a rising edge with pulldown" msgstr "" -#: shared-bindings/audio_i2sin/I2SIn.c shared-bindings/audiobusio/PDMIn.c +#: shared-bindings/audiobusio/PDMIn.c shared-bindings/audioi2sin/I2SIn.c msgid "Destination capacity is smaller than destination_length." msgstr "" @@ -1695,7 +1695,7 @@ msgstr "" msgid "Ok" msgstr "" -#: ports/raspberrypi/common-hal/audio_i2sin/I2SIn.c +#: ports/raspberrypi/common-hal/audioi2sin/I2SIn.c msgid "Only 16, 24, or 32 bit depth supported." msgstr "" @@ -1811,8 +1811,8 @@ msgstr "" msgid "Parameter error" msgstr "" -#: ports/espressif/common-hal/audio_i2sin/I2SIn.c #: ports/espressif/common-hal/audiobusio/__init__.c +#: ports/espressif/common-hal/audioi2sin/I2SIn.c msgid "Peripheral in use" msgstr "" @@ -2702,8 +2702,8 @@ msgstr "" msgid "binary op %q not implemented" msgstr "" -#: ports/espressif/common-hal/audio_i2sin/I2SIn.c #: ports/espressif/common-hal/audiobusio/PDMIn.c +#: ports/espressif/common-hal/audioi2sin/I2SIn.c msgid "bit_depth must be 8, 16, 24, or 32." msgstr "" @@ -3095,20 +3095,15 @@ msgstr "" msgid "default is not a function" msgstr "" -#: shared-bindings/audio_i2sin/I2SIn.c shared-bindings/audiobusio/PDMIn.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/audio_i2sin/I2SIn.c shared-bindings/audiobusio/PDMIn.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 "" @@ -3522,6 +3517,11 @@ msgstr "" msgid "invalid cert" msgstr "" +#: shared-bindings/audioi2sin/I2SIn.c +#, c-format +msgid "invalid destination buffer, must be an array of type: %c" +msgstr "" + #: shared-bindings/bitmaptools/__init__.c #, c-format msgid "invalid element size %d for bits_per_pixel %d\n" diff --git a/ports/espressif/Makefile b/ports/espressif/Makefile index 8faad5fb5def2..b0ba572d438a7 100644 --- a/ports/espressif/Makefile +++ b/ports/espressif/Makefile @@ -592,6 +592,10 @@ ifneq ($(CIRCUITPY_AUDIOBUSIO),0) CHIP_COMPONENTS += esp_driver_i2s endif +ifneq ($(CIRCUITPY_AUDIOI2SIN),0) +CHIP_COMPONENTS += esp_driver_i2s +endif + ifneq ($(CIRCUITPY_BLEIO_NATIVE),0) SRC_C += common-hal/_bleio/ble_events.c endif diff --git a/ports/espressif/common-hal/audioi2sin/I2SIn.c b/ports/espressif/common-hal/audioi2sin/I2SIn.c index 1f458ec79a7cc..3dc0d227b3e1a 100644 --- a/ports/espressif/common-hal/audioi2sin/I2SIn.c +++ b/ports/espressif/common-hal/audioi2sin/I2SIn.c @@ -117,27 +117,39 @@ void common_hal_audioi2sin_i2sin_deinit(audioi2sin_i2sin_obj_t *self) { // the sign bit for its width to convert to unsigned PCM (WAV convention). static void i2sin_convert_to_unsigned(void *buffer, uint32_t samples, uint8_t bit_depth, size_t element_size) { + (void)element_size; + uint32_t *p = (uint32_t *)buffer; + if (bit_depth == 8) { - uint8_t *p = (uint8_t *)buffer; - for (uint32_t i = 0; i < samples; i++) { - p[i] ^= 0x80u; + // 4 samples per word + uint32_t words = samples / 4; + for (uint32_t i = 0; i < words; i++) { + p[i] ^= 0x80808080u; + } + // tail: 0–3 leftover bytes + uint8_t *tail = (uint8_t *)(p + words); + for (uint32_t i = 0; i < (samples & 3u); i++) { + tail[i] ^= 0x80u; } } else if (bit_depth == 16) { - uint16_t *p = (uint16_t *)buffer; - for (uint32_t i = 0; i < samples; i++) { - p[i] ^= 0x8000u; + // 2 samples per word + uint32_t words = samples / 2; + for (uint32_t i = 0; i < words; i++) { + p[i] ^= 0x80008000u; + } + if (samples & 1u) { + ((uint16_t *)(p + words))[0] ^= 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; + // 24- or 32-bit: one sample per 32-bit slot + uint32_t mask = (bit_depth == 24) ? 0x00800000u : 0x80000000u; 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; diff --git a/ports/raspberrypi/common-hal/audioi2sin/I2SIn.c b/ports/raspberrypi/common-hal/audioi2sin/I2SIn.c index 181ed219f2da0..906ca0ed55c46 100644 --- a/ports/raspberrypi/common-hal/audioi2sin/I2SIn.c +++ b/ports/raspberrypi/common-hal/audioi2sin/I2SIn.c @@ -227,6 +227,7 @@ void common_hal_audioi2sin_i2sin_construct(audioi2sin_i2sin_obj_t *self, self->bit_depth = bit_depth; self->mono = mono; self->samples_signed = samples_signed; + self->left_justified = left_justified; self->settled = false; self->ring = NULL; self->ring_size = 0; @@ -322,6 +323,13 @@ static size_t i2sin_write_pos(audioi2sin_i2sin_obj_t *self) { return (size_t)(addr - base); } +// 24-bit non-left-justified data arrives in the low 24 bits of the FIFO word +// with the sign bit at bit 23 and bits 31..24 zero. To make it decode +// correctly as int32 (array typecode "i"), lift the sign bit to bit 31. +static inline uint32_t movesign24(uint32_t val) { + return ((val & 0x800000u) << 8) | (val & 0x7fffffu); +} + uint32_t common_hal_audioi2sin_i2sin_record_to_buffer(audioi2sin_i2sin_obj_t *self, void *buffer, uint32_t output_buffer_length) { uint32_t output_count = 0; @@ -332,10 +340,13 @@ uint32_t common_hal_audioi2sin_i2sin_record_to_buffer(audioi2sin_i2sin_obj_t *se // flip the sign bit per sample (XOR with 0x8000 for 16-bit, 0x800000 for // 24-bit data in a 32-bit slot, 0x80000000 for 32-bit), matching the WAV // convention. - const uint16_t flip16 = self->samples_signed ? 0 : 0x8000u; + const uint32_t flip16 = self->samples_signed ? 0u : 0x80008000u; const uint32_t flip32 = self->samples_signed ? 0u : (self->bit_depth == 24 ? 0x800000u : 0x80000000u); + const bool fix_sign24 = self->bit_depth == 24 + && self->samples_signed + && !self->left_justified; if (self->bit_depth == 16) { // 16-bit mode auto-pushes one stereo frame per FIFO word. The DMA has @@ -366,9 +377,9 @@ uint32_t common_hal_audioi2sin_i2sin_record_to_buffer(audioi2sin_i2sin_obj_t *se continue; } while (avail >= 4 && output_count < output_buffer_length) { - uint32_t frame = *(volatile uint32_t *)(self->ring + self->read_pos); - uint16_t left = (uint16_t)(frame & 0xffff) ^ flip16; - uint16_t right = (uint16_t)(frame >> 16) ^ flip16; + uint32_t frame = *(volatile uint32_t *)(self->ring + self->read_pos) ^ flip16; + uint16_t left = (uint16_t)(frame & 0xffff); + uint16_t right = (uint16_t)(frame >> 16); if (self->mono) { output[output_count++] = left; } else { @@ -405,14 +416,7 @@ uint32_t common_hal_audioi2sin_i2sin_record_to_buffer(audioi2sin_i2sin_obj_t *se self->settled = false; avail = (write_pos + ring_size - self->read_pos) % ring_size; } - if (!self->settled) { - if (avail < 8) { - RUN_BACKGROUND_TASKS; - if (mp_hal_is_interrupted()) { - return output_count; - } - continue; - } + if (!self->settled && avail >= 8) { self->read_pos = (self->read_pos + 8) % ring_size; avail -= 8; self->settled = true; @@ -428,6 +432,10 @@ uint32_t common_hal_audioi2sin_i2sin_record_to_buffer(audioi2sin_i2sin_obj_t *se uint32_t right = *(volatile uint32_t *)(self->ring + self->read_pos) ^ flip32; size_t next_pos = (self->read_pos + 4) % ring_size; uint32_t left = *(volatile uint32_t *)(self->ring + next_pos) ^ flip32; + if (fix_sign24) { + right = movesign24(right); + left = movesign24(left); + } if (self->mono) { output[output_count++] = left; } else { diff --git a/ports/raspberrypi/common-hal/audioi2sin/I2SIn.h b/ports/raspberrypi/common-hal/audioi2sin/I2SIn.h index 399d962afb275..bdd0529c94d7b 100644 --- a/ports/raspberrypi/common-hal/audioi2sin/I2SIn.h +++ b/ports/raspberrypi/common-hal/audioi2sin/I2SIn.h @@ -17,6 +17,7 @@ typedef struct { uint8_t bit_depth; bool mono; bool samples_signed; + bool left_justified; bool settled; rp2pio_statemachine_obj_t state_machine; // Background DMA ring buffer. The state machine alternates DMA writes diff --git a/shared-bindings/audioi2sin/I2SIn.c b/shared-bindings/audioi2sin/I2SIn.c index d8d8e3ebcc3b2..0cbdbdb49d3b3 100644 --- a/shared-bindings/audioi2sin/I2SIn.c +++ b/shared-bindings/audioi2sin/I2SIn.c @@ -44,9 +44,24 @@ //| :param ~microcontroller.Pin data: The data input pin //| :param ~microcontroller.Pin main_clock: The main clock pin. Not all ports support this. //| :param int sample_rate: Target sample rate of the resulting samples. Check `sample_rate` for actual value. -//| :param int bit_depth: Number of bits per sample. Must be 8, 16, 24, or 32. -//| For 8-bit, pass a ``bytearray`` or ``array.array('B', ...)``; for 16-bit, -//| ``array.array('H', ...)``; for 24- or 32-bit, ``array.array('I', ...)``. +//| :param int bit_depth: Number of bits per sample. Must be 8, 16, 24, or 32. 8-bit only supported on espressif. +//| +//| +----------------+-----------+----------------------+ +//| | samples_signed | bit_depth | Required typecode(s) | +//| +================+===========+======================+ +//| | True | 24 or 32 | ``'i'`` | +//| +----------------+-----------+----------------------+ +//| | True | 16 | ``'h'`` | +//| +----------------+-----------+----------------------+ +//| | True | 8 | ``'b'`` or BYTEARRAY | +//| +----------------+-----------+----------------------+ +//| | False | 24 or 32 | ``'I'`` | +//| +----------------+-----------+----------------------+ +//| | False | 16 | ``'H'`` | +//| +----------------+-----------+----------------------+ +//| | False | 8 | ``'B'`` or BYTEARRAY | +//| +----------------+-----------+----------------------+ +//| //| Note that 24-bit samples from mics like the SPH0645LM4H / INMP441 are //| transported in 32-bit slots, so use ``bit_depth=32`` and an ``'I'`` buffer. //| :param bool mono: True when capturing a single channel of audio, captures two channels otherwise. @@ -62,10 +77,11 @@ //| import audioi2sin //| import board //| -//| buf = array.array("H", [0] * 16000) +//| buf = array.array("h", [0] * 16000) //| with audioi2sin.I2SIn(board.D9, board.D10, board.D11, //| sample_rate=16000, bit_depth=16) as mic: //| mic.record(buf, len(buf)) +//| //| """ //| ... //| @@ -165,12 +181,30 @@ static mp_obj_t audioi2sin_i2sin_obj_record(mp_obj_t self_obj, mp_obj_t destinat mp_raise_ValueError(MP_ERROR_TEXT("Destination capacity is smaller than destination_length.")); } uint8_t bit_depth = common_hal_audioi2sin_i2sin_get_bit_depth(self); - if ((bit_depth == 24 || bit_depth == 32) && bufinfo.typecode != 'I') { - mp_raise_ValueError(MP_ERROR_TEXT("destination buffer must be an array of type 'I' for bit_depth = 24 or 32")); - } else if (bit_depth == 16 && bufinfo.typecode != 'H') { - mp_raise_ValueError(MP_ERROR_TEXT("destination buffer must be an array of type 'H' for bit_depth = 16")); - } else if (bit_depth == 8 && bufinfo.typecode != 'B' && bufinfo.typecode != BYTEARRAY_TYPECODE) { - mp_raise_ValueError(MP_ERROR_TEXT("destination buffer must be a bytearray or array of type 'B' for bit_depth = 8")); + char error_type = ' '; + bool samples_signed = common_hal_audioi2sin_i2sin_get_samples_signed(self); + if (samples_signed) { + if ((bit_depth == 24 || bit_depth == 32) && bufinfo.typecode != 'i') { + error_type = 'i'; + } else if (bit_depth == 16 && bufinfo.typecode != 'h') { + error_type = 'h'; + } else if (bit_depth == 8 && bufinfo.typecode != 'b' && bufinfo.typecode != BYTEARRAY_TYPECODE) { + error_type = 'b'; // NOTE: Not identifying as bytearray + } + } else { + if ((bit_depth == 24 || bit_depth == 32) && bufinfo.typecode != 'I') { + error_type = 'I'; + } else if (bit_depth == 16 && bufinfo.typecode != 'H') { + error_type = 'H'; + } else if (bit_depth == 8 && bufinfo.typecode != 'B' && bufinfo.typecode != BYTEARRAY_TYPECODE) { + error_type = 'B'; + } + } + if (error_type != ' ') { + mp_raise_TypeError_varg( + MP_ERROR_TEXT("invalid destination buffer, must be an array of type: %c"), + error_type + ); } uint32_t length_written = common_hal_audioi2sin_i2sin_record_to_buffer(self, bufinfo.buf, length); diff --git a/tests/circuitpython-manual/audioi2sin/i2sin_neopixel_reactive.py b/tests/circuitpython-manual/audioi2sin/i2sin_neopixel_reactive.py index ac9c2c8ba0df5..fc5559c70b1ed 100644 --- a/tests/circuitpython-manual/audioi2sin/i2sin_neopixel_reactive.py +++ b/tests/circuitpython-manual/audioi2sin/i2sin_neopixel_reactive.py @@ -39,15 +39,7 @@ left_justified=False, # set True for SPH0645LM4H ) -buf = array.array("I", [0] * SAMPLES_PER_FRAME) - - -def to_signed24(u32): - # The mic packs a 24-bit signed sample left-justified in 32 bits. - s = u32 >> 8 - if s & 0x800000: - s -= 0x1000000 - return s +buf = array.array("i", [0] * SAMPLES_PER_FRAME) def wheel(pos): @@ -72,8 +64,7 @@ def wheel(pos): # Compute RMS of the window. acc = 0 - for raw in buf: - s = to_signed24(raw) + for s in buf: acc += s * s rms = math.sqrt(acc / len(buf)) diff --git a/tests/circuitpython-manual/audioi2sin/i2sin_record_sdcard.py b/tests/circuitpython-manual/audioi2sin/i2sin_record_sdcard.py index 060df3dc1b7e2..c381e272bf78a 100644 --- a/tests/circuitpython-manual/audioi2sin/i2sin_record_sdcard.py +++ b/tests/circuitpython-manual/audioi2sin/i2sin_record_sdcard.py @@ -45,7 +45,7 @@ actual_rate = mic.sample_rate print("Recording at", actual_rate, "Hz for", RECORD_SECONDS, "s ->", OUTPUT_PATH) -raw = array.array("I", [0] * CHUNK_SAMPLES) +raw = array.array("i", [0] * CHUNK_SAMPLES) pcm16 = array.array("h", [0] * CHUNK_SAMPLES) @@ -90,8 +90,6 @@ def write_wav_header(f, sample_rate, num_samples, bits_per_sample=16, channels=1 for i in range(n): v = raw[i] s = v >> 16 # take top 16 bits - if s & 0x8000: - s -= 0x10000 # sign-extend pcm16[i] = s # Write only the valid portion. f.write(memoryview(pcm16)[:n]) From 4bfcd6c9dd21174e62531c70ba9b5709ac00b42f Mon Sep 17 00:00:00 2001 From: foamyguy Date: Thu, 14 May 2026 14:02:33 -0500 Subject: [PATCH 6/9] disable audioi2sin for esp32c2 --- ports/espressif/mpconfigport.mk | 1 + 1 file changed, 1 insertion(+) diff --git a/ports/espressif/mpconfigport.mk b/ports/espressif/mpconfigport.mk index 8396b89ee09db..cc8e29cecca0f 100644 --- a/ports/espressif/mpconfigport.mk +++ b/ports/espressif/mpconfigport.mk @@ -133,6 +133,7 @@ CIRCUITPY_ANALOGBUFIO = 0 # No I2S CIRCUITPY_AUDIOBUSIO = 0 +CIRCUITPY_AUDIOI2SIN = 0 # No DAC CIRCUITPY_AUDIOIO = 0 From 372829824071f62ce94bde2cb189fe595d6ced7c Mon Sep 17 00:00:00 2001 From: foamyguy Date: Fri, 15 May 2026 09:45:37 -0500 Subject: [PATCH 7/9] output_bit_depth argument --- locale/circuitpython.pot | 12 +- ports/espressif/common-hal/audioi2sin/I2SIn.c | 118 ++++++++++++- ports/espressif/common-hal/audioi2sin/I2SIn.h | 1 + .../raspberrypi/common-hal/audioi2sin/I2SIn.c | 158 +++++++++++++++--- .../raspberrypi/common-hal/audioi2sin/I2SIn.h | 1 + shared-bindings/audioi2sin/I2SIn.c | 105 ++++++++---- shared-bindings/audioi2sin/I2SIn.h | 5 +- 7 files changed, 331 insertions(+), 69 deletions(-) diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot index b6af1d43f406a..7788c683fc094 100644 --- a/locale/circuitpython.pot +++ b/locale/circuitpython.pot @@ -186,6 +186,10 @@ msgstr "" msgid "%q must be 1 when %q is True" msgstr "" +#: shared-bindings/audioi2sin/I2SIn.c +msgid "%q must be 8, 16, 24, or 32" +msgstr "" + #: py/argcheck.c shared-bindings/gifio/GifWriter.c #: shared-module/gifio/OnDiskGif.c msgid "%q must be <= %d" @@ -1695,10 +1699,6 @@ msgstr "" msgid "Ok" msgstr "" -#: ports/raspberrypi/common-hal/audioi2sin/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 @@ -2702,6 +2702,10 @@ msgstr "" msgid "binary op %q not implemented" msgstr "" +#: ports/raspberrypi/common-hal/audioi2sin/I2SIn.c +msgid "bit_depth must be 16, 24, or 32" +msgstr "" + #: ports/espressif/common-hal/audiobusio/PDMIn.c #: ports/espressif/common-hal/audioi2sin/I2SIn.c msgid "bit_depth must be 8, 16, 24, or 32." diff --git a/ports/espressif/common-hal/audioi2sin/I2SIn.c b/ports/espressif/common-hal/audioi2sin/I2SIn.c index 3dc0d227b3e1a..782b691e5292a 100644 --- a/ports/espressif/common-hal/audioi2sin/I2SIn.c +++ b/ports/espressif/common-hal/audioi2sin/I2SIn.c @@ -20,8 +20,8 @@ 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) { + uint32_t sample_rate, uint8_t bit_depth, uint8_t output_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.")); @@ -66,6 +66,7 @@ void common_hal_audioi2sin_i2sin_construct(audioi2sin_i2sin_obj_t *self, self->mclk = main_clock; self->sample_rate = sample_rate; self->bit_depth = bit_depth; + self->output_bit_depth = output_bit_depth; self->mono = mono; self->samples_signed = samples_signed; @@ -113,6 +114,72 @@ void common_hal_audioi2sin_i2sin_deinit(audioi2sin_i2sin_obj_t *self) { self->mclk = NULL; } +// Sign-extend a raw I2S sample (in the low `in_depth` bits of `raw`) to a +// canonical int32 value. +static inline int32_t i2sin_normalize_signed(uint32_t raw, uint8_t in_depth) { + if (in_depth == 32) { + return (int32_t)raw; + } + if (in_depth == 24) { + uint32_t sign_bit = 0x800000u; + return (int32_t)((raw ^ sign_bit) - sign_bit); + } + if (in_depth == 16) { + return (int16_t)(raw & 0xffffu); + } + return (int8_t)(raw & 0xffu); +} + +// Read a single sample from the DMA scratch at the given byte offset for the +// configured input bit depth. +static inline uint32_t i2sin_read_raw(const uint8_t *src, uint8_t in_depth) { + if (in_depth == 8) { + return (uint32_t)(*src); + } + if (in_depth == 16) { + uint16_t v; + memcpy(&v, src, sizeof(v)); + return v; + } + uint32_t v; + memcpy(&v, src, sizeof(v)); + return v; +} + +// Convert `raw` from `in_depth` to `out_depth` (shift-only semantics, sign- +// preserving for signed) and write it to `buffer` at sample index `idx`. +// Output element size: 1 byte at 8, 2 bytes at 16, 4 bytes at 24 or 32. +static inline void i2sin_write_converted(void *buffer, uint32_t idx, + uint32_t raw, uint8_t in_depth, uint8_t out_depth, bool samples_signed) { + int32_t s = i2sin_normalize_signed(raw, in_depth); + int32_t shifted; + if (out_depth >= in_depth) { + shifted = (int32_t)((uint32_t)s << (out_depth - in_depth)); + } else { + shifted = s >> (in_depth - out_depth); + } + uint32_t u = (uint32_t)shifted; + if (!samples_signed) { + if (out_depth >= 32) { + u ^= 0x80000000u; + } else { + uint32_t mask = (1u << out_depth) - 1u; + u = (u & mask) ^ (1u << (out_depth - 1)); + } + } + switch (out_depth) { + case 8: + ((uint8_t *)buffer)[idx] = (uint8_t)(u & 0xffu); + break; + case 16: + ((uint16_t *)buffer)[idx] = (uint16_t)(u & 0xffffu); + break; + default: // 24 or 32 + ((uint32_t *)buffer)[idx] = u; + break; + } +} + // 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, @@ -158,6 +225,49 @@ uint32_t common_hal_audioi2sin_i2sin_record_to_buffer(audioi2sin_i2sin_obj_t *se element_size = 4; } + if (self->output_bit_depth != self->bit_depth) { + // Bit-depth conversion path: always read at input width into scratch, + // convert each sample into the user's buffer at output width. + const uint8_t in_depth = self->bit_depth; + const uint8_t out_depth = self->output_bit_depth; + const bool samples_signed = self->samples_signed; + uint8_t scratch[256]; + const size_t in_frame_bytes = 2 * element_size; + const size_t scratch_frames = sizeof(scratch) / in_frame_bytes; + uint32_t produced = 0; + while (produced < length) { + size_t want_frames; + if (self->mono) { + want_frames = length - produced; + } else { + want_frames = (length - produced + 1) / 2; + } + 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 * in_frame_bytes, &got_bytes, portMAX_DELAY); + CHECK_ESP_RESULT(err); + size_t got_frames = got_bytes / in_frame_bytes; + for (size_t i = 0; i < got_frames && produced < length; i++) { + const uint8_t *frame = scratch + i * in_frame_bytes; + uint32_t left_raw = i2sin_read_raw(frame, in_depth); + i2sin_write_converted(buffer, produced++, left_raw, + in_depth, out_depth, samples_signed); + if (!self->mono && produced < length) { + uint32_t right_raw = i2sin_read_raw(frame + element_size, in_depth); + i2sin_write_converted(buffer, produced++, right_raw, + in_depth, out_depth, samples_signed); + } + } + if (got_frames < want_frames) { + break; + } + } + return produced; + } + uint32_t produced; if (!self->mono) { size_t result = 0; @@ -206,6 +316,10 @@ uint8_t common_hal_audioi2sin_i2sin_get_bit_depth(audioi2sin_i2sin_obj_t *self) return self->bit_depth; } +uint8_t common_hal_audioi2sin_i2sin_get_output_bit_depth(audioi2sin_i2sin_obj_t *self) { + return self->output_bit_depth; +} + uint32_t common_hal_audioi2sin_i2sin_get_sample_rate(audioi2sin_i2sin_obj_t *self) { return self->sample_rate; } diff --git a/ports/espressif/common-hal/audioi2sin/I2SIn.h b/ports/espressif/common-hal/audioi2sin/I2SIn.h index 74e6eb83d0d3c..05b533ac89d85 100644 --- a/ports/espressif/common-hal/audioi2sin/I2SIn.h +++ b/ports/espressif/common-hal/audioi2sin/I2SIn.h @@ -23,6 +23,7 @@ typedef struct { const mcu_pin_obj_t *mclk; uint32_t sample_rate; uint8_t bit_depth; + uint8_t output_bit_depth; bool mono; bool samples_signed; } audioi2sin_i2sin_obj_t; diff --git a/ports/raspberrypi/common-hal/audioi2sin/I2SIn.c b/ports/raspberrypi/common-hal/audioi2sin/I2SIn.c index 906ca0ed55c46..b359b3f77fd1a 100644 --- a/ports/raspberrypi/common-hal/audioi2sin/I2SIn.c +++ b/ports/raspberrypi/common-hal/audioi2sin/I2SIn.c @@ -151,14 +151,14 @@ static const uint16_t i2sin_program_left_justified_swap_32[] = { 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) { + uint32_t sample_rate, uint8_t bit_depth, uint8_t output_bit_depth, + bool mono, bool left_justified, bool samples_signed) { if (main_clock != NULL) { mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_main_clock); } if (bit_depth != 16 && bit_depth != 24 && bit_depth != 32) { - mp_raise_NotImplementedError(MP_ERROR_TEXT("Only 16, 24, or 32 bit depth supported.")); + mp_raise_ValueError(MP_ERROR_TEXT("bit_depth must be 16, 24, or 32")); } // 24- and 32-bit recordings both clock 32 bits per channel; 24-bit MEMS @@ -225,6 +225,7 @@ void common_hal_audioi2sin_i2sin_construct(audioi2sin_i2sin_obj_t *self, uint32_t actual_frequency = common_hal_rp2pio_statemachine_get_frequency(&self->state_machine); self->sample_rate = actual_frequency / pio_clocks_per_frame; self->bit_depth = bit_depth; + self->output_bit_depth = output_bit_depth; self->mono = mono; self->samples_signed = samples_signed; self->left_justified = left_justified; @@ -292,6 +293,10 @@ uint8_t common_hal_audioi2sin_i2sin_get_bit_depth(audioi2sin_i2sin_obj_t *self) return self->bit_depth; } +uint8_t common_hal_audioi2sin_i2sin_get_output_bit_depth(audioi2sin_i2sin_obj_t *self) { + return self->output_bit_depth; +} + uint32_t common_hal_audioi2sin_i2sin_get_sample_rate(audioi2sin_i2sin_obj_t *self) { return self->sample_rate; } @@ -330,6 +335,69 @@ static inline uint32_t movesign24(uint32_t val) { return ((val & 0x800000u) << 8) | (val & 0x7fffffu); } +// Sign-extend a raw FIFO sample to a canonical int32 value. `raw` holds the +// sample as delivered by the PIO program for the given input bit depth and +// alignment. The returned int32 represents the same magnitude with the sign +// bit at bit 31. +static inline int32_t i2sin_normalize_signed(uint32_t raw, uint8_t in_depth, + bool left_justified) { + if (in_depth == 32) { + return (int32_t)raw; + } + if (in_depth == 24) { + if (left_justified) { + // value in bits 31..8, sign at 31; arithmetic shift right 8 + return (int32_t)raw >> 8; + } + // value in bits 23..0, sign at 23 + uint32_t sign_bit = 0x800000u; + return (int32_t)((raw ^ sign_bit) - sign_bit); + } + // 16-bit: low 16 bits, sign at 15 + return (int16_t)(raw & 0xffffu); +} + +// Write `raw` (input-depth bits, just-read FIFO sample) to `buffer` at sample +// index `idx`, converting from `in_depth` to `out_depth` and (if needed) +// flipping the sign bit for the unsigned-WAV convention. Output element size +// follows `out_depth`: 1 byte at 8, 2 bytes at 16, 4 bytes at 24 or 32. +// +// For signed 24-bit output, the int32 slot holds the sign-extended value +// (range -2^23 .. 2^23-1) — unlike the default `output_bit_depth=bit_depth=24` +// path which uses `movesign24`, the converted path returns proper two's +// complement so the result decodes correctly as int32. +static inline void i2sin_write_converted(void *buffer, uint32_t idx, + uint32_t raw, uint8_t in_depth, uint8_t out_depth, + bool samples_signed, bool left_justified) { + int32_t s = i2sin_normalize_signed(raw, in_depth, left_justified); + int32_t shifted; + if (out_depth >= in_depth) { + shifted = (int32_t)((uint32_t)s << (out_depth - in_depth)); + } else { + shifted = s >> (in_depth - out_depth); + } + uint32_t u = (uint32_t)shifted; + if (!samples_signed) { + if (out_depth >= 32) { + u ^= 0x80000000u; + } else { + uint32_t mask = (1u << out_depth) - 1u; + u = (u & mask) ^ (1u << (out_depth - 1)); + } + } + switch (out_depth) { + case 8: + ((uint8_t *)buffer)[idx] = (uint8_t)(u & 0xffu); + break; + case 16: + ((uint16_t *)buffer)[idx] = (uint16_t)(u & 0xffffu); + break; + default: // 24 or 32 + ((uint32_t *)buffer)[idx] = u; + break; + } +} + uint32_t common_hal_audioi2sin_i2sin_record_to_buffer(audioi2sin_i2sin_obj_t *self, void *buffer, uint32_t output_buffer_length) { uint32_t output_count = 0; @@ -339,21 +407,29 @@ uint32_t common_hal_audioi2sin_i2sin_record_to_buffer(audioi2sin_i2sin_obj_t *se // I2S delivers signed PCM. When the caller asked for unsigned samples, // flip the sign bit per sample (XOR with 0x8000 for 16-bit, 0x800000 for // 24-bit data in a 32-bit slot, 0x80000000 for 32-bit), matching the WAV - // convention. - const uint32_t flip16 = self->samples_signed ? 0u : 0x80008000u; - const uint32_t flip32 = self->samples_signed - ? 0u - : (self->bit_depth == 24 ? 0x800000u : 0x80000000u); - const bool fix_sign24 = self->bit_depth == 24 + // convention. The default (no-conversion) path applies the flip to the + // raw FIFO word before splitting into channels; the conversion path + // applies the flip per output sample at output bit width. + const bool convert = self->output_bit_depth != self->bit_depth; + const uint32_t flip16 = (!convert && !self->samples_signed) ? 0x80008000u : 0u; + const uint32_t flip32 = (!convert && !self->samples_signed) + ? (self->bit_depth == 24 ? 0x800000u : 0x80000000u) + : 0u; + const bool fix_sign24 = !convert + && self->bit_depth == 24 && self->samples_signed && !self->left_justified; + const uint8_t in_depth = self->bit_depth; + const uint8_t out_depth = self->output_bit_depth; + const bool left_justified = self->left_justified; + const bool samples_signed = self->samples_signed; if (self->bit_depth == 16) { // 16-bit mode auto-pushes one stereo frame per FIFO word. The DMA has // been streaming since construct time, so the ring already contains // settled data; drop the first 4 bytes once to discard a single // pre-record frame (matches the prior synchronous behaviour). - uint16_t *output = (uint16_t *)buffer; + uint16_t *output = convert ? NULL : (uint16_t *)buffer; while (output_count < output_buffer_length) { size_t write_pos = i2sin_write_pos(self); size_t avail = (write_pos + ring_size - self->read_pos) % ring_size; @@ -380,16 +456,30 @@ uint32_t common_hal_audioi2sin_i2sin_record_to_buffer(audioi2sin_i2sin_obj_t *se uint32_t frame = *(volatile uint32_t *)(self->ring + self->read_pos) ^ flip16; uint16_t left = (uint16_t)(frame & 0xffff); uint16_t right = (uint16_t)(frame >> 16); - if (self->mono) { - output[output_count++] = left; + if (!convert) { + if (self->mono) { + output[output_count++] = left; + } else { + output[output_count++] = left; + if (output_count >= output_buffer_length) { + self->read_pos = (self->read_pos + 4) % ring_size; + avail -= 4; + break; + } + output[output_count++] = right; + } } else { - output[output_count++] = left; - if (output_count >= output_buffer_length) { - self->read_pos = (self->read_pos + 4) % ring_size; - avail -= 4; - break; + i2sin_write_converted(buffer, output_count++, left, + in_depth, out_depth, samples_signed, left_justified); + if (!self->mono) { + if (output_count >= output_buffer_length) { + self->read_pos = (self->read_pos + 4) % ring_size; + avail -= 4; + break; + } + i2sin_write_converted(buffer, output_count++, right, + in_depth, out_depth, samples_signed, left_justified); } - output[output_count++] = right; } self->read_pos = (self->read_pos + 4) % ring_size; avail -= 4; @@ -403,7 +493,7 @@ uint32_t common_hal_audioi2sin_i2sin_record_to_buffer(audioi2sin_i2sin_obj_t *se // counter and always read an even number of words. half_size is a // multiple of 8, so reading 8-byte pairs stays aligned across the // ring wrap. - uint32_t *output = (uint32_t *)buffer; + uint32_t *output = convert ? NULL : (uint32_t *)buffer; while (output_count < output_buffer_length) { size_t write_pos = i2sin_write_pos(self); size_t avail = (write_pos + ring_size - self->read_pos) % ring_size; @@ -436,16 +526,30 @@ uint32_t common_hal_audioi2sin_i2sin_record_to_buffer(audioi2sin_i2sin_obj_t *se right = movesign24(right); left = movesign24(left); } - if (self->mono) { - output[output_count++] = left; + if (!convert) { + if (self->mono) { + output[output_count++] = left; + } else { + output[output_count++] = left; + if (output_count >= output_buffer_length) { + self->read_pos = (self->read_pos + 8) % ring_size; + avail -= 8; + break; + } + output[output_count++] = right; + } } else { - output[output_count++] = left; - if (output_count >= output_buffer_length) { - self->read_pos = (self->read_pos + 8) % ring_size; - avail -= 8; - break; + i2sin_write_converted(buffer, output_count++, left, + in_depth, out_depth, samples_signed, left_justified); + if (!self->mono) { + if (output_count >= output_buffer_length) { + self->read_pos = (self->read_pos + 8) % ring_size; + avail -= 8; + break; + } + i2sin_write_converted(buffer, output_count++, right, + in_depth, out_depth, samples_signed, left_justified); } - output[output_count++] = right; } self->read_pos = (self->read_pos + 8) % ring_size; avail -= 8; diff --git a/ports/raspberrypi/common-hal/audioi2sin/I2SIn.h b/ports/raspberrypi/common-hal/audioi2sin/I2SIn.h index bdd0529c94d7b..525c72e3c5e0a 100644 --- a/ports/raspberrypi/common-hal/audioi2sin/I2SIn.h +++ b/ports/raspberrypi/common-hal/audioi2sin/I2SIn.h @@ -15,6 +15,7 @@ typedef struct { mp_obj_base_t base; uint32_t sample_rate; uint8_t bit_depth; + uint8_t output_bit_depth; bool mono; bool samples_signed; bool left_justified; diff --git a/shared-bindings/audioi2sin/I2SIn.c b/shared-bindings/audioi2sin/I2SIn.c index 0cbdbdb49d3b3..f85daa01a34d4 100644 --- a/shared-bindings/audioi2sin/I2SIn.c +++ b/shared-bindings/audioi2sin/I2SIn.c @@ -28,6 +28,7 @@ //| main_clock: Optional[microcontroller.Pin] = None, //| sample_rate: int = 16000, //| bit_depth: int = 16, +//| output_bit_depth: Optional[int] = None, //| mono: bool = True, //| left_justified: bool = False, //| samples_signed: bool = True, @@ -44,26 +45,33 @@ //| :param ~microcontroller.Pin data: The data input pin //| :param ~microcontroller.Pin main_clock: The main clock pin. Not all ports support this. //| :param int sample_rate: Target sample rate of the resulting samples. Check `sample_rate` for actual value. -//| :param int bit_depth: Number of bits per sample. Must be 8, 16, 24, or 32. 8-bit only supported on espressif. +//| :param int bit_depth: Number of bits per sample on the I2S bus. Must be 8, 16, 24, or +//| 32. 8-bit only supported on espressif. The destination buffer typecode is determined +//| by ``output_bit_depth`` (or ``bit_depth`` when ``output_bit_depth`` is ``None``): //| -//| +----------------+-----------+----------------------+ -//| | samples_signed | bit_depth | Required typecode(s) | -//| +================+===========+======================+ -//| | True | 24 or 32 | ``'i'`` | -//| +----------------+-----------+----------------------+ -//| | True | 16 | ``'h'`` | -//| +----------------+-----------+----------------------+ -//| | True | 8 | ``'b'`` or BYTEARRAY | -//| +----------------+-----------+----------------------+ -//| | False | 24 or 32 | ``'I'`` | -//| +----------------+-----------+----------------------+ -//| | False | 16 | ``'H'`` | -//| +----------------+-----------+----------------------+ -//| | False | 8 | ``'B'`` or BYTEARRAY | -//| +----------------+-----------+----------------------+ +//| +----------------+------------------+----------------------+ +//| | samples_signed | output_bit_depth | Required typecode(s) | +//| +================+==================+======================+ +//| | True | 24 or 32 | ``'i'`` | +//| +----------------+------------------+----------------------+ +//| | True | 16 | ``'h'`` | +//| +----------------+------------------+----------------------+ +//| | True | 8 | ``'b'`` or BYTEARRAY | +//| +----------------+------------------+----------------------+ +//| | False | 24 or 32 | ``'I'`` | +//| +----------------+------------------+----------------------+ +//| | False | 16 | ``'H'`` | +//| +----------------+------------------+----------------------+ +//| | False | 8 | ``'B'`` or BYTEARRAY | +//| +----------------+------------------+----------------------+ //| //| Note that 24-bit samples from mics like the SPH0645LM4H / INMP441 are //| transported in 32-bit slots, so use ``bit_depth=32`` and an ``'I'`` buffer. +//| :param int output_bit_depth: If set, recorded samples are bit-shifted from +//| ``bit_depth`` to this width before being written to the destination buffer +//| (8, 16, 24, or 32). Widening pads the new LSBs with zero; narrowing arithmetic- +//| shifts the value right (sign-preserving when ``samples_signed`` is True). When +//| ``None`` (the default) the destination buffer holds samples at ``bit_depth``. //| :param bool mono: True when capturing a single channel of audio, captures two channels otherwise. //| :param bool left_justified: True when data bits are aligned with the word select clock. False //| when they are shifted by one to match classic I2S protocol. Set True for mics like the SPH0645LM4H. @@ -91,17 +99,19 @@ static mp_obj_t audioi2sin_i2sin_make_new(const mp_obj_type_t *type, size_t n_ar return NULL; // Not reachable. #else enum { ARG_bit_clock, ARG_word_select, ARG_data, ARG_main_clock, - ARG_sample_rate, ARG_bit_depth, ARG_mono, ARG_left_justified, ARG_samples_signed }; + ARG_sample_rate, ARG_bit_depth, ARG_output_bit_depth, + ARG_mono, ARG_left_justified, ARG_samples_signed }; static const mp_arg_t allowed_args[] = { - { MP_QSTR_bit_clock, MP_ARG_REQUIRED | MP_ARG_OBJ }, - { MP_QSTR_word_select, MP_ARG_REQUIRED | MP_ARG_OBJ }, - { MP_QSTR_data, MP_ARG_REQUIRED | MP_ARG_OBJ }, - { MP_QSTR_main_clock, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = mp_const_none} }, - { MP_QSTR_sample_rate, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 16000} }, - { MP_QSTR_bit_depth, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 16} }, - { MP_QSTR_mono, MP_ARG_KW_ONLY | MP_ARG_BOOL, {.u_bool = true} }, - { MP_QSTR_left_justified, MP_ARG_KW_ONLY | MP_ARG_BOOL, {.u_bool = false} }, - { MP_QSTR_samples_signed, MP_ARG_KW_ONLY | MP_ARG_BOOL, {.u_bool = true} }, + { MP_QSTR_bit_clock, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_word_select, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_data, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_main_clock, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = mp_const_none} }, + { MP_QSTR_sample_rate, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 16000} }, + { MP_QSTR_bit_depth, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 16} }, + { MP_QSTR_output_bit_depth, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = mp_const_none} }, + { MP_QSTR_mono, MP_ARG_KW_ONLY | MP_ARG_BOOL, {.u_bool = true} }, + { MP_QSTR_left_justified, MP_ARG_KW_ONLY | MP_ARG_BOOL, {.u_bool = false} }, + { MP_QSTR_samples_signed, MP_ARG_KW_ONLY | MP_ARG_BOOL, {.u_bool = true} }, }; mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); @@ -116,13 +126,24 @@ static mp_obj_t audioi2sin_i2sin_make_new(const mp_obj_type_t *type, size_t n_ar if (bit_depth % 8 != 0) { mp_raise_ValueError_varg(MP_ERROR_TEXT("%q must be multiple of 8."), MP_QSTR_bit_depth); } + uint8_t output_bit_depth; + mp_obj_t output_bit_depth_obj = args[ARG_output_bit_depth].u_obj; + if (output_bit_depth_obj == mp_const_none) { + output_bit_depth = bit_depth; + } else { + mp_int_t v = mp_obj_get_int(output_bit_depth_obj); + if (v != 8 && v != 16 && v != 24 && v != 32) { + mp_raise_ValueError_varg(MP_ERROR_TEXT("%q must be 8, 16, 24, or 32"), MP_QSTR_output_bit_depth); + } + output_bit_depth = (uint8_t)v; + } bool mono = args[ARG_mono].u_bool; bool left_justified = args[ARG_left_justified].u_bool; bool samples_signed = args[ARG_samples_signed].u_bool; audioi2sin_i2sin_obj_t *self = mp_obj_malloc_with_finaliser(audioi2sin_i2sin_obj_t, &audioi2sin_i2sin_type); common_hal_audioi2sin_i2sin_construct(self, bit_clock, word_select, data, main_clock, - sample_rate, bit_depth, mono, left_justified, samples_signed); + sample_rate, bit_depth, output_bit_depth, mono, left_justified, samples_signed); return MP_OBJ_FROM_PTR(self); #endif @@ -180,23 +201,23 @@ static mp_obj_t audioi2sin_i2sin_obj_record(mp_obj_t self_obj, mp_obj_t destinat if (bufinfo.len / mp_binary_get_size('@', bufinfo.typecode, NULL) < length) { mp_raise_ValueError(MP_ERROR_TEXT("Destination capacity is smaller than destination_length.")); } - uint8_t bit_depth = common_hal_audioi2sin_i2sin_get_bit_depth(self); + uint8_t output_bit_depth = common_hal_audioi2sin_i2sin_get_output_bit_depth(self); char error_type = ' '; bool samples_signed = common_hal_audioi2sin_i2sin_get_samples_signed(self); if (samples_signed) { - if ((bit_depth == 24 || bit_depth == 32) && bufinfo.typecode != 'i') { + if ((output_bit_depth == 24 || output_bit_depth == 32) && bufinfo.typecode != 'i') { error_type = 'i'; - } else if (bit_depth == 16 && bufinfo.typecode != 'h') { + } else if (output_bit_depth == 16 && bufinfo.typecode != 'h') { error_type = 'h'; - } else if (bit_depth == 8 && bufinfo.typecode != 'b' && bufinfo.typecode != BYTEARRAY_TYPECODE) { + } else if (output_bit_depth == 8 && bufinfo.typecode != 'b' && bufinfo.typecode != BYTEARRAY_TYPECODE) { error_type = 'b'; // NOTE: Not identifying as bytearray } } else { - if ((bit_depth == 24 || bit_depth == 32) && bufinfo.typecode != 'I') { + if ((output_bit_depth == 24 || output_bit_depth == 32) && bufinfo.typecode != 'I') { error_type = 'I'; - } else if (bit_depth == 16 && bufinfo.typecode != 'H') { + } else if (output_bit_depth == 16 && bufinfo.typecode != 'H') { error_type = 'H'; - } else if (bit_depth == 8 && bufinfo.typecode != 'B' && bufinfo.typecode != BYTEARRAY_TYPECODE) { + } else if (output_bit_depth == 8 && bufinfo.typecode != 'B' && bufinfo.typecode != BYTEARRAY_TYPECODE) { error_type = 'B'; } } @@ -240,6 +261,21 @@ MP_DEFINE_CONST_FUN_OBJ_1(audioi2sin_i2sin_get_bit_depth_obj, audioi2sin_i2sin_o MP_PROPERTY_GETTER(audioi2sin_i2sin_bit_depth_obj, (mp_obj_t)&audioi2sin_i2sin_get_bit_depth_obj); +//| output_bit_depth: int +//| """The bit depth of samples written to the destination buffer. Equals ``bit_depth`` when +//| ``output_bit_depth`` was not supplied (or was ``None``) at construction time. (read-only)""" +//| +//| +static mp_obj_t audioi2sin_i2sin_obj_get_output_bit_depth(mp_obj_t self_in) { + audioi2sin_i2sin_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return MP_OBJ_NEW_SMALL_INT(common_hal_audioi2sin_i2sin_get_output_bit_depth(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(audioi2sin_i2sin_get_output_bit_depth_obj, audioi2sin_i2sin_obj_get_output_bit_depth); + +MP_PROPERTY_GETTER(audioi2sin_i2sin_output_bit_depth_obj, + (mp_obj_t)&audioi2sin_i2sin_get_output_bit_depth_obj); + //| samples_signed: bool //| """True if recorded samples are signed PCM, False for unsigned. (read-only)""" //| @@ -262,6 +298,7 @@ static const mp_rom_map_elem_t audioi2sin_i2sin_locals_dict_table[] = { { MP_ROM_QSTR(MP_QSTR_record), MP_ROM_PTR(&audioi2sin_i2sin_record_obj) }, { MP_ROM_QSTR(MP_QSTR_sample_rate), MP_ROM_PTR(&audioi2sin_i2sin_sample_rate_obj) }, { MP_ROM_QSTR(MP_QSTR_bit_depth), MP_ROM_PTR(&audioi2sin_i2sin_bit_depth_obj) }, + { MP_ROM_QSTR(MP_QSTR_output_bit_depth), MP_ROM_PTR(&audioi2sin_i2sin_output_bit_depth_obj) }, { MP_ROM_QSTR(MP_QSTR_samples_signed), MP_ROM_PTR(&audioi2sin_i2sin_samples_signed_obj) }, }; static MP_DEFINE_CONST_DICT(audioi2sin_i2sin_locals_dict, audioi2sin_i2sin_locals_dict_table); diff --git a/shared-bindings/audioi2sin/I2SIn.h b/shared-bindings/audioi2sin/I2SIn.h index e75bdf2d34bc0..dc9c671741a36 100644 --- a/shared-bindings/audioi2sin/I2SIn.h +++ b/shared-bindings/audioi2sin/I2SIn.h @@ -18,13 +18,14 @@ extern const mp_obj_type_t audioi2sin_i2sin_type; 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); + uint32_t sample_rate, uint8_t bit_depth, uint8_t output_bit_depth, + bool mono, bool left_justified, bool samples_signed); void common_hal_audioi2sin_i2sin_deinit(audioi2sin_i2sin_obj_t *self); bool common_hal_audioi2sin_i2sin_deinited(audioi2sin_i2sin_obj_t *self); uint32_t common_hal_audioi2sin_i2sin_record_to_buffer(audioi2sin_i2sin_obj_t *self, void *buffer, uint32_t length); uint8_t common_hal_audioi2sin_i2sin_get_bit_depth(audioi2sin_i2sin_obj_t *self); +uint8_t common_hal_audioi2sin_i2sin_get_output_bit_depth(audioi2sin_i2sin_obj_t *self); uint32_t common_hal_audioi2sin_i2sin_get_sample_rate(audioi2sin_i2sin_obj_t *self); bool common_hal_audioi2sin_i2sin_get_samples_signed(audioi2sin_i2sin_obj_t *self); #endif From cf41db1900c4ea9e0c75d76a1aa1d9d6eaa8bc14 Mon Sep 17 00:00:00 2001 From: foamyguy Date: Fri, 15 May 2026 09:46:39 -0500 Subject: [PATCH 8/9] output_bit_depth manual test --- .../i2sin_record_sdcard_output_bit_depth.py | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 tests/circuitpython-manual/audioi2sin/i2sin_record_sdcard_output_bit_depth.py diff --git a/tests/circuitpython-manual/audioi2sin/i2sin_record_sdcard_output_bit_depth.py b/tests/circuitpython-manual/audioi2sin/i2sin_record_sdcard_output_bit_depth.py new file mode 100644 index 0000000000000..cc2d2e78c2966 --- /dev/null +++ b/tests/circuitpython-manual/audioi2sin/i2sin_record_sdcard_output_bit_depth.py @@ -0,0 +1,100 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 Tim Cocks for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +# Record a longer I2S audio capture to a WAV file on an SD card, using the +# `output_bit_depth` argument to have the driver downconvert each 32-bit slot +# to signed 16-bit PCM. Compared to `i2sin_record_sdcard.py`, this avoids the +# Python-side shift loop and writes directly from the recording buffer. +# +# Produces /sd/talk.wav. Set left_justified=True for SPH0645LM4H mics. + +import array +import struct +import time + +import board +import sdcardio +import storage +import audioi2sin + +# ---- Recording config ------------------------------------------------------ +SAMPLE_RATE = 16000 +RECORD_SECONDS = 10 +CHUNK_SAMPLES = 1024 # samples captured per record() call +OUTPUT_PATH = "/sd/talk.wav" + +# ---- Mount SD -------------------------------------------------------------- +spi = board.SPI() +sdcard = sdcardio.SDCard(spi, cs=board.D10, baudrate=24_000_000) +vfs = storage.VfsFat(sdcard) +storage.mount(vfs, "/sd") + +# ---- Mic ------------------------------------------------------------------- +# 24-bit MEMS mics ride in 32-bit slots. Ask the driver to downconvert each +# slot to a signed 16-bit PCM sample, so `record()` writes straight into a +# WAV-ready 'h' buffer. +mic = audioi2sin.I2SIn( + bit_clock=board.D5, + word_select=board.D6, + data=board.D9, + sample_rate=SAMPLE_RATE, + bit_depth=32, + output_bit_depth=16, + mono=True, + left_justified=False, # True for SPH0645LM4H +) + +actual_rate = mic.sample_rate +print("Recording at", actual_rate, "Hz for", RECORD_SECONDS, "s ->", OUTPUT_PATH) + +pcm16 = array.array("h", [0] * CHUNK_SAMPLES) + + +def write_wav_header(f, sample_rate, num_samples, bits_per_sample=16, channels=1): + byte_rate = sample_rate * channels * bits_per_sample // 8 + block_align = channels * bits_per_sample // 8 + data_size = num_samples * block_align + f.write(b"RIFF") + f.write(struct.pack(" Date: Fri, 15 May 2026 11:04:02 -0500 Subject: [PATCH 9/9] improve argument validation logic and error messages --- locale/circuitpython.pot | 11 +++++------ ports/espressif/common-hal/audioi2sin/I2SIn.c | 4 ---- ports/raspberrypi/common-hal/audioi2sin/I2SIn.c | 2 +- shared-bindings/audioi2sin/I2SIn.c | 4 ++-- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot index 7788c683fc094..20425d2d77ff9 100644 --- a/locale/circuitpython.pot +++ b/locale/circuitpython.pot @@ -186,6 +186,10 @@ msgstr "" msgid "%q must be 1 when %q is True" msgstr "" +#: ports/raspberrypi/common-hal/audioi2sin/I2SIn.c +msgid "%q must be 16, 24, or 32" +msgstr "" + #: shared-bindings/audioi2sin/I2SIn.c msgid "%q must be 8, 16, 24, or 32" msgstr "" @@ -223,7 +227,7 @@ msgstr "" msgid "%q must be array of type 'h'" msgstr "" -#: shared-bindings/audiobusio/PDMIn.c shared-bindings/audioi2sin/I2SIn.c +#: shared-bindings/audiobusio/PDMIn.c msgid "%q must be multiple of 8." msgstr "" @@ -2702,12 +2706,7 @@ msgstr "" msgid "binary op %q not implemented" msgstr "" -#: ports/raspberrypi/common-hal/audioi2sin/I2SIn.c -msgid "bit_depth must be 16, 24, or 32" -msgstr "" - #: ports/espressif/common-hal/audiobusio/PDMIn.c -#: ports/espressif/common-hal/audioi2sin/I2SIn.c msgid "bit_depth must be 8, 16, 24, or 32." msgstr "" diff --git a/ports/espressif/common-hal/audioi2sin/I2SIn.c b/ports/espressif/common-hal/audioi2sin/I2SIn.c index 782b691e5292a..9953f221b7f35 100644 --- a/ports/espressif/common-hal/audioi2sin/I2SIn.c +++ b/ports/espressif/common-hal/audioi2sin/I2SIn.c @@ -23,10 +23,6 @@ void common_hal_audioi2sin_i2sin_construct(audioi2sin_i2sin_obj_t *self, uint32_t sample_rate, uint8_t bit_depth, uint8_t output_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.")); - } - 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); diff --git a/ports/raspberrypi/common-hal/audioi2sin/I2SIn.c b/ports/raspberrypi/common-hal/audioi2sin/I2SIn.c index b359b3f77fd1a..0de4fe69b8cbc 100644 --- a/ports/raspberrypi/common-hal/audioi2sin/I2SIn.c +++ b/ports/raspberrypi/common-hal/audioi2sin/I2SIn.c @@ -158,7 +158,7 @@ void common_hal_audioi2sin_i2sin_construct(audioi2sin_i2sin_obj_t *self, mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_main_clock); } if (bit_depth != 16 && bit_depth != 24 && bit_depth != 32) { - mp_raise_ValueError(MP_ERROR_TEXT("bit_depth must be 16, 24, or 32")); + mp_raise_ValueError_varg(MP_ERROR_TEXT("%q must be 16, 24, or 32"), MP_QSTR_bit_depth); } // 24- and 32-bit recordings both clock 32 bits per channel; 24-bit MEMS diff --git a/shared-bindings/audioi2sin/I2SIn.c b/shared-bindings/audioi2sin/I2SIn.c index f85daa01a34d4..16ba7bf76f341 100644 --- a/shared-bindings/audioi2sin/I2SIn.c +++ b/shared-bindings/audioi2sin/I2SIn.c @@ -123,8 +123,8 @@ static mp_obj_t audioi2sin_i2sin_make_new(const mp_obj_type_t *type, size_t n_ar uint32_t sample_rate = args[ARG_sample_rate].u_int; uint8_t bit_depth = args[ARG_bit_depth].u_int; - if (bit_depth % 8 != 0) { - mp_raise_ValueError_varg(MP_ERROR_TEXT("%q must be multiple of 8."), MP_QSTR_bit_depth); + if (bit_depth != 8 && bit_depth != 16 && bit_depth != 24 && bit_depth != 32) { + mp_raise_ValueError_varg(MP_ERROR_TEXT("%q must be 8, 16, 24, or 32"), MP_QSTR_bit_depth); } uint8_t output_bit_depth; mp_obj_t output_bit_depth_obj = args[ARG_output_bit_depth].u_obj;