OSSIA
Open Scenario System for Interactive Application
Loading...
Searching...
No Matches
dataflow/nodes/midi.hpp
1#pragma once
2#include <ossia/dataflow/graph_node.hpp>
3#include <ossia/dataflow/node_process.hpp>
4#include <ossia/dataflow/port.hpp>
5#include <ossia/detail/flat_multiset.hpp>
6
7#include <libremidi/ump_events.hpp>
8
9#include <algorithm>
10
11namespace ossia::nodes
12{
13
14using midi_size_t = uint8_t;
15struct note_data
16{
17 time_value start{};
18 time_value duration{};
19
20 midi_size_t pitch{};
21 midi_size_t velocity{};
22};
23
24inline bool is_note_off(const libremidi::ump& m) noexcept
25{
26 return (m.get_status_code() & 0xF0) == 0x80;
27}
28
29inline time_value note_end(const note_data& n) noexcept
30{
31 return n.start + n.duration;
32}
33
34struct note_comparator
35{
36 using is_transparent = std::true_type;
37 bool operator()(const note_data& lhs, const note_data& rhs) const
38 {
39 return lhs.start < rhs.start;
40 }
41 bool operator()(const note_data& lhs, int64_t rhs) const
42 {
43 return lhs.start.impl < rhs;
44 }
45};
46
50{
51 using is_transparent = std::true_type;
52 bool operator()(const note_data& lhs, const note_data& rhs) const
53 {
54 return note_end(lhs) < note_end(rhs);
55 }
56 bool operator()(const note_data& lhs, int64_t rhs) const
57 {
58 return note_end(lhs).impl < rhs;
59 }
60};
61
62class midi final : public ossia::nonowning_graph_node
63{
64 ossia::midi_outlet midi_out;
65
66public:
67 using note_set = ossia::flat_multiset<note_data, note_comparator>;
68 using note_end_set = ossia::flat_multiset<note_data, note_end_comparator>;
69 explicit midi(int64_t notes)
70 {
71 m_outlets.push_back(&midi_out);
72 int64_t to_reserve = std::max(notes * 1.1, 128.);
73 m_notes.reserve(to_reserve);
74 m_orig_notes.reserve(to_reserve);
75 m_by_end.reserve(to_reserve);
76 m_playing_notes.reserve(to_reserve);
77 m_to_stop.reserve(64);
78 m_to_resume.reserve(64);
79 }
80
81 ~midi() override = default;
82
83 std::string label() const noexcept override { return "midi"; }
84
85 void set_channel(int c) { m_channel = c - 1; }
86
87 void add_note(note_data nd)
88 {
89 m_orig_notes.insert(nd);
90 m_by_end.insert(nd);
91 // The playing scan takes start >= t.prev_date, and m_prev_date is where the
92 // next tick starts: a note landing exactly there is still to be played.
93 // With a strict comparison it was dropped, and every note at date 0 with it.
94 if(nd.start >= m_prev_date)
95 {
96 m_notes.insert(nd);
97 }
98 }
99
100 // The comparators only order on one date: erasing by key would remove every
101 // note sharing it, and find() could return a note of another pitch.
102 template <typename Set>
103 static bool erase_exact(Set& set, const note_data& nd)
104 {
105 auto [first, last] = set.equal_range(nd);
106 for(auto it = first; it != last; ++it)
107 {
108 if(it->pitch == nd.pitch)
109 {
110 set.erase(it);
111 return true;
112 }
113 }
114 return false;
115 }
116
117 template <typename Set>
118 static bool contains_exact(const Set& set, const note_data& nd)
119 {
120 auto [first, last] = set.equal_range(nd);
121 for(auto it = first; it != last; ++it)
122 if(it->pitch == nd.pitch)
123 return true;
124 return false;
125 }
126
127 void remove_note(note_data nd)
128 {
129 erase_exact(m_orig_notes, nd);
130 erase_exact(m_by_end, nd);
131 erase_exact(m_notes, nd);
132 if(erase_exact(m_playing_notes, nd))
133 m_to_stop.insert(nd);
134 }
135
136 void replace_notes(note_set&& notes)
137 {
138 for(auto& note : m_playing_notes)
139 m_to_stop.insert(note);
140 m_playing_notes.clear();
141
142 m_to_resume.clear();
143
144 using namespace std;
145 swap(m_orig_notes, notes);
146 rebuild_end_index();
147 m_notes.clear();
148
149 auto start_it = m_orig_notes.lower_bound(m_prev_date.impl);
150 if(start_it != m_orig_notes.end())
151 {
152 m_notes.tree().get_sequence_ref().assign(start_it, m_orig_notes.end());
153 }
154 }
155
156 void transport(ossia::time_value date)
157 {
158 requestTransport = true;
159 m_transport_date = date;
160 }
161
162 void transport_impl(ossia::time_value date)
163 {
164 // Whatever was playing has to be released: the playhead no longer follows
165 // from it.
166 m_to_stop.insert(m_playing_notes.begin(), m_playing_notes.end());
167 m_playing_notes.clear();
168 m_to_resume.clear();
169
170 // Everything starting at or after the new position is to be played again,
171 // and a note the position lands inside of is resumed from its middle -
172 // the note-on scan can never pick those up, since it only matches notes
173 // starting at or after the beginning of a tick.
174 m_notes.clear();
175 for(const note_data& n : m_orig_notes)
176 {
177 if(n.start >= date)
178 m_notes.insert(n);
179 else if((n.start + n.duration) > date)
180 m_to_resume.insert(n);
181 }
182
183 m_prev_date = date;
184 }
185
186 void update_note(note_data oldNote, note_data newNote)
187 {
188 // OPTIMIZEME
189 remove_note(oldNote);
190 add_note(newNote);
191 }
192
193 void set_notes(note_set&& notes)
194 {
195 m_notes = std::move(notes);
196 m_orig_notes = m_notes;
197 rebuild_end_index();
198
199 auto max_it = m_notes.lower_bound({m_prev_date});
200 if(max_it != m_notes.begin()) // TODO handle the begin case correctly
201 m_notes.erase(m_notes.begin(), max_it);
202 }
203
204 bool mustStop{};
205 bool requestTransport{};
206
207private:
208 void rebuild_end_index()
209 {
210 m_by_end.clear();
211 m_by_end.insert(m_orig_notes.begin(), m_orig_notes.end());
212 }
213
216 void resume_notes(ossia::midi_port& mp, int64_t tick_start)
217 {
218 for(const note_data& note : m_to_resume)
219 {
220 mp.messages.push_back(
221 libremidi::from_midi1::note_on(m_channel, note.pitch, note.velocity));
222 mp.messages.back().timestamp = tick_start;
223 m_playing_notes.insert(note);
224 }
225 m_to_resume.clear();
226 }
227
231 void rewind(
232 const ossia::token_request& t, ossia::midi_port& mp, double samplesratio,
233 int64_t tick_start)
234 {
235 // physical_write_duration divides an absolute duration by a signed speed,
236 // so it comes out negative when rewinding.
237 const auto tick_end
238 = tick_start + std::abs(t.physical_write_duration(samplesratio));
239 const auto stamp = [&](time_value at) {
240 return std::clamp(
241 t.to_physical_time_in_tick(at, samplesratio), tick_start,
242 std::max(tick_start, tick_end - 1));
243 };
244
245 // Enter the notes whose end the playhead went back over, from their tail.
246 // ]date; prev_date] : an end landing exactly on prev_date was not sounding
247 // there, the interval of a note being [start; end[.
248 {
249 const note_data lo{t.date, 0_tv};
250 const note_data hi{t.prev_date, 0_tv};
251 for(auto it = m_by_end.upper_bound(lo), last = m_by_end.upper_bound(hi);
252 it != last; ++it)
253 {
254 if(contains_exact(m_playing_notes, *it))
255 continue;
256
257 mp.messages.push_back(
258 libremidi::from_midi1::note_on(m_channel, it->pitch, it->velocity));
259 mp.messages.back().timestamp = stamp(note_end(*it));
260
261 m_playing_notes.insert(*it);
262 erase_exact(m_notes, *it);
263 }
264 }
265
266 // Leave the ones whose start it went back over. After entering, so that a
267 // note falling entirely inside this tick is played then released, the same
268 // way the forward pass handles a note shorter than its tick.
269 for(auto it = m_playing_notes.begin(); it != m_playing_notes.end();)
270 {
271 if(it->start > t.date)
272 {
273 mp.messages.push_back(
274 libremidi::from_midi1::note_off(m_channel, it->pitch, 0));
275 mp.messages.back().timestamp = stamp(it->start);
276 it = m_playing_notes.erase(it);
277 }
278 else
279 {
280 ++it;
281 }
282 }
283
284 // Everything the playhead went back over the start of has to be playable
285 // again. Exactly once: m_notes is a multiset, and a note that had not been
286 // played yet is still in it.
287 const auto first = m_orig_notes.upper_bound({t.date});
288 const auto last = m_orig_notes.upper_bound({t.prev_date});
289 for(auto it = first; it != last; ++it)
290 {
291 erase_exact(m_notes, *it);
292 m_notes.insert(*it);
293 }
294 }
295
300 void stop_finished_notes(
301 const ossia::token_request& t, ossia::midi_port& mp, double samplesratio,
302 int64_t tick_start)
303 {
304 for(auto it = m_playing_notes.begin(); it != m_playing_notes.end();)
305 {
306 const note_data& note = *it;
307 const auto end_time = note.start + note.duration;
308
309 if(end_time < t.date)
310 {
311 mp.messages.push_back(libremidi::from_midi1::note_off(m_channel, note.pitch, 0));
312 mp.messages.back().timestamp
313 = std::max(tick_start, t.to_physical_time_in_tick(end_time, samplesratio));
314
315 it = m_playing_notes.erase(it);
316 }
317 else
318 {
319 ++it;
320 }
321 }
322 }
323
324 void run(const ossia::token_request& t, ossia::exec_state_facade e) noexcept override
325 {
326 ossia::midi_port& mp = *midi_out;
327
328 struct scope_guard
329 {
330 midi& self;
331 const ossia::token_request& t;
332 ossia::midi_port& mp;
333 std::size_t first_message;
334 ~scope_guard()
335 {
336 // Consumers (VST3 event lists, MIDI outputs...) expect events in
337 // chronological order, and a note-off must win over a note-on sharing
338 // its timestamp, else back-to-back notes on the same pitch cancel out.
339 auto begin = mp.messages.begin() + first_message;
340 std::stable_sort(begin, mp.messages.end(), [](const auto& lhs, const auto& rhs) {
341 if(lhs.timestamp != rhs.timestamp)
342 return lhs.timestamp < rhs.timestamp;
343 return is_note_off(lhs) && !is_note_off(rhs);
344 });
345
346 self.m_prev_date = t.date;
347
348 if(self.requestTransport)
349 {
350 self.transport_impl(self.m_transport_date);
351 self.requestTransport = false;
352 }
353 }
354 } guard{*this, t, mp, mp.messages.size()};
355
356 const auto samplesratio = e.modelToSamples();
357 const auto tick_start = t.physical_start(samplesratio);
358
359 if(t.end_discontinuous)
360 {
361 auto& mess = mp.messages;
362 for(auto note : m_playing_notes)
363 {
364 mess.push_back(libremidi::from_midi1::note_off(m_channel, note.pitch, 0));
365 mess.back().timestamp = tick_start;
366 }
367 for(auto note : m_to_stop)
368 {
369 mess.push_back(libremidi::from_midi1::note_off(m_channel, note.pitch, 0));
370 mess.back().timestamp = tick_start;
371 }
372 m_playing_notes.clear();
373 m_to_stop.clear();
374 return;
375 }
376
377 for(const note_data& note : m_to_stop)
378 {
379 mp.messages.push_back(libremidi::from_midi1::note_off(m_channel, note.pitch, 0));
380 mp.messages.back().timestamp = tick_start;
381 }
382 m_to_stop.clear();
383
384 if(mustStop)
385 {
386 for(auto& note : m_playing_notes)
387 {
388 mp.messages.push_back(libremidi::from_midi1::note_off(m_channel, note.pitch, 0));
389 mp.messages.back().timestamp = tick_start;
390 }
391
392 m_notes = m_orig_notes;
393 m_playing_notes.clear();
394 m_to_resume.clear();
395
396 mustStop = false;
397 }
398 else
399 {
400 // Not when rewinding: everything may well have been played already, and
401 // going back over it is exactly what has to bring it back.
402 if(!t.backward() && m_notes.empty() && m_playing_notes.empty()
403 && m_to_resume.empty())
404 return;
405
406 resume_notes(mp, tick_start);
407
408 if(t.backward())
409 {
410 rewind(t, mp, samplesratio, tick_start);
411 }
412 else if(t.forward())
413 {
414 stop_finished_notes(t, mp, samplesratio, tick_start);
415
416 // Look for all the messages
417 auto max_it = m_notes.lower_bound({t.date});
418 for(auto it = m_notes.begin(); it < max_it;)
419 {
420 note_data& note = const_cast<note_data&>(*it);
421 auto start_time = note.start;
422 if(start_time >= t.prev_date && start_time < t.date)
423 {
424 // Send note_on
425 mp.messages.push_back(
426 libremidi::from_midi1::note_on(m_channel, note.pitch, note.velocity));
427 mp.messages.back().timestamp
428 = t.to_physical_time_in_tick(start_time, samplesratio);
429
430 m_playing_notes.insert(note);
431 it = m_notes.erase(it);
432 max_it = std::lower_bound(
433 it, m_notes.end(), t.date.impl + 1, note_comparator{});
434 }
435 else
436 {
437 ++it;
438 }
439 }
440
441 // Notes short enough to begin and end inside this tick were not in
442 // m_playing_notes when the first pass ran: stop them here.
443 stop_finished_notes(t, mp, samplesratio, tick_start);
444 }
445 }
446 }
447
448 note_set m_notes;
449 note_set m_orig_notes;
450 note_set m_playing_notes;
451 note_set m_to_stop;
452 note_set m_to_resume;
453 note_end_set m_by_end;
454 time_value m_prev_date{};
455 time_value m_transport_date{};
456
457 int m_channel{};
458};
459
460class midi_node_process final : public ossia::node_process
461{
462public:
463 using ossia::node_process::node_process;
464
465 void transport_impl(ossia::time_value date) override
466 {
467 midi& n = *static_cast<midi*>(node.get());
468 n.transport(date);
469 }
470
471 void stop() override
472 {
473 midi& n = *static_cast<midi*>(node.get());
474 n.request(ossia::token_request{});
475 n.mustStop = true;
476 }
477};
478}
Definition dataflow/nodes/midi.hpp:50
The time_value class.
Definition ossia/editor/scenario/time_value.hpp:30