saturn-examples/midi/generate.cpp
Zack Buhman 86d12c37ed 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.
2023-07-01 00:15:53 +00:00

209 lines
5.1 KiB
C++

#include <cstdint>
#include <iostream>
#include <cassert>
#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;
}
}
}