Loading...
Searching...
No Matches
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>
16inline std::ostream&
17operator<<(std::ostream& s, decltype(halp::tick_musical::signature) sig)
18{
19 return s << sig.num << " / " << sig.denom;
20}
21namespace Nodes
22{
23namespace PulseToNote
24{
25struct 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 bool fresh{};
78 };
79 std::vector<NoteIn> to_start;
80 std::vector<NoteIn> running_notes;
81
83 {
84 Node& st;
85 uint8_t base_note{};
86 uint8_t base_vel{};
87
88 Note operator()() { return {base_note, base_vel}; }
89 Note operator()(ossia::impulse) { return {base_note, base_vel}; }
90 template <typename T>
91 Note operator()(const T&)
92 {
93 return {base_note, base_vel};
94 }
95 Note operator()(float note) { return {(uint8_t)note, base_vel}; }
96 Note operator()(char note) { return {(uint8_t)note, base_vel}; }
97 Note operator()(int note) { return {(uint8_t)note, base_vel}; }
98 Note operator()(int note, int vel) { return {(uint8_t)note, (uint8_t)vel}; }
99 Note operator()(int note, float vel) { return {(uint8_t)note, (uint8_t)(vel * 127.f)}; }
100 Note operator()(const std::vector<ossia::value>& v)
101 {
102 switch(v.size())
103 {
104 case 0:
105 return operator()();
106 case 1:
107 return operator()(ossia::convert<int>(v[0]));
108 case 2: {
109 int note = ossia::convert<int>(v[0]);
110 switch(v[1].get_type())
111 {
112 case ossia::val_type::FLOAT:
113 return operator()(note, *v[1].v.target<float>());
114 case ossia::val_type::INT:
115 return operator()(note, *v[1].v.target<int>());
116 default:
117 return operator()(note, ossia::convert<int>(v[1]));
118 }
119 }
120 default:
121 return operator()(ossia::convert<int>(v[0]), ossia::convert<int>(v[1]));
122 }
123 }
124 template <std::size_t N>
125 Note operator()(const std::array<float, N>& v)
126 {
127 static_assert(N >= 2);
128 return operator()(v[0], v[1]);
129 }
130 };
131
132 static constexpr uint8_t midi_clamp(int num) { return (uint8_t)ossia::clamp(num, 0, 127); }
133
134 using tick = halp::tick_flicks;
135 void operator()(const tick& tk)
136 {
137 // TODO : when arrays like [ 1, 25, 12, 37, 10, 40 ] are received
138 // send relevant chords
139
140 // When a message is received, we have three cases :
141 // 1. Just an impulse: use base note & base vel
142 // 2. Just an int: it's the velocity, use base note
143 // 3. A tuple [int, int]: it's note and velocity
144
145 // Then once we have a pair [int, int] we randomize and we output a note
146 // on.
147
148 // At the end, scan running_notes: if any is going to end in this buffer,
149 // end it too.
150
151 // FIXME next version choose between these behaviours
152
153#define STOP_AT_NEXT_SAMPLE_FOR_ZERO_END_QUANT 1
154
155 const auto& start = inputs.start_quant.value;
156 // TODO double precision = tightness.rbegin()->second;
157 const auto& end = inputs.end_quant.value;
158 const auto& shiftnote = inputs.shift_note.value;
159 const auto& base_note = midi_clamp(inputs.basenote.value);
160 const auto& base_vel = midi_clamp(inputs.basevel.value);
161 const auto& rand_note = inputs.note_random.value;
162 const auto& rand_vel = inputs.vel_random.value;
163 const auto& chan = inputs.channel.value;
164
165 for(auto& in : inputs.port.value->get_data())
166 {
167 auto note = in.value.apply(val_visitor{*this, base_note, base_vel});
168
169 if(rand_note != 0)
170 note.pitch = std::clamp(
171 (int)note.pitch + (int)rnd::rand(-rand_note, rand_note), 0, 127);
172
173 if(rand_vel != 0)
174 note.vel
175 = std::clamp((int)note.vel + (int)rnd::rand(-rand_vel, rand_vel), 0, 127);
176
177 note.pitch = ossia::clamp((int)note.pitch + shiftnote, 0, 127);
178 note.vel = ossia::clamp((int)note.vel, 0, 127);
179
180 if(note.vel != 0.)
181 {
182 if(start == 0.f) // No quantification, start directly
183 {
184 outputs.midi.note_on(chan, note.pitch, note.vel).timestamp = in.timestamp;
185 if(end > 0.f)
186 {
187 this->running_notes.push_back(
188 {note, tk.position_in_frames + in.timestamp, true});
189 }
190 else if(end == 0.f)
191 {
192 // Stop at the next sample
193#if STOP_AT_NEXT_SAMPLE_FOR_ZERO_END_QUANT
194 outputs.midi.note_off(chan, note.pitch, note.vel).timestamp = in.timestamp;
195#endif
196 }
197 else
198 {
199 // else do nothing and just wait for a note off
200 }
201 }
202 else
203 {
204 // Find next time that matches the requested quantification
205 this->to_start.push_back({note, {}});
206 }
207 }
208 else
209 {
210 // Just stop
211 outputs.midi.note_off(chan, note.pitch, note.vel).timestamp = in.timestamp;
212 }
213 }
214 std::optional<int> start_quant_date{};
215
216 if(!to_start.empty())
217 {
218 if(start != 0.f)
219 {
220 for(auto [date, q] : tk.get_quantification_date(4. * start))
221 {
222 start_all_notes(tk.position_in_frames + date, date, chan, end);
223 start_quant_date = date;
224 break;
225 }
226 }
227 else
228 {
229 start_all_notes(tk.position_in_frames, 0, chan, end);
230 start_quant_date = 0;
231 }
232 }
233
234 if(end != 0.f)
235 {
236 for(auto [date, q] : tk.get_quantification_date(4. * end))
237 {
238 if(!start_quant_date || date > start_quant_date)
239 {
240 stop_notes(date, chan);
241 }
242 }
243 }
244 else
245 {
246 // FIXME this means that the notes cannot run ? instead we should just rely on note off maybe? but it's an impulse...
247 // we should do this for "impulses" and individual ints, but let the normal note off
248#if STOP_AT_NEXT_SAMPLE_FOR_ZERO_END_QUANT
249 stop_notes(0, chan);
250#endif
251 }
252 }
253
254 void start_all_notes(
255 ossia::physical_time abs_date, ossia::physical_time date_phys, int chan,
256 float endq) noexcept
257 {
258 for(auto& note : this->to_start)
259 {
260 outputs.midi.note_on(chan, note.note.pitch, note.note.vel).timestamp = date_phys;
261
262 if(endq > 0.f)
263 {
264 this->running_notes.push_back({{note.note}, abs_date});
265 }
266 else if(endq == 0.f)
267 {
268 // Stop at the next sample
269
270#if STOP_AT_NEXT_SAMPLE_FOR_ZERO_END_QUANT
271 outputs.midi.note_off(chan, note.note.pitch, note.note.vel).timestamp
272 = date_phys;
273#endif
274 }
275 }
276 this->to_start.clear();
277 }
278
279 void stop_notes(ossia::physical_time date_phys, int chan)
280 {
281 for(auto it = this->running_notes.begin(); it != this->running_notes.end();)
282 {
283 auto& note = *it;
284 if(!note.fresh)
285 {
286 it = this->running_notes.erase(it);
287 outputs.midi.note_off(chan, note.note.pitch, note.note.vel).timestamp
288 = date_phys;
289 }
290 else
291 {
292 note.fresh = false;
293 ++it;
294 }
295 }
296 /*
297 for(auto it = this->running_notes.begin(); it != this->running_notes.end();)
298 {
299 auto& note = *it;
300 // note.date is the date at which the note was started.
301
302 if(note.date.impl > tk.date.impl)
303 {
304 // Note was started "in the future", stop it right now as it means we went back in time
305 p2.note_off(chan, note.note.pitch, note.note.vel).timestamp
306 = tk.to_physical_time_in_tick(tk.prev_date, sampleRatio);
307 it = this->running_notes.erase(it);
308 }
309 else if(note.date.impl < tk.prev_date.impl)
310 {
311 // if we're
312 it = this->running_notes.erase(it);
313 }
314 else
315 {
316 ++it;
317 }
318
319 if(note.date > tk.prev_date && note.date.impl < tk.date.impl)
320 {
321 p2.note_off(chan, note.note.pitch, note.note.vel).timestamp
322 = (note.date - tk.prev_date).impl * st.modelToSamples();
323
324 it = this->running_notes.erase(it);
325 }
326 else
327 {
328 ++it;
329 }
330 }
331*/
332 }
333};
334}
335}
Definition VelToNote.hpp:67
Definition VelToNote.hpp:74
Definition VelToNote.hpp:83
Definition VelToNote.hpp:26
Definition Types.hpp:48
Definition Types.hpp:39