|
| 1 | +#include <libremidi/reader.hpp> |
| 2 | + |
| 3 | +#include <algorithm> |
| 4 | +#include <cstdint> |
| 5 | +#include <flat_map> |
| 6 | +#include <flat_set> |
| 7 | +#include <fstream> |
| 8 | +#include <iostream> |
| 9 | +#include <map> |
| 10 | +#include <numeric> |
| 11 | +#include <print> |
| 12 | +#include <string> |
| 13 | +#include <vector> |
| 14 | + |
| 15 | +struct NoteEvent |
| 16 | +{ |
| 17 | + int64_t tick{}; |
| 18 | + bool on{}; |
| 19 | + friend bool operator<(NoteEvent lhs, NoteEvent rhs) noexcept { return lhs.tick < rhs.tick; } |
| 20 | + friend bool operator==(NoteEvent lhs, NoteEvent rhs) noexcept { return lhs.tick == rhs.tick; } |
| 21 | +}; |
| 22 | + |
| 23 | +struct TrackData |
| 24 | +{ |
| 25 | + std::flat_map<int64_t, std::vector<NoteEvent>> events; |
| 26 | + int64_t max_tick = 0; |
| 27 | +}; |
| 28 | + |
| 29 | +static int64_t array_gcd(const auto& arr) |
| 30 | +{ |
| 31 | + if (arr.empty()) |
| 32 | + return 1; |
| 33 | + |
| 34 | + auto it = arr.begin(); |
| 35 | + int64_t res = *it; |
| 36 | + ++it; |
| 37 | + |
| 38 | + for (; it != arr.end(); ++it) |
| 39 | + if ((res = std::gcd(*it, res)) == 1) |
| 40 | + break; |
| 41 | + |
| 42 | + return res; |
| 43 | +} |
| 44 | + |
| 45 | +int main(int argc, char* argv[]) |
| 46 | +{ |
| 47 | + if (argc != 2) |
| 48 | + { |
| 49 | + std::println("Usage: {} <midi_file>", argv[0]); |
| 50 | + return 1; |
| 51 | + } |
| 52 | + |
| 53 | + // Load the midi file |
| 54 | + std::ifstream file{argv[1], std::ios::binary}; |
| 55 | + if (!file) |
| 56 | + { |
| 57 | + std::println("Error: Could not open file '{}'", argv[1]); |
| 58 | + return 1; |
| 59 | + } |
| 60 | + |
| 61 | + std::vector<uint8_t> bytes; |
| 62 | + bytes.assign(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()); |
| 63 | + file.close(); |
| 64 | + |
| 65 | + libremidi::reader reader{true}; // Use absolute tick timing |
| 66 | + auto result = reader.parse(bytes); |
| 67 | + |
| 68 | + if (result == libremidi::reader::invalid) |
| 69 | + { |
| 70 | + std::println("Error: Invalid MIDI file"); |
| 71 | + return 1; |
| 72 | + } |
| 73 | + |
| 74 | + // Process all tracks and collect note events |
| 75 | + TrackData processed_track; |
| 76 | + |
| 77 | + std::flat_set<int64_t> dates; |
| 78 | + for (const auto& track : reader.tracks) |
| 79 | + { |
| 80 | + for (const auto& event : track) |
| 81 | + { |
| 82 | + switch (event.m.get_message_type()) |
| 83 | + { |
| 84 | + case libremidi::message_type::NOTE_ON: { |
| 85 | + int note = event.m[1]; |
| 86 | + bool isOn = event.m[2] > 0; |
| 87 | + |
| 88 | + processed_track.events[note].push_back({event.tick, isOn}); |
| 89 | + processed_track.max_tick = std::max(processed_track.max_tick, (int64_t)event.tick); |
| 90 | + dates.insert(event.tick); |
| 91 | + break; |
| 92 | + } |
| 93 | + case libremidi::message_type::NOTE_OFF: { |
| 94 | + int note = event.m[1]; |
| 95 | + bool isOn = false; |
| 96 | + |
| 97 | + processed_track.events[note].push_back({event.tick, isOn}); |
| 98 | + processed_track.max_tick = std::max(processed_track.max_tick, (int64_t)event.tick); |
| 99 | + break; |
| 100 | + } |
| 101 | + default: |
| 102 | + continue; |
| 103 | + } |
| 104 | + } |
| 105 | + } |
| 106 | + |
| 107 | + // Find GCD of each value |
| 108 | + dates.erase(0); |
| 109 | + const double gcd_in_64th_notes = 16. * (double(array_gcd(dates)) / reader.ticksPerBeat); |
| 110 | + |
| 111 | + const int ticks_per_division = gcd_in_64th_notes * reader.ticksPerBeat / 16; |
| 112 | + const int pattern_length = (processed_track.max_tick / ticks_per_division); |
| 113 | + |
| 114 | + // Generate pattern for each note |
| 115 | + for (const auto& [note, events] : processed_track.events) |
| 116 | + { |
| 117 | + std::string pattern; |
| 118 | + pattern.reserve(pattern_length); |
| 119 | + |
| 120 | + bool note_on = false; |
| 121 | + int event_index = 0; |
| 122 | + |
| 123 | + for (int pos = 0; pos < pattern_length; ++pos) |
| 124 | + { |
| 125 | + int tick = pos * ticks_per_division; |
| 126 | + |
| 127 | + // Process events at or before this position |
| 128 | + while (event_index < events.size() && events[event_index].tick <= tick) |
| 129 | + { |
| 130 | + note_on = events[event_index].on; |
| 131 | + event_index++; |
| 132 | + } |
| 133 | + |
| 134 | + if (note_on) |
| 135 | + { |
| 136 | + // Check if this is the start of a note or a continuation |
| 137 | + bool is_new_note = (pos == 0 || pattern.back() == '-' || pattern.back() == '0'); |
| 138 | + pattern += is_new_note ? '1' : '2'; |
| 139 | + } |
| 140 | + else |
| 141 | + { |
| 142 | + pattern += '-'; |
| 143 | + } |
| 144 | + } |
| 145 | + |
| 146 | + // Only output if there's actual content |
| 147 | + if (!pattern.empty()) |
| 148 | + std::print("{:02} {}\n", note, pattern); |
| 149 | + } |
| 150 | + |
| 151 | + return 0; |
| 152 | +} |
0 commit comments