Loading...
Searching...
No Matches
Quantifier_v2.hpp
1#pragma once
2#include <Fx/Quantifier.hpp>
3
4#include <algorithm>
5#include <cmath>
6
7namespace Nodes::Quantifier::v2
8{
10enum NoteLength
11{
14 UntilNoteOff,
16 FixedDuration,
18 EndOnGrid
19};
20
21struct Node
22{
23 halp_meta(name, "Midi quantify")
24 halp_meta(c_name, "Quantifier")
25 halp_meta(category, "Midi")
26 halp_meta(manual_url, "https://ossia.io/score-docs/processes/midi-utilities.html#quantifier")
27 halp_meta(author, "ossia score")
28 halp_meta(description, "Quantifies a MIDI input")
29 halp_meta(uuid, "a0cb1144-05a8-4f9b-b22c-81cd964314fb")
30
31 struct
32 {
33 halp::midi_bus<"in", libremidi::message> midi;
35 halp::time_chooser<"Grid", halp::range{0., 4., 0.125}> grid;
39 halp::hslider_f32<"Tightness", halp::range{0.f, 1.f, 0.8f}> tightness;
40 halp::enum_t<NoteLength, "Length"> length;
42 halp::time_chooser<"Duration", halp::range{0., 8., 0.25}> duration;
43 } inputs;
44 struct
45 {
46 midi_out midi;
47 } outputs;
48
49 struct Note
50 {
51 uint8_t pitch{};
52 uint8_t vel{};
53 uint8_t chan{};
54 };
55
56 struct Pending
57 {
58 Note note;
59 int64_t start{}; // absolute frame
60 int64_t played{}; // absolute frame of the note-on as played
61 int64_t length{-1}; // frames, once released before starting
62 };
63 struct Running
64 {
65 Note note;
66 int64_t end{-1}; // absolute frame; -1: until the note-off
67 };
68 std::vector<Pending> pending;
69 std::vector<Running> running;
70 int64_t last_end{-1};
71
72 halp::setup setup;
73 void prepare(halp::setup s)
74 {
75 setup = s;
76 // A key (channel, pitch) is at most once in either list: they never grow
77 // on the audio thread.
78 pending.reserve(16 * 128);
79 running.reserve(16 * 128);
80 }
81
85 struct Clock
86 {
87 int64_t pos{};
88 double quarter_at_pos{};
89 double frames_per_quarter{};
90 } clock;
91
92 void set_clock(const halp::tick_musical& tk) noexcept
93 {
94 clock.pos = tk.position_in_frames;
95 clock.quarter_at_pos = tk.start_position_in_quarters;
96 const double dq = tk.end_position_in_quarters - tk.start_position_in_quarters;
97 if(dq > 0. && tk.frames > 0)
98 clock.frames_per_quarter = tk.frames / dq;
99 else if(tk.tempo > 0.)
100 clock.frames_per_quarter = 60. / tk.tempo * setup.rate;
101 else
102 clock.frames_per_quarter = 0.;
103 }
104
105 bool musical(bool sync) const noexcept { return sync && clock.frames_per_quarter > 0.; }
106 double frames_per_unit(bool sync) const noexcept
107 {
108 return musical(sync) ? clock.frames_per_quarter : 1.;
109 }
110 double to_unit(int64_t frame, bool sync) const noexcept
111 {
112 return musical(sync)
113 ? clock.quarter_at_pos + (frame - clock.pos) / clock.frames_per_quarter
114 : double(frame);
115 }
116 int64_t to_frame(double unit, bool sync) const noexcept
117 {
118 return musical(sync) ? clock.pos
119 + std::llround(
120 (unit - clock.quarter_at_pos)
121 * clock.frames_per_quarter)
122 : int64_t(std::llround(unit));
123 }
125 double length(float seconds, bool sync) const noexcept
126 {
127 return std::max(0.f, seconds) * setup.rate / frames_per_unit(sync);
128 }
129
131 int64_t start_of(int64_t at) const noexcept
132 {
133 const bool sync = inputs.grid.sync;
134 const double g = length(inputs.grid.value, sync);
135 if(g * frames_per_unit(sync) < 1.)
136 return at;
137 const double u = to_unit(at, sync);
138 const double prev = std::floor(u / g) * g;
139 const double window = (1. - std::clamp(inputs.tightness.value, 0.f, 1.f)) * g / 2.;
140 if(u - prev <= window)
141 return at; // just missed the grid point: on time
142 return std::max(at, to_frame(prev + g, sync));
143 }
144
146 int64_t end_of(int64_t start) const noexcept
147 {
148 switch(inputs.length.value)
149 {
150 case UntilNoteOff:
151 return -1;
152 case FixedDuration:
153 return start
154 + int64_t(std::llround(std::max(0.f, inputs.duration.value) * setup.rate));
155 case EndOnGrid: {
156 const bool sync = inputs.duration.sync;
157 const double d = length(inputs.duration.value, sync);
158 if(d * frames_per_unit(sync) < 1.)
159 return start;
160 const double u = to_unit(start, sync);
161 return std::max(start, to_frame((std::floor(u / d) + 1.) * d, sync));
162 }
163 }
164 return -1;
165 }
166
167 void note_off(const Note& n, int64_t at, int64_t pos)
168 {
169 auto m = libremidi::channel_events::note_off(n.chan, n.pitch, 0);
170 m.timestamp = std::max<int64_t>(0, at - pos);
171 outputs.midi.push_back(m);
172 }
173
176 void release_all(int64_t at, int64_t pos)
177 {
178 for(auto& r : running)
179 note_off(r.note, at, pos);
180 running.clear();
181 pending.clear();
182 }
183
184 using tick = halp::tick_flicks;
185 void operator()(const tick& tk)
186 {
187 const int64_t pos = tk.position_in_frames;
188 const int64_t end = pos + tk.frames;
189 set_clock(tk);
190
191 // Playback restarted or jumped back: nothing that was waiting still holds
192 if(last_end >= 0 && pos != last_end)
193 release_all(pos, pos);
194 last_end = end;
195
196 for(const libremidi::message& in : inputs.midi)
197 {
198 if(!in.is_note_on_or_off())
199 {
200 outputs.midi.push_back(in);
201 continue;
202 }
203
204 const Note note{in[1], in[2], (uint8_t)in.get_channel()};
205 const int64_t at = pos + in.timestamp;
206 const bool on
207 = in.get_message_type() == libremidi::message_type::NOTE_ON && note.vel != 0;
208 auto same = [&](const auto& x) {
209 return x.note.pitch == note.pitch && x.note.chan == note.chan;
210 };
211
212 if(on)
213 {
214 // The same key again: what was waiting is replaced, what sounds ends
215 std::erase_if(pending, same);
216 for(auto it = running.begin(); it != running.end();)
217 if(same(*it))
218 {
219 note_off(it->note, at, pos);
220 it = running.erase(it);
221 }
222 else
223 ++it;
224 pending.push_back({note, start_of(at), at, -1});
225 }
226 else if(inputs.length.value == UntilNoteOff)
227 {
228 // Not started yet: it will play the length it was played with.
229 // Started: it ends now.
230 for(auto& p : pending)
231 if(same(p) && p.length < 0)
232 p.length = std::max<int64_t>(1, at - p.played);
233 for(auto it = running.begin(); it != running.end();)
234 if(same(*it))
235 {
236 note_off(it->note, at, pos);
237 it = running.erase(it);
238 }
239 else
240 ++it;
241 }
242 // The other lengths do not depend on the note-off
243 }
244
245 // Notes whose start is in this tick
246 for(auto it = pending.begin(); it != pending.end();)
247 {
248 if(it->start < end)
249 {
250 const int64_t start = std::max(it->start, pos);
251 auto no = libremidi::channel_events::note_on(
252 it->note.chan, it->note.pitch, it->note.vel);
253 no.timestamp = start - pos;
254 outputs.midi.push_back(no);
255 const int64_t stop = it->length >= 0 ? start + it->length : end_of(start);
256 running.push_back({it->note, stop});
257 it = pending.erase(it);
258 }
259 else
260 ++it;
261 }
262
263 // Notes whose end is in this tick: after the note-ons at the same frame
264 for(auto it = running.begin(); it != running.end();)
265 {
266 if(it->end >= 0 && it->end < end)
267 {
268 note_off(it->note, std::max(it->end, pos), pos);
269 it = running.erase(it);
270 }
271 else
272 ++it;
273 }
274 // In time order; at the same frame, in the order they were made. An
275 // insertion sort: stable without the buffer std::stable_sort allocates,
276 // and the messages come nearly sorted already.
277 auto& msgs = outputs.midi.midi_messages;
278 for(std::size_t i = 1; i < msgs.size(); i++)
279 {
280 if(!(msgs[i].timestamp < msgs[i - 1].timestamp))
281 continue;
282 auto m = std::move(msgs[i]);
283 std::size_t j = i;
284 for(; j > 0 && m.timestamp < msgs[j - 1].timestamp; j--)
285 msgs[j] = std::move(msgs[j - 1]);
286 msgs[j] = std::move(m);
287 }
288 }
289};
290}
STL namespace.
Definition Quantifier_v2.hpp:86
Definition Quantifier_v2.hpp:50
Definition Quantifier_v2.hpp:57
Definition Quantifier_v2.hpp:64
Definition Quantifier_v2.hpp:22
halp::hslider_f32<"Tightness", halp::range{0.f, 1.f, 0.8f}> tightness
Definition Quantifier_v2.hpp:39
int64_t start_of(int64_t at) const noexcept
When a note played at at starts.
Definition Quantifier_v2.hpp:131
double length(float seconds, bool sync) const noexcept
A time chooser's value, which is in seconds even when synced.
Definition Quantifier_v2.hpp:125
halp::time_chooser<"Duration", halp::range{0., 8., 0.25}> duration
The length of a note, or the grid its end falls on.
Definition Quantifier_v2.hpp:42
halp::time_chooser<"Grid", halp::range{0., 4., 0.125}> grid
Notes start on this grid; 0 lets them through as they come.
Definition Quantifier_v2.hpp:35
int64_t end_of(int64_t start) const noexcept
When a note starting at start ends, or -1 for its note-off.
Definition Quantifier_v2.hpp:146
void release_all(int64_t at, int64_t pos)
Definition Quantifier_v2.hpp:176