From 86d12c37ed459df8bf382ba3f34e942ceaced45e Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Sat, 1 Jul 2023 00:15:53 +0000 Subject: [PATCH] midi: add generator This adds both a midi generator and a midi type 1 simulator. While I would have preferred to use an existing tool for this, I found that timidity++ does not emit pitch wheel events correctly, and I don't know of another widely-distributed tool that does midi-to-midi format conversion. The c++ and python versions were co-developed. I wrote one to test the other. There is more cleanup to do, but `roundtrip.cpp` produces a valid type 0 midi file given a type 1 or type 0 midi file as input. --- m68k/Makefile | 2 +- m68k/midi.cpp | 24 ++--- midi/dump.cpp | 9 +- midi/dump.py | 23 +++++ midi/generate.cpp | 208 ++++++++++++++++++++++++++++++++++++++ midi/generate.hpp | 22 ++++ midi/iterator.cpp | 58 +++++++++++ midi/midi.hpp | 5 + midi/parse.cpp | 157 +++++++++++++++-------------- midi/parse.hpp | 4 +- midi/parser.py | 97 +++++++----------- midi/roundtrip.cpp | 223 +++++++++++++++++++++++++++++++++++++++++ midi/simulate.py | 74 ++++++++++++++ midi/strings.hpp | 3 +- midi/test_generate.cpp | 86 ++++++++++++++++ 15 files changed, 837 insertions(+), 158 deletions(-) create mode 100644 midi/dump.py create mode 100644 midi/generate.cpp create mode 100644 midi/generate.hpp create mode 100644 midi/iterator.cpp create mode 100644 midi/roundtrip.cpp create mode 100644 midi/simulate.py create mode 100644 midi/test_generate.cpp diff --git a/m68k/Makefile b/m68k/Makefile index 3a500db..0ccf28d 100644 --- a/m68k/Makefile +++ b/m68k/Makefile @@ -12,7 +12,7 @@ include $(LIB)/m68k/common.mk sox \ -r 44100 -e signed-integer -b 16 -c 1 -n -B \ $@.raw \ - synth 100s $* 440 vol -10dB + synth 101s $* 441 vol -10dB mv $@.raw $@ %.pcm.o: %.pcm diff --git a/m68k/midi.cpp b/m68k/midi.cpp index 580f4e9..f7280c1 100644 --- a/m68k/midi.cpp +++ b/m68k/midi.cpp @@ -7,8 +7,8 @@ #include "../common/copy.hpp" extern void * _sine_start __asm("_binary_sine_44100_s16be_1ch_100sample_pcm_start"); -//extern void * _midi_start __asm("_binary_midi_test_c_major_scale_mid_start"); -extern void * _midi_start __asm("_binary_f2_mid_start"); +extern void * _midi_start __asm("_binary_midi_test_c_major_scale_mid_start"); +//extern void * _midi_start __asm("_binary_f2_mid_start"); uint16_t midi_note_to_oct_fns(const int8_t midi_note) @@ -75,7 +75,7 @@ void error() // start address (bytes) slot.SA = SA__KYONB | SA__LPCTL__NORMAL | SA__SA(sine_start); // kx kb sbctl[1:0] ssctl[1:0] lpctl[1:0] 8b sa[19:0] slot.LSA = 0; // loop start address (samples) - slot.LEA = 99; // loop end address (samples) + slot.LEA = 100; // loop end address (samples) slot.EG = EG__EGHOLD; // d2r d1r ho ar krs dl rr slot.FM = 0; // stwinh sdir tl mdl mdxsl mdysl //slot.PITCH = PITCH__OCT(0) | PITCH__FNS(0); // oct fns @@ -89,7 +89,7 @@ void error() // start address (bytes) slot.SA = SA__KYONB | SA__LPCTL__NORMAL | SA__SA(sine_start); // kx kb sbctl[1:0] ssctl[1:0] lpctl[1:0] 8b sa[19:0] slot.LSA = 0; // loop start address (samples) - slot.LEA = 99; // loop end address (samples) + slot.LEA = 100; // loop end address (samples) slot.EG = EG__EGHOLD; // d2r d1r ho ar krs dl rr slot.FM = 0; // stwinh sdir tl mdl mdxsl mdysl slot.PITCH = PITCH__OCT(1) | PITCH__FNS(0); // oct fns @@ -133,7 +133,7 @@ static struct vs voice_slot[16][128]; #pragma GCC push_options #pragma GCC optimize ("unroll-loops") -int8_t alloc_slot() +static inline int8_t alloc_slot() { for (int i = 0; i < 32; i++) { uint32_t bit = (1 << i); @@ -146,7 +146,7 @@ int8_t alloc_slot() } #pragma GCC pop_options -void free_slot(int8_t i) +static inline void free_slot(int8_t i) { slot_alloc &= ~(1 << i); } @@ -203,16 +203,14 @@ void midi_step() scsp.ram.u32[3] = midi_event.data.note_on.note; - //slot.SA = SA__KYONB | SA__LPCTL__NORMAL | SA__SA(sine_start); - slot.LOOP = LOOP__KYONB | LOOP__LPCTL__NORMAL | SAH__SA(sine_start); - slot.SAL = SAL__SA(sine_start); + slot.SA = SA__KYONB | SA__LPCTL__NORMAL | SA__SA(sine_start); slot.LSA = 0; - slot.LEA = 99; - slot.EG = EG__EGHOLD; //EG__AR(0x0f) | EG__D1R(0x4) | EG__D2R(0x4) | EG__RR(0x1f); + slot.LEA = 100; + slot.EG = EG__AR(0x1f) | EG__D1R(0x0) | EG__D2R(0x0) | EG__RR(0x1f); slot.FM = 0; slot.PITCH = midi_note_to_oct_fns(midi_event.data.note_on.note); slot.LFO = 0; - slot.MIXER = MIXER__DISDL(0b110); + slot.MIXER = MIXER__DISDL(0b101); if (v.count == 1) kyonex = 1; @@ -230,7 +228,7 @@ void midi_step() v.slot_ix = -1; slot.LOOP = 0; scsp.reg.slot[0].SA |= SA__KYONEX; - kyonex = 1; + //kyonex = 1; } } break; diff --git a/midi/dump.cpp b/midi/dump.cpp index f28fe07..b740bac 100644 --- a/midi/dump.cpp +++ b/midi/dump.cpp @@ -67,12 +67,6 @@ int parse(uint8_t const * start) std::cout << "header.format: " << midi::strings::header_format(header.format) << '\n'; std::cout << "header.ntrks: " << header.ntrks << '\n'; - //while header.n - // while header.ntrks: - // - // for event in events: - // ev - for (int32_t i = 0; i < header.ntrks; i++) { std::cout << "track[" << i << "]:\n"; @@ -132,7 +126,7 @@ int main(int argc, char *argv[]) std::cerr << argv[1] << '\n'; std::ifstream ifs; - ifs.open(argv[1], std::ios::binary | std::ios::ate); + ifs.open(argv[1], std::ios::in | std::ios::binary | std::ios::trunc); if (!ifs.is_open()) { std::cerr << "ifstream\n"; return -1; @@ -145,6 +139,7 @@ int main(int argc, char *argv[]) std::cerr << "read\n"; return -1; } + ifs.close(); parse(start); diff --git a/midi/dump.py b/midi/dump.py new file mode 100644 index 0000000..35eb433 --- /dev/null +++ b/midi/dump.py @@ -0,0 +1,23 @@ +import sys + +from parser import * + +def dump_file(buf): + buf, header = parse_header(buf) + print(header) + assert header.ntrks > 0 + tracks = [] + for track_num in range(header.ntrks): + buf, track = parse_track(buf) + tracks.append(track) + print(f"track {track_num}:") + for i, event in enumerate(track.events): + print(' ' + repr(event)) + + print("remaining data:", len(buf)) + +import sys +with open(sys.argv[1], 'rb') as f: + b = memoryview(f.read()) + +dump_file(b) diff --git a/midi/generate.cpp b/midi/generate.cpp new file mode 100644 index 0000000..ce1f1db --- /dev/null +++ b/midi/generate.cpp @@ -0,0 +1,208 @@ +#include +#include +#include + +#include "midi.hpp" +#include "generate.hpp" + +namespace midi { +namespace generate { + +constexpr inline buf_t +int_variable_length(buf_t buf, const uint32_t n) noexcept +{ + int nonzero = 0; + for (int i = 3; i > 0; i--) { + uint8_t nib = (n >> (7 * i)) & 0x7f; + if (nib != 0) nonzero = 1; + if (nonzero) { + buf[0] = 0x80 | nib; + buf++; + } + } + buf[0] = n & 0x7f; + buf++; + return buf; +} + +// note: there are no alignment requirements for the location of a +// fixed-length integer inside a midi file--reinterpret_cast+byteswap +// would be faster if it weren't that this is possibly-unaligned +// access. + +constexpr inline buf_t +int_fixed_length16(buf_t buf, const uint16_t n) noexcept +{ + buf[0] = (n >> 8) & 0xff; + buf[1] = (n >> 0) & 0xff; + return buf + (sizeof (uint16_t)); +} + +constexpr inline buf_t +int_fixed_length32(buf_t buf, const uint32_t n) noexcept +{ + buf[0] = (n >> 24) & 0xff; + buf[1] = (n >> 16) & 0xff; + buf[2] = (n >> 8 ) & 0xff; + buf[3] = (n >> 0 ) & 0xff; + return buf + (sizeof (uint32_t)); +} + +constexpr inline buf_t +header_chunk_type(buf_t buf) noexcept +{ + buf[0] = 'M'; + buf[1] = 'T'; + buf[2] = 'h'; + buf[3] = 'd'; + return buf + 4; +} + +constexpr inline buf_t +division(buf_t buf, const division_t& division) noexcept +{ + if (division.type == division_t::type_t::metrical) { + return int_fixed_length16(buf, division.metrical.ticks_per_quarter_note & 0x7fff); + } else { // time_code + buf[0] = division.time_code.smpte | 0x80; + buf[1] = division.time_code.ticks_per_frame; + return buf + 2; + } +} + +buf_t +header(buf_t buf, const header_t& header) noexcept +{ + buf = header_chunk_type(buf); + buf = int_fixed_length32(buf, 6); + + uint16_t format; + switch (header.format) { + case header_t::format_t::_0: format = 0; break; + case header_t::format_t::_1: format = 1; break; + case header_t::format_t::_2: format = 2; break; + default: assert(false); + } + buf = int_fixed_length16(buf, format); + + buf = int_fixed_length16(buf, header.ntrks); + + buf = division(buf, header.division); + + return buf; +} + + +constexpr inline buf_t +track_chunk_type(buf_t buf) noexcept +{ + buf[0] = 'M'; + buf[1] = 'T'; + buf[2] = 'r'; + buf[3] = 'k'; + return buf + 4; +} + +buf_t +track(buf_t buf, const track_t& track) noexcept +{ + buf = track_chunk_type(buf); + buf = int_fixed_length32(buf, track.length); + return buf; +} + +constexpr inline uint8_t +midi_status_byte(const midi_event_t::type_t midi_event_type) noexcept +{ + switch (midi_event_type) { + case midi_event_t::type_t::note_off: return 0x80; + case midi_event_t::type_t::note_on: return 0x90; + case midi_event_t::type_t::polyphonic_key_pressure: return 0xa0; + case midi_event_t::type_t::channel_mode: return 0xb0; + case midi_event_t::type_t::control_change: return 0xb0; + case midi_event_t::type_t::program_change: return 0xc0; + case midi_event_t::type_t::channel_pressure: return 0xd0; + case midi_event_t::type_t::pitch_bend_change: return 0xe0; + default: assert(false); + }; +} + +constexpr inline int32_t +midi_event_message_length(const midi_event_t::type_t type) noexcept +{ + if (type == midi_event_t::type_t::program_change || type == midi_event_t::type_t::channel_pressure) + return 1; + else + return 2; +} + +constexpr inline buf_t +midi_event(buf_t buf, const midi_event_t& event, uint8_t& running_status) noexcept +{ + uint8_t status = midi_status_byte(event.type) | event.data.note_off.channel; + if (running_status != status) { + running_status = status; + buf[0] = status; + buf++; + } + + // it doesn't matter which union is used, as long as it is one with + // 3 items. note_off is used throughout, even though it could + // represent a type other than note_off. + + buf[0] = event.data.note_off.note; + // possibly-initializing the third field on a 2-field midi_event + // does not matter, as the caller should ignore it anyway. This + // harmless extraneous initialization means a branch is not needed. + buf[1] = event.data.note_off.velocity; + return buf + midi_event_message_length(event.type); +} + +constexpr inline buf_t +sysex_event(buf_t buf, const sysex_event_t& event) noexcept +{ + if not consteval { + assert(false); // not implemented + } + return buf; +} + +constexpr inline buf_t +meta_event(buf_t buf, const meta_event_t& meta) noexcept +{ + buf[0] = 0xff; + buf[1] = meta.type; + buf = int_variable_length(buf + 2, meta.length); + for (uint32_t i = 0; i < meta.length; i++) { + buf[i] = meta.data[i]; + } + + return buf + meta.length; +} + +constexpr inline buf_t +event(buf_t buf, const event_t& event, uint8_t& running_status) noexcept +{ + switch (event.type) { + case event_t::type_t::midi: + return midi_event(buf, event.event.midi, running_status); + case event_t::type_t::sysex: + running_status = 0; + return sysex_event(buf, event.event.sysex); + case event_t::type_t::meta: + running_status = 0; + return meta_event(buf, event.event.meta); + default: assert(false); + } +} + +buf_t +mtrk_event(buf_t buf, const mtrk_event_t& _event, uint8_t& running_status) noexcept +{ + buf = int_variable_length(buf, _event.delta_time); + buf = event(buf, _event.event, running_status); + return buf; +} + +} +} diff --git a/midi/generate.hpp b/midi/generate.hpp new file mode 100644 index 0000000..9eb3362 --- /dev/null +++ b/midi/generate.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include + +#include "midi.hpp" + +namespace midi { +namespace generate { + +using buf_t = uint8_t *; + +buf_t +header(buf_t buf, const header_t& header) noexcept; + +buf_t +track(buf_t buf, const track_t& track) noexcept; + +buf_t +mtrk_event(buf_t buf, const mtrk_event_t& event, uint8_t& running_status) noexcept; + +} +} diff --git a/midi/iterator.cpp b/midi/iterator.cpp new file mode 100644 index 0000000..8146889 --- /dev/null +++ b/midi/iterator.cpp @@ -0,0 +1,58 @@ +#include +#include +#include +#include + +#include "midi.hpp" +#include "parse.hpp" + +struct mtrk_iterator { + using difference_type = int32_t; + using element_type = midi::mtrk_event_t; + using pointer = element_type *; + using reference = element_type &; + + uint8_t const * track_start; + midi::track_t track; + uint8_t running_status; + uint8_t const * buf; + uint8_t const * next_buf; + midi::mtrk_event_t mtrk_event; + + mtrk_iterator() = delete; + mtrk_iterator(const midi::track_t& track, + uint8_t const * const track_start) + : track_start(track_start) + , track(track) + , running_status(0) + , buf(track_start) + , next_buf(track_start) + { + } + + mtrk_iterator& operator=(mtrk_iterator&&) = default; + constexpr mtrk_iterator(const mtrk_iterator&) = default; + + reference operator*() { + auto mtrk_event_o = midi::parse::mtrk_event(buf, running_status); + std::tie(next_buf, mtrk_event) = *mtrk_event_o; + return mtrk_event; + } + + mtrk_iterator operator++() { + assert(buf != next_buf); + buf = next_buf; + return *this; + } + + mtrk_iterator operator++(int) { + mtrk_iterator tmp = *this; + ++(*this); + return tmp; + } + + bool at_end() { + return buf - track_start >= track.length; + } + +}; diff --git a/midi/midi.hpp b/midi/midi.hpp index 79e5aac..4baa939 100644 --- a/midi/midi.hpp +++ b/midi/midi.hpp @@ -102,6 +102,7 @@ struct midi_event_t { struct sysex_event_t { const uint8_t * data; uint32_t length; + uint8_t tag; }; struct meta_event_t { @@ -128,4 +129,8 @@ struct mtrk_event_t { event_t event; }; +struct track_t { + uint32_t length; +}; + } // midi diff --git a/midi/parse.cpp b/midi/parse.cpp index d4b3dde..21c16c8 100644 --- a/midi/parse.cpp +++ b/midi/parse.cpp @@ -8,7 +8,7 @@ namespace midi { namespace parse { -static constexpr inline std::optional> +constexpr inline std::optional> int_variable_length(buf_t buf) { uint32_t n = 0; @@ -24,31 +24,26 @@ int_variable_length(buf_t buf) return std::nullopt; } -static inline std::tuple +// note: there are no alignment requirements for the location of a +// fixed-length integer inside a midi file--reinterpret_cast+byteswap +// would be faster if it weren't that this is possibly-unaligned +// access. + +constexpr inline std::tuple int_fixed_length16(buf_t buf) { - uint16_t n; - if (0) {// constexpr (std::endian::native == std::endian::big) { - n = *reinterpret_cast(buf); - } else { - n = (buf[0] << 8 | buf[1] << 0); - } + uint16_t n = (buf[0] << 8 | buf[1] << 0); return {buf + (sizeof (uint16_t)), n}; } -static inline std::tuple +constexpr inline std::tuple int_fixed_length32(buf_t buf) { - uint32_t n; - if (0) {//constexpr (std::endian::native == std::endian::big) { - n = *reinterpret_cast(buf); - } else { - n = (buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3] << 0); - } + uint32_t n = (buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3] << 0); return {buf + (sizeof (uint32_t)), n}; } -static constexpr inline std::optional +constexpr inline std::optional header_chunk_type(buf_t buf) { if ( buf[0] == 'M' @@ -60,13 +55,13 @@ header_chunk_type(buf_t buf) return std::nullopt; } -static inline std::tuple +constexpr inline std::tuple division(buf_t buf) { uint16_t n; std::tie(buf, n) = int_fixed_length16(buf); if ((n & (1 << 15)) != 0) { - int8_t smpte = ((n >> 8) & 0x7f) - 0x80; // sign-extend + int8_t smpte = (n >> 8); uint8_t ticks_per_frame = n & 0xff; return { buf, { .type = division_t::type_t::time_code, .time_code = { smpte, ticks_per_frame } @@ -103,28 +98,53 @@ header(buf_t buf) return {{buf, {format, ntrks, division}}}; } -static constexpr inline std::optional> -midi_event_type(buf_t buf) +constexpr inline std::optional +track_chunk_type(buf_t buf) { - uint8_t n = buf[0] & 0xf0; + if ( buf[0] == 'M' + && buf[1] == 'T' + && buf[2] == 'r' + && buf[3] == 'k') + return {buf + 4}; + else + return std::nullopt; +} + +std::optional> +track(buf_t buf) +{ + auto buf_o = track_chunk_type(buf); + if (!buf_o) return std::nullopt; + buf = *buf_o; + + uint32_t length; + std::tie(buf, length) = int_fixed_length32(buf); + + return {{buf, {length}}}; +} + +constexpr inline std::optional +midi_event_type(uint8_t status, uint8_t param0) +{ + uint8_t n = status & 0xf0; // do not increment buf; the caller needs to parse channel switch (n) { - case 0x80: return {{buf, midi_event_t::type_t::note_off}}; - case 0x90: return {{buf, midi_event_t::type_t::note_on}}; - case 0xa0: return {{buf, midi_event_t::type_t::polyphonic_key_pressure}}; + case 0x80: return {midi_event_t::type_t::note_off}; + case 0x90: return {midi_event_t::type_t::note_on}; + case 0xa0: return {midi_event_t::type_t::polyphonic_key_pressure}; case 0xb0: - if (buf[0] >= 121 && buf[0] <= 127) - return {{buf, midi_event_t::type_t::channel_mode}}; + if (param0 >= 121 && param0 <= 127) + return {midi_event_t::type_t::channel_mode}; else - return {{buf, midi_event_t::type_t::control_change}}; - case 0xc0: return {{buf, midi_event_t::type_t::program_change}}; - case 0xd0: return {{buf, midi_event_t::type_t::channel_pressure}}; - case 0xe0: return {{buf, midi_event_t::type_t::pitch_bend_change}}; + return {midi_event_t::type_t::control_change}; + case 0xc0: return {midi_event_t::type_t::program_change}; + case 0xd0: return {midi_event_t::type_t::channel_pressure}; + case 0xe0: return {midi_event_t::type_t::pitch_bend_change}; default: return std::nullopt; }; } -static constexpr inline int32_t +constexpr inline int32_t midi_event_message_length(midi_event_t::type_t type) { if (type == midi_event_t::type_t::program_change || type == midi_event_t::type_t::channel_pressure) @@ -133,31 +153,44 @@ midi_event_message_length(midi_event_t::type_t type) return 2; } -static constexpr inline std::optional> -midi_event(buf_t buf) +constexpr inline std::optional> +midi_event(buf_t buf, uint8_t& running_status) { + if ((buf[0] & 0x80) == 0) { + // invalid running_status + if ((running_status & 0x80) == 0) return std::nullopt; + } else { + // this is a status change + running_status = buf[0]; + buf++; + } + + midi_event_t event; + auto type_o = midi_event_type(running_status, buf[0]); + if (!type_o) return std::nullopt; + event.type = *type_o; + event.data.note_off.channel = running_status & 0x0f; + // it doesn't matter which union is used, as long as it is one with // 3 items. note_off is used throughout, even though it could // represent a type other than note_off. - midi_event_t event; - auto type_o = midi_event_type(buf); // does not increment buf - if (!type_o) return std::nullopt; - std::tie(buf, event.type) = *type_o; - event.data.note_off.channel = buf[0] & 0x0f; - event.data.note_off.note = buf[1]; + event.data.note_off.note = buf[0]; // possibly-initializing the third field on a 2-field midi_event // does not matter, as the caller should ignore it anyway. This // harmless extraneous initialization means a branch is not needed. - event.data.note_off.velocity = buf[2]; - buf += 1 + midi_event_message_length(event.type); + event.data.note_off.velocity = buf[1]; + buf += midi_event_message_length(event.type); // this does not validate that buf[1]/buf[2] are <= 0x7f return {{buf, event}}; } -static constexpr inline std::optional> +constexpr inline std::optional> sysex_event(buf_t buf) { + // not implemented + return std::nullopt; + /* if (buf[0] != 0xf0 && buf[0] != 0xf7) return std::nullopt; buf++; auto length_o = int_variable_length(buf); @@ -165,9 +198,10 @@ sysex_event(buf_t buf) uint32_t length; std::tie(buf, length) = *length_o; return {{buf + length, {buf, length}}}; + */ } -static constexpr inline std::optional> +constexpr inline std::optional> meta_event(buf_t buf) { if (buf[0] != 0xff) return std::nullopt; @@ -180,18 +214,20 @@ meta_event(buf_t buf) return {{buf + length, {buf, length, type}}}; } -static constexpr inline std::optional> -event(buf_t buf) +constexpr inline std::optional> +event(buf_t buf, uint8_t& running_status) { - if (auto midi_o = midi_event(buf)) { + if (auto midi_o = midi_event(buf, running_status)) { midi_event_t midi; std::tie(buf, midi) = *midi_o; return {{buf, {event_t::type_t::midi, {.midi = midi}}}}; } else if (auto meta_o = meta_event(buf)) { + running_status = 0; meta_event_t meta; std::tie(buf, meta) = *meta_o; return {{buf, {event_t::type_t::meta, {.meta = meta}}}}; } else if (auto sysex_o = sysex_event(buf)) { + running_status = 0; sysex_event_t sysex; std::tie(buf, sysex) = *sysex_o; return {{buf, {event_t::type_t::sysex, {.sysex = sysex}}}}; @@ -201,44 +237,19 @@ event(buf_t buf) } std::optional> -mtrk_event(buf_t buf) +mtrk_event(buf_t buf, uint8_t& running_status) { auto delta_time_o = int_variable_length(buf); if (!delta_time_o) return std::nullopt; uint32_t delta_time; std::tie(buf, delta_time) = *delta_time_o; - auto event_o = event(buf); + auto event_o = event(buf, running_status); if (!event_o) return std::nullopt; event_t event; std::tie(buf, event) = *event_o; return {{buf, {delta_time, event}}}; } -static constexpr inline std::optional -track_chunk_type(buf_t buf) -{ - if ( buf[0] == 'M' - && buf[1] == 'T' - && buf[2] == 'r' - && buf[3] == 'k') - return {buf + 4}; - else - return std::nullopt; -} - -std::optional> -track(buf_t buf) -{ - auto buf_o = track_chunk_type(buf); - if (!buf_o) return std::nullopt; - buf = *buf_o; - - uint32_t length; - std::tie(buf, length) = int_fixed_length32(buf); - - return {{buf, length}}; -} - } // namespace parse } // namespace midi diff --git a/midi/parse.hpp b/midi/parse.hpp index 7d84874..309b176 100644 --- a/midi/parse.hpp +++ b/midi/parse.hpp @@ -14,11 +14,11 @@ using buf_t = uint8_t const *; std::optional> header(buf_t buf); -std::optional> +std::optional> track(buf_t buf); std::optional> -mtrk_event(buf_t buf); +mtrk_event(buf_t buf, uint8_t& running_status); } } diff --git a/midi/parser.py b/midi/parser.py index cea0acc..0922b55 100644 --- a/midi/parser.py +++ b/midi/parser.py @@ -175,7 +175,7 @@ class ChannelMode: @dataclass class MIDIEvent: - event = Union[ + event: Union[ NoteOff, NoteOn, PolyphonicKeyPressure, @@ -299,9 +299,11 @@ def parse_meta_event(buf): return None type_n = buf[1] type, value_parser = meta_nums[type_n] + #print(len(buf[2:])) buf, length = parse_variable_length(buf[2:]) + #print(len(buf[0:])) data = buf[:length] - print("meta", length, bytes(data)) + #print("meta", type_n, length, bytes(data)) buf = buf[length:] return buf, MetaEvent(type, value_parser(data)) @@ -316,41 +318,46 @@ midi_messages = dict([ # ChannelMode handled specially ]) +running_status = None + def parse_midi_event(buf): - message_type = buf[0] & 0xf0 + global running_status + if buf[0] & 0x80 == 0: + assert running_status is not None, buf[0] + else: + running_status = buf[0] + buf = buf[1:] + message_type = running_status & 0xf0 + channel = running_status & 0x0f #assert message_type != 0xf0, hex(message_type) if message_type not in midi_messages: return None - channel = buf[0] & 0x0f message_length, cls = midi_messages[message_type] - buf = buf[1:] + data = buf[:message_length] # handle channel mode specially if cls is ControlChange and data[0] >= 121 and data[0] <= 127: # 0xb0 is overloaded for both control change and channel mode cls = ChannelMode - message = cls(channel, *data) + message = MIDIEvent(cls(channel, *data)) buf = buf[message_length:] return buf, message def parse_event(buf): - while True: - b = buf[0] - if (sysex := parse_sysex_event(buf)) is not None: - buf, sysex = sysex - return buf, sysex - elif (meta := parse_meta_event(buf)) is not None: - buf, meta = meta - return buf, meta - elif (midi := parse_midi_event(buf)) is not None: - buf, midi = midi - return buf, midi - else: - print(hex(buf[0]), file=sys.stderr) - buf = buf[1:] - while (buf[0] & 0x80) == 0: - print(hex(buf[0]), file=sys.stderr) - buf = buf[1:] + if (midi := parse_midi_event(buf)) is not None: + buf, midi = midi + return buf, midi + elif (sysex := parse_sysex_event(buf)) is not None: + running_status = None + buf, sysex = sysex + return buf, sysex + elif (meta := parse_meta_event(buf)) is not None: + running_status = None + buf, meta = meta + return buf, meta + else: + assert False, ' '.join([hex(i)[2:] for i in buf[0:40]]) + return None @dataclass class Event: @@ -374,48 +381,16 @@ class Track: events: list[Event] def parse_track(buf): + head_buf = buf buf, _ = parse_track_chunk_type(buf) buf, length = parse_uint32(buf) offset = len(buf) events = [] while (offset - len(buf)) < length: - buf, event = parse_mtrk_event(buf) + try: + buf, event = parse_mtrk_event(buf) + except: + print('len', len(head_buf) - len(buf), length) + raise events.append(event) return buf, Track(events) - -_slots = set() - -def simulate_note(ix, ev): - if type(ev.event) is NoteOn: - print(repr(ev.event)) - - _slots.add((ev.event.channel, ev.event.note)) - assert len(_slots) <= 32, (hex(ix)) - if type(ev.event) is NoteOff: - print(repr(ev.event)) - try: - _slots.remove((ev.event.channel, ev.event.note)) - except: - print("ix", hex(ix)) - raise - -def parse_file(buf): - buf, header = parse_header(buf) - #print(header) - assert header.ntrks > 0 - tracks = [] - for track_num in range(header.ntrks): - buf, track = parse_track(buf) - tracks.append(track) - #print(f"track {track_num}:") - for i, event in enumerate(track.events): - #simulate_note(i, event) - print(event) - - print("remaining data:", len(buf)) - -import sys -with open(sys.argv[1], 'rb') as f: - b = memoryview(f.read()) - -parse_file(b) diff --git a/midi/roundtrip.cpp b/midi/roundtrip.cpp new file mode 100644 index 0000000..70ae344 --- /dev/null +++ b/midi/roundtrip.cpp @@ -0,0 +1,223 @@ +#include +#include +#include +#include +#include + +#include "strings.hpp" +#include "parse.hpp" +#include "generate.hpp" + +#include "iterator.cpp" + +static char output_buf[4096]; + +inline uint32_t output_mtrk_event(const midi::mtrk_event_t& mtrk_event, uint8_t& running_status, + std::ofstream& ofs, const bool write) +{ + uint8_t * output_buf_start = reinterpret_cast(&output_buf[0]); + const uint8_t * output_buf_end = midi::generate::mtrk_event(output_buf_start, mtrk_event, running_status); + const uint32_t event_length = output_buf_end - output_buf_start; + + std::cerr << "event_length " << event_length << '\n'; + + if (write) ofs.write(output_buf, event_length); + + return event_length; +} + +inline void output_track(const midi::track_t& track, std::ofstream& ofs) +{ + uint8_t * output_buf_start = reinterpret_cast(&output_buf[0]); + const uint8_t * output_buf_end = midi::generate::track(output_buf_start, track); + const uint32_t track_length = output_buf_end - output_buf_start; + + ofs.write(output_buf, track_length); +} + +inline void output_header(const midi::header_t& header, std::ofstream& ofs) +{ + uint8_t * output_buf_start = reinterpret_cast(&output_buf[0]); + const uint8_t * output_buf_end = midi::generate::header(output_buf_start, header); + const uint32_t header_length = output_buf_end - output_buf_start; + + ofs.write(output_buf, header_length); +} + +constexpr inline bool +init_track_iterators(const midi::header_t& header, mtrk_iterator * its, uint8_t const * buf) +{ + for (int32_t i = 0; i < header.ntrks; i++) { + auto track_o = midi::parse::track(buf); + if (!track_o) { + std::cerr << "invalid track\n"; + return false; + } + + midi::track_t track; + std::tie(buf, track) = *track_o; + + std::cout << "track[" << i << "] track.length: " << track.length << '\n'; + + uint8_t const * const track_start = buf; + its[i] = mtrk_iterator(track, track_start); + + buf += track.length; + } + return true; +} + +inline std::optional +simulate_delta_time(const midi::header_t& header, uint8_t const * buf, + std::ofstream& ofs, const bool write) +{ + // + // simulate delta_time + // + using mtrk_storage = std::aligned_storage_t; + mtrk_storage its_storage[header.ntrks]; + auto &its = reinterpret_cast(its_storage); + + if (!init_track_iterators(header, its, buf)) + return std::nullopt; + uint32_t track_times[header.ntrks] = {0}; + + uint32_t global_time = 0; + uint32_t last_global_time = 0; + int32_t complete = 0; + uint32_t total_length = 0; + uint8_t output_running_status = 0; + do { + complete = 0; + for (uint32_t i = 0; i < header.ntrks; i++) { + mtrk_iterator& it = its[i]; + uint32_t& track_time = track_times[i]; + + while (!it.at_end()) { + const midi::mtrk_event_t& mtrk_event = *it; + if (track_time + mtrk_event.delta_time <= global_time) { + track_time += mtrk_event.delta_time; + ++it; + } else { + break; + } + + if (mtrk_event.event.type == midi::event_t::type_t::meta + && mtrk_event.event.event.meta.type == 0x2f) { + // suppress end of track + std::cout << "suppress EOT\n"; + continue; + } + + total_length += output_mtrk_event({ + global_time - last_global_time, + mtrk_event.event + }, output_running_status, ofs, write); + last_global_time = global_time; + } + if (it.at_end()) ++complete; + } + // increment global time to the next time step + ++global_time; + } while (complete != header.ntrks); + + + midi::mtrk_event_t mtrk_event_eot = { + 0, + { // event_t + .type = midi::event_t::type_t::meta, + .event = { + .meta = { nullptr, 0, 0x2f } // EndOfTrack + } + } + }; + // emit final EndOfTrack + total_length += output_mtrk_event(mtrk_event_eot, output_running_status, ofs, write); + last_global_time = global_time; + + return total_length; +} + +int roundtrip(uint8_t const * start, std::ofstream& ofs) +{ + + uint8_t const * buf = &start[0]; + auto header_o = midi::parse::header(buf); + if (!header_o) { + std::cerr << "invalid header\n"; + return -1; + } + midi::header_t header; + std::tie(buf, header) = *header_o; + + std::cout << "header.format: " << midi::strings::header_format(header.format) << '\n'; + std::cout << "header.ntrks: " << header.ntrks << '\n'; + + // + // write output header + // + output_header({ + .format = midi::header_t::format_t::_0, + .ntrks = 1, + .division = header.division, + }, ofs); + + // + // first round of simulation: calculate track length (in bytes) + // + auto track_length_o = simulate_delta_time(header, buf, ofs, false); + if (!track_length_o) { + return -1; + } + + output_track({ *track_length_o }, ofs); + + // + // second round of simulation: write the actual track + // + auto _o = simulate_delta_time(header, buf, ofs, true); + if (!_o) { + return -1; + } + + return 0; +} + +int main(int argc, char *argv[]) +{ + if (argc < 2) { + std::cerr << "argc < 3\n"; + return -1; + } + + std::cerr << argv[1] << '\n'; + + std::ifstream ifs; + ifs.open(argv[1], std::ios::in | std::ios::binary | std::ios::ate); + if (!ifs.is_open()) { + std::cerr << "ifstream: " << '\n'; + return -1; + } + + auto size = static_cast(ifs.tellg()); + uint8_t start[size]; + ifs.seekg(0); + if (!ifs.read(reinterpret_cast(&start[0]), size)) { + std::cerr << "read\n"; + return -1; + } + ifs.close(); + + std::ofstream ofs; + ofs.open(argv[2], std::ios::out | std::ios::binary | std::ios::trunc); + if (!ofs.is_open()) { + std::cerr << "ofstream\n"; + return -1; + } + + roundtrip(start, ofs); + + ofs.close(); + + return 0; +} diff --git a/midi/simulate.py b/midi/simulate.py new file mode 100644 index 0000000..567eef7 --- /dev/null +++ b/midi/simulate.py @@ -0,0 +1,74 @@ +from itertools import chain +from parser import * + +""" +_slots = set() + +def simulate_note(ix, ev): + if type(ev.event) is NoteOn: + print(repr(ev.event)) + + _slots.add((ev.event.channel, ev.event.note)) + assert len(_slots) <= 32, (hex(ix)) + if type(ev.event) is NoteOff: + print(repr(ev.event)) + try: + _slots.remove((ev.event.channel, ev.event.note)) + except: + print("ix", hex(ix)) + raise +""" + +def linearize_track(track): + time = 0 + for i, event in enumerate(track.events): + time += event.delta_time + yield time, i, event + +l = { + NoteOff: 0, + NoteOn: 1, + PolyphonicKeyPressure: 2, + ControlChange: 3, + ProgramChange: 4, + ChannelPressure: 5, + PitchBendChange: 6, + ChannelMode: 7, +} + +def sort_key(global_time, i, event): + channel = event.event.event.channel if type(event.event) is MIDIEvent else 99 + priority = -l[type(event.event.event)] if type(event.event) is MIDIEvent else -99 + return global_time, priority, channel + +def linearize_events(tracks): + global_time = 0 + linearized = list(chain.from_iterable(map(linearize_track, tracks))) + linear_sort = sorted(linearized, key=lambda args: sort_key(*args)) + for abs_time, i, event in linear_sort: + if type(event.event) is MetaEvent and event.event.type is MetaType.EndOfTrack: + continue + inner_event = event.event + delta_time = abs_time - global_time + print(Event(delta_time, inner_event)) + global_time = abs_time + print(Event(delta_time=0, event=MetaEvent(type=MetaType.EndOfTrack, value=None))) + +def dump_file(buf): + buf, header = parse_header(buf) + print(header) + assert header.ntrks > 0 + tracks = [] + for track_num in range(header.ntrks): + buf, track = parse_track(buf) + tracks.append(track) + print("remaining data:", len(buf)) + assert len(buf) == 0, bytes(buf) + + linearize_events(tracks) + +import sys +with open(sys.argv[1], 'rb') as f: + b = memoryview(f.read()) + +dump_file(b) diff --git a/midi/strings.hpp b/midi/strings.hpp index 0b2a070..e1d30ac 100644 --- a/midi/strings.hpp +++ b/midi/strings.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include "midi.hpp" @@ -14,8 +15,8 @@ header_format(header_t::format_t format) case header_t::format_t::_0: return "0"; case header_t::format_t::_1: return "1"; case header_t::format_t::_2: return "2"; + default: assert(false); } - while (1); } } diff --git a/midi/test_generate.cpp b/midi/test_generate.cpp new file mode 100644 index 0000000..bd8e2f8 --- /dev/null +++ b/midi/test_generate.cpp @@ -0,0 +1,86 @@ +#include +#include +#include + +#include "midi.hpp" +#include "generate.cpp" + +namespace midi { +namespace test { + +struct buf_n_t { + uint32_t n; + uint8_t buf[4]; + int32_t len; +}; + +void int_variable_length() +{ + static buf_n_t tests[] = { + {0x0000'0000, {0x00}, 1}, + {0x0000'0040, {0x40}, 1}, + {0x0000'007f, {0x7f}, 1}, + + {0x0000'0080, {0x81, 0x00}, 2}, + {0x0000'2000, {0xc0, 0x00}, 2}, + {0x0000'3fff, {0xff, 0x7f}, 2}, + + {0x0000'4000, {0x81, 0x80, 0x00}, 3}, + {0x0010'0000, {0xc0, 0x80, 0x00}, 3}, + {0x001f'ffff, {0xff, 0xff, 0x7f}, 3}, + + {0x0020'0000, {0x81, 0x80, 0x80, 0x00}, 4}, + {0x0800'0000, {0xc0, 0x80, 0x80, 0x00}, 4}, + {0x0fff'ffff, {0xff, 0xff, 0xff, 0x7f}, 4}, + }; + + for (int i = 0; i < 12; i++) { + uint8_t buf1[4]; + buf_n_t& test = tests[i]; + uint8_t * buf2 = generate::int_variable_length(&buf1[0], test.n); + assert(buf2 - buf1 == test.len); + for (int j = 0; j < test.len; j++) + assert(buf1[j] == test.buf[j]); + } +} + +void header() +{ + header_t header = { + .format = header_t::format_t::_2, + .ntrks = 1, + .division = { + .type = division_t::type_t::time_code, + .time_code = { + .smpte = -30, + .ticks_per_frame = 0x50 + } + } + }; + + uint8_t buf[64] = {0xee}; + uint8_t * res = generate::header(buf, header); + uint8_t expect[] = { + 0x4d, 0x54, 0x68, 0x64, // chunk type + 0x00, 0x00, 0x00, 0x06, // length + 0x00, 0x02, // format + 0x00, 0x01, // ntrks + 0xe2, 0x50, // division + }; + assert(res - buf == 14); + for (int i = 0; i < 14; i++) + assert(expect[i] == buf[i]); +} + +} +} + +int main() +{ + using namespace midi::test; + + int_variable_length(); + header(); + + return 0; +}