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")
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;
68 std::vector<Pending> pending;
69 std::vector<Running> running;
73 void prepare(halp::setup s)
78 pending.reserve(16 * 128);
79 running.reserve(16 * 128);
88 double quarter_at_pos{};
89 double frames_per_quarter{};
92 void set_clock(
const halp::tick_musical& tk)
noexcept
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;
102 clock.frames_per_quarter = 0.;
105 bool musical(
bool sync)
const noexcept {
return sync && clock.frames_per_quarter > 0.; }
106 double frames_per_unit(
bool sync)
const noexcept
108 return musical(sync) ? clock.frames_per_quarter : 1.;
110 double to_unit(int64_t frame,
bool sync)
const noexcept
113 ? clock.quarter_at_pos + (frame - clock.pos) / clock.frames_per_quarter
116 int64_t to_frame(
double unit,
bool sync)
const noexcept
118 return musical(sync) ? clock.pos
120 (unit - clock.quarter_at_pos)
121 * clock.frames_per_quarter)
122 : int64_t(
std::llround(unit));
125 double length(
float seconds,
bool sync)
const noexcept
127 return std::max(0.f, seconds) * setup.rate / frames_per_unit(sync);
133 const bool sync = inputs.grid.sync;
134 const double g = length(inputs.grid.value, sync);
135 if(g * frames_per_unit(sync) < 1.)
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)
142 return std::max(at, to_frame(prev + g, sync));
146 int64_t
end_of(int64_t start)
const noexcept
148 switch(inputs.length.value)
154 + int64_t(std::llround(std::max(0.f, inputs.duration.value) * setup.rate));
156 const bool sync = inputs.duration.sync;
157 const double d = length(inputs.duration.value, sync);
158 if(d * frames_per_unit(sync) < 1.)
160 const double u = to_unit(start, sync);
161 return std::max(start, to_frame((std::floor(u / d) + 1.) * d, sync));
167 void note_off(
const Note& n, int64_t at, int64_t pos)
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);
178 for(
auto& r : running)
179 note_off(r.note, at, pos);
184 using tick = halp::tick_flicks;
185 void operator()(
const tick& tk)
187 const int64_t pos = tk.position_in_frames;
188 const int64_t end = pos + tk.frames;
192 if(last_end >= 0 && pos != last_end)
196 for(
const libremidi::message& in : inputs.midi)
198 if(!in.is_note_on_or_off())
200 outputs.midi.push_back(in);
204 const Note note{in[1], in[2], (uint8_t)in.get_channel()};
205 const int64_t at = pos + in.timestamp;
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;
215 std::erase_if(pending, same);
216 for(
auto it = running.begin(); it != running.end();)
219 note_off(it->note, at, pos);
220 it = running.erase(it);
224 pending.push_back({note,
start_of(at), at, -1});
226 else if(inputs.length.value == UntilNoteOff)
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();)
236 note_off(it->note, at, pos);
237 it = running.erase(it);
246 for(
auto it = pending.begin(); it != pending.end();)
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);
264 for(
auto it = running.begin(); it != running.end();)
266 if(it->end >= 0 && it->end < end)
268 note_off(it->note, std::max(it->end, pos), pos);
269 it = running.erase(it);
277 auto& msgs = outputs.midi.midi_messages;
278 for(std::size_t i = 1; i < msgs.size(); i++)
280 if(!(msgs[i].timestamp < msgs[i - 1].timestamp))
282 auto m = std::move(msgs[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);