diff --git a/shared-bindings/synthio/MidiTrack.c b/shared-bindings/synthio/MidiTrack.c index 89c50282bab..83b2fd94100 100644 --- a/shared-bindings/synthio/MidiTrack.c +++ b/shared-bindings/synthio/MidiTrack.c @@ -23,16 +23,25 @@ //| buffer: ReadableBuffer, //| tempo: int, //| *, +//| ticks_per_beat: int = 0, //| sample_rate: int = 11025, //| 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 -//| same time. +//| """Create a MidiTrack from the given stream of MIDI events. +//| +//| "Note On" and "Note Off" events are played; a "Note On" with a velocity of zero is +//| treated as a "Note Off", as the MIDI specification allows. Other channel events are +//| skipped, as are System Exclusive and meta events, except that "End of Track" stops +//| playback and "Set Tempo" adjusts it when ``ticks_per_beat`` is known. Events that omit +//| a repeated status byte ("running status") are understood. Channel numbers and key +//| velocities are otherwise ignored. //| //| :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 ticks_per_beat: Ticks per quarter note, from the MIDI file header. When this +//| is 0 (the default) any "Set Tempo" event in the stream is ignored, because its +//| microseconds-per-quarter-note value cannot be converted without it. //| :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. @@ -55,10 +64,11 @@ //| ... //| 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, ARG_envelope }; + enum { ARG_buffer, ARG_tempo, ARG_ticks_per_beat, 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_ticks_per_beat, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 0} }, { 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 } }, @@ -73,6 +83,7 @@ static mp_obj_t synthio_miditrack_make_new(const mp_obj_type_t *type, size_t n_a common_hal_synthio_miditrack_construct(self, (uint8_t *)bufinfo.buf, bufinfo.len, mp_arg_validate_int_min(args[ARG_tempo].u_int, 1, MP_QSTR_tempo), + args[ARG_ticks_per_beat].u_int, args[ARG_sample_rate].u_int, args[ARG_waveform].u_obj, mp_const_none, diff --git a/shared-bindings/synthio/MidiTrack.h b/shared-bindings/synthio/MidiTrack.h index cb154d3996b..48e430156d5 100644 --- a/shared-bindings/synthio/MidiTrack.h +++ b/shared-bindings/synthio/MidiTrack.h @@ -11,7 +11,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, mp_obj_t waveform_obj, mp_obj_t filter_obj, mp_obj_t envelope_obj); +void common_hal_synthio_miditrack_construct(synthio_miditrack_obj_t *self, const uint8_t *buffer, uint32_t len, uint32_t tempo, uint32_t ticks_per_beat, uint32_t sample_rate, mp_obj_t waveform_obj, mp_obj_t filter_obj, mp_obj_t envelope_obj); void common_hal_synthio_miditrack_deinit(synthio_miditrack_obj_t *self); mp_int_t common_hal_synthio_miditrack_get_error_location(synthio_miditrack_obj_t *self); diff --git a/shared-bindings/synthio/__init__.c b/shared-bindings/synthio/__init__.c index 3f9bc3f9274..b77e312974b 100644 --- a/shared-bindings/synthio/__init__.c +++ b/shared-bindings/synthio/__init__.c @@ -177,6 +177,9 @@ const mp_obj_namedtuple_type_t synthio_envelope_type_obj = { //| """Create an AudioSample from an already opened MIDI file. //| Currently, only single-track MIDI (type 0) is supported. //| +//| The file's tempo map is followed: the track starts at the MIDI default of 120 BPM and +//| each "Set Tempo" event changes the rate from that point on. +//| //| :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) @@ -233,11 +236,18 @@ static mp_obj_t synthio_from_file(size_t n_args, const mp_obj_t *pos_args, mp_ma // TODO: for a multi-track MIDI (type 1), return an AudioMixer } - uint16_t tempo; + uint32_t tempo; + uint32_t ticks_per_beat; if (chunk_header[12] & 0x80) { + // SMPTE: frames per second times ticks per frame is already an + // absolute rate, so Set Tempo events do not apply to it tempo = -(int8_t)chunk_header[12] * chunk_header[13]; + ticks_per_beat = 0; } else { - tempo = 2 * ((chunk_header[12] << 8) | chunk_header[13]); + ticks_per_beat = (chunk_header[12] << 8) | chunk_header[13]; + // until a Set Tempo event says otherwise, MIDI defaults to 120 BPM, + // i.e. two quarter notes per second + tempo = 2 * ticks_per_beat; } bytes_read = mp_stream_rw(file_obj, chunk_header, 8, &errcode, MP_STREAM_RW_READ); @@ -260,7 +270,7 @@ static mp_obj_t synthio_from_file(size_t n_args, const mp_obj_t *pos_args, mp_ma synthio_miditrack_obj_t *result = mp_obj_malloc_with_finaliser(synthio_miditrack_obj_t, &synthio_miditrack_type); common_hal_synthio_miditrack_construct(result, buffer, track_size, - tempo, args[ARG_sample_rate].u_int, args[ARG_waveform].u_obj, + tempo, ticks_per_beat, args[ARG_sample_rate].u_int, args[ARG_waveform].u_obj, mp_const_none, args[ARG_envelope].u_obj ); diff --git a/shared-module/synthio/MidiTrack.c b/shared-module/synthio/MidiTrack.c index 2d090be2696..23641c12a2c 100644 --- a/shared-module/synthio/MidiTrack.c +++ b/shared-module/synthio/MidiTrack.c @@ -92,6 +92,16 @@ static void decode_system_event(synthio_miditrack_obj_t *self, uint8_t status) { self->pos = self->track.len; return; } + if (type == 0x51 && length == 3 && self->ticks_per_beat != 0) { // Set Tempo + uint32_t us_per_beat = (buffer[self->pos] << 16) | + (buffer[self->pos + 1] << 8) | buffer[self->pos + 2]; + if (us_per_beat != 0) { + uint32_t ticks = (uint32_t)(((uint64_t)self->ticks_per_beat * 1000000) / us_per_beat); + if (ticks != 0) { + self->tempo = ticks; + } + } + } self->pos += length; } else if (status == 0xF0 || status == 0xF7) { uint32_t length = decode_varint(self); @@ -165,8 +175,10 @@ static void decode_until_pause(synthio_miditrack_obj_t *self) { static void start_parse(synthio_miditrack_obj_t *self) { self->pos = 0; self->error_location = -1; - self->running_status = 0; self->pending = 0; + self->running_status = 0; + // a tempo map applies from the top of the track, so rewind it too + self->tempo = self->base_tempo; decode_duration(self); if (self->synth.span.dur == 0 && self->pending == 0) { // the usual case: the file starts with some MIDI event, not a delay @@ -175,10 +187,12 @@ static void start_parse(synthio_miditrack_obj_t *self) { } 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, - mp_obj_t waveform_obj, mp_obj_t filter_obj, mp_obj_t envelope_obj) { + const uint8_t *buffer, uint32_t len, uint32_t tempo, uint32_t ticks_per_beat, + uint32_t sample_rate, mp_obj_t waveform_obj, mp_obj_t filter_obj, + mp_obj_t envelope_obj) { - self->tempo = tempo; + self->tempo = self->base_tempo = tempo; + self->ticks_per_beat = ticks_per_beat; self->track.buf = (void *)buffer; self->track.len = len; @@ -204,7 +218,8 @@ void common_hal_synthio_miditrack_set_tempo(synthio_miditrack_obj_t *self, mp_in uint64_t remaining = (uint64_t)self->synth.span.dur + self->pending; remaining = remaining * self->tempo / val; set_remaining(self, remaining > UINT32_MAX ? UINT32_MAX : (uint32_t)remaining); - self->tempo = val; + // becomes the new baseline, so that looping does not undo it + self->tempo = self->base_tempo = val; } void synthio_miditrack_reset_buffer(synthio_miditrack_obj_t *self, @@ -223,7 +238,7 @@ audioio_get_buffer_result_t synthio_miditrack_get_buffer(synthio_miditrack_obj_t synthio_synth_synthesize(&self->synth, buffer, buffer_length, single_channel_output ? 0 : channel); if (self->synth.span.dur == 0) { if (self->pending != 0) { - // still inside a delay too long for a single span + // still inside a gap too long for a single span set_remaining(self, self->pending); } else if (self->pos == self->track.len) { return GET_BUFFER_DONE; diff --git a/shared-module/synthio/MidiTrack.h b/shared-module/synthio/MidiTrack.h index 674e73c6d4e..e8a6a9bf864 100644 --- a/shared-module/synthio/MidiTrack.h +++ b/shared-module/synthio/MidiTrack.h @@ -16,9 +16,14 @@ typedef struct { // invariant: after initial startup, pos always points just after an encoded duration, i.e., at a midi message (or at EOF) size_t pos; mp_int_t error_location; + // current tempo in ticks per second, and the value to rewind it to uint32_t tempo; + uint32_t base_tempo; // samples of the current delay that did not fit in span.dur uint32_t pending; + // ticks per quarter note (15 bits in the file header), or 0 when the + // timing is absolute (SMPTE) and Set Tempo events do not apply + uint16_t ticks_per_beat; // last channel status byte, for events that omit it (running status) uint8_t running_status; } synthio_miditrack_obj_t;