Loading...
Searching...
No Matches
MidiUtil.hpp
1#pragma once
2#include <Fx/Types.hpp>
3
4#include <halp/controls.hpp>
5#include <halp/meta.hpp>
6#include <halp/midi.hpp>
7
8namespace Nodes::MidiUtil
9{
10enum scale_type : int8_t
11{
12 all,
13 ionian,
14 dorian,
15 phyrgian,
16 lydian,
17 mixolydian,
18 aeolian,
19 locrian,
20
21 I,
22 II,
23 III,
24 IV,
25 V,
26 VI,
27 VII,
28 custom,
29
30 SCALES_MAX // always at end, used for counting
31};
32}
33
34template <>
35struct magic_enum::customize::enum_range<Nodes::MidiUtil::scale_type>
36{
37 static constexpr int min = 0;
38 static constexpr int max = Nodes::MidiUtil::custom;
39};
40
41namespace Nodes::MidiUtil
42{
43
44template <typename T>
45static constexpr void constexpr_swap(T& a, T& b)
46{
47 T tmp = a;
48 a = b;
49 b = tmp;
50}
51
52template <typename Iterator>
53static constexpr void constexpr_rotate(Iterator first, Iterator middle, Iterator last)
54{
55 using namespace std;
56 Iterator next = middle;
57 while(first != next)
58 {
59 constexpr_swap(*first++, *next++);
60 if(next == last)
61 next = middle;
62 else if(first == middle)
63 middle = next;
64 }
65}
66
67using scale_array = std::array<bool, 128>;
68using scales_array = std::array<scale_array, 12>;
69static constexpr scales_array make_scale(std::initializer_list<bool> notes)
70{
71 std::array<scale_array, 12> r{};
72 for(std::size_t octave = 0; octave < 11; octave++)
73 {
74 std::size_t pos = 0;
75 for(bool note : notes)
76 {
77 if(octave * 12 + pos < 128)
78 {
79 r[0][octave * 12 + pos] = note;
80 pos++;
81 }
82 }
83 }
84
85 for(std::size_t octave = 1; octave < 12; octave++)
86 {
87 r[octave] = r[0];
88 constexpr_rotate(r[octave].rbegin(), r[octave].rbegin() + octave, r[octave].rend());
89 }
90 return r;
91}
92
93static constexpr bool is_same(std::string_view lhs, std::string_view rhs)
94{
95 if(lhs.size() == rhs.size())
96 {
97 for(std::size_t i = 0; i < lhs.size(); i++)
98 {
99 if(lhs[i] != rhs[i])
100 return false;
101 }
102 return true;
103 }
104 return false;
105}
106
107static constexpr int get_scale(std::string_view s)
108{
109 using namespace std::literals;
110 if(is_same(s, std::string_view("all")))
111 return scale_type::all;
112 else if(is_same(s, std::string_view("ionian")))
113 return scale_type::ionian;
114 else if(is_same(s, std::string_view("dorian")))
115 return scale_type::dorian;
116 else if(is_same(s, std::string_view("phyrgian")))
117 return scale_type::phyrgian;
118 else if(is_same(s, std::string_view("lydian")))
119 return scale_type::lydian;
120 else if(is_same(s, std::string_view("mixolydian")))
121 return scale_type::mixolydian;
122 else if(is_same(s, std::string_view("aeolian")))
123 return scale_type::aeolian;
124 else if(is_same(s, std::string_view("locrian")))
125 return scale_type::locrian;
126 else if(is_same(s, std::string_view("I")))
127 return scale_type::I;
128 else if(is_same(s, std::string_view("II")))
129 return scale_type::II;
130 else if(is_same(s, std::string_view("III")))
131 return scale_type::III;
132 else if(is_same(s, std::string_view("IV")))
133 return scale_type::IV;
134 else if(is_same(s, std::string_view("V")))
135 return scale_type::V;
136 else if(is_same(s, std::string_view("VI")))
137 return scale_type::VI;
138 else if(is_same(s, std::string_view("VII")))
139 return scale_type::VII;
140 else
141 return scale_type::custom;
142}
143
144// clang-format off
145static constexpr std::array<scales_array, scale_type::SCALES_MAX - 1> scales{
146// C D E F G A B
147/* { scale::all, */ make_scale({1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}) /* } */,
148/* { scale::ionian, */ make_scale({1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1}) /* } */,
149/* { scale::dorian, */ make_scale({1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0}) /* } */,
150/* { scale::phyrgian, */ make_scale({1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0}) /* } */,
151/* { scale::lydian, */ make_scale({1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1}) /* } */,
152/* { scale::mixolydian, */ make_scale({1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0}) /* } */,
153/* { scale::aeolian, */ make_scale({1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0}) /* } */,
154/* { scale::locrian, */ make_scale({1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0}) /* } */,
155/* { scale::I, */ make_scale({1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0}) /* } */,
156/* { scale::II, */ make_scale({0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0}) /* } */,
157/* { scale::III, */ make_scale({0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1}) /* } */,
158/* { scale::IV, */ make_scale({1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0}) /* } */,
159/* { scale::V, */ make_scale({0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1}) /* } */,
160/* { scale::VI, */ make_scale({1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0}) /* } */,
161/* { scale::VII, */ make_scale({0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1}) /* } */};
162// clang-format on
163static std::optional<std::size_t> find_closest_index(const scale_array& arr, std::size_t i)
164{
165 if(arr[i] == 1)
166 return i;
167
168 switch(i)
169 {
170 case 0:
171 while(i != 12)
172 {
173 i++;
174 if(arr[i] == 1)
175 return i;
176 }
177 break;
178
179 case 12:
180 while(i != 0)
181 {
182 i--;
183 if(arr[i] == 1)
184 return i;
185 }
186 break;
187
188 default: {
189 std::size_t r = 0;
190 while((i - r) != 0 && (i + r) != 12)
191 {
192 if(arr[i + r] == 1)
193 return i + r;
194 else if(arr[i - r] == 1)
195 return i - r;
196 r++;
197 }
198
199 break;
200 }
201 }
202
203 return std::nullopt;
204}
205
206struct Node
207{
208 halp_meta(name, "Midi scale")
209 halp_meta(c_name, "MidiScale")
210 halp_meta(category, "Midi")
211 halp_meta(author, "ossia score")
212 halp_meta(manual_url, "https://ossia.io/score-docs/processes/midi-utilities.html#midi-scale")
213 halp_meta(description, "Maps a midi input to a given scale")
214 halp_meta(uuid, "06b33b83-bb67-4f7a-9980-f5d66e4266c5")
215
216 struct
217 {
218 halp::midi_bus<"in", libremidi::message> midi;
219 halp::string_enum_t<scale_type, "Scale"> sc; // FIXME check that this works
220 octave_slider<"Base", 0, 1> base;
221 octave_slider<"Transpose", -4, 4> transp;
222 } inputs;
223 struct
224 {
225 midi_out midi;
226 } outputs;
227
228 struct Note
229 {
230 uint8_t pitch{};
231 uint8_t vel{};
232 uint8_t chan{};
233 };
234 ossia::flat_map<uint8_t, Note> map;
235 std::string scale{};
236 int base{};
237 int transpose{};
238
239 void exec(const scale_array& scale, int transp)
240 {
241 auto& midi_in = inputs.midi;
242 auto& midi_out = outputs.midi;
243 for(const auto& msg : midi_in)
244 {
245 switch(msg.get_message_type())
246 {
247 case libremidi::message_type::NOTE_ON: {
248 if(msg.bytes[1] >= 128)
249 continue;
250
251 // map to scale
252 if(auto index = find_closest_index(scale, msg.bytes[1]))
253 {
254 // transpose
255 auto res = msg;
256 res.bytes[1] = (uint8_t)ossia::clamp(int(*index + transp), 0, 127);
257 Note note{
258 (uint8_t)res.bytes[1], (uint8_t)res.bytes[2],
259 (uint8_t)res.get_channel()};
260 auto it = this->map.find(msg.bytes[1]);
261 if(it != this->map.end())
262 {
263 midi_out.note_off(res.get_channel(), it->second.pitch, res.bytes[2])
264 .timestamp
265 = 0;
266 midi_out.push_back(res);
267 midi_out.back().timestamp = 1; // FIXME does not work if last sample
268 const_cast<Note&>(it->second) = note;
269 }
270 else
271 {
272 midi_out.push_back(res);
273 midi_out.back().timestamp = 0;
274 this->map.insert(std::make_pair((uint8_t)msg.bytes[1], note));
275 }
276 }
277 break;
278 }
279 case libremidi::message_type::NOTE_OFF: {
280 if(msg.bytes[1] >= 128)
281 continue;
282
283 auto it = this->map.find(msg.bytes[1]);
284 if(it != this->map.end())
285 {
286 midi_out.note_off(msg.get_channel(), it->second.pitch, msg.bytes[2])
287 .timestamp
288 = 0;
289 this->map.erase(it);
290 }
291 break;
292 }
293 default:
294 midi_out.push_back(msg);
295 break;
296 }
297 }
298 }
299
300 void update(const scale_array& scale, int transp)
301 {
302 auto& midi_out = outputs.midi;
303 for(auto& notes : this->map)
304 {
305 Note& note = const_cast<Note&>(notes.second);
306 if(auto index = find_closest_index(scale, notes.first))
307 {
308 if((*index + transp) != note.pitch)
309 {
310 midi_out.note_off(note.chan, note.pitch, note.vel).timestamp = 0;
311 note.pitch = *index + transp;
312 midi_out.note_on(note.chan, note.pitch, note.vel);
313 midi_out.back().timestamp = 1; // FIXME does not work if last sample
314 }
315 }
316 }
317 }
318
319 using tick = halp::tick_flicks;
320 void operator()(const tick& tk)
321 {
322 const auto& new_scale = inputs.sc.value;
323 const int new_base = inputs.base.value;
324 const int new_transpose = inputs.transp.value;
325 std::string_view scale{new_scale.data(), new_scale.size()};
326
327 const auto new_scale_idx = get_scale(scale);
328
329 auto apply = [&](auto f) {
330 if(new_scale_idx >= 0 && new_scale_idx < scale_type::custom)
331 {
332 f(scales[new_scale_idx][new_base], new_transpose);
333 }
334 else
335 {
336 scale_array arr{{}};
337 for(int oct = 0; oct < 10; oct++)
338 {
339 for(int i = 0; i < ossia::min(std::ssize(scale), 12); i++)
340 {
341 arr[oct * 12 + i] = (scale[i] == '1');
342 }
343 }
344 f(arr, new_transpose);
345 }
346 };
347
348#define forward_to_method(method_name) \
349 [&]<typename... Args>(Args&&... args) { \
350 this->method_name(std::forward<Args>(args)...); \
351 }
352
353 if(!this->map.empty()
354 && (new_scale != this->scale || new_base != this->base
355 || new_transpose != this->transpose))
356 {
357 apply(forward_to_method(update));
358 }
359
360 apply(forward_to_method(exec));
361#undef forward_to_method
362
363 this->scale = new_scale;
364 this->base = new_base;
365 this->transpose = new_transpose;
366 }
367};
368}
STL namespace.
Definition lv2_atom_helpers.hpp:99
Definition MidiUtil.hpp:229
Definition MidiUtil.hpp:207