diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot index 28712b4f1cfa3..6661561e205f9 100644 --- a/locale/circuitpython.pot +++ b/locale/circuitpython.pot @@ -211,6 +211,7 @@ msgstr "" msgid "%q must be array of type 'h'" msgstr "" +#: shared-bindings/audiobusio/I2S.c shared-bindings/audiobusio/I2SIn.c #: shared-bindings/audiobusio/PDMIn.c msgid "%q must be multiple of 8." msgstr "" @@ -630,6 +631,8 @@ msgstr "" msgid "Below minimum frame rate" msgstr "" +#: ports/raspberrypi/common-hal/audiobusio/I2S.c +#: ports/raspberrypi/common-hal/audiobusio/I2SIn.c #: ports/raspberrypi/common-hal/audiobusio/I2SOut.c msgid "Bit clock and word select must be sequential GPIO pins" msgstr "" @@ -638,6 +641,10 @@ msgstr "" msgid "Bitmap size and bits per value must match" msgstr "" +#: ports/raspberrypi/common-hal/audiobusio/I2S.c +msgid "Bits per sample cannot be greater than input." +msgstr "" + #: supervisor/shared/safe_mode.c msgid "Boot device must be first (interface #0)." msgstr "" @@ -1435,6 +1442,8 @@ msgstr "" #: ports/atmel-samd/common-hal/audiobusio/I2SOut.c #: ports/atmel-samd/common-hal/audioio/AudioOut.c +#: ports/raspberrypi/common-hal/audiobusio/I2S.c +#: ports/raspberrypi/common-hal/audiobusio/I2SIn.c #: ports/raspberrypi/common-hal/audiobusio/I2SOut.c #: ports/raspberrypi/common-hal/audiopwmio/PWMAudioOut.c msgid "No DMA channel found" @@ -1467,6 +1476,14 @@ msgstr "" msgid "No connection: length cannot be determined" msgstr "" +#: ports/raspberrypi/common-hal/audiobusio/I2S.c +msgid "No data in" +msgstr "" + +#: ports/raspberrypi/common-hal/audiobusio/I2S.c +msgid "No data out" +msgstr "" + #: shared-bindings/board/__init__.c msgid "No default %q bus" msgstr "" @@ -1546,8 +1563,8 @@ msgstr "" msgid "Not connected" msgstr "" -#: shared-bindings/audiobusio/I2SOut.c shared-bindings/audioio/AudioOut.c -#: shared-bindings/audiopwmio/PWMAudioOut.c +#: shared-bindings/audiobusio/I2S.c shared-bindings/audiobusio/I2SOut.c +#: shared-bindings/audioio/AudioOut.c shared-bindings/audiopwmio/PWMAudioOut.c msgid "Not playing" msgstr "" @@ -1904,6 +1921,10 @@ msgstr "" msgid "SPI re-init" msgstr "" +#: ports/raspberrypi/common-hal/audiobusio/I2S.c +msgid "Sample rate must match." +msgstr "" + #: shared-bindings/is31fl3741/FrameBuffer.c msgid "Scale dimensions must divide by 3" msgstr "" @@ -1922,6 +1943,10 @@ msgstr "" msgid "Server side context cannot have hostname" msgstr "" +#: ports/raspberrypi/common-hal/audiobusio/I2S.c +msgid "Single channel output not supported." +msgstr "" + #: ports/cxd56/common-hal/camera/Camera.c msgid "Size not supported" msgstr "" @@ -2024,6 +2049,7 @@ msgstr "" msgid "Too many channels in sample" msgstr "" +#: ports/raspberrypi/common-hal/audiobusio/I2S.c #: ports/raspberrypi/common-hal/audiobusio/I2SOut.c msgid "Too many channels in sample." msgstr "" @@ -2113,6 +2139,8 @@ msgstr "" #: ports/atmel-samd/common-hal/audiobusio/I2SOut.c #: ports/atmel-samd/common-hal/audioio/AudioOut.c +#: ports/raspberrypi/common-hal/audiobusio/I2S.c +#: ports/raspberrypi/common-hal/audiobusio/I2SIn.c #: ports/raspberrypi/common-hal/audiobusio/I2SOut.c #: ports/raspberrypi/common-hal/audiopwmio/PWMAudioOut.c msgid "Unable to allocate buffers for signed conversion" diff --git a/ports/atmel-samd/common-hal/audiobusio/I2S.c b/ports/atmel-samd/common-hal/audiobusio/I2S.c new file mode 100644 index 0000000000000..587e33852bcac --- /dev/null +++ b/ports/atmel-samd/common-hal/audiobusio/I2S.c @@ -0,0 +1,7 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT + +// Although IS2 is not enabled on this family, this file is still required for the build to pass diff --git a/ports/atmel-samd/common-hal/audiobusio/I2S.h b/ports/atmel-samd/common-hal/audiobusio/I2S.h new file mode 100644 index 0000000000000..53e15e22f2820 --- /dev/null +++ b/ports/atmel-samd/common-hal/audiobusio/I2S.h @@ -0,0 +1,11 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "shared-module/audiocore/__init__.h" + +// Although IS2 is not enabled on this family, this file is still required for the build to pass diff --git a/ports/atmel-samd/common-hal/audiobusio/I2SIn.c b/ports/atmel-samd/common-hal/audiobusio/I2SIn.c new file mode 100644 index 0000000000000..1dd80b347c86a --- /dev/null +++ b/ports/atmel-samd/common-hal/audiobusio/I2SIn.c @@ -0,0 +1,7 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT + +// Although IS2In is not enabled on this family, this file is still required for the build to pass diff --git a/ports/atmel-samd/common-hal/audiobusio/I2SIn.h b/ports/atmel-samd/common-hal/audiobusio/I2SIn.h new file mode 100644 index 0000000000000..bb12b78e6f4a7 --- /dev/null +++ b/ports/atmel-samd/common-hal/audiobusio/I2SIn.h @@ -0,0 +1,11 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "shared-module/audiocore/__init__.h" + +// Although IS2In is not enabled on this family, this file is still required for the build to pass diff --git a/ports/espressif/common-hal/audiobusio/I2S.c b/ports/espressif/common-hal/audiobusio/I2S.c new file mode 100644 index 0000000000000..587e33852bcac --- /dev/null +++ b/ports/espressif/common-hal/audiobusio/I2S.c @@ -0,0 +1,7 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT + +// Although IS2 is not enabled on this family, this file is still required for the build to pass diff --git a/ports/espressif/common-hal/audiobusio/I2S.h b/ports/espressif/common-hal/audiobusio/I2S.h new file mode 100644 index 0000000000000..53e15e22f2820 --- /dev/null +++ b/ports/espressif/common-hal/audiobusio/I2S.h @@ -0,0 +1,11 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "shared-module/audiocore/__init__.h" + +// Although IS2 is not enabled on this family, this file is still required for the build to pass diff --git a/ports/espressif/common-hal/audiobusio/I2SIn.c b/ports/espressif/common-hal/audiobusio/I2SIn.c new file mode 100644 index 0000000000000..1dd80b347c86a --- /dev/null +++ b/ports/espressif/common-hal/audiobusio/I2SIn.c @@ -0,0 +1,7 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT + +// Although IS2In is not enabled on this family, this file is still required for the build to pass diff --git a/ports/espressif/common-hal/audiobusio/I2SIn.h b/ports/espressif/common-hal/audiobusio/I2SIn.h new file mode 100644 index 0000000000000..bb12b78e6f4a7 --- /dev/null +++ b/ports/espressif/common-hal/audiobusio/I2SIn.h @@ -0,0 +1,11 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "shared-module/audiocore/__init__.h" + +// Although IS2In is not enabled on this family, this file is still required for the build to pass diff --git a/ports/mimxrt10xx/common-hal/audiobusio/I2S.c b/ports/mimxrt10xx/common-hal/audiobusio/I2S.c new file mode 100644 index 0000000000000..587e33852bcac --- /dev/null +++ b/ports/mimxrt10xx/common-hal/audiobusio/I2S.c @@ -0,0 +1,7 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT + +// Although IS2 is not enabled on this family, this file is still required for the build to pass diff --git a/ports/mimxrt10xx/common-hal/audiobusio/I2S.h b/ports/mimxrt10xx/common-hal/audiobusio/I2S.h new file mode 100644 index 0000000000000..53e15e22f2820 --- /dev/null +++ b/ports/mimxrt10xx/common-hal/audiobusio/I2S.h @@ -0,0 +1,11 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "shared-module/audiocore/__init__.h" + +// Although IS2 is not enabled on this family, this file is still required for the build to pass diff --git a/ports/mimxrt10xx/common-hal/audiobusio/I2SIn.c b/ports/mimxrt10xx/common-hal/audiobusio/I2SIn.c new file mode 100644 index 0000000000000..1dd80b347c86a --- /dev/null +++ b/ports/mimxrt10xx/common-hal/audiobusio/I2SIn.c @@ -0,0 +1,7 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT + +// Although IS2In is not enabled on this family, this file is still required for the build to pass diff --git a/ports/mimxrt10xx/common-hal/audiobusio/I2SIn.h b/ports/mimxrt10xx/common-hal/audiobusio/I2SIn.h new file mode 100644 index 0000000000000..bb12b78e6f4a7 --- /dev/null +++ b/ports/mimxrt10xx/common-hal/audiobusio/I2SIn.h @@ -0,0 +1,11 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "shared-module/audiocore/__init__.h" + +// Although IS2In is not enabled on this family, this file is still required for the build to pass diff --git a/ports/nordic/common-hal/audiobusio/I2S.c b/ports/nordic/common-hal/audiobusio/I2S.c new file mode 100644 index 0000000000000..587e33852bcac --- /dev/null +++ b/ports/nordic/common-hal/audiobusio/I2S.c @@ -0,0 +1,7 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT + +// Although IS2 is not enabled on this family, this file is still required for the build to pass diff --git a/ports/nordic/common-hal/audiobusio/I2S.h b/ports/nordic/common-hal/audiobusio/I2S.h new file mode 100644 index 0000000000000..53e15e22f2820 --- /dev/null +++ b/ports/nordic/common-hal/audiobusio/I2S.h @@ -0,0 +1,11 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "shared-module/audiocore/__init__.h" + +// Although IS2 is not enabled on this family, this file is still required for the build to pass diff --git a/ports/nordic/common-hal/audiobusio/I2SIn.c b/ports/nordic/common-hal/audiobusio/I2SIn.c new file mode 100644 index 0000000000000..1dd80b347c86a --- /dev/null +++ b/ports/nordic/common-hal/audiobusio/I2SIn.c @@ -0,0 +1,7 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT + +// Although IS2In is not enabled on this family, this file is still required for the build to pass diff --git a/ports/nordic/common-hal/audiobusio/I2SIn.h b/ports/nordic/common-hal/audiobusio/I2SIn.h new file mode 100644 index 0000000000000..bb12b78e6f4a7 --- /dev/null +++ b/ports/nordic/common-hal/audiobusio/I2SIn.h @@ -0,0 +1,11 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "shared-module/audiocore/__init__.h" + +// Although IS2In is not enabled on this family, this file is still required for the build to pass diff --git a/ports/raspberrypi/audio_dma.c b/ports/raspberrypi/audio_dma.c index a067f36287ac9..eb63f4a67a6bb 100644 --- a/ports/raspberrypi/audio_dma.c +++ b/ports/raspberrypi/audio_dma.c @@ -23,11 +23,11 @@ void audio_dma_reset(void) { for (size_t channel = 0; channel < NUM_DMA_CHANNELS; channel++) { - if (MP_STATE_PORT(playing_audio)[channel] == NULL) { - continue; + if (MP_STATE_PORT(playing_audio)[channel] != NULL) { + audio_dma_stop_output(MP_STATE_PORT(playing_audio)[channel]); + } else if (MP_STATE_PORT(recording_audio)[channel] != NULL) { + audio_dma_stop_input(MP_STATE_PORT(recording_audio)[channel]); } - - audio_dma_stop(MP_STATE_PORT(playing_audio)[channel]); } } @@ -118,7 +118,11 @@ static size_t audio_dma_convert_samples(audio_dma_t *dma, uint8_t *input, uint32 // buffer_idx is 0 or 1. static void audio_dma_load_next_block(audio_dma_t *dma, size_t buffer_idx) { - size_t dma_channel = dma->channel[buffer_idx]; + if (!dma->output_register_address) { + return; + } + + size_t dma_channel = dma->output_channel[buffer_idx]; audioio_get_buffer_result_t get_buffer_result; uint8_t *sample_buffer; @@ -127,7 +131,7 @@ static void audio_dma_load_next_block(audio_dma_t *dma, size_t buffer_idx) { dma->single_channel_output, dma->audio_channel, &sample_buffer, &sample_buffer_length); if (get_buffer_result == GET_BUFFER_ERROR) { - audio_dma_stop(dma); + audio_dma_stop_output(dma); return; } @@ -137,9 +141,9 @@ static void audio_dma_load_next_block(audio_dma_t *dma, size_t buffer_idx) { size_t output_length_used = audio_dma_convert_samples( dma, sample_buffer, sample_buffer_length, - dma->buffer[buffer_idx], dma->buffer_length[buffer_idx]); + dma->output_buffer[buffer_idx], dma->output_buffer_length[buffer_idx]); - dma_channel_set_read_addr(dma_channel, dma->buffer[buffer_idx], false /* trigger */); + dma_channel_set_read_addr(dma_channel, dma->output_buffer[buffer_idx], false /* trigger */); dma_channel_set_trans_count(dma_channel, output_length_used / dma->output_size, false /* trigger */); if (get_buffer_result == GET_BUFFER_DONE) { @@ -153,18 +157,18 @@ static void audio_dma_load_next_block(audio_dma_t *dma, size_t buffer_idx) { (dma_channel << DMA_CH0_CTRL_TRIG_CHAIN_TO_LSB); if (output_length_used == 0 && - !dma_channel_is_busy(dma->channel[0]) && - !dma_channel_is_busy(dma->channel[1])) { + !dma_channel_is_busy(dma->output_channel[0]) && + !dma_channel_is_busy(dma->output_channel[1])) { // No data has been read, and both DMA channels have now finished, so it's safe to stop. - audio_dma_stop(dma); + audio_dma_stop_output(dma); dma->playing_in_progress = false; } } } } -// Playback should be shutdown before calling this. -audio_dma_result audio_dma_setup_playback( +// Playback and recording should be shutdown before calling this. +audio_dma_result audio_dma_setup( audio_dma_t *dma, mp_obj_t sample, bool loop, @@ -173,24 +177,57 @@ audio_dma_result audio_dma_setup_playback( bool output_signed, uint8_t output_resolution, uint32_t output_register_address, - uint8_t dma_trigger_source, + uint8_t output_dma_trigger_source, + uint32_t input_register_address, + uint8_t input_dma_trigger_source, bool swap_channel) { - // Use two DMA channels to play because the DMA can't wrap to itself without the - // buffer being power of two aligned. - int dma_channel_0_maybe = dma_claim_unused_channel(false); - if (dma_channel_0_maybe < 0) { - return AUDIO_DMA_DMA_BUSY; - } + int output_dma_channel_0_maybe = -1; + int output_dma_channel_1_maybe = -1; - int dma_channel_1_maybe = dma_claim_unused_channel(false); - if (dma_channel_1_maybe < 0) { - dma_channel_unclaim((uint)dma_channel_0_maybe); - return AUDIO_DMA_DMA_BUSY; + if (output_register_address) { + // Use two DMA channels to play because the DMA can't wrap to itself without the + // buffer being power of two aligned. + output_dma_channel_0_maybe = dma_claim_unused_channel(false); + if (output_dma_channel_0_maybe < 0) { + return AUDIO_DMA_DMA_BUSY; + } + + output_dma_channel_1_maybe = dma_claim_unused_channel(false); + if (output_dma_channel_1_maybe < 0) { + dma_channel_unclaim((uint)output_dma_channel_0_maybe); + return AUDIO_DMA_DMA_BUSY; + } + + dma->output_channel[0] = (uint8_t)output_dma_channel_0_maybe; + dma->output_channel[1] = (uint8_t)output_dma_channel_1_maybe; } - dma->channel[0] = (uint8_t)dma_channel_0_maybe; - dma->channel[1] = (uint8_t)dma_channel_1_maybe; + if (input_register_address) { + // Use two DMA channels to record because the DMA can't wrap to itself without the + // buffer being power of two aligned. + int input_dma_channel_0_maybe = dma_claim_unused_channel(false); + if (input_dma_channel_0_maybe < 0) { + if (output_register_address) { + dma_channel_unclaim((uint)output_dma_channel_0_maybe); + dma_channel_unclaim((uint)output_dma_channel_1_maybe); + } + return AUDIO_DMA_DMA_BUSY; + } + + int input_dma_channel_1_maybe = dma_claim_unused_channel(false); + if (input_dma_channel_1_maybe < 0) { + if (output_register_address) { + dma_channel_unclaim((uint)output_dma_channel_0_maybe); + dma_channel_unclaim((uint)output_dma_channel_1_maybe); + } + dma_channel_unclaim((uint)input_dma_channel_0_maybe); + return AUDIO_DMA_DMA_BUSY; + } + + dma->input_channel[0] = (uint8_t)input_dma_channel_0_maybe; + dma->input_channel[1] = (uint8_t)input_dma_channel_1_maybe; + } dma->sample = sample; dma->loop = loop; @@ -203,9 +240,12 @@ audio_dma_result audio_dma_setup_playback( dma->output_resolution = output_resolution; dma->sample_resolution = audiosample_bits_per_sample(sample); dma->output_register_address = output_register_address; + dma->input_register_address = input_register_address; dma->swap_channel = swap_channel; - audiosample_reset_buffer(sample, single_channel_output, audio_channel); + if (output_register_address) { + audiosample_reset_buffer(sample, single_channel_output, audio_channel); + } bool single_buffer; // True if data fits in one single buffer. @@ -225,18 +265,36 @@ audio_dma_result audio_dma_setup_playback( max_buffer_length /= dma->sample_spacing; } - dma->buffer[0] = (uint8_t *)m_realloc(dma->buffer[0], max_buffer_length); - dma->buffer_length[0] = max_buffer_length; - if (dma->buffer[0] == NULL) { - return AUDIO_DMA_MEMORY_ERROR; + if (output_register_address) { + dma->output_buffer[0] = (uint8_t *)m_realloc(dma->output_buffer[0], max_buffer_length); + dma->output_buffer_length[0] = max_buffer_length; + if (dma->output_buffer[0] == NULL) { + return AUDIO_DMA_MEMORY_ERROR; + } + + if (!single_buffer) { + dma->output_buffer[1] = (uint8_t *)m_realloc(dma->output_buffer[1], max_buffer_length); + dma->output_buffer_length[1] = max_buffer_length; + if (dma->output_buffer[1] == NULL) { + return AUDIO_DMA_MEMORY_ERROR; + } + } } - if (!single_buffer) { - dma->buffer[1] = (uint8_t *)m_realloc(dma->buffer[1], max_buffer_length); - dma->buffer_length[1] = max_buffer_length; - if (dma->buffer[1] == NULL) { + if (input_register_address) { + dma->input_buffer[0] = (uint8_t *)m_realloc(dma->input_buffer[0], max_buffer_length); + dma->input_buffer_length[0] = max_buffer_length; + if (dma->input_buffer[0] == NULL) { return AUDIO_DMA_MEMORY_ERROR; } + + if (!single_buffer) { + dma->input_buffer[1] = (uint8_t *)m_realloc(dma->input_buffer[1], max_buffer_length); + dma->input_buffer_length[1] = max_buffer_length; + if (dma->input_buffer[1] == NULL) { + return AUDIO_DMA_MEMORY_ERROR; + } + } } dma->signed_to_unsigned = !output_signed && samples_signed; @@ -259,68 +317,161 @@ audio_dma_result audio_dma_setup_playback( } for (size_t i = 0; i < 2; i++) { - dma_channel_config c = dma_channel_get_default_config(dma->channel[i]); - channel_config_set_transfer_data_size(&c, dma_size); - channel_config_set_dreq(&c, dma_trigger_source); - channel_config_set_read_increment(&c, true); - channel_config_set_write_increment(&c, false); - - // Chain to the other channel by default. - channel_config_set_chain_to(&c, dma->channel[(i + 1) % 2]); - dma_channel_set_config(dma->channel[i], &c, false /* trigger */); - - dma_channel_set_write_addr(dma->channel[i], (void *)output_register_address, false /* trigger */); - } - - // We keep the audio_dma_t for internal use and the sample as a root pointer because it - // contains the audiodma structure. - MP_STATE_PORT(playing_audio)[dma->channel[0]] = dma; - MP_STATE_PORT(playing_audio)[dma->channel[1]] = dma; - - // Load the first two blocks up front. - audio_dma_load_next_block(dma, 0); - if (!single_buffer) { - audio_dma_load_next_block(dma, 1); - } - - // Special case the DMA for a single buffer. It's commonly used for a single wave length of sound - // and may be short. Therefore, we use DMA chaining to loop quickly without involving interrupts. - // On the RP2040 we chain by having a second DMA writing to the config registers of the first. - // Read and write addresses change with DMA so we need to reset the read address back to the - // start of the sample. - if (single_buffer) { - dma_channel_config c = dma_channel_get_default_config(dma->channel[1]); - channel_config_set_transfer_data_size(&c, DMA_SIZE_32); - channel_config_set_dreq(&c, 0x3f); // dma as fast as possible - channel_config_set_read_increment(&c, false); - channel_config_set_write_increment(&c, false); - channel_config_set_chain_to(&c, dma->channel[1]); // Chain to ourselves so we stop. - dma_channel_configure(dma->channel[1], &c, - &dma_hw->ch[dma->channel[0]].al3_read_addr_trig, // write address - &dma->buffer[0], // read address - 1, // transaction count - false); // trigger - } else { - // Enable our DMA channels on DMA_IRQ_0 to the CPU. This will wake us up when - // we're WFI. - dma_hw->inte0 |= (1 << dma->channel[0]) | (1 << dma->channel[1]); - irq_set_mask_enabled(1 << DMA_IRQ_0, true); + dma_channel_config c; + if (output_register_address) { + c = dma_channel_get_default_config(dma->output_channel[i]); + channel_config_set_transfer_data_size(&c, dma_size); + channel_config_set_dreq(&c, output_dma_trigger_source); + channel_config_set_read_increment(&c, true); + channel_config_set_write_increment(&c, false); + + // Chain to the other channel by default. + channel_config_set_chain_to(&c, dma->output_channel[(i + 1) % 2]); + dma_channel_set_config(dma->output_channel[i], &c, false /* trigger */); + + dma_channel_set_write_addr(dma->output_channel[i], (void *)output_register_address, false /* trigger */); + } + if (input_register_address) { + c = dma_channel_get_default_config(dma->input_channel[i]); + channel_config_set_transfer_data_size(&c, dma_size); + channel_config_set_dreq(&c, input_dma_trigger_source); + channel_config_set_read_increment(&c, false); + channel_config_set_write_increment(&c, true); + + // Chain to the other channel by default. + channel_config_set_chain_to(&c, dma->input_channel[(i + 1) % 2]); + dma_channel_set_config(dma->input_channel[i], &c, false /* trigger */); + + dma_channel_set_read_addr(dma->input_channel[i], (void *)input_register_address, false /* trigger */); + dma_channel_set_write_addr(dma->input_channel[i], dma->input_buffer[i], false /* trigger */); + dma_channel_set_trans_count(dma->input_channel[i], dma->input_buffer_length[i] / dma->output_size, false /* trigger */); + } } - dma->playing_in_progress = true; - dma_channel_start(dma->channel[0]); + // Start input before output to allow the first two blocks to load. + if (input_register_address) { + // We keep the audio_dma_t for internal use and the sample as a root pointer because it + // contains the audiodma structure. + MP_STATE_PORT(recording_audio)[dma->input_channel[0]] = dma; + MP_STATE_PORT(recording_audio)[dma->input_channel[1]] = dma; + + // Special case the DMA for a single buffer. + if (single_buffer) { + dma_channel_config c = dma_channel_get_default_config(dma->input_channel[1]); + channel_config_set_transfer_data_size(&c, DMA_SIZE_32); + channel_config_set_dreq(&c, 0x3f); // dma as fast as possible + channel_config_set_read_increment(&c, false); + channel_config_set_write_increment(&c, false); + channel_config_set_chain_to(&c, dma->input_channel[1]); // Chain to ourselves so we stop. + dma_channel_configure(dma->input_channel[1], &c, + &dma->input_buffer[0], // write address + &dma_hw->ch[dma->input_channel[0]].al2_write_addr_trig, // read address + 1, // transaction count + false); // trigger + } else { + // Enable our DMA channels on DMA_IRQ_1 to the CPU. + dma_hw->inte1 |= (1 << dma->input_channel[0]) | (1 << dma->input_channel[1]); + irq_set_mask_enabled(1 << DMA_IRQ_1, true); + } + + dma->input_index = -1; + dma->recording_in_progress = true; + dma_channel_start(dma->input_channel[0]); + } + + if (output_register_address) { + // We keep the audio_dma_t for internal use and the sample as a root pointer because it + // contains the audiodma structure. + MP_STATE_PORT(playing_audio)[dma->output_channel[0]] = dma; + MP_STATE_PORT(playing_audio)[dma->output_channel[1]] = dma; + + // Load the first two blocks up front. + audio_dma_load_next_block(dma, 0); + if (!single_buffer) { + audio_dma_load_next_block(dma, 1); + } + + // Special case the DMA for a single buffer. It's commonly used for a single wave length of sound + // and may be short. Therefore, we use DMA chaining to loop quickly without involving interrupts. + // On the RP2040 we chain by having a second DMA writing to the config registers of the first. + // Read and write addresses change with DMA so we need to reset the read address back to the + // start of the sample. + if (single_buffer) { + dma_channel_config c = dma_channel_get_default_config(dma->output_channel[1]); + channel_config_set_transfer_data_size(&c, DMA_SIZE_32); + channel_config_set_dreq(&c, 0x3f); // dma as fast as possible + channel_config_set_read_increment(&c, false); + channel_config_set_write_increment(&c, false); + channel_config_set_chain_to(&c, dma->output_channel[1]); // Chain to ourselves so we stop. + dma_channel_configure(dma->output_channel[1], &c, + &dma_hw->ch[dma->output_channel[0]].al3_read_addr_trig, // write address + &dma->output_buffer[0], // read address + 1, // transaction count + false); // trigger + } else { + // Enable our DMA channels on DMA_IRQ_0 to the CPU. This will wake us up when + // we're WFI. + dma_hw->inte0 |= (1 << dma->output_channel[0]) | (1 << dma->output_channel[1]); + irq_set_mask_enabled(1 << DMA_IRQ_0, true); + } + + dma->playing_in_progress = true; + dma_channel_start(dma->output_channel[0]); + } return AUDIO_DMA_OK; } -void audio_dma_stop(audio_dma_t *dma) { +// Playback should be shutdown before calling this. +audio_dma_result audio_dma_setup_playback( + audio_dma_t *dma, + mp_obj_t sample, + bool loop, + bool single_channel_output, + uint8_t audio_channel, + bool output_signed, + uint8_t output_resolution, + uint32_t output_register_address, + uint8_t dma_trigger_source, + bool swap_channel) { + return audio_dma_setup(dma, sample, + loop, single_channel_output, audio_channel, + output_signed, output_resolution, + output_register_address, dma_trigger_source, + 0, 0, + swap_channel + ); +} + +// Recording should be shutdown before calling this. +audio_dma_result audio_dma_setup_record( + audio_dma_t *dma, + mp_obj_t sample, + bool loop, + bool single_channel_output, + uint8_t audio_channel, + bool output_signed, + uint8_t output_resolution, + uint32_t input_register_address, + uint8_t dma_trigger_source, + bool swap_channel) { + return audio_dma_setup(dma, sample, + loop, single_channel_output, audio_channel, + output_signed, output_resolution, + 0, 0, + input_register_address, dma_trigger_source, + swap_channel + ); +} + +void audio_dma_stop_output(audio_dma_t *dma) { // Disable our interrupts. uint32_t channel_mask = 0; - if (dma->channel[0] < NUM_DMA_CHANNELS) { - channel_mask |= 1 << dma->channel[0]; + if (dma->output_channel[0] < NUM_DMA_CHANNELS) { + channel_mask |= 1 << dma->output_channel[0]; } - if (dma->channel[1] < NUM_DMA_CHANNELS) { - channel_mask |= 1 << dma->channel[1]; + if (dma->output_channel[1] < NUM_DMA_CHANNELS) { + channel_mask |= 1 << dma->output_channel[1]; } dma_hw->inte0 &= ~channel_mask; if (!dma_hw->inte0) { @@ -332,13 +483,13 @@ void audio_dma_stop(audio_dma_t *dma) { RUN_BACKGROUND_TASKS; for (size_t i = 0; i < 2; i++) { - size_t channel = dma->channel[i]; + size_t channel = dma->output_channel[i]; if (channel == NUM_DMA_CHANNELS) { // Channel not in use. continue; } - dma_channel_config c = dma_channel_get_default_config(dma->channel[i]); + dma_channel_config c = dma_channel_get_default_config(dma->output_channel[i]); channel_config_set_enable(&c, false); dma_channel_set_config(channel, &c, false /* trigger */); @@ -351,38 +502,105 @@ void audio_dma_stop(audio_dma_t *dma) { dma_channel_set_trans_count(channel, 0, false /* trigger */); dma_channel_unclaim(channel); MP_STATE_PORT(playing_audio)[channel] = NULL; - dma->channel[i] = NUM_DMA_CHANNELS; + dma->output_channel[i] = NUM_DMA_CHANNELS; } dma->playing_in_progress = false; // Hold onto our buffers. } +void audio_dma_stop_input(audio_dma_t *dma) { + // Disable our interrupts. + uint32_t channel_mask = 0; + if (dma->input_channel[0] < NUM_DMA_CHANNELS) { + channel_mask |= 1 << dma->input_channel[0]; + } + if (dma->input_channel[1] < NUM_DMA_CHANNELS) { + channel_mask |= 1 << dma->input_channel[1]; + } + dma_hw->inte0 &= ~channel_mask; + if (!dma_hw->inte0) { + irq_set_mask_enabled(1 << DMA_IRQ_0, false); + } + + // Run any remaining audio tasks because we remove ourselves from + // playing_audio. + RUN_BACKGROUND_TASKS; + + for (size_t i = 0; i < 2; i++) { + size_t channel = dma->input_channel[i]; + if (channel == NUM_DMA_CHANNELS) { + // Channel not in use. + continue; + } + + dma_channel_config c = dma_channel_get_default_config(dma->input_channel[i]); + channel_config_set_enable(&c, false); + dma_channel_set_config(channel, &c, false /* trigger */); + + if (dma_channel_is_busy(channel)) { + dma_channel_abort(channel); + } + + dma_channel_set_read_addr(channel, NULL, false /* trigger */); + dma_channel_set_write_addr(channel, NULL, false /* trigger */); + dma_channel_set_trans_count(channel, 0, false /* trigger */); + dma_channel_unclaim(channel); + MP_STATE_PORT(playing_audio)[channel] = NULL; + dma->input_channel[i] = NUM_DMA_CHANNELS; + } + dma->recording_in_progress = false; + + // Hold onto our buffers. +} + +void audio_dma_stop(audio_dma_t *dma) { + if (dma->output_register_address) { + audio_dma_stop_output(dma); + } + if (dma->input_register_address) { + audio_dma_stop_input(dma); + } +} + // To pause we simply stop the DMA. It is the responsibility of the output peripheral // to hold the previous value. void audio_dma_pause(audio_dma_t *dma) { - dma_hw->ch[dma->channel[0]].al1_ctrl &= ~DMA_CH0_CTRL_TRIG_EN_BITS; - dma_hw->ch[dma->channel[1]].al1_ctrl &= ~DMA_CH1_CTRL_TRIG_EN_BITS; + dma_hw->ch[dma->output_channel[0]].al1_ctrl &= ~DMA_CH0_CTRL_TRIG_EN_BITS; + dma_hw->ch[dma->output_channel[1]].al1_ctrl &= ~DMA_CH1_CTRL_TRIG_EN_BITS; + dma_hw->ch[dma->input_channel[0]].al1_ctrl &= ~DMA_CH0_CTRL_TRIG_EN_BITS; + dma_hw->ch[dma->input_channel[1]].al1_ctrl &= ~DMA_CH1_CTRL_TRIG_EN_BITS; } void audio_dma_resume(audio_dma_t *dma) { // Always re-enable the non-busy channel first so it's ready to continue when the busy channel // finishes and chains to it. (An interrupt could make the time between enables long.) - if (dma_channel_is_busy(dma->channel[0])) { - dma_hw->ch[dma->channel[1]].al1_ctrl |= DMA_CH1_CTRL_TRIG_EN_BITS; - dma_hw->ch[dma->channel[0]].al1_ctrl |= DMA_CH0_CTRL_TRIG_EN_BITS; + if (dma_channel_is_busy(dma->output_channel[0])) { + dma_hw->ch[dma->output_channel[1]].al1_ctrl |= DMA_CH1_CTRL_TRIG_EN_BITS; + dma_hw->ch[dma->output_channel[0]].al1_ctrl |= DMA_CH0_CTRL_TRIG_EN_BITS; + } else { + dma_hw->ch[dma->output_channel[0]].al1_ctrl |= DMA_CH0_CTRL_TRIG_EN_BITS; + dma_hw->ch[dma->output_channel[1]].al1_ctrl |= DMA_CH1_CTRL_TRIG_EN_BITS; + } + if (dma_channel_is_busy(dma->input_channel[0])) { + dma_hw->ch[dma->input_channel[1]].al1_ctrl |= DMA_CH1_CTRL_TRIG_EN_BITS; + dma_hw->ch[dma->input_channel[0]].al1_ctrl |= DMA_CH0_CTRL_TRIG_EN_BITS; } else { - dma_hw->ch[dma->channel[0]].al1_ctrl |= DMA_CH0_CTRL_TRIG_EN_BITS; - dma_hw->ch[dma->channel[1]].al1_ctrl |= DMA_CH1_CTRL_TRIG_EN_BITS; + dma_hw->ch[dma->input_channel[0]].al1_ctrl |= DMA_CH0_CTRL_TRIG_EN_BITS; + dma_hw->ch[dma->input_channel[1]].al1_ctrl |= DMA_CH1_CTRL_TRIG_EN_BITS; } } bool audio_dma_get_paused(audio_dma_t *dma) { - if (dma->channel[0] >= NUM_DMA_CHANNELS) { + uint32_t channel = NUM_DMA_CHANNELS; + if (dma->output_channel[0] < NUM_DMA_CHANNELS) { + channel = dma->output_channel[0]; + } else if (dma->input_channel[0] < NUM_DMA_CHANNELS) { + channel = dma->input_channel[0]; + } else { return false; } - uint32_t control = dma_hw->ch[dma->channel[0]].ctrl_trig; - + uint32_t control = dma_hw->ch[channel].ctrl_trig; return (control & DMA_CH0_CTRL_TRIG_EN_BITS) == 0; } @@ -390,6 +608,9 @@ uint32_t audio_dma_pause_all(void) { uint32_t result = 0; for (size_t channel = 0; channel < NUM_DMA_CHANNELS; channel++) { audio_dma_t *dma = MP_STATE_PORT(playing_audio)[channel]; + if (dma == NULL) { + dma = MP_STATE_PORT(recording_audio)[channel]; + } if (dma != NULL && !audio_dma_get_paused(dma)) { audio_dma_pause(dma); result |= (1 << channel); @@ -401,6 +622,9 @@ uint32_t audio_dma_pause_all(void) { void audio_dma_unpause_mask(uint32_t channel_mask) { for (size_t channel = 0; channel < NUM_DMA_CHANNELS; channel++) { audio_dma_t *dma = MP_STATE_PORT(playing_audio)[channel]; + if (dma == NULL) { + dma = MP_STATE_PORT(recording_audio)[channel]; + } if (dma != NULL && (channel_mask & (1 << channel))) { audio_dma_resume(dma); } @@ -408,28 +632,54 @@ void audio_dma_unpause_mask(uint32_t channel_mask) { } void audio_dma_init(audio_dma_t *dma) { - dma->buffer[0] = NULL; - dma->buffer[1] = NULL; + dma->output_buffer[0] = NULL; + dma->output_buffer[1] = NULL; - dma->channel[0] = NUM_DMA_CHANNELS; - dma->channel[1] = NUM_DMA_CHANNELS; + dma->output_channel[0] = NUM_DMA_CHANNELS; + dma->output_channel[1] = NUM_DMA_CHANNELS; + + dma->input_buffer[0] = NULL; + dma->input_buffer[1] = NULL; + + dma->input_channel[0] = NUM_DMA_CHANNELS; + dma->input_channel[1] = NUM_DMA_CHANNELS; } void audio_dma_deinit(audio_dma_t *dma) { - m_free(dma->buffer[0]); - dma->buffer[0] = NULL; + m_free(dma->output_buffer[0]); + dma->output_buffer[0] = NULL; + + m_free(dma->output_buffer[1]); + dma->output_buffer[1] = NULL; + + m_free(dma->input_buffer[0]); + dma->input_buffer[0] = NULL; - m_free(dma->buffer[1]); - dma->buffer[1] = NULL; + m_free(dma->input_buffer[1]); + dma->input_buffer[1] = NULL; } bool audio_dma_get_playing(audio_dma_t *dma) { - if (dma->channel[0] == NUM_DMA_CHANNELS) { + if (dma->output_channel[0] == NUM_DMA_CHANNELS) { return false; } return dma->playing_in_progress; } +bool audio_dma_get_recording(audio_dma_t *dma) { + if (dma->input_channel[0] == NUM_DMA_CHANNELS) { + return false; + } + return dma->recording_in_progress; +} + +uint8_t *audio_dma_get_buffer(audio_dma_t *dma) { + if (!dma->input_register_address || dma->input_index >= 2) { + return NULL; + } + return dma->input_buffer[dma->input_index]; +} + // WARN(tannewt): DO NOT print from here, or anything it calls. Printing calls // background tasks such as this and causes a stack overflow. // NOTE(dhalbert): I successfully printed from here while debugging. @@ -445,14 +695,14 @@ static void dma_callback_fun(void *arg) { dma->channels_to_load_mask = 0; common_hal_mcu_enable_interrupts(); - // Load the blocks for the requested channels. + // Load the blocks for the requested output channels. uint32_t channel = 0; while (channels_to_load_mask) { if (channels_to_load_mask & 1) { - if (dma->channel[0] == channel) { + if (dma->output_channel[0] == channel) { audio_dma_load_next_block(dma, 0); } - if (dma->channel[1] == channel) { + if (dma->output_channel[1] == channel) { audio_dma_load_next_block(dma, 1); } } @@ -496,6 +746,14 @@ void __not_in_flash_func(isr_dma_1)(void) { // completed by the time callback_add() / dma_complete() returned. This // affected PIO continuous write more than audio. dma_hw->ints1 = mask; + if (MP_STATE_PORT(recording_audio)[i] != NULL) { + audio_dma_t *dma = MP_STATE_PORT(recording_audio)[i]; + // Update last recorded buffer. + dma->input_index = (uint8_t)(i != dma->input_channel[0]); + // Reset destination buffer for the dma channel. + dma_channel_set_write_addr(i, dma->input_buffer[dma->input_index], false /* trigger */); + dma_channel_set_trans_count(i, dma->input_buffer_length[dma->input_index] / dma->output_size, false /* trigger */); + } if (MP_STATE_PORT(background_pio)[i] != NULL) { rp2pio_statemachine_obj_t *pio = MP_STATE_PORT(background_pio)[i]; rp2pio_statemachine_dma_complete_read(pio, i); @@ -504,4 +762,5 @@ void __not_in_flash_func(isr_dma_1)(void) { } MP_REGISTER_ROOT_POINTER(mp_obj_t playing_audio[enum_NUM_DMA_CHANNELS]); +MP_REGISTER_ROOT_POINTER(mp_obj_t recording_audio[enum_NUM_DMA_CHANNELS]); #endif diff --git a/ports/raspberrypi/audio_dma.h b/ports/raspberrypi/audio_dma.h index 7c33a9e2ac319..32b12586223ec 100644 --- a/ports/raspberrypi/audio_dma.h +++ b/ports/raspberrypi/audio_dma.h @@ -13,12 +13,16 @@ typedef struct { mp_obj_t sample; - uint8_t *buffer[2]; - size_t buffer_length[2]; + uint8_t *output_buffer[2]; + size_t output_buffer_length[2]; + uint8_t *input_buffer[2]; + size_t input_buffer_length[2]; uint32_t channels_to_load_mask; uint32_t output_register_address; + uint32_t input_register_address; background_callback_t callback; - uint8_t channel[2]; + uint8_t output_channel[2]; + uint8_t input_channel[2]; uint8_t audio_channel; uint8_t output_size; uint8_t sample_spacing; @@ -30,7 +34,9 @@ typedef struct { bool unsigned_to_signed; bool output_signed; bool playing_in_progress; + bool recording_in_progress; bool swap_channel; + uint8_t input_index; } audio_dma_t; typedef enum { @@ -53,6 +59,19 @@ void audio_dma_reset(void); // output_signed is true if the dma'd data should be signed. False and it will be unsigned. // output_register_address is the address to copy data to. // dma_trigger_source is the DMA trigger source which cause another copy +audio_dma_result audio_dma_setup(audio_dma_t *dma, + mp_obj_t sample, + bool loop, + bool single_channel_output, + uint8_t audio_channel, + bool output_signed, + uint8_t output_resolution, + uint32_t output_register_address, + uint8_t output_dma_trigger_source, + uint32_t input_register_address, + uint8_t input_dma_trigger_source, + bool swap_channel); + audio_dma_result audio_dma_setup_playback(audio_dma_t *dma, mp_obj_t sample, bool loop, @@ -64,8 +83,23 @@ audio_dma_result audio_dma_setup_playback(audio_dma_t *dma, uint8_t dma_trigger_source, bool swap_channel); +audio_dma_result audio_dma_setup_record(audio_dma_t *dma, + mp_obj_t sample, + bool loop, + bool single_channel_output, + uint8_t audio_channel, + bool output_signed, + uint8_t output_resolution, + uint32_t input_register_address, + uint8_t dma_trigger_source, + bool swap_channel); + void audio_dma_stop(audio_dma_t *dma); +void audio_dma_stop_output(audio_dma_t *dma); +void audio_dma_stop_input(audio_dma_t *dma); bool audio_dma_get_playing(audio_dma_t *dma); +bool audio_dma_get_recording(audio_dma_t *dma); +uint8_t *audio_dma_get_buffer(audio_dma_t *dma); void audio_dma_pause(audio_dma_t *dma); void audio_dma_resume(audio_dma_t *dma); bool audio_dma_get_paused(audio_dma_t *dma); diff --git a/ports/raspberrypi/common-hal/audiobusio/I2S.c b/ports/raspberrypi/common-hal/audiobusio/I2S.c new file mode 100644 index 0000000000000..a57b84f044141 --- /dev/null +++ b/ports/raspberrypi/common-hal/audiobusio/I2S.c @@ -0,0 +1,378 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT + +#include +#include + +#include "mpconfigport.h" + +#include "py/mperrno.h" +#include "py/runtime.h" +#include "common-hal/audiobusio/I2S.h" +#include "shared-bindings/audiobusio/I2S.h" +#include "shared-bindings/microcontroller/__init__.h" +#include "shared-bindings/microcontroller/Pin.h" +#include "shared-module/audiocore/__init__.h" +#include "bindings/rp2pio/StateMachine.h" + +#include "audio_dma.h" + +#include "src/rp2_common/hardware_pio/include/hardware/pio_instructions.h" + +#define I2S_CODE(bits_per_sample, out, in, left_justified, swap) \ + { \ +/* 00 */ pio_encode_set(pio_y, bits_per_sample - 2) | pio_encode_sideset(2, 0b01 << swap | left_justified << !swap) | pio_encode_delay(1), \ + /* .wrap_target */ \ +/* 01 */ (out ? pio_encode_pull(false, false) : pio_encode_nop()) | pio_encode_sideset(2, 0b01 << swap | left_justified << !swap), \ +/* 02 */ (out ? pio_encode_mov(pio_x, pio_osr) : pio_encode_nop()) | pio_encode_sideset(2, 0b01 << swap | left_justified << !swap), \ +/* 03 */ (out ? pio_encode_out(pio_pins, 1) : pio_encode_nop()) | pio_encode_sideset(2, 0b00) | pio_encode_delay(3), \ +/* 04 */ (in ? pio_encode_in(pio_pins, 1) : pio_encode_nop()) | pio_encode_sideset(2, 0b01 << swap), \ +/* 05 */ pio_encode_jmp_y_dec(3) | pio_encode_sideset(2, 0b01 << swap) | pio_encode_delay(2), \ +/* 06 */ (out ? pio_encode_out(pio_pins, 1) : pio_encode_nop()) | pio_encode_sideset(2, 0b00 | !left_justified << !swap) | pio_encode_delay(3), \ +/* 07 */ (in ? pio_encode_in(pio_pins, 1) : pio_encode_nop()) | pio_encode_sideset(2, 0b01 << swap | !left_justified << !swap), \ +/* 08 */ pio_encode_set(pio_y, bits_per_sample - 2) | pio_encode_sideset(2, 0b01 << swap | !left_justified << !swap) | pio_encode_delay(2), \ +/* 09 */ (out ? pio_encode_out(pio_pins, 1) : pio_encode_nop()) | pio_encode_sideset(2, 0b10 >> swap) | pio_encode_delay(3), \ +/* 10 */ (in ? pio_encode_in(pio_pins, 1) : pio_encode_nop()) | pio_encode_sideset(2, 0b11), \ +/* 11 */ pio_encode_jmp_y_dec(9) | pio_encode_sideset(2, 0b11) | pio_encode_delay(2), \ +/* 12 */ (out ? pio_encode_out(pio_pins, 1) : pio_encode_nop()) | pio_encode_sideset(2, 0b00 | left_justified << !swap) | pio_encode_delay(2), \ +/* 13 */ pio_encode_set(pio_y, bits_per_sample - 2) | pio_encode_sideset(2, 0b00 | left_justified << !swap), \ +/* 13 */ (in ? pio_encode_in(pio_pins, 1) : pio_encode_nop()) | pio_encode_sideset(2, 0b01 << swap | left_justified << !swap), \ +/* 14 */ (in ? pio_encode_push(false, false) : pio_encode_nop()) | pio_encode_sideset(2, 0b01 << swap | left_justified << !swap), \ + /* .wrap */ \ + } + +// Caller validates that pins are free. +void common_hal_audiobusio_i2s_construct(audiobusio_i2s_obj_t *self, + const mcu_pin_obj_t *bit_clock, const mcu_pin_obj_t *word_select, + const mcu_pin_obj_t *data_out, const mcu_pin_obj_t *data_in, + const mcu_pin_obj_t *main_clock, bool left_justified, + uint32_t buffer_size, uint8_t channel_count, uint32_t sample_rate, + uint8_t bits_per_sample, bool samples_signed) { + if (main_clock != NULL) { + mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_main_clock); + } + + const mcu_pin_obj_t *sideset_pin = NULL; + bool swap = false; + + if (bit_clock->number == word_select->number - 1) { + sideset_pin = bit_clock; + } else if (bit_clock->number == word_select->number + 1) { + sideset_pin = word_select; + swap = true; + } else { + mp_raise_ValueError(MP_ERROR_TEXT("Bit clock and word select must be sequential GPIO pins")); + } + + uint16_t program[] = I2S_CODE(bits_per_sample, data_out != NULL, data_in != NULL, left_justified, swap); + + // Use the state machine to manage pins. + common_hal_rp2pio_statemachine_construct( + &self->state_machine, + program, MP_ARRAY_SIZE(program), + sample_rate * bits_per_sample * 16, // Frequency based on sample rate and bit width + NULL, 0, // init + NULL, 0, // may_exec + data_out, 1, PIO_PINMASK32_NONE, PIO_PINMASK32_ALL, // out pin + data_in, 1, // in pins + PIO_PINMASK32_NONE, PIO_PINMASK32_NONE, // in pulls + NULL, 1, PIO_PINMASK32_NONE, PIO_PINMASK32_NONE, // 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 + false, // Wait for txstall + false, 32, false, // in settings + false, // Not user-interruptible. + 1, -1, // wrap settings + PIO_ANY_OFFSET, + PIO_FIFO_TYPE_DEFAULT, + PIO_MOV_STATUS_DEFAULT, + PIO_MOV_N_DEFAULT + ); + + self->buffer_size = buffer_size; + self->channel_count = channel_count; + self->sample_rate = sample_rate; + self->bits_per_sample = bits_per_sample; + self->samples_signed = samples_signed; + + self->playing = false; + audio_dma_init(&self->dma); + + if (self->state_machine.in) { + self->buffer[0] = m_malloc(self->buffer_size); + if (self->buffer[0] == NULL) { + common_hal_audiobusio_i2s_deinit(self); + m_malloc_fail(self->buffer_size); + } + memset(self->buffer[0], 0, self->buffer_size); + + self->buffer[1] = m_malloc(self->buffer_size); + if (self->buffer[1] == NULL) { + common_hal_audiobusio_i2s_deinit(self); + m_malloc_fail(self->buffer_size); + } + memset(self->buffer[1], 0, self->buffer_size); + + self->last_buf_idx = 1; // Which buffer to use first, toggle between 0 and 1 + } +} + +void i2s_configure_audio_dma(audiobusio_i2s_obj_t *self, mp_obj_t sample, bool loop, uint32_t sample_rate, uint8_t bits_per_sample, bool force) { + if (self->dma.output_channel[0] != NUM_DMA_CHANNELS || self->dma.input_channel[0] != NUM_DMA_CHANNELS) { + if (!force) { + return; + } + + audio_dma_stop(&self->dma); + common_hal_rp2pio_statemachine_stop(&self->state_machine); + } + + common_hal_rp2pio_statemachine_set_frequency(&self->state_machine, sample_rate * bits_per_sample * 16); + common_hal_rp2pio_statemachine_restart(&self->state_machine); + + // On the RP2040, output registers are always written with a 32-bit write. + // If the write is 8 or 16 bits wide, the data will be replicated in upper bytes. + // See section 2.1.4 Narrow IO Register Writes in the RP2040 datasheet. + // This means that identical 16-bit audio data will be written in both halves of the incoming PIO + // FIFO register. Thus we get mono-to-stereo conversion for the I2S output for free. + audio_dma_result result = audio_dma_setup( + &self->dma, + sample, + loop, + false, // single channel + 0, // audio channel + true, // output signed + bits_per_sample, + (self->state_machine.out ? (uint32_t)&self->state_machine.pio->txf[self->state_machine.state_machine] : 0), // output register + (self->state_machine.out ? self->state_machine.tx_dreq : 0), // output data request line + (self->state_machine.in ? (uint32_t)&self->state_machine.pio->rxf[self->state_machine.state_machine] : 0), // input register + (self->state_machine.in ? self->state_machine.rx_dreq : 0), // input data request line + false); // swap channel + + if (result == AUDIO_DMA_DMA_BUSY) { + common_hal_audiobusio_i2s_stop(self); + mp_raise_RuntimeError(MP_ERROR_TEXT("No DMA channel found")); + } else if (result == AUDIO_DMA_MEMORY_ERROR) { + common_hal_audiobusio_i2s_stop(self); + mp_raise_RuntimeError(MP_ERROR_TEXT("Unable to allocate buffers for signed conversion")); + } +} + +bool common_hal_audiobusio_i2s_deinited(audiobusio_i2s_obj_t *self) { + return common_hal_rp2pio_statemachine_deinited(&self->state_machine); +} + +void common_hal_audiobusio_i2s_deinit(audiobusio_i2s_obj_t *self) { + if (common_hal_audiobusio_i2s_deinited(self)) { + return; + } + + common_hal_rp2pio_statemachine_deinit(&self->state_machine); + + audio_dma_deinit(&self->dma); + + self->buffer[0] = NULL; + self->buffer[1] = NULL; +} + +// output_buffer may be a byte buffer or a halfword buffer. +// output_buffer_length is the number of slots, not the number of bytes. +uint32_t common_hal_audiobusio_i2s_record_to_buffer(audiobusio_i2s_obj_t *self, + int16_t *output_buffer, uint32_t output_buffer_length) { + if (!self->state_machine.in) { + mp_raise_RuntimeError(MP_ERROR_TEXT("No data in")); + } + + // Make sure that dma is running. + i2s_configure_audio_dma(self, self, true, self->sample_rate, self->bits_per_sample, true); + + size_t output_count = 0; + int16_t *buffer[2]; + int8_t buffer_idx = 1; + size_t buffer_length = MIN((output_buffer_length - output_count), self->buffer_size / sizeof(int16_t)); + + while (output_count < output_buffer_length) { + do { + buffer_idx = !buffer_idx; + buffer[buffer_idx] = (int16_t *)audio_dma_get_buffer(&self->dma); + } while (buffer[buffer_idx] == NULL || buffer[0] == buffer[1]); + + for (size_t i = 0; i < buffer_length; i++) { + output_buffer[i + output_count] = buffer[buffer_idx][i]; + } + + output_count += buffer_length; + } + + return output_count; +} + +void common_hal_audiobusio_i2s_play(audiobusio_i2s_obj_t *self, + mp_obj_t sample, bool loop) { + if (!self->state_machine.out) { + mp_raise_RuntimeError(MP_ERROR_TEXT("No data out")); + } + + if (common_hal_audiobusio_i2s_get_playing(self)) { + common_hal_audiobusio_i2s_stop(self); + } + + uint8_t bits_per_sample = audiosample_bits_per_sample(sample); + uint32_t sample_rate = audiosample_sample_rate(sample); + uint8_t channel_count = audiosample_channel_count(sample); + if (channel_count > 2) { + mp_raise_ValueError(MP_ERROR_TEXT("Too many channels in sample.")); + } + + if (self->state_machine.in) { + if (bits_per_sample > self->bits_per_sample) { + mp_raise_ValueError(MP_ERROR_TEXT("Bits per sample cannot be greater than input.")); + } + if (sample_rate != self->sample_rate) { + mp_raise_ValueError(MP_ERROR_TEXT("Sample rate must match.")); + } + } + + i2s_configure_audio_dma(self, sample, loop, sample_rate, bits_per_sample, true); + self->playing = true; +} + +void common_hal_audiobusio_i2s_pause(audiobusio_i2s_obj_t *self) { + if (!self->state_machine.out) { + mp_raise_RuntimeError(MP_ERROR_TEXT("No data out")); + } + audio_dma_pause(&self->dma); +} + +void common_hal_audiobusio_i2s_resume(audiobusio_i2s_obj_t *self) { + if (!self->state_machine.out) { + mp_raise_RuntimeError(MP_ERROR_TEXT("No data out")); + } + // Maybe: Clear any overrun/underrun errors + audio_dma_resume(&self->dma); +} + +bool common_hal_audiobusio_i2s_get_paused(audiobusio_i2s_obj_t *self) { + if (!self->state_machine.out) { + mp_raise_RuntimeError(MP_ERROR_TEXT("No data out")); + } + return audio_dma_get_paused(&self->dma); +} + +void common_hal_audiobusio_i2s_stop(audiobusio_i2s_obj_t *self) { + if (!self->state_machine.out) { + mp_raise_RuntimeError(MP_ERROR_TEXT("No data out")); + } + + audio_dma_stop(&self->dma); + + common_hal_rp2pio_statemachine_stop(&self->state_machine); + + self->playing = false; +} + +bool common_hal_audiobusio_i2s_get_playing(audiobusio_i2s_obj_t *self) { + if (!self->state_machine.out) { + mp_raise_RuntimeError(MP_ERROR_TEXT("No data out")); + } + + bool playing = audio_dma_get_playing(&self->dma); + if (!playing && self->playing) { + common_hal_audiobusio_i2s_stop(self); + } + return playing; +} + +uint32_t common_hal_audiobusio_i2s_get_sample_rate(audiobusio_i2s_obj_t *self) { + if (!self->state_machine.in) { + mp_raise_RuntimeError(MP_ERROR_TEXT("No data in")); + } + return self->sample_rate; +} + +uint8_t common_hal_audiobusio_i2s_get_channel_count(audiobusio_i2s_obj_t *self) { + if (!self->state_machine.in) { + mp_raise_RuntimeError(MP_ERROR_TEXT("No data in")); + } + return self->channel_count; +} + +uint8_t common_hal_audiobusio_i2s_get_bits_per_sample(audiobusio_i2s_obj_t *self) { + if (!self->state_machine.in) { + mp_raise_RuntimeError(MP_ERROR_TEXT("No data in")); + } + return self->bits_per_sample; +} + +void audiobusio_i2s_reset_buffer(audiobusio_i2s_obj_t *self, + bool single_channel_output, + uint8_t channel) { + if (!self->state_machine.in) { + mp_raise_RuntimeError(MP_ERROR_TEXT("No data in")); + } + + if (single_channel_output) { + mp_raise_NotImplementedError(MP_ERROR_TEXT("Single channel output not supported.")); + } + + memset(self->buffer[0], 0, self->buffer_size); + memset(self->buffer[1], 0, self->buffer_size); + + i2s_configure_audio_dma(self, self, true, self->sample_rate, self->bits_per_sample, false); +} + +audioio_get_buffer_result_t audiobusio_i2s_get_buffer(audiobusio_i2s_obj_t *self, + bool single_channel_output, + uint8_t channel, + uint8_t **buffer, + uint32_t *buffer_length) { + if (!self->state_machine.in) { + mp_raise_RuntimeError(MP_ERROR_TEXT("No data in")); + } + + if (single_channel_output) { + mp_raise_NotImplementedError(MP_ERROR_TEXT("Single channel output not supported.")); + } + + // Switch our buffers to the other buffer + self->last_buf_idx = !self->last_buf_idx; + + uint8_t *dma_buffer; + do { + dma_buffer = audio_dma_get_buffer(&self->dma); + } while (dma_buffer == NULL); + + // Copy dma buffer to output buffer + memcpy(self->buffer[self->last_buf_idx], dma_buffer, self->buffer_size); + + // Finally pass our buffer and length to the calling audio function + *buffer = (uint8_t *)self->buffer[self->last_buf_idx]; + *buffer_length = self->buffer_size; + + // I2S always returns more data unless an error occured (see audiocore/__init__.h) + return GET_BUFFER_MORE_DATA; +} + +void audiobusio_i2s_get_buffer_structure(audiobusio_i2s_obj_t *self, bool single_channel_output, + bool *single_buffer, bool *samples_signed, + uint32_t *max_buffer_length, uint8_t *spacing) { + if (!self->state_machine.in) { + mp_raise_RuntimeError(MP_ERROR_TEXT("No data in")); + } + + *single_buffer = false; + *samples_signed = self->samples_signed; + *max_buffer_length = self->buffer_size; + if (single_channel_output) { + *spacing = self->channel_count; + } else { + *spacing = 1; + } +} diff --git a/ports/raspberrypi/common-hal/audiobusio/I2S.h b/ports/raspberrypi/common-hal/audiobusio/I2S.h new file mode 100644 index 0000000000000..3d4af7a27128b --- /dev/null +++ b/ports/raspberrypi/common-hal/audiobusio/I2S.h @@ -0,0 +1,47 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "common-hal/microcontroller/Pin.h" +#include "common-hal/rp2pio/StateMachine.h" + +#include "audio_dma.h" +#include "py/obj.h" + +#include "shared-module/audiocore/__init__.h" + +// We don't bit pack because we'll only have two at most. Its better to save code size instead. +typedef struct { + mp_obj_base_t base; + rp2pio_statemachine_obj_t state_machine; + audio_dma_t dma; + bool left_justified; + bool playing; + uint32_t buffer_size; + uint8_t channel_count; + uint32_t sample_rate; + uint8_t bits_per_sample; + bool samples_signed; + uint8_t *buffer[2]; + uint8_t last_buf_idx; +} audiobusio_i2s_obj_t; + +// These are not available from Python because it may be called in an interrupt. +void audiobusio_i2s_reset_buffer(audiobusio_i2s_obj_t *self, + bool single_channel_output, + uint8_t channel); +audioio_get_buffer_result_t audiobusio_i2s_get_buffer(audiobusio_i2s_obj_t *self, + bool single_channel_output, + uint8_t channel, + uint8_t **buffer, + uint32_t *buffer_length); // length in bytes +void audiobusio_i2s_get_buffer_structure(audiobusio_i2s_obj_t *self, bool single_channel_output, + bool *single_buffer, bool *samples_signed, + uint32_t *max_buffer_length, uint8_t *spacing); + +void i2s_configure_audio_dma(audiobusio_i2s_obj_t *self, mp_obj_t sample, bool loop, + uint32_t sample_rate, uint8_t bits_per_sample, bool force); diff --git a/ports/raspberrypi/common-hal/audiobusio/I2SIn.c b/ports/raspberrypi/common-hal/audiobusio/I2SIn.c new file mode 100644 index 0000000000000..3cfb11166aafd --- /dev/null +++ b/ports/raspberrypi/common-hal/audiobusio/I2SIn.c @@ -0,0 +1,330 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT + +#include +#include + +#include "mpconfigport.h" + +#include "py/mperrno.h" +#include "py/runtime.h" +#include "common-hal/audiobusio/I2SIn.h" +#include "shared-bindings/audiobusio/I2SIn.h" +#include "shared-bindings/microcontroller/Pin.h" +#include "bindings/rp2pio/StateMachine.h" + +#include "audio_dma.h" + +#if CIRCUITPY_AUDIOBUSIO_I2SIN && !CIRCUITPY_AUDIOBUSIO_I2SOUT + +const uint16_t i2sin_program_mono[] = { +// pull block side 0b11 ; Load OSR with bits_per_sample-2 + 0x98a0, +// out y 8 side 0b11 ; Save the value in y + 0x7848, +// nop side 0b01 + 0xa842, +// mov x y side 0b01 + 0xa822, +// lbit: +// nop side 0b00 [1] + 0xa142, +// in pins 1 side 0b01 + 0x4801, +// jmp x-- lbit side 0b01 + 0x0844, +// nop side 0b10 [1] + 0xb142, +// in pins 1 side 0b11 + 0x5801, +// mov x y side 0b11 + 0xb822, +// rbit: +// nop side 0b10 [1] + 0xb142, +// nop side 0b11 + 0xb842, +// jmp x-- rbit side 0b11 + 0x184a, +// nop side 0b00 [1] + 0xa142, +// nop side 0b01 + 0xa842, +}; + +const uint16_t i2sin_program_mono_swap[] = { +// pull block side 0b11 ; Load OSR with bits_per_sample-2 + 0x98a0, +// out y 8 side 0b11 ; Save the value in y + 0x7848, +// nop side 0b10 + 0xb042, +// mov x y side 0b10 + 0xb022, +// lbit: +// nop side 0b00 [1] + 0xa142, +// in pins 1 side 0b10 + 0x5001, +// jmp x-- lbit side 0b10 + 0x1044, +// nop side 0b01 [1] + 0xa942, +// in pins 1 side 0b11 + 0x5801, +// mov x y side 0b11 + 0xb822, +// rbit: +// nop side 0b01 [1] + 0xa942, +// nop side 0b11 + 0xb842, +// jmp x-- rbit side 0b11 + 0x184a, +// nop side 0b00 [1] + 0xa142, +// nop side 0b10 + 0xb042, +}; + +const uint16_t i2sin_program_stereo[] = { +// ; /--- LRCLK +// ; |/-- BCLK +// ; || +// pull block side 0b11 ; Load OSR with bits_per_sample-2 + 0x98a0, +// out y 8 side 0b11 ; Save the value in y + 0x7848, +// nop side 0b01 + 0xa842, +// mov x y side 0b01 + 0xa822, +// lbit: +// nop side 0b00 [1] + 0xa142, +// in pins 1 side 0b01 + 0x4801, +// jmp x-- lbit side 0b01 + 0x0844, +// nop side 0b10 [1] + 0xb142, +// in pins 1 side 0b11 + 0x5801, +// mov x y side 0b11 + 0xb822, +// rbit: +// nop side 0b10 [1] + 0xb142, +// in pins 1 side 0b11 + 0x5801, +// jmp x-- rbit side 0b11 + 0x184a, +// nop side 0b00 [1] + 0xa142, +// in pins 1 side 0b01 + 0x4801, +}; + +const uint16_t i2sin_program_stereo_swap[] = { +// ; /--- LRCLK +// ; |/-- BCLK +// ; || +// pull block side 0b11 ; Load OSR with bits_per_sample-2 + 0x98a0, +// out y 8 side 0b11 ; Save the value in y + 0x7848, +// nop side 0b10 + 0xb042, +// mov x y side 0b10 + 0xb022, +// lbit: +// nop side 0b00 [1] + 0xa142, +// in pins 1 side 0b10 + 0x5001, +// jmp x-- lbit side 0b10 + 0x1044, +// nop side 0b01 [1] + 0xa942, +// in pins 1 side 0b11 + 0x5801, +// mov x y side 0b11 + 0xb822, +// rbit: +// nop side 0b01 [1] + 0xa942, +// in pins 1 side 0b11 + 0x5801, +// jmp x-- rbit side 0b11 + 0x184a, +// nop side 0b00 [1] + 0xa142, +// in pins 1 side 0b10 + 0x5001, +}; + +// Caller validates that pins are free. +void common_hal_audiobusio_i2sin_construct(audiobusio_i2sin_obj_t *self, + const mcu_pin_obj_t *bit_clock, const mcu_pin_obj_t *word_select, const mcu_pin_obj_t *data, + uint32_t buffer_size, uint8_t channel_count, uint32_t sample_rate, uint8_t bits_per_sample, + bool samples_signed) { + + 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 (channel_count == 1) { + program_len = MP_ARRAY_SIZE(i2sin_program_mono); + program = i2sin_program_mono; + } else { + program_len = MP_ARRAY_SIZE(i2sin_program_stereo); + program = i2sin_program_stereo; + } + + } else if (bit_clock->number == word_select->number + 1) { + sideset_pin = word_select; + + if (channel_count == 1) { + program_len = MP_ARRAY_SIZE(i2sin_program_mono_swap); + program = i2sin_program_mono_swap; + } else { + program_len = MP_ARRAY_SIZE(i2sin_program_stereo_swap); + program = i2sin_program_stereo_swap; + } + + } else { + mp_raise_ValueError(MP_ERROR_TEXT("Bit clock and word select must be sequential GPIO pins")); + } + + // Use the state machine to manage pins. + common_hal_rp2pio_statemachine_construct( + &self->state_machine, + program, program_len, + sample_rate * bits_per_sample * 2 * 4, // Frequency based on sample rate and bit width + NULL, 0, // init + NULL, 0, // may_exec + NULL, 1, PIO_PINMASK32_NONE, PIO_PINMASK32_NONE, // out pin + data, 1, // in pins + PIO_PINMASK32_NONE, PIO_PINMASK32_NONE, // in pulls + NULL, 1, PIO_PINMASK32_NONE, PIO_PINMASK32_NONE, // 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, 8, false, // out settings + false, // Wait for txstall + true, bits_per_sample, false, // in settings + false, // Not user-interruptible. + 3, -1, // wrap settings + PIO_ANY_OFFSET, + PIO_FIFO_TYPE_DEFAULT, + PIO_MOV_STATUS_DEFAULT, + PIO_MOV_N_DEFAULT + ); + + audio_dma_init(&self->dma); + + self->buffer_size = buffer_size; + self->channel_count = channel_count; + self->sample_rate = sample_rate; + self->bits_per_sample = bits_per_sample; + self->samples_signed = samples_signed; +} + +bool common_hal_audiobusio_i2sin_deinited(audiobusio_i2sin_obj_t *self) { + return common_hal_rp2pio_statemachine_deinited(&self->state_machine); +} + +void common_hal_audiobusio_i2sin_deinit(audiobusio_i2sin_obj_t *self) { + if (common_hal_audiobusio_i2sin_deinited(self)) { + return; + } + + common_hal_rp2pio_statemachine_deinit(&self->state_machine); + + audio_dma_deinit(&self->dma); +} + +uint32_t common_hal_audiobusio_i2sin_get_sample_rate(audiobusio_i2sin_obj_t *self) { + return self->sample_rate; +} + +uint8_t common_hal_audiobusio_i2sin_get_channel_count(audiobusio_i2sin_obj_t *self) { + return self->channel_count; +} + +uint8_t common_hal_audiobusio_i2sin_get_bits_per_sample(audiobusio_i2sin_obj_t *self) { + return self->bits_per_sample; +} + +void audiobusio_i2sin_reset_buffer(audiobusio_i2sin_obj_t *self, + bool single_channel_output, + uint8_t channel) { + + common_hal_rp2pio_statemachine_restart(&self->state_machine); + + // Send bit width + const uint8_t bit_width_data[1] = { self->bits_per_sample - 2 }; + common_hal_rp2pio_statemachine_write(&self->state_machine, bit_width_data, 1, 1, false); + + audio_dma_result result = audio_dma_setup_record( + &self->dma, + self, + true, + single_channel_output, // single channel + channel, // audio channel + true, // output signed + self->bits_per_sample, // output resolution + (uint32_t)&self->state_machine.pio->rxf[self->state_machine.state_machine], // input register + self->state_machine.rx_dreq, // data request line + false); // swap channel + + if (result == AUDIO_DMA_DMA_BUSY) { + common_hal_rp2pio_statemachine_stop(&self->state_machine); + mp_raise_RuntimeError(MP_ERROR_TEXT("No DMA channel found")); + } else if (result == AUDIO_DMA_MEMORY_ERROR) { + common_hal_rp2pio_statemachine_stop(&self->state_machine); + mp_raise_RuntimeError(MP_ERROR_TEXT("Unable to allocate buffers for signed conversion")); + } + + self->last_index = -1; +} + +audioio_get_buffer_result_t audiobusio_i2sin_get_buffer(audiobusio_i2sin_obj_t *self, + bool single_channel_output, + uint8_t channel, + uint8_t **buffer, + uint32_t *buffer_length) { + + // Do other things while we wait for the buffer to fill. + while (self->last_index == self->dma.input_index) { + RUN_BACKGROUND_TASKS; + } + + *buffer_length = self->buffer_size; + *buffer = audio_dma_get_buffer(&self->dma); + return GET_BUFFER_MORE_DATA; +} + +void audiobusio_i2sin_get_buffer_structure(audiobusio_i2sin_obj_t *self, bool single_channel_output, + bool *single_buffer, bool *samples_signed, + uint32_t *max_buffer_length, uint8_t *spacing) { + + *single_buffer = false; + *samples_signed = self->samples_signed; + *max_buffer_length = self->buffer_size; + if (single_channel_output) { + *spacing = self->channel_count; + } else { + *spacing = 1; + } +} + +#endif // CIRCUITPY_AUDIOBUSIO_I2SIN && !CIRCUITPY_AUDIOBUSIO_I2SOUT diff --git a/ports/raspberrypi/common-hal/audiobusio/I2SIn.h b/ports/raspberrypi/common-hal/audiobusio/I2SIn.h new file mode 100644 index 0000000000000..746fc0b82bb57 --- /dev/null +++ b/ports/raspberrypi/common-hal/audiobusio/I2SIn.h @@ -0,0 +1,46 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "common-hal/microcontroller/Pin.h" +#include "common-hal/rp2pio/StateMachine.h" + +#include "audio_dma.h" +#include "py/obj.h" + +#include "shared-module/audiocore/__init__.h" + +#if CIRCUITPY_AUDIOBUSIO_I2SIN && !CIRCUITPY_AUDIOBUSIO_I2SOUT + +// We don't bit pack because we'll only have two at most. Its better to save code size instead. +typedef struct { + mp_obj_base_t base; + rp2pio_statemachine_obj_t state_machine; + audio_dma_t dma; + uint32_t buffer_size; + uint8_t channel_count; + uint32_t sample_rate; + uint8_t bits_per_sample; + bool samples_signed; + uint8_t last_index; +} audiobusio_i2sin_obj_t; + + +// These are not available from Python because it may be called in an interrupt. +void audiobusio_i2sin_reset_buffer(audiobusio_i2sin_obj_t *self, + bool single_channel_output, + uint8_t channel); +audioio_get_buffer_result_t audiobusio_i2sin_get_buffer(audiobusio_i2sin_obj_t *self, + bool single_channel_output, + uint8_t channel, + uint8_t **buffer, + uint32_t *buffer_length); // length in bytes +void audiobusio_i2sin_get_buffer_structure(audiobusio_i2sin_obj_t *self, bool single_channel_output, + bool *single_buffer, bool *samples_signed, + uint32_t *max_buffer_length, uint8_t *spacing); + +#endif // CIRCUITPY_AUDIOBUSIO_I2SIN && !CIRCUITPY_AUDIOBUSIO_I2SOUT diff --git a/ports/raspberrypi/common-hal/audiobusio/I2SOut.c b/ports/raspberrypi/common-hal/audiobusio/I2SOut.c index f9484f546a898..149b9f232cd34 100644 --- a/ports/raspberrypi/common-hal/audiobusio/I2SOut.c +++ b/ports/raspberrypi/common-hal/audiobusio/I2SOut.c @@ -18,6 +18,8 @@ #include "shared-module/audiocore/__init__.h" #include "bindings/rp2pio/StateMachine.h" +#if CIRCUITPY_AUDIOBUSIO_I2SOUT && !CIRCUITPY_AUDIOBUSIO_I2SIN + const uint16_t i2s_program[] = { // ; Load the next set of samples // ; /--- LRCLK @@ -308,3 +310,5 @@ bool common_hal_audiobusio_i2sout_get_playing(audiobusio_i2sout_obj_t *self) { } return playing; } + +#endif // CIRCUITPY_AUDIOBUSIO_I2SOUT && !CIRCUITPY_AUDIOBUSIO_I2SIN diff --git a/ports/raspberrypi/common-hal/audiobusio/I2SOut.h b/ports/raspberrypi/common-hal/audiobusio/I2SOut.h index 2996640dc2d49..5c968532ad230 100644 --- a/ports/raspberrypi/common-hal/audiobusio/I2SOut.h +++ b/ports/raspberrypi/common-hal/audiobusio/I2SOut.h @@ -12,6 +12,8 @@ #include "audio_dma.h" #include "py/obj.h" +#if CIRCUITPY_AUDIOBUSIO_I2SOUT && !CIRCUITPY_AUDIOBUSIO_I2SIN + // We don't bit pack because we'll only have two at most. Its better to save code size instead. typedef struct { mp_obj_base_t base; @@ -22,3 +24,5 @@ typedef struct { } audiobusio_i2sout_obj_t; void i2sout_reset(void); + +#endif // CIRCUITPY_AUDIOBUSIO_I2SOUT && !CIRCUITPY_AUDIOBUSIO_I2SIN diff --git a/ports/raspberrypi/mpconfigport.mk b/ports/raspberrypi/mpconfigport.mk index d619e78bd97a8..16b10942215dd 100644 --- a/ports/raspberrypi/mpconfigport.mk +++ b/ports/raspberrypi/mpconfigport.mk @@ -41,6 +41,7 @@ CIRCUITPY_ANALOGBUFIO = 1 # Audio via PWM CIRCUITPY_AUDIOIO = 0 CIRCUITPY_AUDIOBUSIO ?= 1 +CIRCUITPY_AUDIOBUSIO_I2SIN ?= 1 CIRCUITPY_AUDIOCORE ?= 1 CIRCUITPY_AUDIOPWMIO ?= 1 diff --git a/ports/stm/common-hal/audiobusio/I2S.c b/ports/stm/common-hal/audiobusio/I2S.c new file mode 100644 index 0000000000000..8ebbbb240a76e --- /dev/null +++ b/ports/stm/common-hal/audiobusio/I2S.c @@ -0,0 +1,7 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT + +// Although IS2 is not enabled on the STM32L4 family, this file is still required for the build to pass diff --git a/ports/stm/common-hal/audiobusio/I2S.h b/ports/stm/common-hal/audiobusio/I2S.h new file mode 100644 index 0000000000000..7605a80972cb2 --- /dev/null +++ b/ports/stm/common-hal/audiobusio/I2S.h @@ -0,0 +1,11 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "shared-module/audiocore/__init__.h" + +// Although IS2 is not enabled on the STM32L4 family, this file is still required for the build to pass diff --git a/ports/stm/common-hal/audiobusio/I2SIn.c b/ports/stm/common-hal/audiobusio/I2SIn.c new file mode 100644 index 0000000000000..9332f224b8e4e --- /dev/null +++ b/ports/stm/common-hal/audiobusio/I2SIn.c @@ -0,0 +1,7 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT + +// Although IS2In is not enabled on the STM32L4 family, this file is still required for the build to pass diff --git a/ports/stm/common-hal/audiobusio/I2SIn.h b/ports/stm/common-hal/audiobusio/I2SIn.h new file mode 100644 index 0000000000000..fbbc045750797 --- /dev/null +++ b/ports/stm/common-hal/audiobusio/I2SIn.h @@ -0,0 +1,11 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "shared-module/audiocore/__init__.h" + +// Although IS2In is not enabled on the STM32L4 family, this file is still required for the build to pass diff --git a/py/circuitpy_defns.mk b/py/circuitpy_defns.mk index dc3d35d8df22f..311a0af335cc5 100644 --- a/py/circuitpy_defns.mk +++ b/py/circuitpy_defns.mk @@ -472,6 +472,8 @@ SRC_COMMON_HAL_ALL = \ analogio/AnalogIn.c \ analogio/AnalogOut.c \ analogio/__init__.c \ + audiobusio/I2S.c \ + audiobusio/I2SIn.c \ audiobusio/I2SOut.c \ audiobusio/PDMIn.c \ audiobusio/__init__.c \ diff --git a/py/circuitpy_mpconfig.mk b/py/circuitpy_mpconfig.mk index 1a831dbf314b5..38eba4182092d 100644 --- a/py/circuitpy_mpconfig.mk +++ b/py/circuitpy_mpconfig.mk @@ -121,6 +121,10 @@ CFLAGS += -DCIRCUITPY_AUDIOBUSIO_I2SOUT=$(CIRCUITPY_AUDIOBUSIO_I2SOUT) CIRCUITPY_AUDIOBUSIO_PDMIN ?= $(CIRCUITPY_AUDIOBUSIO) CFLAGS += -DCIRCUITPY_AUDIOBUSIO_PDMIN=$(CIRCUITPY_AUDIOBUSIO_PDMIN) +# Only RP2xxx boards currently support I2SIn +CIRCUITPY_AUDIOBUSIO_I2SIN ?= 0 +CFLAGS += -DCIRCUITPY_AUDIOBUSIO_I2SIN=$(CIRCUITPY_AUDIOBUSIO_I2SIN) + CIRCUITPY_AUDIOIO ?= $(CIRCUITPY_FULL_BUILD) CFLAGS += -DCIRCUITPY_AUDIOIO=$(CIRCUITPY_AUDIOIO) diff --git a/shared-bindings/audiobusio/I2S.c b/shared-bindings/audiobusio/I2S.c new file mode 100644 index 0000000000000..3e8a8087a337b --- /dev/null +++ b/shared-bindings/audiobusio/I2S.c @@ -0,0 +1,353 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT + +#include + +#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/audiobusio/I2S.h" +#include "shared-bindings/util.h" + +//| class I2S: +//| """Connect with an I2S bus to input and/or output an audio stream""" +//| +//| def __init__( +//| self, +//| bit_clock: microcontroller.Pin, +//| word_select: microcontroller.Pin, +//| *, +//| data_out: Optional[microcontroller.Pin] = None, +//| data_in: Optional[microcontroller.Pin] = None, +//| main_clock: Optional[microcontroller.Pin] = None, +//| left_justified: bool = False, +//| buffer_size: int = 512, +//| channel_count: int = 2, +//| sample_rate: int = 8000, +//| bits_per_sample: int = 16, +//| samples_signed: bool = True +//| ) -> None: +//| """Create a I2S object associated with the given pins. +//| +//| :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_out: The data output pin +//| :param ~microcontroller.Pin data_in: The data input pin +//| :param ~microcontroller.Pin main_clock: The main clock pin +//| :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. +//| :param int buffer_size: The total size in bytes of the input buffer. Only used if handling +//| input. +//| :param int channel_count: The number of channels. 1 = mono; 2 = stereo. Only used if handling +//| input. +//| :param int sample_rate: The desired sample rate. Only used if handling input. +//| :param int bits_per_sample: Number of bits per sample. Must be divisible by 8. Only used if +//| handling input. +//| :param bool samples_signed: Samples are signed (True) or unsigned (False). Only used if +//| handling input. +//| +//| Simple 8ksps 440 Hz sine wave on `Metro M0 Express `_ +//| using `UDA1334 Breakout `_:: +//| +//| import audiobusio +//| import audiocore +//| import board +//| import array +//| import time +//| import math +//| +//| # Generate one period of sine wave. +//| length = 8000 // 440 +//| sine_wave = array.array("H", [0] * length) +//| for i in range(length): +//| sine_wave[i] = int(math.sin(math.pi * 2 * i / length) * (2 ** 15) + 2 ** 15) +//| +//| sine_wave = audiocore.RawSample(sine_wave, sample_rate=8000) +//| i2s = audiobusio.I2S(board.D1, board.D0, data_out=board.D9) +//| i2s.play(sine_wave, loop=True) +//| time.sleep(1) +//| i2s.stop() +//| +//| Playing a wave file from flash:: +//| +//| import board +//| import audiocore +//| import audiobusio +//| import digitalio +//| +//| f = open("cplay-5.1-16bit-16khz.wav", "rb") +//| wav = audiocore.WaveFile(f) +//| +//| a = audiobusio.I2S(board.D1, board.D0, data_out=board.D9) +//| +//| print("playing") +//| a.play(wav) +//| while a.playing: +//| pass +//| print("stopped") +//| +//| Playing an I2S input signal to a PWMAudioOut:: +//| +//| import audiobusio +//| import board +//| import audiopwmio +//| +//| mic = audiobusio.I2S(board.GP0, board.GP1, data_in=board.GP2, channel_count=1, sample_rate=16000) +//| dac = audiopwmio.PWMAudioOut(board.GP3) +//| dac.play(mic) +//| """ +//| ... +static mp_obj_t audiobusio_i2s_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { + #if !CIRCUITPY_AUDIOBUSIO_I2SOUT || !CIRCUITPY_AUDIOBUSIO_I2SIN + mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_I2S); + return NULL; // Not reachable. + #else + enum { ARG_bit_clock, ARG_word_select, ARG_data_out, ARG_data_in, ARG_main_clock, ARG_left_justified, ARG_buffer_size, ARG_channel_count, ARG_sample_rate, ARG_bits_per_sample, 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_out, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = mp_const_none} }, + { MP_QSTR_data_in, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = mp_const_none} }, + { MP_QSTR_main_clock, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = mp_const_none} }, + { MP_QSTR_left_justified, MP_ARG_KW_ONLY | MP_ARG_BOOL, {.u_bool = false} }, + { MP_QSTR_buffer_size, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 512} }, + { MP_QSTR_channel_count, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 2} }, + { MP_QSTR_sample_rate, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 8000} }, + { MP_QSTR_bits_per_sample, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 16} }, + { 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); + + 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_out = validate_obj_is_free_pin_or_none(args[ARG_data_out].u_obj, MP_QSTR_data_out); + const mcu_pin_obj_t *data_in = validate_obj_is_free_pin_or_none(args[ARG_data_in].u_obj, MP_QSTR_data_in); + const mcu_pin_obj_t *main_clock = validate_obj_is_free_pin_or_none(args[ARG_main_clock].u_obj, MP_QSTR_main_clock); + + mp_int_t channel_count = mp_arg_validate_int_range(args[ARG_channel_count].u_int, 1, 2, MP_QSTR_channel_count); + mp_int_t sample_rate = mp_arg_validate_int_min(args[ARG_sample_rate].u_int, 1, MP_QSTR_sample_rate); + mp_int_t bits_per_sample = mp_arg_validate_int_range(args[ARG_bits_per_sample].u_int, 8, 32, MP_QSTR_bits_per_sample); + if (bits_per_sample % 8 != 0) { + mp_raise_ValueError_varg(MP_ERROR_TEXT("%q must be multiple of 8."), MP_QSTR_bits_per_sample); + } + + audiobusio_i2s_obj_t *self = mp_obj_malloc(audiobusio_i2s_obj_t, &audiobusio_i2s_type); + common_hal_audiobusio_i2s_construct(self, bit_clock, word_select, data_out, data_in, main_clock, args[ARG_left_justified].u_bool, args[ARG_buffer_size].u_int, channel_count, sample_rate, bits_per_sample, args[ARG_samples_signed].u_bool); + + return MP_OBJ_FROM_PTR(self); + #endif +} + +#if CIRCUITPY_AUDIOBUSIO_I2SOUT && CIRCUITPY_AUDIOBUSIO_I2SIN + +//| def deinit(self) -> None: +//| """Deinitialises the I2S and releases any hardware resources for reuse.""" +//| ... +static mp_obj_t audiobusio_i2s_deinit(mp_obj_t self_in) { + audiobusio_i2s_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_audiobusio_i2s_deinit(self); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(audiobusio_i2s_deinit_obj, audiobusio_i2s_deinit); + +static void check_for_deinit(audiobusio_i2s_obj_t *self) { + if (common_hal_audiobusio_i2s_deinited(self)) { + raise_deinited_error(); + } +} + +//| def __enter__(self) -> I2S: +//| """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.""" +//| ... +static mp_obj_t audiobusio_i2s_obj___exit__(size_t n_args, const mp_obj_t *args) { + (void)n_args; + common_hal_audiobusio_i2s_deinit(args[0]); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(audiobusio_i2s___exit___obj, 4, 4, audiobusio_i2s_obj___exit__); + +//| def record(self, destination: WriteableBuffer, destination_length: int) -> int: +//| """Records destination_length bytes of samples to destination. This is +//| blocking. +//| +//| An IOError may be raised when the destination is too slow to record the +//| audio at the given rate. For internal flash, writing all 1s to the file +//| before recording is recommended to speed up writes. +//| +//| :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 audiobusio_i2s_obj_record(mp_obj_t self_obj, mp_obj_t destination, mp_obj_t destination_length) { + audiobusio_i2s_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")); + } else if (mp_get_buffer(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_audiobusio_i2s_get_bits_per_sample(self); + if (bufinfo.typecode != 'h' && bit_depth == 16) { + mp_raise_ValueError(MP_ERROR_TEXT("destination buffer must be an array of type 'h' for bit_depth = 16")); + } else if (bufinfo.typecode != 'B' && bufinfo.typecode != BYTEARRAY_TYPECODE && bit_depth == 8) { + mp_raise_ValueError(MP_ERROR_TEXT("destination buffer must be a bytearray or array of type 'B' for bit_depth = 8")); + } + // length is the buffer length in slots, not bytes. + uint32_t length_written = + common_hal_audiobusio_i2s_record_to_buffer(self, bufinfo.buf, length); + return MP_OBJ_NEW_SMALL_INT(length_written); + } + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_3(audiobusio_i2s_record_obj, audiobusio_i2s_obj_record); + +//| def play(self, sample: circuitpython_typing.AudioSample, *, loop: bool = False) -> None: +//| """Plays the sample once when loop=False and continuously when loop=True. +//| Does not block. Use `playing` to block. +//| +//| Sample must be an `audiocore.WaveFile`, `audiocore.RawSample`, `audiomixer.Mixer` or `audiomp3.MP3Decoder`. +//| +//| The sample itself should consist of 8 bit or 16 bit samples.""" +//| ... +static mp_obj_t audiobusio_i2s_obj_play(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_sample, ARG_loop }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_sample, MP_ARG_OBJ | MP_ARG_REQUIRED }, + { MP_QSTR_loop, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = false} }, + }; + audiobusio_i2s_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]); + check_for_deinit(self); + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + mp_obj_t sample = args[ARG_sample].u_obj; + common_hal_audiobusio_i2s_play(self, sample, args[ARG_loop].u_bool); + + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_KW(audiobusio_i2s_play_obj, 1, audiobusio_i2s_obj_play); + +//| def stop(self) -> None: +//| """Stops playback.""" +//| ... +static mp_obj_t audiobusio_i2s_obj_stop(mp_obj_t self_in) { + audiobusio_i2s_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + common_hal_audiobusio_i2s_stop(self); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_1(audiobusio_i2s_stop_obj, audiobusio_i2s_obj_stop); + +//| playing: bool +//| """True when the audio sample is being output. (read-only)""" +static mp_obj_t audiobusio_i2s_obj_get_playing(mp_obj_t self_in) { + audiobusio_i2s_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return mp_obj_new_bool(common_hal_audiobusio_i2s_get_playing(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(audiobusio_i2s_get_playing_obj, audiobusio_i2s_obj_get_playing); + +MP_PROPERTY_GETTER(audiobusio_i2s_playing_obj, + (mp_obj_t)&audiobusio_i2s_get_playing_obj); + +//| def pause(self) -> None: +//| """Stops playback temporarily while remembering the position. Use `resume` to resume playback.""" +//| ... +static mp_obj_t audiobusio_i2s_obj_pause(mp_obj_t self_in) { + audiobusio_i2s_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + + if (!common_hal_audiobusio_i2s_get_playing(self)) { + mp_raise_RuntimeError(MP_ERROR_TEXT("Not playing")); + } + common_hal_audiobusio_i2s_pause(self); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_1(audiobusio_i2s_pause_obj, audiobusio_i2s_obj_pause); + +//| def resume(self) -> None: +//| """Resumes sample playback after :py:func:`pause`.""" +//| ... +static mp_obj_t audiobusio_i2s_obj_resume(mp_obj_t self_in) { + audiobusio_i2s_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + + if (common_hal_audiobusio_i2s_get_paused(self)) { + common_hal_audiobusio_i2s_resume(self); + } + + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_1(audiobusio_i2s_resume_obj, audiobusio_i2s_obj_resume); + +//| paused: bool +//| """True when playback is paused. (read-only)""" +//| +static mp_obj_t audiobusio_i2s_obj_get_paused(mp_obj_t self_in) { + audiobusio_i2s_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return mp_obj_new_bool(common_hal_audiobusio_i2s_get_paused(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(audiobusio_i2s_get_paused_obj, audiobusio_i2s_obj_get_paused); + +MP_PROPERTY_GETTER(audiobusio_i2s_paused_obj, + (mp_obj_t)&audiobusio_i2s_get_paused_obj); + +#endif // CIRCUITPY_AUDIOBUSIO_I2SOUT && CIRCUITPY_AUDIOBUSIO_I2SIN + +static const mp_rom_map_elem_t audiobusio_i2s_locals_dict_table[] = { + #if CIRCUITPY_AUDIOBUSIO_I2SOUT && CIRCUITPY_AUDIOBUSIO_I2SIN + // Methods + { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&audiobusio_i2s_deinit_obj) }, + { MP_ROM_QSTR(MP_QSTR___enter__), MP_ROM_PTR(&default___enter___obj) }, + { MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&audiobusio_i2s___exit___obj) }, + { MP_ROM_QSTR(MP_QSTR_record), MP_ROM_PTR(&audiobusio_i2s_record_obj) }, + { MP_ROM_QSTR(MP_QSTR_play), MP_ROM_PTR(&audiobusio_i2s_play_obj) }, + { MP_ROM_QSTR(MP_QSTR_stop), MP_ROM_PTR(&audiobusio_i2s_stop_obj) }, + { MP_ROM_QSTR(MP_QSTR_pause), MP_ROM_PTR(&audiobusio_i2s_pause_obj) }, + { MP_ROM_QSTR(MP_QSTR_resume), MP_ROM_PTR(&audiobusio_i2s_resume_obj) }, + + // Properties + { MP_ROM_QSTR(MP_QSTR_playing), MP_ROM_PTR(&audiobusio_i2s_playing_obj) }, + { MP_ROM_QSTR(MP_QSTR_paused), MP_ROM_PTR(&audiobusio_i2s_paused_obj) }, + #endif // CIRCUITPY_AUDIOBUSIO_I2SOUT && CIRCUITPY_AUDIOBUSIO_I2SIN +}; +static MP_DEFINE_CONST_DICT(audiobusio_i2s_locals_dict, audiobusio_i2s_locals_dict_table); + +static const audiosample_p_t audiobusio_i2s_proto = { + MP_PROTO_IMPLEMENT(MP_QSTR_protocol_audiosample) + #if CIRCUITPY_AUDIOBUSIO_I2SOUT && CIRCUITPY_AUDIOBUSIO_I2SIN + .sample_rate = (audiosample_sample_rate_fun)common_hal_audiobusio_i2s_get_sample_rate, + .bits_per_sample = (audiosample_bits_per_sample_fun)common_hal_audiobusio_i2s_get_bits_per_sample, + .channel_count = (audiosample_channel_count_fun)common_hal_audiobusio_i2s_get_channel_count, + .reset_buffer = (audiosample_reset_buffer_fun)audiobusio_i2s_reset_buffer, + .get_buffer = (audiosample_get_buffer_fun)audiobusio_i2s_get_buffer, + .get_buffer_structure = (audiosample_get_buffer_structure_fun)audiobusio_i2s_get_buffer_structure, + #endif // CIRCUITPY_AUDIOBUSIO_I2SOUT && CIRCUITPY_AUDIOBUSIO_I2SIN +}; + +MP_DEFINE_CONST_OBJ_TYPE( + audiobusio_i2s_type, + MP_QSTR_I2S, + MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS, + make_new, audiobusio_i2s_make_new, + locals_dict, &audiobusio_i2s_locals_dict, + protocol, &audiobusio_i2s_proto + ); diff --git a/shared-bindings/audiobusio/I2S.h b/shared-bindings/audiobusio/I2S.h new file mode 100644 index 0000000000000..82509cfd6946a --- /dev/null +++ b/shared-bindings/audiobusio/I2S.h @@ -0,0 +1,40 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "common-hal/audiobusio/I2S.h" +#include "common-hal/microcontroller/Pin.h" +#include "extmod/vfs_fat.h" + +extern const mp_obj_type_t audiobusio_i2s_type; + +#if CIRCUITPY_AUDIOBUSIO_I2SOUT && CIRCUITPY_AUDIOBUSIO_I2SIN + +void common_hal_audiobusio_i2s_construct(audiobusio_i2s_obj_t *self, + const mcu_pin_obj_t *bit_clock, const mcu_pin_obj_t *word_select, const mcu_pin_obj_t *data_out, const mcu_pin_obj_t *data_in, + const mcu_pin_obj_t *main_clock, bool left_justified, + uint32_t buffer_size, uint8_t channel_count, uint32_t sample_rate, uint8_t bits_per_sample, + bool samples_signed); + +void common_hal_audiobusio_i2s_deinit(audiobusio_i2s_obj_t *self); +bool common_hal_audiobusio_i2s_deinited(audiobusio_i2s_obj_t *self); + +uint32_t common_hal_audiobusio_i2s_record_to_buffer(audiobusio_i2s_obj_t *self, + int16_t *buffer, uint32_t length); + +void common_hal_audiobusio_i2s_play(audiobusio_i2s_obj_t *self, mp_obj_t sample, bool loop); +void common_hal_audiobusio_i2s_stop(audiobusio_i2s_obj_t *self); +bool common_hal_audiobusio_i2s_get_playing(audiobusio_i2s_obj_t *self); +void common_hal_audiobusio_i2s_pause(audiobusio_i2s_obj_t *self); +void common_hal_audiobusio_i2s_resume(audiobusio_i2s_obj_t *self); +bool common_hal_audiobusio_i2s_get_paused(audiobusio_i2s_obj_t *self); + +uint32_t common_hal_audiobusio_i2s_get_sample_rate(audiobusio_i2s_obj_t *self); +uint8_t common_hal_audiobusio_i2s_get_channel_count(audiobusio_i2s_obj_t *self); +uint8_t common_hal_audiobusio_i2s_get_bits_per_sample(audiobusio_i2s_obj_t *self); + +#endif // CIRCUITPY_AUDIOBUSIO_I2SOUT && CIRCUITPY_AUDIOBUSIO_I2SIN diff --git a/shared-bindings/audiobusio/I2SIn.c b/shared-bindings/audiobusio/I2SIn.c new file mode 100644 index 0000000000000..eecc9fdc0382d --- /dev/null +++ b/shared-bindings/audiobusio/I2SIn.c @@ -0,0 +1,153 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT + +#include + +#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/audiobusio/I2SIn.h" +#include "shared-bindings/util.h" + +//| class I2SIn: +//| """Record an input I2S audio stream""" +//| +//| def __init__( +//| self, +//| bit_clock: microcontroller.Pin, +//| word_select: microcontroller.Pin, +//| data: microcontroller.Pin, +//| *, +//| buffer_size: int = 512, +//| channel_count: int = 2, +//| sample_rate: int = 8000, +//| bits_per_sample: int = 16, +//| samples_signed: bool = True +//| ) -> None: +//| """Create a I2SIn object associated with the given pins. This allows you to +//| record audio signals from the given pins. Individual ports may put further +//| restrictions on the recording parameters. +//| +//| :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 pin +//| :param int buffer_size: The total size in bytes of the input buffer +//| :param int channel_count: The number of channels. 1 = mono; 2 = stereo. +//| :param int sample_rate: The desired sample rate +//| :param int bits_per_sample: Number of bits per sample. Must be divisible by 8 +//| :param bool samples_signed: Samples are signed (True) or unsigned (False) +//| +//| Playing an I2SIn signal to a PWMAudioOut:: +//| +//| import audiobusio +//| import board +//| import audiopwmio +//| +//| mic = audiobusio.I2SIn(board.GP0, board.GP1, board.GP2, channel_count=1, sample_rate=16000) +//| dac = audiopwmio.PWMAudioOut(board.GP3) +//| dac.play(mic) +//| """ +//| ... +static mp_obj_t audiobusio_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_AUDIOBUSIO_I2SIN || CIRCUITPY_AUDIOBUSIO_I2SOUT + 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_buffer_size, ARG_channel_count, ARG_sample_rate, ARG_bits_per_sample, 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_buffer_size, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 512} }, + { MP_QSTR_channel_count, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 2} }, + { MP_QSTR_sample_rate, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 8000} }, + { MP_QSTR_bits_per_sample, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 16} }, + { 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); + + 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); + + mp_int_t channel_count = mp_arg_validate_int_range(args[ARG_channel_count].u_int, 1, 2, MP_QSTR_channel_count); + mp_int_t sample_rate = mp_arg_validate_int_min(args[ARG_sample_rate].u_int, 1, MP_QSTR_sample_rate); + mp_int_t bits_per_sample = mp_arg_validate_int_range(args[ARG_bits_per_sample].u_int, 8, 32, MP_QSTR_bits_per_sample); + if (bits_per_sample % 8 != 0) { + mp_raise_ValueError_varg(MP_ERROR_TEXT("%q must be multiple of 8."), MP_QSTR_bits_per_sample); + } + + audiobusio_i2sin_obj_t *self = mp_obj_malloc(audiobusio_i2sin_obj_t, &audiobusio_i2sin_type); + common_hal_audiobusio_i2sin_construct(self, bit_clock, word_select, data, args[ARG_buffer_size].u_int, channel_count, sample_rate, bits_per_sample, args[ARG_samples_signed].u_bool); + + return MP_OBJ_FROM_PTR(self); + #endif +} + +#if CIRCUITPY_AUDIOBUSIO_I2SIN && !CIRCUITPY_AUDIOBUSIO_I2SOUT + +//| def deinit(self) -> None: +//| """Deinitialises the I2SIn and releases any hardware resources for reuse.""" +//| ... +static mp_obj_t audiobusio_i2sin_deinit(mp_obj_t self_in) { + audiobusio_i2sin_obj_t *self = MP_OBJ_TO_PTR(self_in); + common_hal_audiobusio_i2sin_deinit(self); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(audiobusio_i2sin_deinit_obj, audiobusio_i2sin_deinit); + +//| 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.""" +//| ... +//| +static mp_obj_t audiobusio_i2sin_obj___exit__(size_t n_args, const mp_obj_t *args) { + (void)n_args; + common_hal_audiobusio_i2sin_deinit(args[0]); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(audiobusio_i2sin___exit___obj, 4, 4, audiobusio_i2sin_obj___exit__); + +#endif // CIRCUITPY_AUDIOBUSIO_I2SIN + +static const mp_rom_map_elem_t audiobusio_i2sin_locals_dict_table[] = { + #if CIRCUITPY_AUDIOBUSIO_I2SIN && !CIRCUITPY_AUDIOBUSIO_I2SOUT + // Methods + { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&audiobusio_i2sin_deinit_obj) }, + { MP_ROM_QSTR(MP_QSTR___enter__), MP_ROM_PTR(&default___enter___obj) }, + { MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&audiobusio_i2sin___exit___obj) }, + #endif // CIRCUITPY_AUDIOBUSIO_I2SIN +}; +static MP_DEFINE_CONST_DICT(audiobusio_i2sin_locals_dict, audiobusio_i2sin_locals_dict_table); + +static const audiosample_p_t audiobusio_i2sin_proto = { + MP_PROTO_IMPLEMENT(MP_QSTR_protocol_audiosample) + #if CIRCUITPY_AUDIOBUSIO_I2SIN && !CIRCUITPY_AUDIOBUSIO_I2SOUT + .sample_rate = (audiosample_sample_rate_fun)common_hal_audiobusio_i2sin_get_sample_rate, + .bits_per_sample = (audiosample_bits_per_sample_fun)common_hal_audiobusio_i2sin_get_bits_per_sample, + .channel_count = (audiosample_channel_count_fun)common_hal_audiobusio_i2sin_get_channel_count, + .reset_buffer = (audiosample_reset_buffer_fun)audiobusio_i2sin_reset_buffer, + .get_buffer = (audiosample_get_buffer_fun)audiobusio_i2sin_get_buffer, + .get_buffer_structure = (audiosample_get_buffer_structure_fun)audiobusio_i2sin_get_buffer_structure, + #endif // CIRCUITPY_AUDIOBUSIO_I2SIN +}; + +MP_DEFINE_CONST_OBJ_TYPE( + audiobusio_i2sin_type, + MP_QSTR_I2SIn, + MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS, + make_new, audiobusio_i2sin_make_new, + locals_dict, &audiobusio_i2sin_locals_dict, + protocol, &audiobusio_i2sin_proto + ); diff --git a/shared-bindings/audiobusio/I2SIn.h b/shared-bindings/audiobusio/I2SIn.h new file mode 100644 index 0000000000000..65d9867f8f2d9 --- /dev/null +++ b/shared-bindings/audiobusio/I2SIn.h @@ -0,0 +1,28 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2024 Cooper Dalrymple +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "common-hal/audiobusio/I2SIn.h" +#include "common-hal/microcontroller/Pin.h" +#include "extmod/vfs_fat.h" + +extern const mp_obj_type_t audiobusio_i2sin_type; + +#if CIRCUITPY_AUDIOBUSIO_I2SIN && !CIRCUITPY_AUDIOBUSIO_I2SOUT +void common_hal_audiobusio_i2sin_construct(audiobusio_i2sin_obj_t *self, + const mcu_pin_obj_t *bit_clock, const mcu_pin_obj_t *word_select, const mcu_pin_obj_t *data, + uint32_t buffer_size, uint8_t channel_count, uint32_t sample_rate, uint8_t bits_per_sample, + bool samples_signed); + +void common_hal_audiobusio_i2sin_deinit(audiobusio_i2sin_obj_t *self); +bool common_hal_audiobusio_i2sin_deinited(audiobusio_i2sin_obj_t *self); + +uint32_t common_hal_audiobusio_i2sin_get_sample_rate(audiobusio_i2sin_obj_t *self); +uint8_t common_hal_audiobusio_i2sin_get_channel_count(audiobusio_i2sin_obj_t *self); +uint8_t common_hal_audiobusio_i2sin_get_bits_per_sample(audiobusio_i2sin_obj_t *self); + +#endif // CIRCUITPY_AUDIOBUSIO_I2SIN && !CIRCUITPY_AUDIOBUSIO_I2SOUT diff --git a/shared-bindings/audiobusio/I2SOut.c b/shared-bindings/audiobusio/I2SOut.c index 7c24e8a9efe81..2213a17eb4572 100644 --- a/shared-bindings/audiobusio/I2SOut.c +++ b/shared-bindings/audiobusio/I2SOut.c @@ -77,7 +77,7 @@ //| print("stopped")""" //| ... static mp_obj_t audiobusio_i2sout_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { - #if !CIRCUITPY_AUDIOBUSIO_I2SOUT + #if !CIRCUITPY_AUDIOBUSIO_I2SOUT || CIRCUITPY_AUDIOBUSIO_I2SIN mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("%q"), MP_QSTR_I2SOut); return NULL; // Not reachable. #else @@ -87,7 +87,7 @@ static mp_obj_t audiobusio_i2sout_make_new(const mp_obj_type_t *type, size_t n_a { MP_QSTR_word_select, MP_ARG_OBJ | MP_ARG_REQUIRED }, { MP_QSTR_data, MP_ARG_OBJ | MP_ARG_REQUIRED }, { MP_QSTR_main_clock, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = mp_const_none} }, - { MP_QSTR_left_justified, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_bool = false} }, + { MP_QSTR_left_justified, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.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); @@ -104,7 +104,7 @@ static mp_obj_t audiobusio_i2sout_make_new(const mp_obj_type_t *type, size_t n_a #endif } -#if CIRCUITPY_AUDIOBUSIO_I2SOUT +#if CIRCUITPY_AUDIOBUSIO_I2SOUT && !CIRCUITPY_AUDIOBUSIO_I2SIN //| def deinit(self) -> None: //| """Deinitialises the I2SOut and releases any hardware resources for reuse.""" @@ -234,7 +234,7 @@ MP_PROPERTY_GETTER(audiobusio_i2sout_paused_obj, static const mp_rom_map_elem_t audiobusio_i2sout_locals_dict_table[] = { // Methods - #if CIRCUITPY_AUDIOBUSIO_I2SOUT + #if CIRCUITPY_AUDIOBUSIO_I2SOUT && !CIRCUITPY_AUDIOBUSIO_I2SIN { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&audiobusio_i2sout_deinit_obj) }, { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&audiobusio_i2sout_deinit_obj) }, { MP_ROM_QSTR(MP_QSTR___enter__), MP_ROM_PTR(&default___enter___obj) }, diff --git a/shared-bindings/audiobusio/I2SOut.h b/shared-bindings/audiobusio/I2SOut.h index a53997ef91b68..1ba7b2ca9d1de 100644 --- a/shared-bindings/audiobusio/I2SOut.h +++ b/shared-bindings/audiobusio/I2SOut.h @@ -12,7 +12,7 @@ extern const mp_obj_type_t audiobusio_i2sout_type; // Some boards don't have the I2SOut pins available. -#if CIRCUITPY_AUDIOBUSIO_I2SOUT +#if CIRCUITPY_AUDIOBUSIO_I2SOUT && !CIRCUITPY_AUDIOBUSIO_I2SIN void common_hal_audiobusio_i2sout_construct(audiobusio_i2sout_obj_t *self, const mcu_pin_obj_t *bit_clock, const mcu_pin_obj_t *word_select, const mcu_pin_obj_t *data, @@ -27,4 +27,4 @@ void common_hal_audiobusio_i2sout_pause(audiobusio_i2sout_obj_t *self); void common_hal_audiobusio_i2sout_resume(audiobusio_i2sout_obj_t *self); bool common_hal_audiobusio_i2sout_get_paused(audiobusio_i2sout_obj_t *self); -#endif // CIRCUITPY_AUDIOBUSIO_I2SOUT +#endif // CIRCUITPY_AUDIOBUSIO_I2SOUT && !CIRCUITPY_AUDIOBUSIO_I2SIN diff --git a/shared-bindings/audiobusio/__init__.c b/shared-bindings/audiobusio/__init__.c index 70680b1cf16e4..2e90f9b6cce32 100644 --- a/shared-bindings/audiobusio/__init__.c +++ b/shared-bindings/audiobusio/__init__.c @@ -11,6 +11,8 @@ #include "shared-bindings/microcontroller/Pin.h" #include "shared-bindings/audiobusio/__init__.h" +#include "shared-bindings/audiobusio/I2S.h" +#include "shared-bindings/audiobusio/I2SIn.h" #include "shared-bindings/audiobusio/I2SOut.h" #include "shared-bindings/audiobusio/PDMIn.h" @@ -27,6 +29,8 @@ static const mp_rom_map_elem_t audiobusio_module_globals_table[] = { { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_audiobusio) }, + { MP_ROM_QSTR(MP_QSTR_I2S), MP_ROM_PTR(&audiobusio_i2s_type) }, + { MP_ROM_QSTR(MP_QSTR_I2SIn), MP_ROM_PTR(&audiobusio_i2sin_type) }, { MP_ROM_QSTR(MP_QSTR_I2SOut), MP_ROM_PTR(&audiobusio_i2sout_type) }, { MP_ROM_QSTR(MP_QSTR_PDMIn), MP_ROM_PTR(&audiobusio_pdmin_type) }, };