diff --git a/shared-bindings/audiocore/__init__.c b/shared-bindings/audiocore/__init__.c index 6f32979099..3ecd7391b6 100644 --- a/shared-bindings/audiocore/__init__.c +++ b/shared-bindings/audiocore/__init__.c @@ -27,6 +27,7 @@ #include #include "py/obj.h" +#include "py/gc.h" #include "py/runtime.h" #include "shared-bindings/audiocore/__init__.h" @@ -46,8 +47,23 @@ STATIC mp_obj_t audiocore_get_buffer(mp_obj_t sample_in) { mp_obj_t result[2] = {mp_obj_new_int_from_uint(gbr), mp_const_none}; if (gbr != GET_BUFFER_ERROR) { + bool single_buffer, samples_signed; + uint32_t max_buffer_length; + uint8_t spacing; + + uint8_t bits_per_sample = audiosample_bits_per_sample(sample_in); + audiosample_get_buffer_structure(sample_in, false, &single_buffer, &samples_signed, &max_buffer_length, &spacing); // copies the data because the gc semantics of get_buffer are unclear - result[1] = mp_obj_new_bytes(buffer, buffer_length); + void *result_buf = gc_alloc(buffer_length, 0, false); + memcpy(result_buf, buffer, buffer_length); + char typecode = + (bits_per_sample == 8 && samples_signed) ? 'b' : + (bits_per_sample == 8 && !samples_signed) ? 'B' : + (bits_per_sample == 16 && samples_signed) ? 'h' : + (bits_per_sample == 16 && !samples_signed) ? 'H' : + 'b'; + size_t nitems = buffer_length / (bits_per_sample / 8); + result[1] = mp_obj_new_memoryview(typecode, nitems, result_buf); } return mp_obj_new_tuple(2, result); diff --git a/shared-bindings/synthio/MidiTrack.c b/shared-bindings/synthio/MidiTrack.c index 20a203ce9c..6916a21689 100644 --- a/shared-bindings/synthio/MidiTrack.c +++ b/shared-bindings/synthio/MidiTrack.c @@ -44,7 +44,8 @@ //| tempo: int, //| *, //| sample_rate: int = 11025, -//| waveform: ReadableBuffer = None +//| waveform: Optional[ReadableBuffer] = None, +//| envelope: Optional[Envelope] = 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 @@ -54,6 +55,7 @@ //| :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) +//| :param Envelope envelope: An object that defines the loudness of a note over time. The default envelope provides no ramping, voices turn instantly on and off. //| //| Simple melody:: //| @@ -72,12 +74,13 @@ //| 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, ARG_waveform }; + enum { ARG_buffer, ARG_tempo, ARG_sample_rate, ARG_waveform, ARG_envelope }; 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_QSTR_envelope, 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); @@ -96,7 +99,9 @@ STATIC mp_obj_t synthio_miditrack_make_new(const mp_obj_type_t *type, size_t n_a args[ARG_tempo].u_int, args[ARG_sample_rate].u_int, bufinfo_waveform.buf, - bufinfo_waveform.len / 2); + bufinfo_waveform.len / 2, + args[ARG_envelope].u_obj + ); return MP_OBJ_FROM_PTR(self); } diff --git a/shared-bindings/synthio/MidiTrack.h b/shared-bindings/synthio/MidiTrack.h index 90ddab8728..046163c2de 100644 --- a/shared-bindings/synthio/MidiTrack.h +++ b/shared-bindings/synthio/MidiTrack.h @@ -24,20 +24,18 @@ * THE SOFTWARE. */ -#ifndef MICROPY_INCLUDED_SHARED_BINDINGS_SYNTHIO_MIDITRACK_H -#define MICROPY_INCLUDED_SHARED_BINDINGS_SYNTHIO_MIDITRACK_H +#pragma once #include "shared-module/synthio/MidiTrack.h" 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 int16_t *waveform, uint16_t waveform_len); + const uint8_t *buffer, uint32_t len, uint32_t tempo, uint32_t sample_rate, const int16_t *waveform, uint16_t waveform_len, + mp_obj_t envelope); void common_hal_synthio_miditrack_deinit(synthio_miditrack_obj_t *self); bool common_hal_synthio_miditrack_deinited(synthio_miditrack_obj_t *self); uint32_t common_hal_synthio_miditrack_get_sample_rate(synthio_miditrack_obj_t *self); uint8_t common_hal_synthio_miditrack_get_bits_per_sample(synthio_miditrack_obj_t *self); uint8_t common_hal_synthio_miditrack_get_channel_count(synthio_miditrack_obj_t *self); - -#endif // MICROPY_INCLUDED_SHARED_BINDINGS_SYNTHIO_MIDITRACK_H diff --git a/shared-bindings/synthio/Synthesizer.c b/shared-bindings/synthio/Synthesizer.c index 4a4256b5ac..97941b860b 100644 --- a/shared-bindings/synthio/Synthesizer.c +++ b/shared-bindings/synthio/Synthesizer.c @@ -37,24 +37,29 @@ #include "supervisor/shared/translate/translate.h" //| class Synthesizer: -//| def __init__(self, *, sample_rate: int = 11025, waveform: ReadableBuffer = None) -> None: +//| def __init__( +//| self, +//| *, +//| sample_rate: int = 11025, +//| waveform: Optional[ReadableBuffer] = None, +//| envelope: Optional[Envelope] = None, +//| ) -> None: //| """Create a synthesizer object. //| //| This API is experimental. //| -//| At least 2 simultaneous notes are supported. mimxrt10xx and rp2040 platforms support up to -//| 12 notes. -//| //| Notes use MIDI note numbering, with 60 being C4 or Middle C, approximately 262Hz. //| //| :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). It is permitted to modify this buffer during synthesis. This can be used, for instance, to control the overall volume or timbre of the notes. +//| :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) +//| :param Optional[Envelope] envelope: An object that defines the loudness of a note over time. The default envelope, `None` provides no ramping, voices turn instantly on and off. //| """ STATIC mp_obj_t synthio_synthesizer_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { - enum { ARG_sample_rate, ARG_waveform }; + enum { ARG_sample_rate, ARG_waveform, ARG_envelope }; static const mp_arg_t allowed_args[] = { { 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_QSTR_envelope, 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); @@ -68,7 +73,9 @@ STATIC mp_obj_t synthio_synthesizer_make_new(const mp_obj_type_t *type, size_t n common_hal_synthio_synthesizer_construct(self, args[ARG_sample_rate].u_int, bufinfo_waveform.buf, - bufinfo_waveform.len / 2); + bufinfo_waveform.len / 2, + args[ARG_envelope].u_obj); + return MP_OBJ_FROM_PTR(self); } @@ -191,6 +198,28 @@ STATIC mp_obj_t synthio_synthesizer_obj___exit__(size_t n_args, const mp_obj_t * return mp_const_none; } STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(synthio_synthesizer___exit___obj, 4, 4, synthio_synthesizer_obj___exit__); + +//| envelope: Optional[Envelope] +//| """The envelope to apply to all notes. `None`, the default envelope, instantly turns notes on and off. The envelope may be changed dynamically, but it affects all notes (even currently playing notes)""" +STATIC mp_obj_t synthio_synthesizer_obj_get_envelope(mp_obj_t self_in) { + synthio_synthesizer_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + return synthio_synth_envelope_get(&self->synth); +} +MP_DEFINE_CONST_FUN_OBJ_1(synthio_synthesizer_get_envelope_obj, synthio_synthesizer_obj_get_envelope); + +STATIC mp_obj_t synthio_synthesizer_obj_set_envelope(mp_obj_t self_in, mp_obj_t envelope) { + synthio_synthesizer_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + synthio_synth_envelope_set(&self->synth, envelope); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_2(synthio_synthesizer_set_envelope_obj, synthio_synthesizer_obj_set_envelope); + +MP_PROPERTY_GETSET(synthio_synthesizer_envelope_obj, + (mp_obj_t)&synthio_synthesizer_get_envelope_obj, + (mp_obj_t)&synthio_synthesizer_set_envelope_obj); + //| sample_rate: int //| """32 bit value that tells how quickly samples are played in Hertz (cycles per second).""" STATIC mp_obj_t synthio_synthesizer_obj_get_sample_rate(mp_obj_t self_in) { @@ -232,6 +261,7 @@ STATIC const mp_rom_map_elem_t synthio_synthesizer_locals_dict_table[] = { { MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&synthio_synthesizer___exit___obj) }, // Properties + { MP_ROM_QSTR(MP_QSTR_envelope), MP_ROM_PTR(&synthio_synthesizer_envelope_obj) }, { MP_ROM_QSTR(MP_QSTR_sample_rate), MP_ROM_PTR(&synthio_synthesizer_sample_rate_obj) }, { MP_ROM_QSTR(MP_QSTR_max_polyphony), MP_ROM_INT(CIRCUITPY_SYNTHIO_MAX_CHANNELS) }, { MP_ROM_QSTR(MP_QSTR_pressed), MP_ROM_PTR(&synthio_synthesizer_pressed_obj) }, diff --git a/shared-bindings/synthio/Synthesizer.h b/shared-bindings/synthio/Synthesizer.h index 92e75a1c23..ba6c3b99e3 100644 --- a/shared-bindings/synthio/Synthesizer.h +++ b/shared-bindings/synthio/Synthesizer.h @@ -32,8 +32,8 @@ extern const mp_obj_type_t synthio_synthesizer_type; void common_hal_synthio_synthesizer_construct(synthio_synthesizer_obj_t *self, - uint32_t sample_rate, const int16_t *waveform, uint16_t waveform_len); - + uint32_t sample_rate, const int16_t *waveform, uint16_t waveform_length, + mp_obj_t envelope); void common_hal_synthio_synthesizer_deinit(synthio_synthesizer_obj_t *self); bool common_hal_synthio_synthesizer_deinited(synthio_synthesizer_obj_t *self); uint32_t common_hal_synthio_synthesizer_get_sample_rate(synthio_synthesizer_obj_t *self); diff --git a/shared-bindings/synthio/__init__.c b/shared-bindings/synthio/__init__.c index 3c50377ff8..703966defc 100644 --- a/shared-bindings/synthio/__init__.c +++ b/shared-bindings/synthio/__init__.c @@ -28,6 +28,7 @@ #include "py/mperrno.h" #include "py/obj.h" +#include "py/objnamedtuple.h" #include "py/runtime.h" #include "extmod/vfs_fat.h" #include "extmod/vfs_posix.h" @@ -36,16 +37,111 @@ #include "shared-bindings/synthio/MidiTrack.h" #include "shared-bindings/synthio/Synthesizer.h" -//| """Support for MIDI synthesis""" +//| """Support for multi-channel audio synthesis //| -//| def from_file(file: typing.BinaryIO, *, sample_rate: int = 11025) -> MidiTrack: +//| At least 2 simultaneous notes are supported. samd5x, mimxrt10xx and rp2040 platforms support up to 12 notes. +//| +//| I'm a little teapot. I'm not on line 11, but I don't know what is. +//| """ +//| +//| class Envelope: +//| def __init__( +//| self, +//| attack_time: float, +//| decay_time: float, +//| release_time: float, +//| attack_level: float, +//| sustain_level: float, +//| ) -> None: +//| """Construct an Envelope object +//| +//| The Envelope defines an ADSR (Attack, Decay, Sustain, Release) envelope with linear amplitude ramping. A note starts at 0 volume, then increases to ``attack_level`` over ``attack_time`` seconds; then it decays to ``sustain_level`` over ``decay_time`` seconds. Finally, when the note is released, it decreases to ``0`` volume over ``release_time``. +//| +//| If the ``sustain_level`` of an envelope is 0, then the decay and sustain phases of the note are always omitted. The note is considered to be released as soon as the envelope reaches the end of the attack phase. The ``decay_time`` is ignored. This is similar to how a plucked or struck instrument behaves. +//| +//| If a note is released before it reaches its sustain phase, it decays with the same slope indicated by ``sustain_level/release_time`` (or ``attack_level/release_time`` for plucked envelopes) +//| +//| :param float attack_time: The time in seconds it takes to ramp from 0 volume to attack_volume +//| :param float decay_time: The time in seconds it takes to ramp from attack_volume to sustain_volume +//| :param float release_time: The time in seconds it takes to ramp from sustain_volume to release_volume. When a note is released before it has reached the sustain phase, the release is done with the same slope indicated by ``release_time`` and ``sustain_level`` +//| :param float attack_level: The relative level, in the range ``0.0`` to ``1.0`` of the peak volume of the attack phase +//| :param float sustain_level: The relative level, in the range ``0.0`` to ``1.0`` of the volume of the sustain phase +//| """ +//| attack_time: float +//| """The time in seconds it takes to ramp from 0 volume to attack_volume""" +//| +//| decay_time: float +//| """The time in seconds it takes to ramp from attack_volume to sustain_volume""" +//| +//| release_time: float +//| """The time in seconds it takes to ramp from sustain_volume to release_volume. When a note is released before it has reached the sustain phase, the release is done with the same slope indicated by ``release_time`` and ``sustain_level``""" +//| +//| attack_level: float +//| """The relative level, in the range ``0.0`` to ``1.0`` of the peak volume of the attack phase""" +//| +//| sustain_level: float +//| """The relative level, in the range ``0.0`` to ``1.0`` of the volume of the sustain phase""" +//| + +STATIC mp_obj_t synthio_envelope_make_new(const mp_obj_type_t *type_in, size_t n_args, size_t n_kw, const mp_obj_t *args) { + mp_obj_t new_obj = namedtuple_make_new(type_in, n_args, n_kw, args); + mp_obj_t *fields; + size_t len; + mp_obj_tuple_get(new_obj, &len, &fields); + + mp_arg_validate_obj_float_non_negative(fields[0], 0., MP_QSTR_attack_time); + mp_arg_validate_obj_float_non_negative(fields[1], 0., MP_QSTR_decay_time); + mp_arg_validate_obj_float_non_negative(fields[2], 0., MP_QSTR_release_time); + + mp_arg_validate_obj_float_range(fields[3], 0, 1, MP_QSTR_attack_level); + mp_arg_validate_obj_float_range(fields[4], 0, 1, MP_QSTR_sustain_level); + + return new_obj; +}; + +const mp_obj_namedtuple_type_t synthio_envelope_type_obj = { + .base = { + .base = { + .type = &mp_type_type + }, + .flags = MP_TYPE_FLAG_EXTENDED, + .name = MP_QSTR_Envelope, + .print = namedtuple_print, + .parent = &mp_type_tuple, + .make_new = synthio_envelope_make_new, + .attr = namedtuple_attr, + MP_TYPE_EXTENDED_FIELDS( + .unary_op = mp_obj_tuple_unary_op, + .binary_op = mp_obj_tuple_binary_op, + .subscr = mp_obj_tuple_subscr, + .getiter = mp_obj_tuple_getiter, + ), + }, + .n_fields = 5, + .fields = { + MP_QSTR_attack_time, + MP_QSTR_decay_time, + MP_QSTR_release_time, + MP_QSTR_attack_level, + MP_QSTR_sustain_level, + }, +}; + + +//| def from_file( +//| file: typing.BinaryIO, +//| *, +//| sample_rate: int = 11025, +//| waveform: Optional[ReadableBuffer] = None, +//| envelope: Optional[ReadableBuffer] = None, +//| ) -> MidiTrack: //| """Create an AudioSample from an already opened MIDI file. //| Currently, only single-track MIDI (type 0) is supported. //| //| :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) -//| +//| :param Envelope envelope: An object that defines the loudness of a note over time. The default envelope provides no ramping, voices turn instantly on and off. //| //| Playing a MIDI file from flash:: //| @@ -65,11 +161,12 @@ //| ... //| 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, ARG_waveform }; + enum { ARG_file, ARG_sample_rate, ARG_waveform, ARG_envelope }; 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_QSTR_envelope, 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); @@ -121,7 +218,9 @@ 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, bufinfo_waveform.buf, bufinfo_waveform.len / 2); + tempo, args[ARG_sample_rate].u_int, bufinfo_waveform.buf, bufinfo_waveform.len / 2, + args[ARG_envelope].u_obj + ); #if MICROPY_MALLOC_USES_ALLOCATED_SIZE m_free(buffer, track_size); @@ -133,12 +232,12 @@ STATIC mp_obj_t synthio_from_file(size_t n_args, const mp_obj_t *pos_args, mp_ma } MP_DEFINE_CONST_FUN_OBJ_KW(synthio_from_file_obj, 1, synthio_from_file); - STATIC const mp_rom_map_elem_t synthio_module_globals_table[] = { { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_synthio) }, { MP_ROM_QSTR(MP_QSTR_MidiTrack), MP_ROM_PTR(&synthio_miditrack_type) }, { MP_ROM_QSTR(MP_QSTR_Synthesizer), MP_ROM_PTR(&synthio_synthesizer_type) }, { MP_ROM_QSTR(MP_QSTR_from_file), MP_ROM_PTR(&synthio_from_file_obj) }, + { MP_ROM_QSTR(MP_QSTR_Envelope), MP_ROM_PTR(&synthio_envelope_type_obj) }, }; STATIC MP_DEFINE_CONST_DICT(synthio_module_globals, synthio_module_globals_table); diff --git a/shared-bindings/synthio/__init__.h b/shared-bindings/synthio/__init__.h index 3b7d47db28..6ea323905a 100644 --- a/shared-bindings/synthio/__init__.h +++ b/shared-bindings/synthio/__init__.h @@ -26,4 +26,10 @@ #pragma once +#include "py/objnamedtuple.h" + +typedef struct synthio_synth synthio_synth_t; extern int16_t shared_bindings_synthio_square_wave[]; +extern const mp_obj_namedtuple_type_t synthio_envelope_type_obj; +void synthio_synth_envelope_set(synthio_synth_t *synth, mp_obj_t envelope_obj); +mp_obj_t synthio_synth_envelope_get(synthio_synth_t *synth); diff --git a/shared-module/synthio/MidiTrack.c b/shared-module/synthio/MidiTrack.c index f8b39c29e3..6a021af85c 100644 --- a/shared-module/synthio/MidiTrack.c +++ b/shared-module/synthio/MidiTrack.c @@ -28,118 +28,109 @@ #include "shared-bindings/synthio/MidiTrack.h" -STATIC NORETURN void raise_midi_stream_error(uint32_t pos) { - mp_raise_ValueError_varg(translate("Error in MIDI stream at position %d"), pos); +STATIC void print_midi_stream_error(synthio_miditrack_obj_t *self) { + mp_cprintf(&mp_plat_print, translate("Error in MIDI stream at position %d"), self->pos); + self->pos = self->track.len; } -STATIC uint8_t parse_note(const uint8_t *buffer, uint32_t len, uint32_t *pos) { - if (*pos + 1 >= len) { - raise_midi_stream_error(*pos); +STATIC uint8_t parse_note(synthio_miditrack_obj_t *self) { + uint8_t *buffer = self->track.buf; + size_t len = self->track.len; + if (self->pos + 1 >= len) { + print_midi_stream_error(self); } - uint8_t note = buffer[(*pos)++]; - if (note > 127 || buffer[(*pos)++] > 127) { - raise_midi_stream_error(*pos); + uint8_t note = buffer[(self->pos)++]; + if (note > 127 || buffer[(self->pos)++] > 127) { + print_midi_stream_error(self); } return note; } -STATIC void terminate_span(synthio_miditrack_obj_t *self, uint16_t *dur) { - if (*dur) { - self->track[self->total_spans - 1].dur = *dur; - *dur = 0; - } else { - self->total_spans--; +static int decode_duration(synthio_miditrack_obj_t *self) { + uint8_t *buffer = self->track.buf; + size_t len = self->track.len; + uint8_t c; + uint32_t delta = 0; + do { + c = buffer[self->pos++]; + delta <<= 7; + delta |= c & 0x7f; + } while ((c & 0x80) && (self->pos < len)); + + // errors cannot be raised from the background task, so simply end the track. + if (c & 0x80) { + self->pos = self->track.len; + print_midi_stream_error(self); } + return delta * self->synth.sample_rate / self->tempo; } -STATIC void add_span(synthio_miditrack_obj_t *self, const synthio_midi_span_t *span) { - self->track = m_renew(synthio_midi_span_t, self->track, self->total_spans, self->total_spans + 1); - self->track[self->total_spans++] = *span; -} - -STATIC void change_span_note(synthio_miditrack_obj_t *self, uint8_t old_note, uint8_t new_note, uint16_t *dur) { - synthio_midi_span_t span = self->track[self->total_spans - 1]; - if (synthio_span_change_note(&span, old_note, new_note)) { - terminate_span(self, dur); - add_span(self, &span); - *dur = 0; - } -} - -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 int16_t *waveform, uint16_t waveform_length) { - - self->synth.sample_rate = sample_rate; - self->track = m_malloc(sizeof(synthio_midi_span_t), false); - synthio_span_init(self->track); - self->next_span = 0; - self->total_spans = 1; - self->synth.waveform = waveform; - self->synth.waveform_length = waveform_length; - - uint16_t dur = 0; - uint32_t pos = 0; - while (pos < len) { - uint8_t c; - uint32_t delta = 0; - do { - c = buffer[pos++]; - delta <<= 7; - delta |= c & 0x7f; - } while ((c & 0x80) && (pos < len)); - - if (c & 0x80) { - raise_midi_stream_error(pos); - } - - // dur is carried over here so that if a note on/off message doesn't actually produce a change, the - // underlying "span" is extended. Otherwise, it is zeroed out in the call to `terminate_span`. - dur += delta * sample_rate / tempo; - - switch (buffer[pos++] >> 4) { +// invariant: pointing at a MIDI message +static void decode_until_pause(synthio_miditrack_obj_t *self) { + uint8_t *buffer = self->track.buf; + size_t len = self->track.len; + do { + switch (buffer[self->pos++] >> 4) { case 8: { // Note Off - uint8_t note = parse_note(buffer, len, &pos); - change_span_note(self, note, SYNTHIO_SILENCE, &dur); + uint8_t note = parse_note(self); + synthio_span_change_note(&self->synth, note, SYNTHIO_SILENCE); break; } case 9: { // Note On - uint8_t note = parse_note(buffer, len, &pos); - change_span_note(self, SYNTHIO_SILENCE, note, &dur); + uint8_t note = parse_note(self); + synthio_span_change_note(&self->synth, SYNTHIO_SILENCE, note); break; } case 10: case 11: case 14: // two data bytes to ignore - parse_note(buffer, len, &pos); + parse_note(self); break; case 12: case 13: // one data byte to ignore - if (pos >= len || buffer[pos++] > 127) { - raise_midi_stream_error(pos); + if (self->pos >= len || buffer[self->pos++] > 127) { + print_midi_stream_error(self); } break; case 15: // the full syntax is too complicated, just assume it's "End of Track" event - pos = len; + self->pos = len; break; default: // invalid event - raise_midi_stream_error(pos); + print_midi_stream_error(self); } - } - terminate_span(self, &dur); + if (self->pos < len) { + self->synth.span.dur = decode_duration(self); + } + } while (self->pos < len && self->synth.span.dur == 0); +} - uint16_t max_dur = 0; - for (int i = 0; i < self->total_spans; i++) { - max_dur = MAX(self->track[i].dur, max_dur); +STATIC void start_parse(synthio_miditrack_obj_t *self) { + self->pos = 0; + self->synth.span.dur = decode_duration(self); + if (self->synth.span.dur == 0) { + // the usual case: the file starts with some MIDI event, not a delay + decode_until_pause(self); } - synthio_synth_init(&self->synth, max_dur); +} + +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 int16_t *waveform, uint16_t waveform_length, + mp_obj_t envelope) { + + self->tempo = tempo; + self->track.buf = (void *)buffer; + self->track.len = len; + + synthio_synth_init(&self->synth, sample_rate, waveform, waveform_length, envelope); + + start_parse(self); } void common_hal_synthio_miditrack_deinit(synthio_miditrack_obj_t *self) { synthio_synth_deinit(&self->synth); - m_del(synthio_midi_span_t, self->track, self->total_spans + 1); - self->track = NULL; } + bool common_hal_synthio_miditrack_deinited(synthio_miditrack_obj_t *self) { return synthio_synth_deinited(&self->synth); } @@ -157,25 +148,21 @@ uint8_t common_hal_synthio_miditrack_get_channel_count(synthio_miditrack_obj_t * void synthio_miditrack_reset_buffer(synthio_miditrack_obj_t *self, bool single_channel_output, uint8_t channel) { synthio_synth_reset_buffer(&self->synth, single_channel_output, channel); - self->synth.span.dur = 0; - self->next_span = 0; + start_parse(self); } audioio_get_buffer_result_t synthio_miditrack_get_buffer(synthio_miditrack_obj_t *self, bool single_channel_output, uint8_t channel, uint8_t **buffer, uint32_t *buffer_length) { - if (self->synth.span.dur == 0) { - if (self->next_span >= self->total_spans) { - *buffer_length = 0; - return GET_BUFFER_DONE; - } - self->synth.span = self->track[self->next_span++]; - } - synthio_synth_synthesize(&self->synth, buffer, buffer_length, single_channel_output ? 0 : channel); - - return (self->synth.span.dur == 0 && self->next_span >= self->total_spans) ? - GET_BUFFER_DONE : GET_BUFFER_MORE_DATA; + if (self->synth.span.dur == 0) { + if (self->pos == self->track.len) { + return GET_BUFFER_DONE; + } else { + decode_until_pause(self); + } + } + return GET_BUFFER_MORE_DATA; } void synthio_miditrack_get_buffer_structure(synthio_miditrack_obj_t *self, bool single_channel_output, diff --git a/shared-module/synthio/MidiTrack.h b/shared-module/synthio/MidiTrack.h index e301ef355f..549e72fb25 100644 --- a/shared-module/synthio/MidiTrack.h +++ b/shared-module/synthio/MidiTrack.h @@ -34,9 +34,10 @@ typedef struct { mp_obj_base_t base; synthio_synth_t synth; - uint16_t next_span; - uint16_t total_spans; - synthio_midi_span_t *track; + mp_buffer_info_t track; + // invariant: after initial startup, pos always points just after an encoded duration, i.e., at a midi message (or at EOF) + size_t pos; + uint32_t tempo; } synthio_miditrack_obj_t; diff --git a/shared-module/synthio/Synthesizer.c b/shared-module/synthio/Synthesizer.c index 25c91ecbe0..42d0468845 100644 --- a/shared-module/synthio/Synthesizer.c +++ b/shared-module/synthio/Synthesizer.c @@ -30,13 +30,10 @@ void common_hal_synthio_synthesizer_construct(synthio_synthesizer_obj_t *self, - uint32_t sample_rate, const int16_t *waveform, uint16_t waveform_length) { + uint32_t sample_rate, const int16_t *waveform, uint16_t waveform_length, + mp_obj_t envelope) { - self->synth.sample_rate = sample_rate; - self->synth.waveform = waveform; - self->synth.waveform_length = waveform_length; - synthio_synth_init(&self->synth, SYNTHIO_MAX_DUR); - common_hal_synthio_synthesizer_release_all(self); + synthio_synth_init(&self->synth, sample_rate, waveform, waveform_length, envelope); } void common_hal_synthio_synthesizer_deinit(synthio_synthesizer_obj_t *self) { @@ -74,14 +71,18 @@ void synthio_synthesizer_get_buffer_structure(synthio_synthesizer_obj_t *self, b } void common_hal_synthio_synthesizer_release_all(synthio_synthesizer_obj_t *self) { - synthio_span_init(&self->synth.span); + for (size_t i = 0; i < CIRCUITPY_SYNTHIO_MAX_CHANNELS; i++) { + if (self->synth.span.note[i] != SYNTHIO_SILENCE) { + synthio_span_change_note(&self->synth, self->synth.span.note[i], SYNTHIO_SILENCE); + } + } } void common_hal_synthio_synthesizer_release(synthio_synthesizer_obj_t *self, mp_obj_t to_release) { mp_obj_iter_buf_t iter_buf; mp_obj_t iterable = mp_getiter(to_release, &iter_buf); mp_obj_t item; while ((item = mp_iternext(iterable)) != MP_OBJ_STOP_ITERATION) { - synthio_span_change_note(&self->synth.span, mp_arg_validate_int_range(mp_obj_get_int(item), 0, 127, MP_QSTR_note), SYNTHIO_SILENCE); + synthio_span_change_note(&self->synth, mp_arg_validate_int_range(mp_obj_get_int(item), 0, 127, MP_QSTR_note), SYNTHIO_SILENCE); } } @@ -90,15 +91,21 @@ void common_hal_synthio_synthesizer_press(synthio_synthesizer_obj_t *self, mp_ob mp_obj_t iterable = mp_getiter(to_press, &iter_buf); mp_obj_t item; while ((item = mp_iternext(iterable)) != MP_OBJ_STOP_ITERATION) { - synthio_span_change_note(&self->synth.span, SYNTHIO_SILENCE, mp_arg_validate_int_range(mp_obj_get_int(item), 0, 127, MP_QSTR_note)); + synthio_span_change_note(&self->synth, SYNTHIO_SILENCE, mp_arg_validate_int_range(mp_obj_get_int(item), 0, 127, MP_QSTR_note)); } } mp_obj_t common_hal_synthio_synthesizer_get_pressed_notes(synthio_synthesizer_obj_t *self) { - mp_obj_tuple_t *result = MP_OBJ_TO_PTR(mp_obj_new_tuple(synthio_span_count_active_channels(&self->synth.span), NULL)); - for (size_t i = 0, j = 0; i < CIRCUITPY_SYNTHIO_MAX_CHANNELS; i++) { - if (self->synth.span.note[i] != SYNTHIO_SILENCE) { - result->items[j++] = MP_OBJ_NEW_SMALL_INT(self->synth.span.note[i]); + int count = 0; + for (int chan = 0; chan < CIRCUITPY_SYNTHIO_MAX_CHANNELS; chan++) { + if (self->synth.span.note[chan] != SYNTHIO_SILENCE && self->synth.envelope_state[chan].state != SYNTHIO_ENVELOPE_STATE_RELEASE) { + count += 1; + } + } + mp_obj_tuple_t *result = MP_OBJ_TO_PTR(mp_obj_new_tuple(count, NULL)); + for (size_t chan = 0, j = 0; chan < CIRCUITPY_SYNTHIO_MAX_CHANNELS; chan++) { + if (self->synth.span.note[chan] != SYNTHIO_SILENCE && self->synth.envelope_state[chan].state != SYNTHIO_ENVELOPE_STATE_RELEASE) { + result->items[j++] = MP_OBJ_NEW_SMALL_INT(self->synth.span.note[chan]); } } return MP_OBJ_FROM_PTR(result); diff --git a/shared-module/synthio/__init__.c b/shared-module/synthio/__init__.c index 11e4c1dcc9..1385059b99 100644 --- a/shared-module/synthio/__init__.c +++ b/shared-module/synthio/__init__.c @@ -26,18 +26,117 @@ */ #include "shared-module/synthio/__init__.h" +#include "shared-bindings/synthio/__init__.h" #include "py/runtime.h" +#include +#include -STATIC const int16_t square_wave[] = {-32768, 32767}; +STATIC const int16_t square_wave[] = {-32768, 0}; STATIC const uint16_t notes[] = {8372, 8870, 9397, 9956, 10548, 11175, 11840, 12544, 13290, 14080, 14917, 15804}; // 9th octave -int synthio_span_count_active_channels(synthio_midi_span_t *span) { - int result = 0; - for (int i = 0; i < CIRCUITPY_SYNTHIO_MAX_CHANNELS; i++) { - if (span->note[i] != SYNTHIO_SILENCE) { - result += 1; +STATIC int16_t convert_time_to_rate(uint32_t sample_rate, mp_obj_t time_in, int16_t difference) { + mp_float_t time = mp_obj_get_float(time_in); + int num_samples = (int)MICROPY_FLOAT_C_FUN(round)(time * sample_rate); + if (num_samples == 0) { + return 0; + } + int16_t result = MIN(32767, MAX(1, abs(difference * SYNTHIO_MAX_DUR) / num_samples)); + return (difference < 0) ? -result : result; +} + +STATIC void synthio_envelope_definition_set(synthio_envelope_definition_t *envelope, mp_obj_t obj, uint32_t sample_rate) { + if (obj == mp_const_none) { + envelope->attack_level = 32767; + envelope->sustain_level = 32767; + envelope->attack_step = 32767; + envelope->decay_step = -32767; + envelope->release_step = -32767; + return; + } + mp_arg_validate_type(obj, (mp_obj_type_t *)&synthio_envelope_type_obj, MP_QSTR_envelope); + + size_t len; + mp_obj_t *fields; + mp_obj_tuple_get(obj, &len, &fields); + + envelope->attack_level = (int)(32767 * mp_obj_get_float(fields[3])); + envelope->sustain_level = (int)(32767 * mp_obj_get_float(fields[4])); + + envelope->attack_step = convert_time_to_rate( + sample_rate, fields[0], envelope->attack_level); + + envelope->decay_step = -convert_time_to_rate( + sample_rate, fields[1], envelope->attack_level - envelope->sustain_level); + + envelope->release_step = -convert_time_to_rate( + sample_rate, fields[2], + envelope->decay_step + ? envelope->sustain_level + : envelope->attack_level); +} + +STATIC void synthio_envelope_state_step(synthio_envelope_state_t *state, synthio_envelope_definition_t *def, size_t n_steps) { + state->substep += n_steps; + while (state->substep >= SYNTHIO_MAX_DUR) { + // max n_steps should be SYNTHIO_MAX_DUR so this loop executes at most + // once + state->substep -= SYNTHIO_MAX_DUR; + switch (state->state) { + case SYNTHIO_ENVELOPE_STATE_SUSTAIN: + break; + case SYNTHIO_ENVELOPE_STATE_ATTACK: + if (def->attack_step != 0) { + state->level = MIN(state->level + def->attack_step, def->attack_level); + if (state->level == def->attack_level) { + state->state = SYNTHIO_ENVELOPE_STATE_DECAY; + } + break; + } + state->state = SYNTHIO_ENVELOPE_STATE_DECAY; + MP_FALLTHROUGH; + case SYNTHIO_ENVELOPE_STATE_DECAY: + if (def->decay_step != 0) { + state->level = MAX(state->level + def->decay_step, def->sustain_level); + assert(state->level >= 0); + if (state->level == def->sustain_level) { + state->state = SYNTHIO_ENVELOPE_STATE_SUSTAIN; + } + break; + } + state->state = SYNTHIO_ENVELOPE_STATE_RELEASE; + MP_FALLTHROUGH; + case SYNTHIO_ENVELOPE_STATE_RELEASE: + if (def->release_step != 0) { + int delta = def->release_step; + state->level = MAX(state->level + delta, 0); + } else { + state->level = 0; + } + break; + } + } +} + +STATIC void synthio_envelope_state_init(synthio_envelope_state_t *state, synthio_envelope_definition_t *def) { + state->level = 0; + state->substep = 0; + state->state = SYNTHIO_ENVELOPE_STATE_ATTACK; + + synthio_envelope_state_step(state, def, SYNTHIO_MAX_DUR); +} + +STATIC void synthio_envelope_state_release(synthio_envelope_state_t *state, synthio_envelope_definition_t *def) { + state->state = SYNTHIO_ENVELOPE_STATE_RELEASE; +} + + +STATIC uint32_t synthio_synth_sum_envelope(synthio_synth_t *synth) { + uint32_t result = 0; + for (int chan = 0; chan < CIRCUITPY_SYNTHIO_MAX_CHANNELS; chan++) { + if (synth->span.note[chan] != SYNTHIO_SILENCE) { + result += synth->envelope_state[chan].level; } } return result; @@ -62,16 +161,23 @@ void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **bufptr, uint32_t memset(out_buffer, 0, synth->buffer_length); int32_t sample_rate = synth->sample_rate; - int active_channels = synthio_span_count_active_channels(&synth->span); + uint32_t total_envelope = synthio_synth_sum_envelope(synth); const int16_t *waveform = synth->waveform; uint32_t waveform_length = synth->waveform_length; - if (active_channels) { - int16_t loudness = 0xffff / (1 + 2 * active_channels); + if (total_envelope > 0) { + uint16_t ovl_loudness = 0x7fffffff / MAX(0x8000, total_envelope); for (int chan = 0; chan < CIRCUITPY_SYNTHIO_MAX_CHANNELS; chan++) { if (synth->span.note[chan] == SYNTHIO_SILENCE) { synth->accum[chan] = 0; continue; } + // adjust loudness by envelope + uint16_t loudness = (ovl_loudness * synth->envelope_state[chan].level) >> 16; + + if (synth->envelope_state[chan].level == 0) { + // note is truly finished + synth->span.note[chan] = SYNTHIO_SILENCE; + } uint8_t octave = synth->span.note[chan] / 12; uint16_t base_freq = notes[synth->span.note[chan] % 12]; uint32_t accum = synth->accum[chan]; @@ -95,6 +201,11 @@ void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **bufptr, uint32_t } } + // advance envelope states + for (int chan = 0; chan < CIRCUITPY_SYNTHIO_MAX_CHANNELS; chan++) { + synthio_envelope_state_step(&synth->envelope_state[chan], &synth->envelope_definition, dur); + } + *buffer_length = synth->last_buffer_length = dur * SYNTHIO_BYTES_PER_SAMPLE; *bufptr = (uint8_t *)out_buffer; } @@ -117,11 +228,28 @@ void synthio_synth_deinit(synthio_synth_t *synth) { synth->buffers[1] = NULL; } -void synthio_synth_init(synthio_synth_t *synth, uint16_t max_dur) { - synth->buffer_length = MIN(SYNTHIO_MAX_DUR, max_dur) * SYNTHIO_BYTES_PER_SAMPLE; +void synthio_synth_envelope_set(synthio_synth_t *synth, mp_obj_t envelope_obj) { + synthio_envelope_definition_set(&synth->envelope_definition, envelope_obj, synth->sample_rate); + synth->envelope_obj = envelope_obj; +} + +mp_obj_t synthio_synth_envelope_get(synthio_synth_t *synth) { + return synth->envelope_obj; +} + +void synthio_synth_init(synthio_synth_t *synth, uint32_t sample_rate, const int16_t *waveform, uint16_t waveform_length, mp_obj_t envelope_obj) { + synth->buffer_length = SYNTHIO_MAX_DUR * SYNTHIO_BYTES_PER_SAMPLE; synth->buffers[0] = m_malloc(synth->buffer_length, false); synth->buffers[1] = m_malloc(synth->buffer_length, false); synth->other_channel = -1; + synth->waveform = waveform; + synth->waveform_length = waveform_length; + synth->sample_rate = sample_rate; + synthio_synth_envelope_set(synth, envelope_obj); + + for (size_t i = 0; i < CIRCUITPY_SYNTHIO_MAX_CHANNELS; i++) { + synth->span.note[i] = SYNTHIO_SILENCE; + } } void synthio_synth_get_buffer_structure(synthio_synth_t *synth, bool single_channel_output, @@ -132,40 +260,57 @@ void synthio_synth_get_buffer_structure(synthio_synth_t *synth, bool single_chan *spacing = 1; } +static bool parse_common(mp_buffer_info_t *bufinfo, mp_obj_t o, int16_t what) { + if (o != mp_const_none) { + mp_get_buffer_raise(o, bufinfo, MP_BUFFER_READ); + if (bufinfo->typecode != 'h') { + mp_raise_ValueError_varg(translate("%q must be array of type 'h'"), what); + } + mp_arg_validate_length_range(bufinfo->len / 2, 2, 1024, what); + return true; + } + return false; +} + void synthio_synth_parse_waveform(mp_buffer_info_t *bufinfo_waveform, mp_obj_t waveform_obj) { *bufinfo_waveform = ((mp_buffer_info_t) { .buf = (void *)square_wave, .len = 4 }); + parse_common(bufinfo_waveform, waveform_obj, MP_QSTR_waveform); +} - if (waveform_obj != mp_const_none) { - mp_get_buffer_raise(waveform_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); +STATIC int find_channel_with_note(synthio_synth_t *synth, uint8_t note) { + for (int i = 0; i < CIRCUITPY_SYNTHIO_MAX_CHANNELS; i++) { + if (synth->span.note[i] == note) { + return i; } } - mp_arg_validate_length_range(bufinfo_waveform->len / 2, 2, 1024, MP_QSTR_waveform); -} - -void synthio_span_init(synthio_midi_span_t *span) { - span->dur = 0; - for (size_t i = 0; i < CIRCUITPY_SYNTHIO_MAX_CHANNELS; i++) { span->note[i] = SYNTHIO_SILENCE; - } -} - -STATIC int find_channel_with_note(const synthio_midi_span_t *span, uint8_t note) { - for (int i = 0; i < CIRCUITPY_SYNTHIO_MAX_CHANNELS; i++) { - if (span->note[i] == note) { - return i; + if (note == SYNTHIO_SILENCE) { + // we need a victim note that is releasing. simple algorithm: lowest numbered slot + for (int i = 0; i < CIRCUITPY_SYNTHIO_MAX_CHANNELS; i++) { + if (SYNTHIO_VOICE_IS_RELEASING(synth, i)) { + return i; + } } } return -1; } -bool synthio_span_change_note(synthio_midi_span_t *span, uint8_t old_note, uint8_t new_note) { - if (new_note != SYNTHIO_SILENCE && find_channel_with_note(span, new_note) != -1) { - return false; // note already pressed, do nothing +bool synthio_span_change_note(synthio_synth_t *synth, uint8_t old_note, uint8_t new_note) { + int channel; + if (new_note != SYNTHIO_SILENCE && (channel = find_channel_with_note(synth, new_note)) != -1) { + // note already playing, re-strike + synthio_envelope_state_init(&synth->envelope_state[channel], &synth->envelope_definition); + synth->accum[channel] = 0; + return true; } - int channel = find_channel_with_note(span, old_note); + channel = find_channel_with_note(synth, old_note); if (channel != -1) { - span->note[channel] = new_note; + if (new_note == SYNTHIO_SILENCE) { + synthio_envelope_state_release(&synth->envelope_state[channel], &synth->envelope_definition); + } else { + synth->span.note[channel] = new_note; + synthio_envelope_state_init(&synth->envelope_state[channel], &synth->envelope_definition); + synth->accum[channel] = 0; + } return true; } return false; diff --git a/shared-module/synthio/__init__.h b/shared-module/synthio/__init__.h index e14e5ead10..56521d3208 100644 --- a/shared-module/synthio/__init__.h +++ b/shared-module/synthio/__init__.h @@ -30,6 +30,7 @@ #define SYNTHIO_BYTES_PER_SAMPLE (SYNTHIO_BITS_PER_SAMPLE / 8) #define SYNTHIO_MAX_DUR (256) #define SYNTHIO_SILENCE (0x80) +#define SYNTHIO_VOICE_IS_RELEASING(synth, i) (synth->envelope_state[i].state == SYNTHIO_ENVELOPE_STATE_RELEASE) #include "shared-module/audiocore/__init__.h" @@ -39,6 +40,25 @@ typedef struct { } synthio_midi_span_t; typedef struct { + // the number of attack or decay steps (signed) per sample + // therefore the maximum time is 32767 samples or 0.68s at 48kHz + // provided the level is maximum (this should be increased!) + int16_t attack_step, decay_step, release_step; + uint16_t attack_level, sustain_level; +} synthio_envelope_definition_t; + +typedef enum { + SYNTHIO_ENVELOPE_STATE_ATTACK, SYNTHIO_ENVELOPE_STATE_DECAY, + SYNTHIO_ENVELOPE_STATE_SUSTAIN, SYNTHIO_ENVELOPE_STATE_RELEASE +} envelope_state_e; + +typedef struct { + int16_t level; + uint16_t substep; + envelope_state_e state; +} synthio_envelope_state_t; + +typedef struct synthio_synth { uint32_t sample_rate; int16_t *buffers[2]; const int16_t *waveform; @@ -46,19 +66,24 @@ typedef struct { uint16_t last_buffer_length; uint8_t other_channel, buffer_index, other_buffer_index; uint16_t waveform_length; + synthio_envelope_definition_t envelope_definition; + mp_obj_t envelope_obj; synthio_midi_span_t span; uint32_t accum[CIRCUITPY_SYNTHIO_MAX_CHANNELS]; + synthio_envelope_state_t envelope_state[CIRCUITPY_SYNTHIO_MAX_CHANNELS]; } synthio_synth_t; -void synthio_span_init(synthio_midi_span_t *span); void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **buffer, uint32_t *buffer_length, uint8_t channel); void synthio_synth_deinit(synthio_synth_t *synth); bool synthio_synth_deinited(synthio_synth_t *synth); -void synthio_synth_init(synthio_synth_t *synth, uint16_t max_dur); +void synthio_synth_init(synthio_synth_t *synth, uint32_t sample_rate, const int16_t *waveform, uint16_t waveform_length, + mp_obj_t envelope); void synthio_synth_get_buffer_structure(synthio_synth_t *synth, bool single_channel_output, bool *single_buffer, bool *samples_signed, uint32_t *max_buffer_length, uint8_t *spacing); void synthio_synth_reset_buffer(synthio_synth_t *synth, bool single_channel_output, uint8_t channel); void synthio_synth_parse_waveform(mp_buffer_info_t *bufinfo_waveform, mp_obj_t waveform_obj); +void synthio_synth_parse_envelope(uint16_t *envelope_sustain_index, mp_buffer_info_t *bufinfo_envelope, mp_obj_t envelope_obj, mp_obj_t envelope_hold_obj); -bool synthio_span_change_note(synthio_midi_span_t *span, uint8_t old_note, uint8_t new_note); -int synthio_span_count_active_channels(synthio_midi_span_t *span); +bool synthio_span_change_note(synthio_synth_t *synth, uint8_t old_note, uint8_t new_note); + +void synthio_envelope_step(synthio_envelope_definition_t *definition, synthio_envelope_state_t *state, int n_samples); diff --git a/tests/circuitpython/miditrack.py b/tests/circuitpython/miditrack.py index 53a8ca631d..2eac69b152 100644 --- a/tests/circuitpython/miditrack.py +++ b/tests/circuitpython/miditrack.py @@ -11,10 +11,12 @@ SCORE = b"\0\x90@\0\x20\x90b\0\x20\x80@\0\0\x80\b\0" with MidiTrack(SCORE, sample_rate=8000, tempo=640) as m: print(get_structure(m)) - print(get_buffer(m)) + p, q = get_buffer(m) + print(p, list(q)) with MidiTrack( SCORE, sample_rate=8000, tempo=640, waveform=array.array("h", [0, 32767, 0, -32768]) ) as m: print(get_structure(m)) - print(get_buffer(m)) + p, q = get_buffer(m) + print(p, list(q)) diff --git a/tests/circuitpython/miditrack.py.exp b/tests/circuitpython/miditrack.py.exp index bfb082bfe7..2d9e2548a0 100644 --- a/tests/circuitpython/miditrack.py.exp +++ b/tests/circuitpython/miditrack.py.exp @@ -1,4 +1,4 @@ (0, 1, 512, 1) -(1, b'V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\xaa*\xaa*') +1 [-16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0] (0, 1, 512, 1) -(1, b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00V\xd5V\xd5V\xd5V\xd5V\xd5V\xd5\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xaa*\xaa*\xaa*\xaa*\xaa*\xaa*\x00\x00\x00\x00') +1 [0, 0, 0, 0, 0, 0, 16382, 16382, 16382, 16382, 16382, 16382, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16382, 16382, 16382, 16382, 16382, 16382, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16382, 16382, 16382, 16382, 16382, 16382, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16382, 16382, 16382, 16382, 16382, 16382, 0, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16382, 16382, 16382, 16382, 16382, 16382, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16382, 16382, 16382, 16382, 16382, 16382, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16382, 16382, 16382, 16382, 16382, 16382, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16382, 16382, 16382, 16382, 16382, 16382, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16382, 16382, 16382, 16382, 16382, 16382, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16382, 16382, 16382, 16382, 16382, 16382, 0, 0] diff --git a/tests/circuitpython/synthesizer.py b/tests/circuitpython/synthesizer.py index bdb273f861..326fef0994 100644 --- a/tests/circuitpython/synthesizer.py +++ b/tests/circuitpython/synthesizer.py @@ -1,10 +1,11 @@ import struct import synthio import audiocore +import ulab.numpy as np def dump_samples(): - print(struct.unpack("12h", audiocore.get_buffer(s)[1][:24])) + print([i for i in audiocore.get_buffer(s)[1][:24]]) s = synthio.Synthesizer(sample_rate=8000) @@ -22,3 +23,16 @@ dump_samples() s.release_then_press((80,)) print(s.pressed) dump_samples() + +envelope = synthio.Envelope( + attack_time=0.1, decay_time=0.05, release_time=0.2, attack_level=1, sustain_level=0.8 +) +s = synthio.Synthesizer(sample_rate=8000, envelope=envelope) +s.press((60,)) +for _ in range(12): + buf = audiocore.get_buffer(s)[1] + print((min(buf), max(buf))) +s.release_all() +for _ in range(12): + buf = audiocore.get_buffer(s)[1] + print((min(buf), max(buf))) diff --git a/tests/circuitpython/synthesizer.py.exp b/tests/circuitpython/synthesizer.py.exp index 79c88b3248..70eb647bab 100644 --- a/tests/circuitpython/synthesizer.py.exp +++ b/tests/circuitpython/synthesizer.py.exp @@ -1,8 +1,32 @@ () -(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) +[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] (80,) -(-10922, -10922, -10922, -10922, 10922, 10922, 10922, 10922, 10922, -10922, -10922, -10922) +[-16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383] (80, 91) -(0, 0, 13106, 13106, 0, -13106, -13106, 0, 13106, 13106, 0, 0) +[-8191, -8191, 0, 0, -8191, -16382, -16382, -8191, 0, 0, -8191, -8191, 0, -8191, -8191, -16382, -16382, -8191, 0, 0, -8191, -8191, 0, -8191] (91,) -(-10922, 10922, 10922, 10922, -10922, -10922, 10922, 10922, 10922, -10922, -10922, 10922) +[-16382, -8191, -8191, 0, -8191, -8191, 0, 0, -8191, -16382, -16382, -8191, 0, 0, -8191, -8191, 0, -8191, -8191, -16382, -16382, -16382, 0, 0] +(-5242, 0) +(-10484, 0) +(-15727, 0) +(-16383, 0) +(-14286, 0) +(-13106, 0) +(-13106, 0) +(-13106, 0) +(-13106, 0) +(-13106, 0) +(-13106, 0) +(-13106, 0) +(-13106, 0) +(-11009, 0) +(-8912, 0) +(-6815, 0) +(-4718, 0) +(-2621, 0) +(-524, 0) +(0, 0) +(0, 0) +(0, 0) +(0, 0) +(0, 0)