Skip to content

Commit 80ca5cf

Browse files
committed
examples: add a .mid to pattern converter
1 parent 4cd5114 commit 80ca5cf

3 files changed

Lines changed: 158 additions & 0 deletions

File tree

cmake/libremidi.deps.cmake

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
### C++ features ###
22
check_cxx_source_compiles("#include <thread>\nint main() { std::jthread t; }" LIBREMIDI_HAS_STD_JTHREAD)
3+
check_cxx_source_compiles("#include <flat_set>\nint main() { std::flat_set<int> t; }" LIBREMIDI_HAS_STD_FLAT_SET)
4+
check_cxx_source_compiles("#include <print>\nint main() { std::println(\"foo: {}\", 123); }" LIBREMIDI_HAS_STD_PRINTLN)
35
check_cxx_source_compiles("#include <semaphore>\nint main() { std::binary_semaphore t{0}; }" LIBREMIDI_HAS_STD_SEMAPHORE)
46
check_cxx_source_compiles("#include <stop_token>\n#include <thread>\nint main() { std::jthread t; }" LIBREMIDI_HAS_STD_STOP_TOKEN)
57

cmake/libremidi.examples.cmake

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ add_example(minimal)
2929
add_example(midi2_echo)
3030
add_example(rawmidiin)
3131

32+
if(LIBREMIDI_HAS_STD_FLAT_SET AND LIBREMIDI_HAS_STD_PRINTLN)
33+
add_example(midi_to_pattern)
34+
endif()
35+
3236
add_example(protocols/remote_control)
3337

3438
if(LIBREMIDI_NI_MIDI2)

examples/midi_to_pattern.cpp

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
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

Comments
 (0)