MidiUtil.hpp
1 #pragma once
2 #include <Engine/Node/SimpleApi.hpp>
3 #undef slots
4 #include <ossia/detail/ssize.hpp>
5 namespace Nodes::MidiUtil
6 {
7 using Note = Control::Note;
8 enum scale : int8_t
9 {
10  all,
11  ionian,
12  dorian,
13  phyrgian,
14  lydian,
15  mixolydian,
16  aeolian,
17  locrian,
18 
19  I,
20  II,
21  III,
22  IV,
23  V,
24  VI,
25  VII,
26  custom,
27 
28  SCALES_MAX // always at end, used for counting
29 };
30 
31 template <typename T>
32 constexpr void constexpr_swap(T& a, T& b)
33 {
34  T tmp = a;
35  a = b;
36  b = tmp;
37 }
38 
39 template <typename Iterator>
40 constexpr void constexpr_rotate(Iterator first, Iterator middle, Iterator last)
41 {
42  using namespace std;
43  Iterator next = middle;
44  while(first != next)
45  {
46  constexpr_swap(*first++, *next++);
47  if(next == last)
48  next = middle;
49  else if(first == middle)
50  middle = next;
51  }
52 }
53 
54 using scale_array = std::array<bool, 128>;
55 using scales_array = std::array<scale_array, 12>;
56 constexpr scales_array make_scale(std::initializer_list<bool> notes)
57 {
58  std::array<scale_array, 12> r{};
59  for(std::size_t octave = 0; octave < 11; octave++)
60  {
61  std::size_t pos = 0;
62  for(bool note : notes)
63  {
64  if(octave * 12 + pos < 128)
65  {
66  r[0][octave * 12 + pos] = note;
67  pos++;
68  }
69  }
70  }
71 
72  for(std::size_t octave = 1; octave < 12; octave++)
73  {
74  r[octave] = r[0];
75  constexpr_rotate(r[octave].rbegin(), r[octave].rbegin() + octave, r[octave].rend());
76  }
77  return r;
78 }
79 
80 constexpr bool is_same(std::string_view lhs, std::string_view rhs)
81 {
82  if(lhs.size() == rhs.size())
83  {
84  for(std::size_t i = 0; i < lhs.size(); i++)
85  {
86  if(lhs[i] != rhs[i])
87  return false;
88  }
89  return true;
90  }
91  return false;
92 }
93 
94 constexpr int get_scale(std::string_view s)
95 {
96  using namespace std::literals;
97  if(is_same(s, std::string_view("all")))
98  return scale::all;
99  else if(is_same(s, std::string_view("ionian")))
100  return scale::ionian;
101  else if(is_same(s, std::string_view("dorian")))
102  return scale::dorian;
103  else if(is_same(s, std::string_view("phyrgian")))
104  return scale::phyrgian;
105  else if(is_same(s, std::string_view("lydian")))
106  return scale::lydian;
107  else if(is_same(s, std::string_view("mixolydian")))
108  return scale::mixolydian;
109  else if(is_same(s, std::string_view("aeolian")))
110  return scale::aeolian;
111  else if(is_same(s, std::string_view("locrian")))
112  return scale::locrian;
113  else if(is_same(s, std::string_view("I")))
114  return scale::I;
115  else if(is_same(s, std::string_view("II")))
116  return scale::II;
117  else if(is_same(s, std::string_view("III")))
118  return scale::III;
119  else if(is_same(s, std::string_view("IV")))
120  return scale::IV;
121  else if(is_same(s, std::string_view("V")))
122  return scale::V;
123  else if(is_same(s, std::string_view("VI")))
124  return scale::VI;
125  else if(is_same(s, std::string_view("VII")))
126  return scale::VII;
127  else
128  return scale::custom;
129 }
130 #if defined(_MSC_VER)
131 #define MSVC_CONSTEXPR const
132 #else
133 #define MSVC_CONSTEXPR constexpr
134 #endif
135 static MSVC_CONSTEXPR std::array<scales_array, scale::SCALES_MAX - 1> scales{
136  // C D E F G A B
137  /* { scale::all, */ make_scale({1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}) /* } */,
138  /* { scale::ionian, */ make_scale({1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1}) /* } */,
139  /* { scale::dorian, */ make_scale({1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0}) /* } */,
140  /* { scale::phyrgian, */ make_scale({1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0}) /* } */,
141  /* { scale::lydian, */ make_scale({1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1}) /* } */,
142  /* { scale::mixolydian, */ make_scale({1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0}) /* } */,
143  /* { scale::aeolian, */ make_scale({1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0}) /* } */,
144  /* { scale::locrian, */ make_scale({1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0}) /* } */,
145  /* { scale::I, */ make_scale({1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0}) /* } */,
146  /* { scale::II, */ make_scale({0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0}) /* } */,
147  /* { scale::III, */ make_scale({0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1}) /* } */,
148  /* { scale::IV, */ make_scale({1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0}) /* } */,
149  /* { scale::V, */ make_scale({0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1}) /* } */,
150  /* { scale::VI, */ make_scale({1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0}) /* } */,
151  /* { scale::VII, */
152  make_scale({0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1}) /* } */};
153 
154 static std::optional<std::size_t>
155 find_closest_index(const scale_array& arr, std::size_t i)
156 {
157  if(arr[i] == 1)
158  return i;
159 
160  switch(i)
161  {
162  case 0:
163  while(i != 12)
164  {
165  i++;
166  if(arr[i] == 1)
167  return i;
168  }
169  break;
170 
171  case 12:
172  while(i != 0)
173  {
174  i--;
175  if(arr[i] == 1)
176  return i;
177  }
178  break;
179 
180  default: {
181  std::size_t r = 0;
182  while((i - r) != 0 && (i + r) != 12)
183  {
184  if(arr[i + r] == 1)
185  return i + r;
186  else if(arr[i - r] == 1)
187  return i - r;
188  r++;
189  }
190 
191  break;
192  }
193  }
194 
195  return std::nullopt;
196 }
197 
198 struct Node
199 {
201  {
202  static const constexpr auto prettyName = "Midi scale";
203  static const constexpr auto objectKey = "MidiScale";
204  static const constexpr auto category = "Midi";
205  static const constexpr auto author = "ossia score";
206  static const constexpr auto tags = std::array<const char*, 0>{};
207  static const constexpr auto kind = Process::ProcessCategory::MidiEffect;
208  static const constexpr auto description = "Maps a midi input to a given scale";
209  static const uuid_constexpr auto uuid
210  = make_uuid("06b33b83-bb67-4f7a-9980-f5d66e4266c5");
211 
212  static const constexpr midi_in midi_ins[]{"in"};
213  static const constexpr midi_out midi_outs[]{"out"};
214  static const constexpr auto controls = tuplet::make_tuple(
215  Control::make_unvalidated_enum(
216  "Scale", 0U,
217  ossia::make_array(
218  "all", "ionian", "dorian", "phyrgian", "lydian", "mixolydian", "aeolian",
219  "locrian", "I", "II", "III", "IV", "V", "VI", "VII")),
220  Control::Widgets::OctaveSlider("Base", 0, 1),
221  Control::Widgets::OctaveSlider("Transpose", -4, 4));
222  };
223 
224  struct State
225  {
226  ossia::flat_map<uint8_t, Note> map;
227  std::string scale{};
228  int base{};
229  int transpose{};
230  };
231 
232  static void exec(
233  const ossia::midi_port& midi_in, const scale_array& scale, int transp,
234  ossia::midi_port& midi_out, const int64_t offset, State& self)
235  {
236  for(const auto& msg : midi_in.messages)
237  {
238  switch(msg.get_message_type())
239  {
240  case libremidi::message_type::NOTE_ON: {
241  if(msg.bytes[1] >= 128)
242  continue;
243 
244  // map to scale
245  if(auto index = find_closest_index(scale, msg.bytes[1]))
246  {
247  // transpose
248  auto res = msg;
249  res.bytes[1] = (uint8_t)ossia::clamp(int(*index + transp), 0, 127);
250  Note note{
251  (uint8_t)res.bytes[1], (uint8_t)res.bytes[2],
252  (uint8_t)res.get_channel()};
253  auto it = self.map.find(msg.bytes[1]);
254  if(it != self.map.end())
255  {
256  midi_out.messages.push_back(libremidi::channel_events::note_off(
257  res.get_channel(), it->second.pitch, res.bytes[2]));
258  midi_out.messages.back().timestamp = offset;
259  midi_out.messages.push_back(res);
260  midi_out.messages.back().timestamp = offset + 1;
261  const_cast<Note&>(it->second) = note;
262  }
263  else
264  {
265  midi_out.messages.push_back(res);
266  midi_out.messages.back().timestamp = offset;
267  self.map.insert(std::make_pair((uint8_t)msg.bytes[1], note));
268  }
269  }
270  break;
271  }
272  case libremidi::message_type::NOTE_OFF: {
273  if(msg.bytes[1] >= 128)
274  continue;
275 
276  auto it = self.map.find(msg.bytes[1]);
277  if(it != self.map.end())
278  {
279  midi_out.messages.push_back(libremidi::channel_events::note_off(
280  msg.get_channel(), it->second.pitch, msg.bytes[2]));
281  midi_out.messages.back().timestamp = offset;
282  self.map.erase(it);
283  }
284  break;
285  }
286  default:
287  midi_out.messages.push_back(msg);
288  break;
289  }
290  }
291  }
292 
293  static void update(
294  const ossia::midi_port& midi_in, const scale_array& scale, int transp,
295  ossia::midi_port& midi_out, const int64_t offset, State& self)
296  {
297  for(auto& notes : self.map)
298  {
299  Note& note = const_cast<Note&>(notes.second);
300  if(auto index = find_closest_index(scale, notes.first))
301  {
302  if((*index + transp) != note.pitch)
303  {
304  midi_out.messages.push_back(
305  libremidi::channel_events::note_off(note.chan, note.pitch, note.vel));
306  note.pitch = *index + transp;
307  midi_out.messages.back().timestamp = offset;
308  midi_out.messages.push_back(
309  libremidi::channel_events::note_on(note.chan, note.pitch, note.vel));
310  midi_out.messages.back().timestamp = offset + 1;
311  }
312  }
313  }
314  }
315 
316  using control_policy = ossia::safe_nodes::default_tick_controls;
317  static void
318  run(const ossia::midi_port& midi_in, const ossia::timed_vec<std::string>& sc,
319  const ossia::timed_vec<int>& base, const ossia::timed_vec<int>& transp,
320  ossia::midi_port& midi_out, ossia::token_request tk, ossia::exec_state_facade st,
321  State& self)
322  {
323  const auto& new_scale = sc.rbegin()->second;
324  const int new_base = base.rbegin()->second;
325  const int new_transpose = transp.rbegin()->second;
326  std::string_view scale{new_scale.data(), new_scale.size()};
327 
328  const auto new_scale_idx = get_scale(scale);
329 
330  auto apply = [&](auto f) {
331  const auto [tick_start, d] = st.timings(tk);
332  if(new_scale_idx >= 0 && new_scale_idx < scale::custom)
333  {
334  f(midi_in, scales[new_scale_idx][new_base], new_transpose, midi_out, tick_start,
335  self);
336  }
337  else
338  {
339  scale_array arr{{}};
340  for(int oct = 0; oct < 10; oct++)
341  {
342  for(int i = 0; i < ossia::min(std::ssize(scale), 12); i++)
343  {
344  arr[oct * 12 + i] = (scale[i] == '1');
345  }
346  }
347  f(midi_in, arr, new_transpose, midi_out, tick_start, self);
348  }
349  };
350 
351  if(!self.map.empty()
352  && (new_scale != self.scale || new_base != self.base
353  || new_transpose != self.transpose))
354  {
355  apply(update);
356  }
357 
358  apply(exec);
359 
360  self.scale = new_scale;
361  self.base = new_base;
362  self.transpose = new_transpose;
363  }
364 };
365 
366 }
367 
368 namespace Nodes::PitchToValue
369 {
370 struct Node
371 {
373  {
374  static const constexpr auto prettyName = "Midi Pitch";
375  static const constexpr auto objectKey = "PitchToValue";
376  static const constexpr auto category = "Midi";
377  static const constexpr auto author = "ossia score";
378  static const constexpr auto kind = Process::ProcessCategory::MidiEffect;
379  static const constexpr auto description = "Extract a MIDI pitch";
380  static const constexpr auto tags = std::array<const char*, 0>{};
381  static const uuid_constexpr auto uuid
382  = make_uuid("29ce484f-cb56-4501-af79-88768fa261c3");
383 
384  static const constexpr midi_in midi_ins[]{"in"};
385  static const constexpr value_out value_outs[]{{"out", "midipitch"}};
386  };
387 
388  using control_policy = ossia::safe_nodes::default_tick;
389  static void
390  run(const ossia::midi_port& in, ossia::value_port& res, ossia::token_request tk,
391  ossia::exec_state_facade st)
392  {
393  for(const auto& note : in.messages)
394  {
395  if(note.get_message_type() == libremidi::message_type::NOTE_ON)
396  res.write_value(ossia::value{(int)note.bytes[1]}, note.timestamp);
397  }
398  }
399 };
400 }
Utilities for OSSIA data structures.
Definition: DeviceInterface.hpp:33
Definition: SimpleApi.hpp:32
Definition: SimpleApi.hpp:81
Definition: lv2_atom_helpers.hpp:99
Definition: MidiUtil.hpp:201
Definition: MidiUtil.hpp:199
Definition: MidiUtil.hpp:373
Definition: MidiUtil.hpp:371