From e9e4ce9546806de4e9f4b1dd6bf37d825391b673 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Sat, 1 Apr 2023 10:21:22 -0500 Subject: [PATCH] add waveform support in synthio a waveform object (array of 'h') can be passed in, replacing the standard square wave. This waveform must be a 'single cycle waveform' and some obvious things to pass in are sine, triangle or sawtooth waves, but you can construct whatever you like. --- locale/circuitpython.pot | 23 +++++++++------------- shared-bindings/synthio/MidiTrack.c | 30 +++++++++++++++++++++++++---- shared-bindings/synthio/MidiTrack.h | 2 +- shared-bindings/synthio/__init__.c | 20 +++++++++++++++++-- shared-bindings/synthio/__init__.h | 9 ++------- shared-module/synthio/MidiTrack.c | 21 ++++++++++++++++---- shared-module/synthio/MidiTrack.h | 4 +++- 7 files changed, 76 insertions(+), 33 deletions(-) diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot index b5026f7829..f87fc02975 100644 --- a/locale/circuitpython.pot +++ b/locale/circuitpython.pot @@ -122,6 +122,7 @@ msgid "%q in %q must be of type %q, not %q" msgstr "" #: ports/espressif/common-hal/espulp/ULP.c +#: ports/mimxrt10xx/common-hal/audiobusio/__init__.c #: ports/raspberrypi/common-hal/rp2pio/StateMachine.c #: shared-bindings/digitalio/DigitalInOut.c #: shared-bindings/microcontroller/Pin.c @@ -192,6 +193,10 @@ msgstr "" msgid "%q must be array of type 'H'" msgstr "" +#: shared-bindings/synthio/MidiTrack.c shared-bindings/synthio/__init__.c +msgid "%q must be array of type 'h'" +msgstr "" + #: ports/raspberrypi/bindings/cyw43/__init__.c py/argcheck.c py/objexcept.c #: shared-bindings/canio/CAN.c shared-bindings/digitalio/Pull.c msgid "%q must be of type %q or %q, not %q" @@ -1217,6 +1222,7 @@ msgstr "" msgid "Interrupt error." msgstr "" +#: ports/mimxrt10xx/common-hal/audiobusio/__init__.c #: ports/mimxrt10xx/common-hal/pwmio/PWMOut.c py/argcheck.c #: shared-bindings/digitalio/DigitalInOut.c #: shared-bindings/displayio/EPaperDisplay.c @@ -1224,6 +1230,7 @@ msgid "Invalid %q" msgstr "" #: ports/atmel-samd/common-hal/microcontroller/Pin.c +#: ports/mimxrt10xx/common-hal/microcontroller/Pin.c #: shared-bindings/microcontroller/Pin.c msgid "Invalid %q pin" msgstr "" @@ -3827,11 +3834,7 @@ msgstr "" msgid "out must be a float dense array" msgstr "" -#: shared-bindings/displayio/Bitmap.c -msgid "out of range of source" -msgstr "" - -#: shared-bindings/bitmaptools/__init__.c shared-bindings/displayio/Bitmap.c +#: shared-bindings/bitmaptools/__init__.c msgid "out of range of target" msgstr "" @@ -3856,14 +3859,10 @@ msgstr "" msgid "parameters must be registers in sequence r0 to r3" msgstr "" -#: shared-bindings/bitmaptools/__init__.c shared-bindings/displayio/Bitmap.c +#: shared-bindings/bitmaptools/__init__.c msgid "pixel coordinates out of bounds" msgstr "" -#: shared-bindings/displayio/Bitmap.c -msgid "pixel value requires too many bits" -msgstr "" - #: shared-bindings/displayio/TileGrid.c shared-bindings/vectorio/VectorShape.c msgid "pixel_shader must be displayio.Palette or displayio.ColorConverter" msgstr "" @@ -4276,10 +4275,6 @@ msgstr "" msgid "value out of range of target" msgstr "" -#: shared-bindings/displayio/Bitmap.c -msgid "value_count must be > 0" -msgstr "" - #: ports/espressif/common-hal/watchdog/WatchDogTimer.c msgid "watchdog not initialized" msgstr "" diff --git a/shared-bindings/synthio/MidiTrack.c b/shared-bindings/synthio/MidiTrack.c index b987337eaa..727b9ba335 100644 --- a/shared-bindings/synthio/MidiTrack.c +++ b/shared-bindings/synthio/MidiTrack.c @@ -32,13 +32,19 @@ #include "py/runtime.h" #include "shared-bindings/util.h" #include "shared-bindings/synthio/MidiTrack.h" +#include "shared-bindings/synthio/__init__.h" #include "supervisor/shared/translate/translate.h" //| class MidiTrack: -//| """Simple square-wave MIDI synth""" +//| """Simple MIDI synth""" //| //| def __init__( -//| self, buffer: ReadableBuffer, tempo: int, *, sample_rate: int = 11025 +//| self, +//| buffer: ReadableBuffer, +//| tempo: int, +//| *, +//| sample_rate: int = 11025, +//| waveform: ReadableBuffer = None //| ) -> None: //| """Create a MidiTrack from the given stream of MIDI events. Only "Note On" and "Note Off" events //| are supported; channel numbers and key velocities are ignored. Up to two notes may be on at the @@ -47,6 +53,7 @@ //| :param ~circuitpython_typing.ReadableBuffer buffer: Stream of MIDI events, as stored in a MIDI file track chunk //| :param int tempo: Tempo of the streamed events, in MIDI ticks per second //| :param int sample_rate: The desired playback sample rate; higher sample rate requires more memory +//| :param ReadableBuffer waveform: A single-cycle waveform. Default is a 50% duty cycle square wave. If specified, must be a ReadableBuffer of type 'h' (signed 16 bit) //| //| Simple melody:: //| @@ -65,11 +72,12 @@ //| print("stopped")""" //| ... STATIC mp_obj_t synthio_miditrack_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_tempo, ARG_sample_rate }; + enum { ARG_buffer, ARG_tempo, ARG_sample_rate, ARG_waveform }; static const mp_arg_t allowed_args[] = { { MP_QSTR_buffer, MP_ARG_OBJ | MP_ARG_REQUIRED }, { MP_QSTR_tempo, MP_ARG_INT | MP_ARG_REQUIRED }, { MP_QSTR_sample_rate, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 11025} }, + { MP_QSTR_waveform, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = mp_const_none } }, }; 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); @@ -77,13 +85,27 @@ STATIC mp_obj_t synthio_miditrack_make_new(const mp_obj_type_t *type, size_t n_a mp_buffer_info_t bufinfo; mp_get_buffer_raise(args[ARG_buffer].u_obj, &bufinfo, MP_BUFFER_READ); + mp_buffer_info_t bufinfo_waveform = { + .buf = shared_bindings_synthio_square_wave, + .len = 4 + }; + + if (args[ARG_waveform].u_obj != mp_const_none) { + mp_get_buffer_raise(args[ARG_waveform].u_obj, &bufinfo_waveform, MP_BUFFER_READ); + if (bufinfo_waveform.typecode != 'h') { + mp_raise_ValueError_varg(translate("%q must be array of type 'h'"), MP_QSTR_waveform); + } + } + synthio_miditrack_obj_t *self = m_new_obj(synthio_miditrack_obj_t); self->base.type = &synthio_miditrack_type; common_hal_synthio_miditrack_construct(self, (uint8_t *)bufinfo.buf, bufinfo.len, args[ARG_tempo].u_int, - args[ARG_sample_rate].u_int); + args[ARG_sample_rate].u_int, + bufinfo_waveform.buf, + bufinfo_waveform.len / 2); return MP_OBJ_FROM_PTR(self); } diff --git a/shared-bindings/synthio/MidiTrack.h b/shared-bindings/synthio/MidiTrack.h index d44d4c3bb7..90ddab8728 100644 --- a/shared-bindings/synthio/MidiTrack.h +++ b/shared-bindings/synthio/MidiTrack.h @@ -32,7 +32,7 @@ extern const mp_obj_type_t synthio_miditrack_type; void common_hal_synthio_miditrack_construct(synthio_miditrack_obj_t *self, - const uint8_t *buffer, uint32_t len, uint32_t tempo, uint32_t sample_rate); + const uint8_t *buffer, uint32_t len, uint32_t tempo, uint32_t sample_rate, const int16_t *waveform, uint16_t waveform_len); void common_hal_synthio_miditrack_deinit(synthio_miditrack_obj_t *self); bool common_hal_synthio_miditrack_deinited(synthio_miditrack_obj_t *self); diff --git a/shared-bindings/synthio/__init__.c b/shared-bindings/synthio/__init__.c index 29a0c4d293..dc627adb36 100644 --- a/shared-bindings/synthio/__init__.c +++ b/shared-bindings/synthio/__init__.c @@ -35,6 +35,8 @@ #include "shared-bindings/synthio/__init__.h" #include "shared-bindings/synthio/MidiTrack.h" +int16_t shared_bindings_synthio_square_wave[] = {-32768, 32767}; + //| """Support for MIDI synthesis""" //| //| def from_file(file: typing.BinaryIO, *, sample_rate: int = 11025) -> MidiTrack: @@ -43,6 +45,7 @@ //| //| :param typing.BinaryIO file: Already opened MIDI file //| :param int sample_rate: The desired playback sample rate; higher sample rate requires more memory +//| :param ReadableBuffer waveform: A single-cycle waveform. Default is a 50% duty cycle square wave. If specified, must be a ReadableBuffer of type 'h' (signed 16 bit) //| //| //| Playing a MIDI file from flash:: @@ -63,10 +66,11 @@ //| ... //| STATIC mp_obj_t synthio_from_file(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { - enum { ARG_file, ARG_sample_rate }; + enum { ARG_file, ARG_sample_rate, ARG_waveform }; static const mp_arg_t allowed_args[] = { { MP_QSTR_file, MP_ARG_OBJ | MP_ARG_REQUIRED }, { MP_QSTR_sample_rate, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 11025} }, + { MP_QSTR_waveform, MP_ARG_OBJ | MP_ARG_KW_ONLY, {.u_obj = mp_const_none } }, }; mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); @@ -75,6 +79,18 @@ STATIC mp_obj_t synthio_from_file(size_t n_args, const mp_obj_t *pos_args, mp_ma } pyb_file_obj_t *file = MP_OBJ_TO_PTR(args[ARG_file].u_obj); + mp_buffer_info_t bufinfo_waveform = { + .buf = shared_bindings_synthio_square_wave, + .len = 4 + }; + + if (args[ARG_waveform].u_obj != mp_const_none) { + mp_get_buffer_raise(args[ARG_waveform].u_obj, &bufinfo_waveform, MP_BUFFER_READ); + if (bufinfo_waveform.typecode != 'h') { + mp_raise_ValueError_varg(translate("%q must be array of type 'h'"), MP_QSTR_waveform); + } + } + uint8_t chunk_header[14]; f_rewind(&file->fp); UINT bytes_read; @@ -114,7 +130,7 @@ STATIC mp_obj_t synthio_from_file(size_t n_args, const mp_obj_t *pos_args, mp_ma result->base.type = &synthio_miditrack_type; common_hal_synthio_miditrack_construct(result, buffer, track_size, - tempo, args[ARG_sample_rate].u_int); + tempo, args[ARG_sample_rate].u_int, bufinfo_waveform.buf, bufinfo_waveform.len / 2); #if MICROPY_MALLOC_USES_ALLOCATED_SIZE m_free(buffer, track_size); diff --git a/shared-bindings/synthio/__init__.h b/shared-bindings/synthio/__init__.h index 14af1a5805..3b7d47db28 100644 --- a/shared-bindings/synthio/__init__.h +++ b/shared-bindings/synthio/__init__.h @@ -24,11 +24,6 @@ * THE SOFTWARE. */ -#ifndef MICROPY_INCLUDED_SHARED_BINDINGS_SYNTHIO___INIT___H -#define MICROPY_INCLUDED_SHARED_BINDINGS_SYNTHIO___INIT___H +#pragma once -#include "py/obj.h" - -// Nothing now. - -#endif // MICROPY_INCLUDED_SHARED_BINDINGS_SYNTHIO___INIT___H +extern int16_t shared_bindings_synthio_square_wave[]; diff --git a/shared-module/synthio/MidiTrack.c b/shared-module/synthio/MidiTrack.c index dbc2d4ced1..f7c17427d1 100644 --- a/shared-module/synthio/MidiTrack.c +++ b/shared-module/synthio/MidiTrack.c @@ -32,6 +32,7 @@ #define MAX_DUR (512) #define SILENCE (0x80) + STATIC NORETURN void raise_midi_stream_error(uint32_t pos) { mp_raise_ValueError_varg(translate("Error in MIDI stream at position %d"), pos); } @@ -82,7 +83,8 @@ STATIC void change_span_note(synthio_miditrack_obj_t *self, uint8_t old_note, ui } void common_hal_synthio_miditrack_construct(synthio_miditrack_obj_t *self, - const uint8_t *buffer, uint32_t len, uint32_t tempo, uint32_t sample_rate) { + const uint8_t *buffer, uint32_t len, uint32_t tempo, uint32_t sample_rate, + const int16_t *waveform, uint16_t waveform_length) { synthio_midi_span_t initial = { 0, {[0 ... (CIRCUITPY_SYNTHIO_MAX_CHANNELS - 1)] = SILENCE} }; self->sample_rate = sample_rate; @@ -90,6 +92,11 @@ void common_hal_synthio_miditrack_construct(synthio_miditrack_obj_t *self, self->next_span = 0; self->total_spans = 1; *self->track = initial; + self->waveform = waveform; + self->waveform_length = waveform_length; + (void)mp_arg_validate_length_min(waveform_length, 2, MP_QSTR_waveform); + mp_printf(&mp_plat_print, "note: waveform_length = %d\n", waveform_length); + mp_printf(&mp_plat_print, "waveform[0:2] = %d %d\n", waveform[0], waveform[1]); uint16_t dur = 0; uint32_t pos = 0; @@ -209,6 +216,9 @@ audioio_get_buffer_result_t synthio_miditrack_get_buffer(synthio_miditrack_obj_t int32_t sample_rate = self->sample_rate; int active_channels = count_active_channels(&span); + const int16_t *waveform = self->waveform; + int16_t waveform_length = self->waveform_length; + int16_t *out_buffer = self->buffer; if (active_channels) { int16_t loudness = 0x3fff / (1 + active_channels); for (int chan = 0; chan < CIRCUITPY_SYNTHIO_MAX_CHANNELS; chan++) { @@ -219,11 +229,14 @@ audioio_get_buffer_result_t synthio_miditrack_get_buffer(synthio_miditrack_obj_t uint8_t octave = span.note[chan] / 12; uint16_t base_freq = notes[span.note[chan] % 12]; uint32_t accum = self->accum[chan]; - uint32_t rate = base_freq * 2; + uint32_t rate = base_freq * waveform_length; for (uint16_t i = 0; i < dur; i++) { accum += rate; - int16_t semiperiod = (accum / sample_rate) >> (10 - octave); - self->buffer[i] += semiperiod % 2 ? loudness : -loudness; + int16_t idx = ((accum / sample_rate) >> (10 - octave)) % waveform_length; + if (chan == 0 && i < 10) { + mp_printf(&mp_plat_print, "%d %d %d\n", accum, idx, waveform[idx]); + } + out_buffer[i] += (waveform[idx] * loudness) / 65536; } self->accum[chan] = accum; } diff --git a/shared-module/synthio/MidiTrack.h b/shared-module/synthio/MidiTrack.h index 42ef9c93ae..c9206c69bb 100644 --- a/shared-module/synthio/MidiTrack.h +++ b/shared-module/synthio/MidiTrack.h @@ -39,11 +39,13 @@ typedef struct { typedef struct { mp_obj_base_t base; uint32_t sample_rate; - uint16_t *buffer; + int16_t *buffer; uint16_t buffer_length; uint16_t remaining_dur; uint16_t next_span; uint16_t total_spans; + uint16_t waveform_length; + const int16_t *waveform; uint32_t accum[CIRCUITPY_SYNTHIO_MAX_CHANNELS]; synthio_midi_span_t *track; } synthio_miditrack_obj_t;