diff --git a/patterns/midi.hexpat b/patterns/midi.hexpat index f96a565..d654763 100644 --- a/patterns/midi.hexpat +++ b/patterns/midi.hexpat @@ -1,72 +1,278 @@ +#include + #pragma MIME audio/midi +#pragma endian big -using Delta = u8; -using NoteValue = u8; -using Velocity = u8; -using EOF = u8; - -// this is just for debugging midi file generation -// I'm testing a known good file against a bad one -// the file is hard coded in file format 0, and if -// you're expecting meta events anywhere add a specific -// call for those +// Only supports format 0 // https://www.music.mcgill.ca/~ich/classes/mumt306/StandardMIDIfileformat.html -enum NoteEvent : u8 { - NoteOn = 0x90, - NoteOff = 0x80 +bool g_has_more_messages = true; +bool g_has_more_tracks = true; + +fn signed14(s16 n) { + // Converts to 14-bit in range 0..16383. Note that MIDI data bytes + // have their first (most significant) bit clear (0), thus each byte + // is in range 0..127 + n = ((n & 0x7f) << 7) | ((n >> 8) & 0x7f); + if (n >= 0) { + n -= 0x2000; + } + return n; }; -enum MetaFlag : u16 { - Footer = 0xFF2F, - KeySigEvent = 0xFF59, - TimeSigEvent = 0xFF58, - TempoEvent = 0xFF51, - TrackNameEvent = 0xFF03 +fn u32ify(u32 vlq) { + // Converts from variable-length quantity to u32. These numbers are + // represented 7 bits per byte, most significant bits first. All bytes + // except the last have bit 7 set, and the last byte has bit 7 clear. + // If the number is in range 0..127, it is thus represented exactly + // as one byte. + u32 n = vlq & 0x7f; + if (vlq & 0x8000 == 0x8000) { + n += ((vlq & 0x7f00) >> 8) * 0x80; + } + if (vlq & 0x800000 == 0x800000) { + n += ((vlq & 0x7f0000) >> 8 * 2) * 0x80 * 0x80; + } + if (vlq & 0x80000000 == 0x80000000) { + n += ((vlq & 0x7f000000) >> 8 * 3) * 0x80 * 0x80 * 0x80; + } + return n; +}; + +enum Status : u8 { + NoteOff = 0x80 ... 0x8F, + NoteOn = 0x90 ... 0x9F, + PolyphonicAfterTouch = 0xA0 ... 0xAF, + ControlChange = 0xB0 ... 0xBF, + ProgramChange = 0xC0 ... 0xCF, + ChannelAfterTouch = 0xD0 ... 0xDF, + PitchWheel = 0xE0 ... 0xEF, + SysEx = 0xF0, + TimeCodeQtrFrame = 0xF1, + SongPositionPointer = 0xF2, + SongSelect = 0xF3, + Undefined = 0xF4 ... 0xF5, + TuneRequest = 0xF6, + EndOfSysEx = 0xF7, + TimingClock = 0xF8, + Undefined = 0xF9, + Start = 0xFA, + Continue = 0xFB, + Stop = 0xFC, + Undefined = 0xFD, + ActiveSensing = 0xFE, + MetaEvent = 0xFF, +}; + +Status g_next_status; + +enum MetaType : u8 { + SequenceNumber = 0x00, + Text = 0x01, + CopyrightNotice = 0x02, + TrackName = 0x03, + InstrumentName = 0x04, + Lyric = 0x05, + Marker = 0x06, + CuePoint = 0x07, + ChannelPrefix = 0x20, + EndOfTrack = 0x2F, + SetTempo = 0x51, + SMPTEOffset = 0x54, + TimeSignature = 0x58, + KeySignature = 0x59, + SequencerSpecific = 0x7F +}; + +enum ControlChangeType : u8 { + Bank_Select_MSB = 0, + Modulation_Wheel_MSB = 1, + Breath_Controller_MSB = 2, + Undefined_MSB = 3, + Foot_Pedal_MSB = 4, + Portamento_Time_MSB = 5, + Data_Entry_MSB = 6, + Volume_MSB = 7, + Balance_MSB = 8, + Undefined_MSB = 9, + Pan_MSB = 10, + Expression_MSB = 11, + Effect_Controller_1_MSB = 12, + Effect_Controller_2_MSB = 13, + Undefined_MSB = 14, + Undefined_MSB = 15, + General_Purpose_MSB = 16 ... 19, + Undefined_MSB = 20 ... 31, + Bank_Select_LSB = 32, + Modulation_Wheel_LSB = 33, + Breath_Controller_LSB = 34, + Undefined_LSB = 35, + Foot_Pedal_LSB = 36, + Portamento_Time_LSB = 37, + Data_Entry_LSB = 38, + Volume_LSB = 39, + Balance_LSB = 40, + Undefined_LSB = 41, + Pan_LSB = 42, + Expression_LSB = 43, + Effect_Controller_1_LSB = 44, + Effect_Controller_2_LSB = 45, + Undefined_LSB = 46, + Undefined_LSB = 47, + General_Purpose_LSB = 48 ... 51, + Undefined_LSB = 52 ... 63, + Damper_Pedal = 64, + Portamento = 65, + Sostenuto_Pedal = 66, + Soft_Pedal = 67, + Legato_FootSwitch = 68, + Hold_2 = 69, + Sound_Controller_1 = 70, + Sound_Controller_2 = 71, + Sound_Controller_3 = 72, + Sound_Controller_4 = 73, + Sound_Controller_6 = 75, + Sound_Controller_7 = 76, + Sound_Controller_8 = 77, + Sound_Controller_9 = 78, + Sound_Controller_10 = 79, + General_Purpose_CC_Control = 80 ... 83, + Portamento_CC_Control = 84, + Undefined = 85 ... 87, + High_Resolution_Velocity_Prefix = 88, + Effect_2_Depth = 92, + Effect_3_Depth = 93, + Effect_4_Depth = 94, + Effect_5_Depth = 95, + Data_Increment = 96, + Data_Decrement = 97, + Non_Registered_Parameter_Number_LSB = 98, + Non_Registered_Parameter_Number_MSB = 99, + Registered_Parameter_Number_LSB = 100, + Registered_Parameter_Number_MSB = 101, + Undefined = 102 ... 119, + All_Sound_Off = 120, + Reset_All_Controllers = 121, + Local_Switch = 122, + All_Notes_Off = 123, + Omni_Mode_Off = 124, + Omni_Mode_On = 125, + Mono_Mode = 126, + Poly_Mode = 127, }; enum HeaderFlag : u32 { MThd = 0x4D546864 }; -enum TrackChunk : u32 { +enum TrackFlag : u32 { MTrk = 0x4D54726B }; -struct TimeSigEvent { - Delta delta; - MetaFlag flag; - u16 numerator; - u8 denominator; - u8 ticks_per_click; // not used - u8 thirty_second_notes_per_crotchet; +struct VariableLengthQuantity { + if (std::mem::read_unsigned($, 1) & 0x80 == 0x80) { + if (std::mem::read_unsigned($ + 1, 1) & 0x80 == 0x80) { + if (std::mem::read_unsigned($ + 2, 1) & 0x80 == 0x80) { + u32 value [[format("u32ify"), name(name)]]; + } else { + u24 value [[format("u32ify"), name(name)]]; + } + } else { + u16 value [[format("u32ify"), name(name)]]; + } + } else { + u8 value [[format("u32ify"), name(name)]]; + } }; -struct KeySigEvent { - Delta delta; - MetaFlag flag; - u16 key; - u8 mode; +struct MessageData { + match (u8(g_next_status) & 0xf0) { + (Status::NoteOff | Status::NoteOn): { + u8 note_number; + u8 velocity; + } + (Status::PolyphonicAfterTouch): { + u8 note_number; + u8 amount; + } + (Status::ControlChange): { + ControlChangeType cc_number; + u8 value; + } + (Status::ProgramChange): { + u8 program_number; + } + (Status::ChannelAfterTouch): { + u8 amount; + } + (Status::PitchWheel): { + s16 value [[format("signed14")]]; + } + } + match (g_next_status) { + (Status::SysEx | Status::EndOfSysEx): { + // "EndOfSysEx" can appear as the last data byte in a + // system exclusive message, or as the starting byte in + // multi-packet messages + VariableLengthQuantity<"sysex_length"> sysex_length [[inline]]; + u8 sysex_data[sysex_length.value]; + } + (Status::SongPositionPointer): { + u8 lsb; + u8 msb; + } + (Status::SongSelect): { + u8 song_number; + } + (Status::MetaEvent): { + MetaType meta_type; + VariableLengthQuantity<"meta_length"> meta_length [[inline]]; + match (meta_type) { + (MetaType::EndOfTrack): { + g_has_more_messages = false; + g_has_more_tracks = std::mem::read_unsigned($, 4) == le u32(TrackFlag::MTrk); + } + (MetaType::Text + | MetaType::CopyrightNotice + | MetaType::TrackName + | MetaType::InstrumentName + | MetaType::Lyric + | MetaType::Marker + | MetaType::CuePoint): { + char text[meta_length.value]; + } + (MetaType::TimeSignature): { + u8 numerator; + u8 denominator; + u8 ticks_per_click; + u8 thirty_second_notes_per_crotchet; + } + (MetaType::KeySignature): { + u8 key; + u8 mode; + } + (MetaType::SetTempo): { + u24 micro_seconds_per_click; // default 1 million + } + (_): { + u8 meta_data[meta_length.value]; + } + } + } + } }; -struct TempoEvent { - Delta delta; - MetaFlag flag; - u32 micro_seconds_per_click; // default 1 million -}; +struct Message { + VariableLengthQuantity<"delta"> delta [[inline]]; -struct TrackNameEvent { - Delta delta; - MetaFlag flag; - u8 length; - u8 text; -}; + // Status bytes of MIDI channel messages may be omitted if the + // preceding event is a MIDI channel message with the same status + if (std::mem::read_unsigned($, 1) > 0x7f) { + Status status; + g_next_status = status; + } -struct Note { - Delta delta; - NoteEvent ne; - NoteValue note; - Velocity vel; + MessageData data [[inline]]; }; struct HeaderChunk { @@ -75,21 +281,18 @@ struct HeaderChunk { u16 mode; u16 num_tracks; u16 ticks_per_quarter; - TrackChunk chunk; - u32 track_length; }; -struct Footer { - Delta d; - MetaFlag m; - EOF eof; +struct TrackChunk { + TrackFlag flag; + u32 length; + g_has_more_messages = true; + Message messages[while(g_has_more_messages)]; }; struct MidiFile { HeaderChunk header; - // whatever meta flags can be in here - Note notes[12]; //however many notes you're looking at - Footer f; + TrackChunk tracks[while(g_has_more_tracks)]; }; MidiFile midi_file @ 0x00; diff --git a/tests/patterns/test_data/midi.hexpat.mid b/tests/patterns/test_data/midi.hexpat.mid new file mode 100644 index 0000000..ac41827 Binary files /dev/null and b/tests/patterns/test_data/midi.hexpat.mid differ