VelToNote.hpp
1 #pragma once
2 #include <Fx/Types.hpp>
3 
4 #include <ossia/dataflow/value_port.hpp>
5 #include <ossia/detail/math.hpp>
6 
7 #include <boost/pfr.hpp>
8 
9 #include <halp/controls.hpp>
10 #include <halp/meta.hpp>
11 #include <halp/midi.hpp>
12 #include <libremidi/message.hpp>
13 #include <rnd/random.hpp>
14 
15 #include <iostream>
16 inline std::ostream&
17 operator<<(std::ostream& s, decltype(halp::tick_musical::signature) sig)
18 {
19  return s << sig.num << " / " << sig.denom;
20 }
21 namespace Nodes
22 {
23 namespace PulseToNote
24 {
25 struct Node
26 {
27  halp_meta(name, "Pulse to Midi")
28  halp_meta(c_name, "VelToNote")
29  halp_meta(category, "Midi")
30  halp_meta(author, "ossia score")
31  halp_meta(manual_url, "https://ossia.io/score-docs/processes/midi-utilities.html#pulse-to-note")
32  halp_meta(
33  description,
34  "Converts a message into MIDI.\n"
35  "If the input is an impulse, the output will be the default pitch "
36  "at the default velocity.\n"
37  "If the input is a single integer in [0; 127], the output will be "
38  "the relevant note at the default velocity"
39  "If the input is an array of two values between [0; 127], the "
40  "output will be the relevant note.")
41 
42  halp_meta(uuid, "2c6493c3-5449-4e52-ae04-9aee3be5fb6a")
43 
44  struct
45  {
46  // FIXME is_event
47  // FIXME all incorrect when token_request smaller than tick
48  // Implement polyphonic behaviour at the avendish level?
49  ossia_port<"in", ossia::value_port> port{};
50  quant_selector<"Start quant."> start_quant;
51  halp::hslider_f32<"Tightness", halp::range{0.f, 1.f, 0.8f}> tightness;
52  quant_selector<"End quant."> end_quant;
53  midi_spinbox<"Default pitch"> basenote;
54  midi_spinbox<"Default vel."> basevel;
55  octave_slider<"Pitch shift", -5, 5> shift_note;
56  octave_slider<"Pitch random", 0, 2> note_random;
57  octave_slider<"Vel. random", 0, 2> vel_random;
58  midi_channel<"Channel"> channel;
59 
60  } inputs;
61  struct
62  {
63  midi_out midi;
64  } outputs;
65 
66  struct Note
67  {
68  uint8_t pitch{};
69  uint8_t vel{};
70  uint8_t chan{};
71  };
72 
73  struct NoteIn
74  {
75  Note note{};
76  int64_t date{};
77  };
78  std::vector<NoteIn> to_start;
79  std::vector<NoteIn> running_notes;
80 
81  struct val_visitor
82  {
83  Node& st;
84  uint8_t base_note{};
85  uint8_t base_vel{};
86 
87  Note operator()() { return {base_note, base_vel}; }
88  Note operator()(ossia::impulse) { return {base_note, base_vel}; }
89  template <typename T>
90  Note operator()(const T&)
91  {
92  return {base_note, base_vel};
93  }
94  Note operator()(float note) { return {(uint8_t)note, base_vel}; }
95  Note operator()(char note) { return {(uint8_t)note, base_vel}; }
96  Note operator()(int note) { return {(uint8_t)note, base_vel}; }
97  Note operator()(int note, int vel) { return {(uint8_t)note, (uint8_t)vel}; }
98  Note operator()(int note, float vel) { return {(uint8_t)note, (uint8_t)(vel * 127.f)}; }
99  Note operator()(const std::vector<ossia::value>& v)
100  {
101  switch(v.size())
102  {
103  case 0:
104  return operator()();
105  case 1:
106  return operator()(ossia::convert<int>(v[0]));
107  case 2: {
108  int note = ossia::convert<int>(v[0]);
109  switch(v[1].get_type())
110  {
111  case ossia::val_type::FLOAT:
112  return operator()(note, *v[1].v.target<float>());
113  case ossia::val_type::INT:
114  return operator()(note, *v[1].v.target<int>());
115  default:
116  return operator()(note, ossia::convert<int>(v[1]));
117  }
118  }
119  default:
120  return operator()(ossia::convert<int>(v[0]), ossia::convert<int>(v[1]));
121  }
122  }
123  template <std::size_t N>
124  Note operator()(const std::array<float, N>& v)
125  {
126  static_assert(N >= 2);
127  return operator()(v[0], v[1]);
128  }
129  };
130 
131  static constexpr uint8_t midi_clamp(int num) { return (uint8_t)ossia::clamp(num, 0, 127); }
132 
133  using tick = halp::tick_flicks;
134  void operator()(const tick& tk)
135  {
136  // TODO : when arrays like [ 1, 25, 12, 37, 10, 40 ] are received
137  // send relevant chords
138 
139  // When a message is received, we have three cases :
140  // 1. Just an impulse: use base note & base vel
141  // 2. Just an int: it's the velocity, use base note
142  // 3. A tuple [int, int]: it's note and velocity
143 
144  // Then once we have a pair [int, int] we randomize and we output a note
145  // on.
146 
147  // At the end, scan running_notes: if any is going to end in this buffer,
148  // end it too.
149 
150  // FIXME next version choose between these behaviours
151 
152 #define STOP_AT_NEXT_SAMPLE_FOR_ZERO_END_QUANT 1
153 
154  const auto& start = inputs.start_quant.value;
155  // TODO double precision = tightness.rbegin()->second;
156  const auto& end = inputs.end_quant.value;
157  const auto& shiftnote = inputs.shift_note.value;
158  const auto& base_note = midi_clamp(inputs.basenote.value);
159  const auto& base_vel = midi_clamp(inputs.basevel.value);
160  const auto& rand_note = inputs.note_random.value;
161  const auto& rand_vel = inputs.vel_random.value;
162  const auto& chan = inputs.channel.value;
163 
164  for(auto& in : inputs.port.value->get_data())
165  {
166  auto note = in.value.apply(val_visitor{*this, base_note, base_vel});
167 
168  if(rand_note != 0)
169  note.pitch += rnd::rand(-rand_note, rand_note);
170  if(rand_vel != 0)
171  note.vel += rnd::rand(-rand_vel, rand_vel);
172 
173  note.pitch = ossia::clamp((int)note.pitch + shiftnote, 0, 127);
174  note.vel = ossia::clamp((int)note.vel, 0, 127);
175 
176  if(note.vel != 0)
177  {
178  if(start == 0.f) // No quantification, start directly
179  {
180  outputs.midi.note_on(chan, note.pitch, note.vel).timestamp = in.timestamp;
181  if(end > 0.f)
182  {
183  this->running_notes.push_back({note, tk.position_in_frames + in.timestamp});
184  }
185  else if(end == 0.f)
186  {
187  // Stop at the next sample
188 #if STOP_AT_NEXT_SAMPLE_FOR_ZERO_END_QUANT
189  outputs.midi.note_off(chan, note.pitch, note.vel).timestamp = in.timestamp;
190 #endif
191  }
192  // else do nothing and just wait for a note off
193  }
194  else
195  {
196  // Find next time that matches the requested quantification
197  this->to_start.push_back({note, {}});
198  }
199  }
200  else
201  {
202  // Just stop
203  outputs.midi.note_off(chan, note.pitch, note.vel).timestamp = in.timestamp;
204  }
205  }
206  std::optional<int> start_quant_date{};
207 
208  if(!to_start.empty())
209  {
210  if(start != 0.f)
211  {
212  for(auto [date, q] : tk.get_quantification_date(4. * start))
213  {
214  start_all_notes(tk.position_in_frames + date, date, chan, end);
215  start_quant_date = date;
216  break;
217  }
218  }
219  else
220  {
221  start_all_notes(tk.position_in_frames, 0, chan, end);
222  start_quant_date = 0;
223  }
224  }
225 
226  if(end != 0.f)
227  {
228  for(auto [date, q] : tk.get_quantification_date(4. * end))
229  {
230  if(!start_quant_date || date > start_quant_date)
231  {
232  stop_notes(date, chan);
233  }
234  }
235  }
236  else
237  {
238  // FIXME this means that the notes cannot run ? instead we should just rely on note off maybe? but it's an impulse...
239  // we should do this for "impulses" and individual ints, but let the normal note off
240 #if STOP_AT_NEXT_SAMPLE_FOR_ZERO_END_QUANT
241  stop_notes(0, chan);
242 #endif
243  }
244  }
245 
246  void start_all_notes(
247  ossia::physical_time abs_date, ossia::physical_time date_phys, int chan,
248  float endq) noexcept
249  {
250  for(auto& note : this->to_start)
251  {
252  outputs.midi.note_on(chan, note.note.pitch, note.note.vel).timestamp = date_phys;
253 
254  if(endq > 0.f)
255  {
256  this->running_notes.push_back({{note.note}, abs_date});
257  }
258  else if(endq == 0.f)
259  {
260  // Stop at the next sample
261 
262 #if STOP_AT_NEXT_SAMPLE_FOR_ZERO_END_QUANT
263  outputs.midi.note_off(chan, note.note.pitch, note.note.vel).timestamp
264  = date_phys;
265 #endif
266  }
267  }
268  this->to_start.clear();
269  }
270 
271  void stop_notes(ossia::physical_time date_phys, int chan)
272  {
273  for(auto& note : this->running_notes)
274  {
275  outputs.midi.note_off(chan, note.note.pitch, note.note.vel).timestamp = date_phys;
276  }
277  this->running_notes.clear();
278  /*
279  for(auto it = this->running_notes.begin(); it != this->running_notes.end();)
280  {
281  auto& note = *it;
282  // note.date is the date at which the note was started.
283 
284  if(note.date.impl > tk.date.impl)
285  {
286  // Note was started "in the future", stop it right now as it means we went back in time
287  p2.note_off(chan, note.note.pitch, note.note.vel).timestamp
288  = tk.to_physical_time_in_tick(tk.prev_date, sampleRatio);
289  it = this->running_notes.erase(it);
290  }
291  else if(note.date.impl < tk.prev_date.impl)
292  {
293  // if we're
294  it = this->running_notes.erase(it);
295  }
296  else
297  {
298  ++it;
299  }
300 
301  if(note.date > tk.prev_date && note.date.impl < tk.date.impl)
302  {
303  p2.note_off(chan, note.note.pitch, note.note.vel).timestamp
304  = (note.date - tk.prev_date).impl * st.modelToSamples();
305 
306  it = this->running_notes.erase(it);
307  }
308  else
309  {
310  ++it;
311  }
312  }
313 */
314  }
315 };
316 }
317 }
Definition: VelToNote.hpp:67
Definition: VelToNote.hpp:74
Definition: VelToNote.hpp:82
Definition: VelToNote.hpp:26
Definition: Types.hpp:48
Definition: Types.hpp:39