diff options
Diffstat (limited to 'tools/mid2agb/midi.cpp')
-rw-r--r-- | tools/mid2agb/midi.cpp | 964 |
1 files changed, 964 insertions, 0 deletions
diff --git a/tools/mid2agb/midi.cpp b/tools/mid2agb/midi.cpp new file mode 100644 index 0000000..fa7d9ce --- /dev/null +++ b/tools/mid2agb/midi.cpp @@ -0,0 +1,964 @@ +// Copyright(c) 2016 YamaArashi +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#include <cstdio> +#include <cassert> +#include <string> +#include <vector> +#include <algorithm> +#include <memory> +#include "midi.h" +#include "main.h" +#include "error.h" +#include "agb.h" +#include "tables.h" + +enum class MidiEventCategory +{ + Control, + SysEx, + Meta, + Invalid, +}; + +MidiFormat g_midiFormat; +std::int_fast32_t g_midiTrackCount; +std::int16_t g_midiTimeDiv; + +int g_midiChan; +std::int32_t g_initialWait; + +static long s_trackDataStart; +static std::vector<Event> s_seqEvents; +static std::vector<Event> s_trackEvents; +static std::int32_t s_absoluteTime; +static int s_blockCount = 0; +static int s_minNote; +static int s_maxNote; +static int s_runningStatus; + +void Seek(long offset) +{ + if (std::fseek(g_inputFile, offset, SEEK_SET) != 0) + RaiseError("failed to seek to %l", offset); +} + +void Skip(long offset) +{ + if (std::fseek(g_inputFile, offset, SEEK_CUR) != 0) + RaiseError("failed to skip %l bytes", offset); +} + +std::string ReadSignature() +{ + char signature[4]; + + if (std::fread(signature, 4, 1, g_inputFile) != 1) + RaiseError("failed to read signature"); + + return std::string(signature, 4); +} + +std::uint32_t ReadInt8() +{ + int c = std::fgetc(g_inputFile); + + if (c < 0) + RaiseError("unexpected EOF"); + + return c; +} + +std::uint32_t ReadInt16() +{ + std::uint32_t val = 0; + val |= ReadInt8() << 8; + val |= ReadInt8(); + return val; +} + +std::uint32_t ReadInt24() +{ + std::uint32_t val = 0; + val |= ReadInt8() << 16; + val |= ReadInt8() << 8; + val |= ReadInt8(); + return val; +} + +std::uint32_t ReadInt32() +{ + std::uint32_t val = 0; + val |= ReadInt8() << 24; + val |= ReadInt8() << 16; + val |= ReadInt8() << 8; + val |= ReadInt8(); + return val; +} + +std::uint32_t ReadVLQ() +{ + std::uint32_t val = 0; + std::uint32_t c; + + do + { + c = ReadInt8(); + val <<= 7; + val |= (c & 0x7F); + } while (c & 0x80); + + return val; +} + +void ReadMidiFileHeader() +{ + Seek(0); + + if (ReadSignature() != "MThd") + RaiseError("MIDI file header signature didn't match \"MThd\""); + + std::uint32_t headerLength = ReadInt32(); + + if (headerLength != 6) + RaiseError("MIDI file header length isn't 6"); + + std::uint16_t midiFormat = ReadInt16(); + + if (midiFormat >= 2) + RaiseError("unsupported MIDI format (%u)", midiFormat); + + g_midiFormat = (MidiFormat)midiFormat; + g_midiTrackCount = ReadInt16(); + g_midiTimeDiv = ReadInt16(); + + if (g_midiTimeDiv < 0) + RaiseError("unsupported MIDI time division (%d)", g_midiTimeDiv); +} + +long ReadMidiTrackHeader(long offset) +{ + Seek(offset); + + if (ReadSignature() != "MTrk") + RaiseError("MIDI track header signature didn't match \"MTrk\""); + + long size = ReadInt32(); + + s_trackDataStart = std::ftell(g_inputFile); + + return size + 8; +} + +void StartTrack() +{ + Seek(s_trackDataStart); + s_absoluteTime = 0; + s_runningStatus = 0; +} + +void SkipEventData() +{ + Skip(ReadVLQ()); +} + +void DetermineEventCategory(MidiEventCategory& category, int& typeChan, int& size) +{ + typeChan = ReadInt8(); + + if (typeChan < 0x80) + { + // If data byte was found, use the running status. + ungetc(typeChan, g_inputFile); + typeChan = s_runningStatus; + } + + if (typeChan == 0xFF) + { + category = MidiEventCategory::Meta; + size = 0; + s_runningStatus = 0; + } + else if (typeChan >= 0xF0) + { + category = MidiEventCategory::SysEx; + size = 0; + s_runningStatus = 0; + } + else if (typeChan >= 0x80) + { + category = MidiEventCategory::Control; + + switch (typeChan >> 4) + { + case 0xC: + case 0xD: + size = 1; + break; + default: + size = 2; + break; + } + s_runningStatus = typeChan; + } + else + { + category = MidiEventCategory::Invalid; + } +} + +void MakeBlockEvent(Event& event, EventType type) +{ + event.type = type; + event.param1 = s_blockCount++; + event.param2 = 0; +} + +std::string ReadEventText() +{ + char buffer[2]; + std::uint32_t length = ReadVLQ(); + + if (length <= 2) + { + if (fread(buffer, length, 1, g_inputFile) != 1) + RaiseError("failed to read event text"); + } + else + { + Skip(length); + length = 0; + } + + return std::string(buffer, length); +} + +bool ReadSeqEvent(Event& event) +{ + s_absoluteTime += ReadVLQ(); + event.time = s_absoluteTime; + + MidiEventCategory category; + int typeChan; + int size; + + DetermineEventCategory(category, typeChan, size); + + if (category == MidiEventCategory::Control) + { + Skip(size); + return false; + } + + if (category == MidiEventCategory::SysEx) + { + SkipEventData(); + return false; + } + + if (category == MidiEventCategory::Invalid) + RaiseError("invalid event"); + + // meta event + int metaEventType = ReadInt8(); + + if (metaEventType >= 1 && metaEventType <= 7) + { + // text event + std::string text = ReadEventText(); + + if (text == "[") + MakeBlockEvent(event, EventType::LoopBegin); + else if (text == "][") + MakeBlockEvent(event, EventType::LoopEndBegin); + else if (text == "]") + MakeBlockEvent(event, EventType::LoopEnd); + else if (text == ":") + MakeBlockEvent(event, EventType::Label); + else + return false; + } + else + { + switch (metaEventType) + { + case 0x2F: // end of track + SkipEventData(); + event.type = EventType::EndOfTrack; + event.param1 = 0; + event.param2 = 0; + break; + case 0x51: // tempo + if (ReadVLQ() != 3) + RaiseError("invalid tempo size"); + + event.type = EventType::Tempo; + event.param1 = 0; + event.param2 = ReadInt24(); + break; + case 0x58: // time signature + { + if (ReadVLQ() != 4) + RaiseError("invalid time signature size"); + + int numerator = ReadInt8(); + int denominatorExponent = ReadInt8(); + + if (denominatorExponent >= 16) + RaiseError("invalid time signature denominator"); + + Skip(2); // ignore other values + + int clockTicks = 96 * numerator * g_clocksPerBeat; + int denominator = 1 << denominatorExponent; + int timeSig = clockTicks / denominator; + + if (timeSig <= 0 || timeSig >= 0x10000) + RaiseError("invalid time signature"); + + event.type = EventType::TimeSignature; + event.param1 = 0; + event.param2 = timeSig; + break; + } + default: + SkipEventData(); + return false; + } + } + + return true; +} + +void ReadSeqEvents() +{ + StartTrack(); + + for (;;) + { + Event event = {}; + + if (ReadSeqEvent(event)) + { + s_seqEvents.push_back(event); + + if (event.type == EventType::EndOfTrack) + return; + } + } +} + +bool CheckNoteEnd(Event& event) +{ + event.param2 += ReadVLQ(); + + MidiEventCategory category; + int typeChan; + int size; + + DetermineEventCategory(category, typeChan, size); + + if (category == MidiEventCategory::Control) + { + int chan = typeChan & 0xF; + + if (chan != g_midiChan) + { + Skip(size); + return false; + } + + switch (typeChan & 0xF0) + { + case 0x80: // note off + { + int note = ReadInt8(); + ReadInt8(); // ignore velocity + if (note == event.note) + return true; + break; + } + case 0x90: // note on + { + int note = ReadInt8(); + int velocity = ReadInt8(); + if (velocity == 0 && note == event.note) + return true; + break; + } + default: + Skip(size); + break; + } + + return false; + } + + if (category == MidiEventCategory::SysEx) + { + SkipEventData(); + return false; + } + + if (category == MidiEventCategory::Meta) + { + int metaEventType = ReadInt8(); + SkipEventData(); + + if (metaEventType == 0x2F) + RaiseError("note doesn't end"); + + return false; + } + + RaiseError("invalid event"); +} + +void FindNoteEnd(Event& event) +{ + // Save the current file position and running status + // which get modified by CheckNoteEnd. + long startPos = ftell(g_inputFile); + int savedRunningStatus = s_runningStatus; + + event.param2 = 0; + + while (!CheckNoteEnd(event)) + ; + + Seek(startPos); + s_runningStatus = savedRunningStatus; +} + +bool ReadTrackEvent(Event& event) +{ + s_absoluteTime += ReadVLQ(); + event.time = s_absoluteTime; + + MidiEventCategory category; + int typeChan; + int size; + + DetermineEventCategory(category, typeChan, size); + + if (category == MidiEventCategory::Control) + { + int chan = typeChan & 0xF; + + if (chan != g_midiChan) + { + Skip(size); + return false; + } + + switch (typeChan & 0xF0) + { + case 0x90: // note on + { + int note = ReadInt8(); + int velocity = ReadInt8(); + + if (velocity != 0) + { + event.type = EventType::Note; + event.note = note; + event.param1 = velocity; + FindNoteEnd(event); + if (event.param2 > 0) + { + if (note < s_minNote) + s_minNote = note; + if (note > s_maxNote) + s_maxNote = note; + } + } + break; + } + case 0xB0: // controller event + event.type = EventType::Controller; + event.param1 = ReadInt8(); // controller index + event.param2 = ReadInt8(); // value + break; + case 0xC0: // instrument change + event.type = EventType::InstrumentChange; + event.param1 = ReadInt8(); // instrument + event.param2 = 0; + break; + case 0xE0: // pitch bend + event.type = EventType::PitchBend; + event.param1 = ReadInt8(); + event.param2 = ReadInt8(); + break; + default: + Skip(size); + return false; + } + + return true; + } + + if (category == MidiEventCategory::SysEx) + { + SkipEventData(); + return false; + } + + if (category == MidiEventCategory::Meta) + { + int metaEventType = ReadInt8(); + SkipEventData(); + + if (metaEventType == 0x2F) + { + event.type = EventType::EndOfTrack; + event.param1 = 0; + event.param2 = 0; + return true; + } + + return false; + } + + RaiseError("invalid event"); +} + +void ReadTrackEvents() +{ + StartTrack(); + + s_trackEvents.clear(); + + s_minNote = 0xFF; + s_maxNote = 0; + + for (;;) + { + Event event = {}; + + if (ReadTrackEvent(event)) + { + s_trackEvents.push_back(event); + + if (event.type == EventType::EndOfTrack) + return; + } + } +} + +bool EventCompare(const Event& event1, const Event& event2) +{ + if (event1.time < event2.time) + return true; + + if (event1.time > event2.time) + return false; + + unsigned event1Type = (unsigned)event1.type; + unsigned event2Type = (unsigned)event2.type; + + if (event1.type == EventType::Note) + event1Type += event1.note; + + if (event2.type == EventType::Note) + event2Type += event2.note; + + if (event1Type < event2Type) + return true; + + if (event1Type > event2Type) + return false; + + if (event1.type == EventType::EndOfTie) + { + if (event1.note < event2.note) + return true; + + if (event1.note > event2.note) + return false; + } + + return false; +} + +std::unique_ptr<std::vector<Event>> MergeEvents() +{ + std::unique_ptr<std::vector<Event>> events(new std::vector<Event>()); + + unsigned trackEventPos = 0; + unsigned seqEventPos = 0; + + while (s_trackEvents[trackEventPos].type != EventType::EndOfTrack + && s_seqEvents[seqEventPos].type != EventType::EndOfTrack) + { + if (EventCompare(s_trackEvents[trackEventPos], s_seqEvents[seqEventPos])) + events->push_back(s_trackEvents[trackEventPos++]); + else + events->push_back(s_seqEvents[seqEventPos++]); + } + + while (s_trackEvents[trackEventPos].type != EventType::EndOfTrack) + events->push_back(s_trackEvents[trackEventPos++]); + + while (s_seqEvents[seqEventPos].type != EventType::EndOfTrack) + events->push_back(s_seqEvents[seqEventPos++]); + + // Push the EndOfTrack event with the larger time. + if (EventCompare(s_trackEvents[trackEventPos], s_seqEvents[seqEventPos])) + events->push_back(s_seqEvents[seqEventPos]); + else + events->push_back(s_trackEvents[trackEventPos]); + + return events; +} + +void ConvertTimes(std::vector<Event>& events) +{ + for (Event& event : events) + { + event.time = (24 * g_clocksPerBeat * event.time) / g_midiTimeDiv; + + if (event.type == EventType::Note) + { + event.param1 = g_noteVelocityLUT[event.param1]; + + std::uint32_t duration = (24 * g_clocksPerBeat * event.param2) / g_midiTimeDiv; + + if (duration == 0) + duration = 1; + + if (!g_exactGateTime && duration < 96) + duration = g_noteDurationLUT[duration]; + + event.param2 = duration; + } + } +} + +std::unique_ptr<std::vector<Event>> InsertTimingEvents(std::vector<Event>& inEvents) +{ + std::unique_ptr<std::vector<Event>> outEvents(new std::vector<Event>()); + + Event timingEvent = {}; + timingEvent.time = 0; + timingEvent.type = EventType::TimeSignature; + timingEvent.param2 = 96 * g_clocksPerBeat; + + for (const Event& event : inEvents) + { + while (EventCompare(timingEvent, event)) + { + outEvents->push_back(timingEvent); + timingEvent.time += timingEvent.param2; + } + + if (event.type == EventType::TimeSignature) + { + if (g_agbTrack == 1 && event.param2 != timingEvent.param2) + { + Event originalTimingEvent = event; + originalTimingEvent.type = EventType::OriginalTimeSignature; + outEvents->push_back(originalTimingEvent); + } + timingEvent.param2 = event.param2; + timingEvent.time = event.time + timingEvent.param2; + } + + outEvents->push_back(event); + } + + return outEvents; +} + +std::unique_ptr<std::vector<Event>> SplitTime(std::vector<Event>& inEvents) +{ + std::unique_ptr<std::vector<Event>> outEvents(new std::vector<Event>()); + + std::int32_t time = 0; + + for (const Event& event : inEvents) + { + std::int32_t diff = event.time - time; + + if (diff > 96) + { + int wholeNoteCount = (diff - 1) / 96; + diff -= 96 * wholeNoteCount; + + for (int i = 0; i < wholeNoteCount; i++) + { + time += 96; + Event timeSplitEvent = {}; + timeSplitEvent.time = time; + timeSplitEvent.type = EventType::TimeSplit; + outEvents->push_back(timeSplitEvent); + } + } + + std::int32_t lutValue = g_noteDurationLUT[diff]; + + if (lutValue != diff) + { + Event timeSplitEvent = {}; + timeSplitEvent.time = time + lutValue; + timeSplitEvent.type = EventType::TimeSplit; + outEvents->push_back(timeSplitEvent); + } + + time = event.time; + + outEvents->push_back(event); + } + + return outEvents; +} + +std::unique_ptr<std::vector<Event>> CreateTies(std::vector<Event>& inEvents) +{ + std::unique_ptr<std::vector<Event>> outEvents(new std::vector<Event>()); + + for (const Event& event : inEvents) + { + if (event.type == EventType::Note && event.param2 > 96) + { + Event tieEvent = event; + tieEvent.param2 = -1; + outEvents->push_back(tieEvent); + + Event eotEvent = {}; + eotEvent.time = event.time + event.param2; + eotEvent.type = EventType::EndOfTie; + eotEvent.note = event.note; + outEvents->push_back(eotEvent); + } + else + { + outEvents->push_back(event); + } + } + + return outEvents; +} + +void CalculateWaits(std::vector<Event>& events) +{ + g_initialWait = events[0].time; + int wholeNoteCount = 0; + + for (unsigned i = 0; i < events.size() && events[i].type != EventType::EndOfTrack; i++) + { + events[i].time = events[i + 1].time - events[i].time; + + if (events[i].type == EventType::TimeSignature) + { + events[i].type = EventType::WholeNoteMark; + events[i].param2 = wholeNoteCount++; + } + } +} + +int CalculateCompressionScore(std::vector<Event>& events, int index) +{ + int score = 0; + std::uint8_t lastParam1 = events[index].param1; + std::uint8_t lastVelocity = 0x80u; + EventType lastType = events[index].type; + std::int32_t lastDuration = 0x80000000; + std::uint8_t lastNote = 0x40u; + + if (events[index].time > 0) + score++; + + for (int i = index + 1; !IsPatternBoundary(events[i].type); i++) + { + if (events[i].type == EventType::Note) + { + int val = 0; + + if (events[i].note != lastNote) + { + val++; + lastNote = events[i].note; + } + + if (events[i].param1 != lastVelocity) + { + val++; + lastVelocity = events[i].param1; + } + + std::int32_t duration = events[i].param2; + + if (g_noteDurationLUT[duration] != lastDuration) + { + val++; + lastDuration = g_noteDurationLUT[duration]; + } + + if (duration != lastDuration) + val++; + + if (val == 0) + val = 1; + + score += val; + } + else + { + lastDuration = 0x80000000; + + if (events[i].type == lastType) + { + if ((lastType != EventType::Controller && (int)lastType != 0x25 && lastType != EventType::EndOfTie) || events[i].param1 == lastParam1) + { + score++; + } + else + { + score += 2; + } + } + else + { + score += 2; + } + } + + lastParam1 = events[i].param1; + lastType = events[i].type; + + if (events[i].time) + score++; + } + + return score; +} + +bool IsCompressionMatch(std::vector<Event>& events, int index1, int index2) +{ + if (events[index1].type != events[index2].type || + events[index1].note != events[index2].note || + events[index1].param1 != events[index2].param1 || + events[index1].time != events[index2].time) + return false; + + index1++; + index2++; + + do + { + if (events[index1] != events[index2]) + return false; + + index1++; + index2++; + } while (!IsPatternBoundary(events[index1].type)); + + return IsPatternBoundary(events[index2].type); +} + +void CompressWholeNote(std::vector<Event>& events, int index) +{ + for (int j = index + 1; events[j].type != EventType::EndOfTrack; j++) + { + while (events[j].type != EventType::WholeNoteMark) + { + j++; + + if (events[j].type == EventType::EndOfTrack) + return; + } + + if (IsCompressionMatch(events, index, j)) + { + events[j].type = EventType::Pattern; + events[j].param2 = events[index].param2 & 0x7FFFFFFF; + events[index].param2 |= 0x80000000; + } + } +} + +void Compress(std::vector<Event>& events) +{ + for (int i = 0; events[i].type != EventType::EndOfTrack; i++) + { + while (events[i].type != EventType::WholeNoteMark) + { + i++; + + if (events[i].type == EventType::EndOfTrack) + return; + } + + if (CalculateCompressionScore(events, i) >= 6) + { + CompressWholeNote(events, i); + } + } +} + +void ReadMidiTracks() +{ + long trackHeaderStart = 14; + + ReadMidiTrackHeader(trackHeaderStart); + ReadSeqEvents(); + + g_agbTrack = 1; + + for (int midiTrack = 0; midiTrack < g_midiTrackCount; midiTrack++) + { + trackHeaderStart += ReadMidiTrackHeader(trackHeaderStart); + + for (g_midiChan = 0; g_midiChan < 16; g_midiChan++) + { + ReadTrackEvents(); + + if (s_minNote != 0xFF) + { +#ifdef DEBUG + printf("Track%d = Midi-Ch.%d\n", g_agbTrack, g_midiChan + 1); +#endif + + std::unique_ptr<std::vector<Event>> events(MergeEvents()); + + // We don't need TEMPO in anything but track 1. + if (g_agbTrack == 1) + { + auto it = std::remove_if(s_seqEvents.begin(), s_seqEvents.end(), [](const Event& event) { return event.type == EventType::Tempo; }); + s_seqEvents.erase(it, s_seqEvents.end()); + } + + ConvertTimes(*events); + events = InsertTimingEvents(*events); + events = CreateTies(*events); + std::stable_sort(events->begin(), events->end(), EventCompare); + events = SplitTime(*events); + CalculateWaits(*events); + + if (g_compressionEnabled) + Compress(*events); + + PrintAgbTrack(*events); + + g_agbTrack++; + } + } + } +} |