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.
This commit is contained in:
Jeff Epler 2023-04-01 10:21:22 -05:00
parent 13e17e6dcd
commit e9e4ce9546
No known key found for this signature in database
GPG Key ID: D5BF15AB975AB4DE
7 changed files with 76 additions and 33 deletions

View File

@ -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 ""

View File

@ -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);
}

View File

@ -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);

View File

@ -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);

View File

@ -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[];

View File

@ -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;
}

View File

@ -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;