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 
8 namespace Nodes::MidiUtil
9 {
10 enum 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 
34 template <>
35 struct 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 
41 namespace Nodes::MidiUtil
42 {
43 
44 template <typename T>
45 static constexpr void constexpr_swap(T& a, T& b)
46 {
47  T tmp = a;
48  a = b;
49  b = tmp;
50 }
51 
52 template <typename Iterator>
53 static 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 
67 using scale_array = std::array<bool, 128>;
68 using scales_array = std::array<scale_array, 12>;
69 static 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 
93 static 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 
107 static 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
145 static 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
163 static 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 
206 struct 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 }
Definition: lv2_atom_helpers.hpp:99
Definition: MidiUtil.hpp:229
Definition: MidiUtil.hpp:207