diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot index e707386d0a47..40fd2e2d5b4d 100644 --- a/locale/circuitpython.pot +++ b/locale/circuitpython.pot @@ -1177,6 +1177,7 @@ msgid "Interrupted by output function" msgstr "" #: ports/espressif/common-hal/espulp/ULP.c +#: ports/espressif/common-hal/microcontroller/Processor.c #: ports/mimxrt10xx/common-hal/audiobusio/__init__.c #: ports/mimxrt10xx/common-hal/pwmio/PWMOut.c #: ports/raspberrypi/bindings/picodvi/Framebuffer.c @@ -1284,6 +1285,10 @@ msgstr "" msgid "Layer must be a Group or TileGrid subclass" msgstr "" +#: shared-bindings/audiocore/RawSample.c +msgid "Length of %q must be an even multiple of channel_count * type_size" +msgstr "" + #: ports/espressif/common-hal/espidf/__init__.c msgid "MAC address was invalid" msgstr "" diff --git a/shared-bindings/audiocore/RawSample.c b/shared-bindings/audiocore/RawSample.c index 26df89b11644..8da693b7c599 100644 --- a/shared-bindings/audiocore/RawSample.c +++ b/shared-bindings/audiocore/RawSample.c @@ -17,7 +17,12 @@ //| """A raw audio sample buffer in memory""" //| //| def __init__( -//| self, buffer: ReadableBuffer, *, channel_count: int = 1, sample_rate: int = 8000 +//| self, +//| buffer: ReadableBuffer, +//| *, +//| channel_count: int = 1, +//| sample_rate: int = 8000, +//| single_buffer: bool = True //| ) -> None: //| """Create a RawSample based on the given buffer of values. If channel_count is more than //| 1 then each channel's samples should alternate. In other words, for a two channel buffer, the @@ -27,34 +32,58 @@ //| :param ~circuitpython_typing.ReadableBuffer buffer: A buffer with samples //| :param int channel_count: The number of channels in the buffer //| :param int sample_rate: The desired playback sample rate +//| :param bool single_buffer: Selects single buffered or double buffered transfer mode. This affects +//| what happens if the sample buffer is changed while the sample is playing. +//| In single buffered transfers, a change in buffer contents will not affect active playback. +//| In double buffered transfers, changed buffer contents will +//| be played back when the transfer reaches the next half-buffer point. //| -//| Simple 8ksps 440 Hz sin wave:: +//| Playing 8ksps 440 Hz and 880 Hz sine waves:: //| +//| import analogbufio +//| import array //| import audiocore -//| import audioio +//| import audiopwmio //| import board -//| import array -//| import time //| import math +//| import time //| -//| # Generate one period of sine wav. +//| # 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)) +//| pwm = audiopwmio.PWMAudioOut(left_channel=board.D12, right_channel=board.D13) //| -//| dac = audioio.AudioOut(board.SPEAKER) -//| sine_wave = audiocore.RawSample(sine_wave) -//| dac.play(sine_wave, loop=True) +//| # Play single-buffered +//| sample = audiocore.RawSample(sine_wave) +//| pwm.play(sample, loop=True) +//| time.sleep(3) +//| # changing the wave has no effect +//| for i in range(length): +//| sine_wave[i] = int(math.sin(math.pi * 4 * i / length) * (2 ** 15)) +//| time.sleep(3) +//| pwm.stop() //| time.sleep(1) -//| dac.stop()""" +//| +//| # Play double-buffered +//| sample = audiocore.RawSample(sine_wave, single_buffer=False) +//| pwm.play(sample, loop=True) +//| time.sleep(3) +//| # changing the wave takes effect almost immediately +//| for i in range(length): +//| sine_wave[i] = int(math.sin(math.pi * 2 * i / length) * (2 ** 15)) +//| time.sleep(3) +//| pwm.stop() +//| pwm.deinit()""" //| ... static mp_obj_t audioio_rawsample_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { - enum { ARG_buffer, ARG_channel_count, ARG_sample_rate }; + enum { ARG_buffer, ARG_channel_count, ARG_sample_rate, ARG_single_buffer }; static const mp_arg_t allowed_args[] = { { MP_QSTR_buffer, MP_ARG_OBJ | MP_ARG_REQUIRED, {.u_obj = MP_OBJ_NULL } }, { MP_QSTR_channel_count, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 1 } }, { MP_QSTR_sample_rate, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 8000} }, + { MP_QSTR_single_buffer, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.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); @@ -69,9 +98,12 @@ static mp_obj_t audioio_rawsample_make_new(const mp_obj_type_t *type, size_t n_a } else if (bufinfo.typecode != 'b' && bufinfo.typecode != 'B' && bufinfo.typecode != BYTEARRAY_TYPECODE) { mp_raise_ValueError_varg(MP_ERROR_TEXT("%q must be a bytearray or array of type 'h', 'H', 'b', or 'B'"), MP_QSTR_buffer); } + if (!args[ARG_single_buffer].u_bool && bufinfo.len % (bytes_per_sample * args[ARG_channel_count].u_int * 2) != 0) { + mp_raise_ValueError_varg(MP_ERROR_TEXT("Length of %q must be an even multiple of channel_count * type_size"), MP_QSTR_buffer); + } common_hal_audioio_rawsample_construct(self, ((uint8_t *)bufinfo.buf), bufinfo.len, bytes_per_sample, signed_samples, args[ARG_channel_count].u_int, - args[ARG_sample_rate].u_int); + args[ARG_sample_rate].u_int, args[ARG_single_buffer].u_bool); return MP_OBJ_FROM_PTR(self); } diff --git a/shared-bindings/audiocore/RawSample.h b/shared-bindings/audiocore/RawSample.h index ad138c1d362c..1f4ca7f8ec52 100644 --- a/shared-bindings/audiocore/RawSample.h +++ b/shared-bindings/audiocore/RawSample.h @@ -12,7 +12,7 @@ extern const mp_obj_type_t audioio_rawsample_type; void common_hal_audioio_rawsample_construct(audioio_rawsample_obj_t *self, uint8_t *buffer, uint32_t len, uint8_t bytes_per_sample, bool samples_signed, - uint8_t channel_count, uint32_t sample_rate); + uint8_t channel_count, uint32_t sample_rate, bool single_buffer); void common_hal_audioio_rawsample_deinit(audioio_rawsample_obj_t *self); bool common_hal_audioio_rawsample_deinited(audioio_rawsample_obj_t *self); diff --git a/shared-module/audiocore/RawSample.c b/shared-module/audiocore/RawSample.c index a985c29fb691..9944597d08f1 100644 --- a/shared-module/audiocore/RawSample.c +++ b/shared-module/audiocore/RawSample.c @@ -2,6 +2,8 @@ // // SPDX-FileCopyrightText: Copyright (c) 2018 Scott Shawcroft for Adafruit Industries // +// SPDX-FileCopyrightText: Copyright (c) 2024 Tim Chinowsky +// // SPDX-License-Identifier: MIT #include "shared-bindings/audiocore/RawSample.h" @@ -16,13 +18,17 @@ void common_hal_audioio_rawsample_construct(audioio_rawsample_obj_t *self, uint8_t bytes_per_sample, bool samples_signed, uint8_t channel_count, - uint32_t sample_rate) { + uint32_t sample_rate, + bool single_buffer) { + self->buffer = buffer; self->bits_per_sample = bytes_per_sample * 8; self->samples_signed = samples_signed; self->len = len; self->channel_count = channel_count; self->sample_rate = sample_rate; + self->single_buffer = single_buffer; + self->buffer_index = 0; } void common_hal_audioio_rawsample_deinit(audioio_rawsample_obj_t *self) { @@ -56,19 +62,33 @@ audioio_get_buffer_result_t audioio_rawsample_get_buffer(audioio_rawsample_obj_t uint8_t channel, uint8_t **buffer, uint32_t *buffer_length) { - *buffer_length = self->len; - if (single_channel_output) { - *buffer = self->buffer + (channel % self->channel_count) * (self->bits_per_sample / 8); + + if (self->single_buffer) { + *buffer_length = self->len; + if (single_channel_output) { + *buffer = self->buffer + (channel % self->channel_count) * (self->bits_per_sample / 8); + } else { + *buffer = self->buffer; + } + return GET_BUFFER_DONE; } else { - *buffer = self->buffer; + *buffer_length = self->len / 2; + if (single_channel_output) { + *buffer = self->buffer + (channel % self->channel_count) * (self->bits_per_sample / 8) + \ + self->len / 2 * self->buffer_index; + } else { + *buffer = self->buffer + self->len / 2 * self->buffer_index; + } + self->buffer_index = 1 - self->buffer_index; + return GET_BUFFER_DONE; } - return GET_BUFFER_DONE; } void audioio_rawsample_get_buffer_structure(audioio_rawsample_obj_t *self, bool single_channel_output, bool *single_buffer, bool *samples_signed, uint32_t *max_buffer_length, uint8_t *spacing) { - *single_buffer = true; + + *single_buffer = self->single_buffer; *samples_signed = self->samples_signed; *max_buffer_length = self->len; if (single_channel_output) { diff --git a/shared-module/audiocore/RawSample.h b/shared-module/audiocore/RawSample.h index 343cd0bfe848..3fa94404f1c4 100644 --- a/shared-module/audiocore/RawSample.h +++ b/shared-module/audiocore/RawSample.h @@ -18,6 +18,8 @@ typedef struct { bool samples_signed; uint8_t channel_count; uint32_t sample_rate; + bool single_buffer; + uint8_t buffer_index; } audioio_rawsample_obj_t; diff --git a/tests/circuitpython-manual/live_audio/mix.py b/tests/circuitpython-manual/live_audio/mix.py new file mode 100644 index 000000000000..2e8351a83f4b --- /dev/null +++ b/tests/circuitpython-manual/live_audio/mix.py @@ -0,0 +1,67 @@ +import audiocore +import audiopwmio +import audiomixer +import board +import array +import time +import math + +CHANNELS = 2 +RATE = 8000 +SAMPLE_TYPE = "H" +OFFSET = 2**15 - 1 +BUFFER_SIZE = 640 +SINGLE_BUFFER = True +LOOP = True + +# (frequency, amp_left, amp_right) +VOICES = ((200, 1, 0), (400, 0, 1), (100, 1, 1)) + + +def play( + voices=VOICES, + channels=CHANNELS, + rate=RATE, + sample_type=SAMPLE_TYPE, + offset=OFFSET, + buffer_size=BUFFER_SIZE, + single_buffer=SINGLE_BUFFER, + loop=LOOP, +): + waves = [] + samples = [] + for v in voices: + print(v) + sample_length = int(rate // v[0]) + wave = array.array(sample_type, [offset] * sample_length * channels) + for i in range(0, sample_length): + if channels == 1: + wave[i] = int( + math.sin(math.pi * 2 * i / sample_length) * v[1] * (2**15 - 1) + offset + ) + else: + wave[2 * i] = int( + math.sin(math.pi * 2 * i / sample_length) * v[1] * (2**15 - 1) + offset + ) + wave[2 * i + 1] = int( + math.sin(math.pi * 2 * i / sample_length) * v[2] * (2**15 - 1) + offset + ) + waves.append(wave) + samples.append( + audiocore.RawSample( + wave, sample_rate=rate, channel_count=channels, single_buffer=single_buffer + ) + ) + mixer = audiomixer.Mixer( + voice_count=len(voices), + sample_rate=rate, + channel_count=channels, + bits_per_sample=16, + samples_signed=False, + buffer_size=buffer_size, + ) + pwm = audiopwmio.PWMAudioOut(left_channel=board.D12, right_channel=board.D13) + pwm.play(mixer) + for i in range(len(samples)): + mixer.voice[i].play(samples[i], loop=loop) + mixer.voice[i].level = 0.5