VelToNote.hpp
1 #pragma once
2 #include <Engine/Node/SimpleApi.hpp>
3 #include <Fx/Quantifier.hpp>
4 
5 #include <ossia/detail/math.hpp>
6 
7 #include <rnd/random.hpp>
8 namespace Nodes
9 {
10 namespace PulseToNote
11 {
12 struct Node
13 {
14  using Note = Control::Note;
16  {
17  static const constexpr auto prettyName = "Pulse to Midi";
18  static const constexpr auto objectKey = "VelToNote";
19  static const constexpr auto category = "Midi";
20  static const constexpr auto author = "ossia score";
21  static const constexpr auto kind = Process::ProcessCategory::Other;
22  static const constexpr auto description
23  = "Converts a message into MIDI.\n"
24  "If the input is an impulse, the output will be the default pitch "
25  "at the default velocity.\n"
26  "If the input is a single integer in [0; 127], the output will be "
27  "the relevant note at the default velocity"
28  "If the input is an array of two values between [0; 127], the "
29  "output will be the relevant note.";
30 
31  static const constexpr auto tags = std::array<const char*, 0>{};
32  static const uuid_constexpr auto uuid
33  = make_uuid("2c6493c3-5449-4e52-ae04-9aee3be5fb6a");
34 
35  static const constexpr value_in value_ins[]{{"in", true}};
36  static const constexpr midi_out midi_outs[]{"out"};
37  static const constexpr auto controls = tuplet::make_tuple(
38  Control::ComboBox<float, std::size(Control::Widgets::notes)>(
39  "Start quant.", 2, Control::Widgets::notes),
40  Control::FloatSlider{"Tightness", 0.f, 1.f, 0.8f},
42  "End quant.", 2, Control::Widgets::notes),
43  Control::Widgets::MidiSpinbox("Default pitch"),
44  Control::Widgets::MidiSpinbox("Default vel."),
45  Control::Widgets::OctaveSlider("Pitch shift", -5, 5),
46  Control::Widgets::OctaveSlider("Pitch random", 0, 2),
47  Control::Widgets::OctaveSlider("Vel. random", 0, 2),
48  Control::Widgets::MidiChannel("Channel"));
49  };
50 
51  struct NoteIn
52  {
53  Note note{};
54  ossia::time_value date{};
55  };
56  struct State
57  {
58  std::vector<NoteIn> to_start;
59  std::vector<NoteIn> running_notes;
60  };
61 
62  using control_policy = ossia::safe_nodes::default_tick_controls;
63  struct val_visitor
64  {
65  State& st;
66  uint8_t base_note{};
67  uint8_t base_vel{};
68 
69  Note operator()() { return {base_note, base_vel}; }
70  Note operator()(ossia::impulse) { return {base_note, base_vel}; }
71  template <typename T>
72  Note operator()(const T&)
73  {
74  return {base_note, base_vel};
75  }
76  Note operator()(float note) { return {(uint8_t)note, base_vel}; }
77  Note operator()(char note) { return {(uint8_t)note, base_vel}; }
78  Note operator()(int note) { return {(uint8_t)note, base_vel}; }
79  Note operator()(int note, int vel) { return {(uint8_t)note, (uint8_t)vel}; }
80  Note operator()(int note, float vel)
81  {
82  return {(uint8_t)note, (uint8_t)(vel * 127.f)};
83  }
84  Note operator()(const std::vector<ossia::value>& v)
85  {
86  switch(v.size())
87  {
88  case 0:
89  return operator()();
90  case 1:
91  return operator()(ossia::convert<int>(v[0]));
92  case 2: {
93  int note = ossia::convert<int>(v[0]);
94  switch(v[1].get_type())
95  {
96  case ossia::val_type::FLOAT:
97  return operator()(note, *v[1].v.target<float>());
98  case ossia::val_type::INT:
99  return operator()(note, *v[1].v.target<int>());
100  default:
101  return operator()(note, ossia::convert<int>(v[1]));
102  }
103  }
104  default:
105  return operator()(ossia::convert<int>(v[0]), ossia::convert<int>(v[1]));
106  }
107  }
108  template <std::size_t N>
109  Note operator()(const std::array<float, N>& v)
110  {
111  static_assert(N >= 2);
112  return operator()(v[0], v[1]);
113  }
114  };
115 
116  static constexpr uint8_t midi_clamp(int num)
117  {
118  return (uint8_t)ossia::clamp(num, 0, 127);
119  }
120  static void
121  run(const ossia::value_port& p1, const ossia::timed_vec<float>& startq,
122  const ossia::timed_vec<float>& tightness, const ossia::timed_vec<float>& endq,
123  const ossia::timed_vec<int>& basenote, const ossia::timed_vec<int>& basevel,
124  const ossia::timed_vec<int>& shift_note, const ossia::timed_vec<int>& note_random,
125  const ossia::timed_vec<int>& vel_random, const ossia::timed_vec<int>& chan_vec,
126  ossia::midi_port& p2, ossia::token_request tk, ossia::exec_state_facade st,
127  State& self)
128  {
129  // TODO : when arrays like [ 1, 25, 12, 37, 10, 40 ] are received
130  // send relevant chords
131 
132  // When a message is received, we have three cases :
133  // 1. Just an impulse: use base note & base vel
134  // 2. Just an int: it's the velocity, use base note
135  // 3. A tuple [int, int]: it's note and velocity
136 
137  // Then once we have a pair [int, int] we randomize and we output a note
138  // on.
139 
140  // At the end, scan running_notes: if any is going to end in this buffer,
141  // end it too.
142 
143  auto start = startq.rbegin()->second;
144  // TODO double precision = tightness.rbegin()->second;
145  auto end = endq.rbegin()->second;
146  auto shiftnote = shift_note.rbegin()->second;
147  auto base_note = midi_clamp(basenote.rbegin()->second);
148  auto base_vel = midi_clamp(basevel.rbegin()->second);
149  auto rand_note = note_random.rbegin()->second;
150  auto rand_vel = vel_random.rbegin()->second;
151  auto chan = chan_vec.rbegin()->second;
152 
153  const double sampleRatio = st.modelToSamples();
154  for(auto& in : p1.get_data())
155  {
156  auto note = in.value.apply(val_visitor{self, base_note, base_vel});
157 
158  if(rand_note != 0)
159  note.pitch += rnd::rand(-rand_note, rand_note);
160  if(rand_vel != 0)
161  note.vel += rnd::rand(-rand_vel, rand_vel);
162 
163  note.pitch = ossia::clamp((int)note.pitch + shiftnote, 0, 127);
164  note.vel = ossia::clamp((int)note.vel, 0, 127);
165 
166  if(note.vel != 0)
167  {
168  if(start == 0.f) // No quantification, start directly
169  {
170  auto& no = p2.note_on(chan, note.pitch, note.vel);
171  no.timestamp = in.timestamp;
172  if(end > 0.f)
173  {
174  self.running_notes.push_back(
175  {note, tk.from_physical_time_in_tick(in.timestamp, sampleRatio)});
176  }
177  else if(end == 0.f)
178  {
179  // Stop at the next sample
180  p2.note_off(chan, note.pitch, note.vel).timestamp = no.timestamp;
181  }
182  // else do nothing and just wait for a note off
183  }
184  else
185  {
186  // Find next time that matches the requested quantification
187  self.to_start.push_back({note, ossia::time_value{}});
188  }
189  }
190  else
191  {
192  // Just stop
193  p2.note_off(chan, note.pitch, note.vel).timestamp = in.timestamp;
194  }
195  }
196 
197  if(start != 0.f)
198  {
199  if(auto date = tk.get_quantification_date(1. / start))
200  {
201  start_all_notes(
202  *date, tk.to_physical_time_in_tick(*date, sampleRatio), chan, end, p2, self);
203  }
204  }
205  else
206  {
207  start_all_notes(
208  tk.prev_date, tk.to_physical_time_in_tick(tk.prev_date, sampleRatio), chan,
209  end, p2, self);
210  }
211 
212  if(end != 0.f)
213  {
214  if(auto date = tk.get_quantification_date(1. / end))
215  {
216  stop_notes(tk.to_physical_time_in_tick(*date, sampleRatio), chan, p2, self);
217  }
218  }
219  else
220  {
221  stop_notes(tk.to_physical_time_in_tick(tk.prev_date, sampleRatio), chan, p2, self);
222  }
223  }
224 
225  static void start_all_notes(
226  ossia::time_value date, ossia::physical_time date_phys, int chan, float endq,
227  ossia::midi_port& p2, State& self) noexcept
228  {
229  for(auto& note : self.to_start)
230  {
231  p2.note_on(chan, note.note.pitch, note.note.vel).timestamp = date_phys;
232 
233  if(endq > 0.f)
234  {
235  self.running_notes.push_back({{note.note}, date});
236  }
237  else if(endq == 0.f)
238  {
239  // Stop at the next sample
240  p2.note_off(chan, note.note.pitch, note.note.vel).timestamp = date_phys;
241  }
242  }
243  self.to_start.clear();
244  }
245 
246  static void
247  stop_notes(ossia::physical_time date_phys, int chan, ossia::midi_port& p2, State& self)
248  {
249  for(auto& note : self.running_notes)
250  {
251  p2.note_off(chan, note.note.pitch, note.note.vel).timestamp = date_phys;
252  }
253  self.running_notes.clear();
254  /*
255  for (auto it = self.running_notes.begin(); it !=
256  self.running_notes.end();)
257  {
258  auto& note = *it;
259  // note.date is the date at which the note was started.
260 
261  if (note.date.impl > tk.date.impl)
262  {
263  // Note was started "in the future", stop it right now as it means
264  we went back in time p2.note_off(chan, note.note.pitch,
265  note.note.vel).timestamp = tk.to_physical_time_in_tick(tk.prev_date,
266  sampleRatio); it = self.running_notes.erase(it);
267  }
268  else if(note.date.impl < tk.prev_date.impl)
269  {
270  // if we're
271  it = self.running_notes.erase(it);
272  }
273  else
274  {
275  ++it;
276  }
277 
278  if (note.date > tk.prev_date && note.date.impl < tk.date.impl)
279  {
280  p2.note_off(chan, note.note.pitch, note.note.vel)
281  .timestamp = (note.date - tk.prev_date).impl *
282  st.modelToSamples();
283 
284  it = self.running_notes.erase(it);
285  }
286  else
287  {
288  ++it;
289  }
290  }
291  */
292  }
293 };
294 }
295 }
Utilities for OSSIA data structures.
Definition: DeviceInterface.hpp:33
Definition: score-lib-process/Control/Widgets.hpp:486
Definition: score-lib-process/Control/Widgets.hpp:77
Definition: SimpleApi.hpp:32
Definition: SimpleApi.hpp:81
Definition: VelToNote.hpp:16
Definition: VelToNote.hpp:52
Definition: VelToNote.hpp:64
Definition: VelToNote.hpp:13