Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions shared-bindings/synthio/MidiTrack.c
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 } },
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion shared-bindings/synthio/MidiTrack.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
16 changes: 13 additions & 3 deletions shared-bindings/synthio/__init__.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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);
Expand All @@ -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
);
Expand Down
27 changes: 21 additions & 6 deletions shared-module/synthio/MidiTrack.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand All @@ -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;

Expand All @@ -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,
Expand All @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions shared-module/synthio/MidiTrack.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading