Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
routing_utility_kernels.hpp
Go to the documentation of this file.
1#pragma once
2
4
5#include <memory>
6
7namespace pulp::midi {
8
9namespace routing_detail {
10
11inline UmpPacket note_off_packet(bool midi2, std::uint8_t group,
12 std::uint8_t channel, std::uint8_t note) noexcept {
13 if (midi2) return UmpPacket::note_off_2(group, channel, note);
14 UmpPacket packet;
15 packet.word_count = 1;
16 packet.words[0] = (std::uint32_t{0x2} << 28) |
17 (static_cast<std::uint32_t>(group & 0x0f) << 24) |
18 (static_cast<std::uint32_t>(0x80 | (channel & 0x0f)) << 16) |
19 (static_cast<std::uint32_t>(note & 0x7f) << 8);
20 return packet;
21}
22
24 public:
25 static constexpr std::size_t kMaxTrackedOwnership = 4096;
26
27 constexpr bool empty() const noexcept { return total_ == 0; }
28 bool output_ownership_empty() const noexcept {
29 if (midi_debt_total_ != 0 || ump_debt_total_ != 0)
30 return false;
31 return !has_forwarded(midi_entries_, midi_size_) &&
32 !has_forwarded(ump_entries_, ump_size_);
33 }
34 void discard_input_ownership() const noexcept { clear_all(); }
35
36 bool can_forward(const MidiEvent& event) const noexcept {
37 if (!is_attack(event)) return true;
38 const auto key = utility_detail::key_index(event.channel(), event.note());
39 return midi_overflow_suppressed_[key] == 0 && midi_size_ < midi_entries_.size();
40 }
41 bool can_forward(const UmpPacket& packet) const noexcept {
42 if (!is_attack(packet)) return true;
43 const auto key = ump_key(packet);
44 return ump_overflow_suppressed_[key] == 0 && ump_size_ < ump_entries_.size();
45 }
46
47 bool consume_suppressed_release(const MidiEvent& event) noexcept {
48 if (!is_release(event)) return false;
49 return consume_suppressed(midi_entries_, midi_size_, midi_flushed_suppressed_,
50 midi_overflow_suppressed_,
51 utility_detail::key_index(event.channel(), event.note()));
52 }
53 bool consume_suppressed_release(const UmpPacket& packet) noexcept {
54 if (!is_release(packet)) return false;
55 return consume_suppressed(ump_entries_, ump_size_, ump_flushed_suppressed_,
56 ump_overflow_suppressed_,
57 ump_key(packet));
58 }
59
60 void record(const MidiEvent& event, bool forwarded) noexcept {
61 const auto key = utility_detail::key_index(event.channel(), event.note());
62 if (is_attack(event))
63 record_attack(midi_entries_, midi_size_, midi_overflow_suppressed_, key,
64 forwarded);
65 else if (is_release(event))
66 record_release(midi_entries_, midi_size_, midi_release_debt_,
67 midi_debt_total_, key, forwarded);
68 }
69 void record(const UmpPacket& packet, bool forwarded) noexcept {
70 const auto key = ump_key(packet);
71 if (is_attack(packet))
72 record_attack(ump_entries_, ump_size_, ump_overflow_suppressed_, key,
73 forwarded);
74 else if (is_release(packet))
75 record_release(ump_entries_, ump_size_, ump_release_debt_,
76 ump_debt_total_, key, forwarded);
77 }
78
79 void retain_unprocessed_ownership(const MidiBuffer& input) noexcept {
80 for (const auto& event : input) {
81 if (!consume_suppressed_release(event)) record(event, false);
82 }
83 if (const auto* ump = input.ump())
84 for (const auto& event : *ump) {
85 if (!consume_suppressed_release(event.packet))
86 record(event.packet, false);
87 }
88 }
89
90 template <typename Emit>
91 bool drain_midi_releases(Emit&& emit) const noexcept {
92 return drain_pending(midi_release_debt_, midi_debt_total_, midi_drain_cursor_,
93 [&](std::size_t key) {
94 return emit(static_cast<std::uint8_t>(key / 128),
95 static_cast<std::uint8_t>(key % 128));
96 });
97 }
98
99 template <typename Emit>
100 bool drain_ump_releases(Emit&& emit) const noexcept {
101 return drain_pending(ump_release_debt_, ump_debt_total_, ump_drain_cursor_,
102 [&](std::size_t key) {
103 constexpr std::size_t kKeysPerProtocol = 16 * 16 * 128;
104 const bool midi2 = key >= kKeysPerProtocol;
105 const auto protocol_key = key % kKeysPerProtocol;
106 const auto group = static_cast<std::uint8_t>(
107 protocol_key / (16 * 128));
108 const auto remainder = protocol_key % (16 * 128);
109 return emit(
110 midi2, group,
111 static_cast<std::uint8_t>(remainder / 128),
112 static_cast<std::uint8_t>(remainder % 128));
113 });
114 }
115
116 template <typename EmitMidi, typename EmitUmp>
117 bool flush(EmitMidi&& emit_midi, EmitUmp&& emit_ump) const noexcept {
118 if (!drain_midi_releases(emit_midi) || !drain_ump_releases(emit_ump))
119 return false;
120 if (!flush_forwarded(midi_entries_, midi_size_, midi_flushed_suppressed_,
121 [&](std::uint32_t key) {
122 return emit_midi(static_cast<std::uint8_t>(key / 128),
123 static_cast<std::uint8_t>(key % 128));
124 }))
125 return false;
126 if (!flush_forwarded(ump_entries_, ump_size_, ump_flushed_suppressed_,
127 [&](std::uint32_t key) {
128 constexpr std::size_t kKeysPerProtocol = 16 * 16 * 128;
129 const bool midi2 = key >= kKeysPerProtocol;
130 const auto protocol_key = key % kKeysPerProtocol;
131 const auto group = static_cast<std::uint8_t>(protocol_key / (16 * 128));
132 const auto remainder = protocol_key % (16 * 128);
133 return emit_ump(midi2, group,
134 static_cast<std::uint8_t>(remainder / 128),
135 static_cast<std::uint8_t>(remainder % 128));
136 }))
137 return false;
138 return output_ownership_empty();
139 }
140
141 private:
142 enum class OwnershipState : std::uint8_t {
143 Forwarded,
144 Suppressed,
145 };
146 struct OwnershipEntry {
147 std::uint32_t key = 0;
148 std::uint32_t count = 0;
149 OwnershipState state = OwnershipState::Suppressed;
150 };
151
152 static std::size_t ump_key(const UmpPacket& packet) noexcept {
153 constexpr std::size_t kKeysPerProtocol = 16 * 16 * 128;
154 const std::size_t protocol =
155 packet.message_type() == UmpMessageType::Midi2ChannelVoice ? 1 : 0;
156 return protocol * kKeysPerProtocol +
157 (static_cast<std::size_t>(packet.group()) * 16 + packet.channel()) * 128 +
158 packet.note_number();
159 }
160 static bool is_attack(const MidiEvent& event) noexcept {
161 return event.is_note_on() && event.velocity() != 0;
162 }
163 static bool is_release(const MidiEvent& event) noexcept {
164 return event.is_note_off() || (event.is_note_on() && event.velocity() == 0);
165 }
166 static bool is_attack(const UmpPacket& packet) noexcept {
167 if (!utility_detail::is_channel_voice(packet)) return false;
168 const auto status = static_cast<std::uint8_t>(packet.status() & 0xf0);
169 if (status != 0x90) return false;
170 return packet.message_type() != UmpMessageType::Midi1ChannelVoice ||
171 (packet.words[0] & 0x7f) != 0;
172 }
173 static bool is_release(const UmpPacket& packet) noexcept {
174 if (!utility_detail::is_channel_voice(packet)) return false;
175 const auto status = static_cast<std::uint8_t>(packet.status() & 0xf0);
176 return status == 0x80 ||
177 (status == 0x90 && packet.message_type() == UmpMessageType::Midi1ChannelVoice &&
178 (packet.words[0] & 0x7f) == 0);
179 }
180 template <std::size_t N, std::size_t KeyCount>
181 bool consume_suppressed(std::array<OwnershipEntry, N>& entries,
182 std::size_t& size,
183 std::array<std::uint64_t, KeyCount>& flushed,
184 std::array<std::uint32_t, KeyCount>& overflow,
185 std::uint32_t key) noexcept {
186 if (flushed[key] != 0) {
187 --flushed[key];
188 --total_;
189 return true;
190 }
191 for (std::size_t i = 0; i < size; ++i) {
192 if (entries[i].key != key) continue;
193 if (entries[i].state == OwnershipState::Forwarded)
194 return false;
195 if (--entries[i].count == 0) erase(entries, size, i);
196 --total_;
197 return true;
198 }
199 if (overflow[key] == 0) return false;
200 --overflow[key];
201 --total_;
202 return true;
203 }
204
205 template <std::size_t N, std::size_t KeyCount>
206 void record_attack(std::array<OwnershipEntry, N>& entries, std::size_t& size,
207 std::array<std::uint32_t, KeyCount>& overflow,
208 std::uint32_t key, bool forwarded) noexcept {
209 if (overflow[key] != 0 || size == entries.size()) {
210 if (overflow[key] != std::numeric_limits<std::uint32_t>::max()) {
211 ++overflow[key];
212 ++total_;
213 }
214 return;
215 }
216 const auto state = forwarded ? OwnershipState::Forwarded
217 : OwnershipState::Suppressed;
218 for (std::size_t i = size; i != 0; --i) {
219 auto& previous = entries[i - 1];
220 if (previous.key != key) continue;
221 if (previous.state == state &&
222 previous.count != std::numeric_limits<std::uint32_t>::max()) {
223 ++previous.count;
224 ++total_;
225 return;
226 }
227 break;
228 }
229 entries[size++] = {key, 1, state};
230 ++total_;
231 }
232
233 template <std::size_t N, std::size_t KeyCount>
234 void record_release(std::array<OwnershipEntry, N>& entries,
235 std::size_t& size,
236 std::array<std::uint32_t, KeyCount>& debt,
237 std::size_t& debt_total, std::uint32_t key,
238 bool forwarded) noexcept {
239 for (std::size_t i = 0; i < size; ++i) {
240 auto& entry = entries[i];
241 if (entry.key != key || entry.state != OwnershipState::Forwarded)
242 continue;
243 if (--entry.count == 0) erase(entries, size, i);
244 if (forwarded)
245 --total_;
246 else if (debt[key] != std::numeric_limits<std::uint32_t>::max()) {
247 ++debt[key];
248 ++debt_total;
249 }
250 return;
251 }
252 }
253
254 template <std::size_t N, typename Emit>
255 bool drain_pending(std::array<std::uint32_t, N>& debt,
256 std::size_t& debt_total, std::size_t& cursor,
257 Emit&& emit) const noexcept {
258 if (debt_total == 0) return true;
259 constexpr std::size_t kAttemptsPerBlock = 32;
260 std::size_t attempts = 0;
261 std::size_t visited = 0;
262 while (visited < debt.size() && attempts < kAttemptsPerBlock) {
263 const auto key = cursor;
264 cursor = (cursor + 1) % debt.size();
265 ++visited;
266 if (debt[key] == 0) continue;
267 ++attempts;
268 if (emit(key)) {
269 --debt[key];
270 --debt_total;
271 --total_;
272 }
273 }
274 return debt_total == 0;
275 }
276
277 template <std::size_t N, std::size_t KeyCount, typename Emit>
278 bool flush_forwarded(std::array<OwnershipEntry, N>& entries,
279 std::size_t& size,
280 std::array<std::uint64_t, KeyCount>& flushed,
281 Emit&& emit) const noexcept {
282 constexpr std::size_t kAttemptsPerCall = 32;
283 std::size_t attempts = 0;
284 for (std::size_t i = 0; i < size;) {
285 auto& entry = entries[i];
286 if (entry.state == OwnershipState::Suppressed) {
287 ++i;
288 continue;
289 }
290 while (entry.count != 0 && attempts < kAttemptsPerCall) {
291 if (flushed[entry.key] == std::numeric_limits<std::uint64_t>::max())
292 return false;
293 ++attempts;
294 if (!emit(entry.key))
295 return false;
296 --entry.count;
297 ++flushed[entry.key];
298 }
299 if (entry.count != 0)
300 return false;
301 erase(entries, size, i);
302 }
303 return true;
304 }
305
306 template <std::size_t N>
307 static bool has_forwarded(const std::array<OwnershipEntry, N>& entries,
308 std::size_t size) noexcept {
309 for (std::size_t i = 0; i < size; ++i)
310 if (entries[i].state == OwnershipState::Forwarded)
311 return true;
312 return false;
313 }
314
315 void clear_all() const noexcept {
316 midi_entries_.fill({});
317 ump_entries_.fill({});
318 midi_overflow_suppressed_.fill(0);
319 ump_overflow_suppressed_.fill(0);
320 midi_flushed_suppressed_.fill(0);
321 ump_flushed_suppressed_.fill(0);
322 midi_release_debt_.fill(0);
323 ump_release_debt_.fill(0);
324 midi_size_ = 0;
325 ump_size_ = 0;
326 midi_drain_cursor_ = 0;
327 ump_drain_cursor_ = 0;
328 midi_debt_total_ = 0;
329 ump_debt_total_ = 0;
330 total_ = 0;
331 }
332
333 template <std::size_t N>
334 static void erase(std::array<OwnershipEntry, N>& entries,
335 std::size_t& size, std::size_t index) noexcept {
336 for (std::size_t i = index + 1; i < size; ++i)
337 entries[i - 1] = entries[i];
338 --size;
339 }
340
341 mutable std::array<OwnershipEntry, kMaxTrackedOwnership> midi_entries_{};
342 mutable std::array<OwnershipEntry, kMaxTrackedOwnership> ump_entries_{};
343 mutable std::array<std::uint32_t, 16 * 128> midi_overflow_suppressed_{};
344 mutable std::array<std::uint32_t, 2 * 16 * 16 * 128> ump_overflow_suppressed_{};
345 mutable std::array<std::uint64_t, 16 * 128> midi_flushed_suppressed_{};
346 mutable std::array<std::uint64_t, 2 * 16 * 16 * 128> ump_flushed_suppressed_{};
347 mutable std::array<std::uint32_t, 16 * 128> midi_release_debt_{};
348 mutable std::array<std::uint32_t, 2 * 16 * 16 * 128> ump_release_debt_{};
349 mutable std::size_t midi_size_ = 0;
350 mutable std::size_t ump_size_ = 0;
351 mutable std::size_t midi_drain_cursor_ = 0;
352 mutable std::size_t ump_drain_cursor_ = 0;
353 mutable std::size_t midi_debt_total_ = 0;
354 mutable std::size_t ump_debt_total_ = 0;
355 mutable std::size_t total_ = 0;
356};
357
358} // namespace routing_detail
359
361 std::uint16_t accepted_channels = 0xffff;
362 std::array<std::uint8_t, 16> output_channel{0, 1, 2, 3, 4, 5, 6, 7,
363 8, 9, 10, 11, 12, 13, 14, 15};
364};
365
367 public:
373
374 static constexpr bool valid_spec(const ChannelRouteSpec& spec) noexcept {
375 for (const auto channel : spec.output_channel)
376 if (channel > 15)
377 return false;
378 return true;
379 }
380 // The ownership ledger is about 1.2 MB, so it lives on the heap: one
381 // allocation here, none in process(), flush(), reset() or replace_spec().
382 explicit ChannelRouter(ChannelRouteSpec spec = {})
383 : spec_(spec), valid_(valid_spec(spec)),
384 held_notes_(std::make_unique<routing_detail::HeldNoteLedger>()) {}
385 // A moved-from object has no ledger and refuses every entry point.
386 bool valid() const noexcept {
387 return valid_ && held_notes_ != nullptr;
388 }
389 bool replace_spec(ChannelRouteSpec spec) noexcept {
390 if (!held_notes_ || !valid_spec(spec) || !held_notes_->empty())
391 return false;
392 spec_ = spec;
393 valid_ = true;
394 return true;
395 }
396
397 MidiUtilityProcessReport flush(MidiBuffer& output) const noexcept {
399 if (!held_notes_)
400 return {0, 0, 0, false};
402 if (held_notes_->output_ownership_empty())
403 return report;
404 if (!utility_detail::ready(output))
405 return {0, 0, 1, false};
406 report.complete = held_notes_->flush(
407 [&](std::uint8_t channel, std::uint8_t note) {
409 output,
411 MidiEvent::note_off(channel, note), spec_.output_channel[channel]),
412 report);
413 },
414 [&](bool midi2, std::uint8_t group, std::uint8_t channel, std::uint8_t note) {
415 const bool emitted = utility_detail::emit_ump(
416 output.ump(),
417 {routing_detail::note_off_packet(
418 midi2, group, spec_.output_channel[channel], note),
419 0});
420 if (!emitted) {
421 ++report.dropped;
422 report.complete = false;
423 }
424 return emitted;
425 });
426 if (!report.complete)
427 ++report.deferred;
428 return report;
429 }
430
431 MidiUtilityProcessReport reset(MidiBuffer& output) const noexcept {
432 auto report = flush(output);
433 if (report.complete)
434 held_notes_->discard_input_ownership();
435 return report;
436 }
437
439 if (!valid_spec(spec)) {
441 return {0, 0, 0, false};
442 }
443 auto report = flush(output);
444 if (report.complete) {
445 spec_ = spec;
446 valid_ = true;
447 }
448 return report;
449 }
450
452 if (utility_detail::blocks_alias(input, output))
453 return {0, input.size(), 0, false};
455 if (!held_notes_)
456 return {0, input.size(), 0, false};
458 if (!utility_detail::ready(output)) {
459 held_notes_->retain_unprocessed_ownership(input);
460 return {0, input.size(), 0, false};
461 }
462 if (!valid_) {
463 held_notes_->retain_unprocessed_ownership(input);
464 return {0, input.size(), 0, false};
465 }
466 const bool midi_drained =
467 held_notes_->drain_midi_releases([&](std::uint8_t channel, std::uint8_t note) {
469 output,
471 MidiEvent::note_off(channel, note), spec_.output_channel[channel]),
472 report);
473 });
474 const bool ump_drained = held_notes_->drain_ump_releases(
475 [&](bool midi2, std::uint8_t group, std::uint8_t channel, std::uint8_t note) {
476 const bool emitted = utility_detail::emit_ump(
477 output.ump(),
478 {routing_detail::note_off_packet(
479 midi2, group, spec_.output_channel[channel], note),
480 0});
481 if (!emitted) {
482 ++report.dropped;
483 report.complete = false;
484 }
485 return emitted;
486 });
487 if (!midi_drained || !ump_drained) {
488 held_notes_->retain_unprocessed_ownership(input);
489 report.dropped += input.size();
490 report.complete = false;
491 return report;
492 }
493 for (const auto& event : input) {
495 utility_detail::emit(output, event, report);
496 continue;
497 }
498 if (held_notes_->consume_suppressed_release(event))
499 continue;
500 const auto channel = event.channel();
501 if ((spec_.accepted_channels & (std::uint16_t{1} << channel)) == 0) {
502 held_notes_->record(event, false);
503 continue;
504 }
505 if (!held_notes_->can_forward(event)) {
506 held_notes_->record(event, false);
507 ++report.dropped;
508 report.complete = false;
509 continue;
510 }
511 const bool forwarded = utility_detail::emit(
512 output, utility_detail::with_channel(event, spec_.output_channel[channel]), report);
513 held_notes_->record(event, forwarded);
514 }
515 bool copied_sidecars = utility_detail::copy_sysex_sidecar(input, output);
516 if (const auto* source = input.ump()) {
517 for (const auto& event : *source) {
518 if (!utility_detail::is_channel_voice(event.packet)) {
519 copied_sidecars =
520 utility_detail::emit_ump(output.ump(), event) && copied_sidecars;
521 continue;
522 }
523 if (held_notes_->consume_suppressed_release(event.packet))
524 continue;
525 const auto channel = event.packet.channel();
526 if ((spec_.accepted_channels & (std::uint16_t{1} << channel)) == 0) {
527 held_notes_->record(event.packet, false);
528 continue;
529 }
530 auto routed = event;
531 routed.packet =
532 utility_detail::with_channel(event.packet, spec_.output_channel[channel]);
533 if (!held_notes_->can_forward(event.packet)) {
534 held_notes_->record(event.packet, false);
535 ++report.dropped;
536 report.complete = false;
537 continue;
538 }
539 const bool forwarded = utility_detail::emit_ump(output.ump(), routed);
540 held_notes_->record(event.packet, forwarded);
541 copied_sidecars = forwarded && copied_sidecars;
542 }
543 }
544 if (!copied_sidecars) {
545 ++report.dropped;
546 report.complete = false;
547 }
548 return report;
549 }
550
551 private:
552 ChannelRouteSpec spec_{};
553 bool valid_ = true;
554 std::unique_ptr<routing_detail::HeldNoteLedger> held_notes_;
555};
556
557// Stack-safe by construction: the ledger is heap-owned. A bound here fails
558// every consumer's compile if a large array is ever inlined again.
559static_assert(sizeof(ChannelRouter) <= 4096);
560
562 std::uint8_t lowest = 0;
563 std::uint8_t highest = 127;
564};
565
567 public:
573
574 static constexpr bool valid_spec(NoteRangeSpec spec) noexcept {
575 return spec.lowest <= 127 && spec.highest <= 127 && spec.lowest <= spec.highest;
576 }
577 // The ownership ledger is about 1.2 MB, so it lives on the heap: one
578 // allocation here, none in process(), flush(), reset() or replace_spec().
579 explicit NoteRangeFilter(NoteRangeSpec spec = {})
580 : spec_(spec), valid_(valid_spec(spec)),
581 held_notes_(std::make_unique<routing_detail::HeldNoteLedger>()) {}
582 // A moved-from object has no ledger and refuses every entry point.
583 bool valid() const noexcept {
584 return valid_ && held_notes_ != nullptr;
585 }
586 bool replace_spec(NoteRangeSpec spec) noexcept {
587 if (!held_notes_ || !valid_spec(spec) || !held_notes_->empty())
588 return false;
589 spec_ = spec;
590 valid_ = true;
591 return true;
592 }
593
594 MidiUtilityProcessReport flush(MidiBuffer& output) const noexcept {
596 if (!held_notes_)
597 return {0, 0, 0, false};
599 if (held_notes_->output_ownership_empty())
600 return report;
601 if (!utility_detail::ready(output))
602 return {0, 0, 1, false};
603 report.complete = held_notes_->flush(
604 [&](std::uint8_t channel, std::uint8_t note) {
605 return utility_detail::emit(output, MidiEvent::note_off(channel, note), report);
606 },
607 [&](bool midi2, std::uint8_t group, std::uint8_t channel, std::uint8_t note) {
608 const bool emitted = utility_detail::emit_ump(
609 output.ump(),
610 {routing_detail::note_off_packet(midi2, group, channel, note), 0});
611 if (!emitted) {
612 ++report.dropped;
613 report.complete = false;
614 }
615 return emitted;
616 });
617 if (!report.complete)
618 ++report.deferred;
619 return report;
620 }
621
622 MidiUtilityProcessReport reset(MidiBuffer& output) const noexcept {
623 auto report = flush(output);
624 if (report.complete)
625 held_notes_->discard_input_ownership();
626 return report;
627 }
628
630 if (!valid_spec(spec)) {
632 return {0, 0, 0, false};
633 }
634 auto report = flush(output);
635 if (report.complete) {
636 spec_ = spec;
637 valid_ = true;
638 }
639 return report;
640 }
641
643 if (utility_detail::blocks_alias(input, output))
644 return {0, input.size(), 0, false};
646 if (!held_notes_)
647 return {0, input.size(), 0, false};
649 if (!utility_detail::ready(output)) {
650 held_notes_->retain_unprocessed_ownership(input);
651 return {0, input.size(), 0, false};
652 }
653 if (!valid_) {
654 held_notes_->retain_unprocessed_ownership(input);
655 return {0, input.size(), 0, false};
656 }
657 const bool midi_drained =
658 held_notes_->drain_midi_releases([&](std::uint8_t channel, std::uint8_t note) {
660 output, MidiEvent::note_off(channel, note), report);
661 });
662 const bool ump_drained = held_notes_->drain_ump_releases(
663 [&](bool midi2, std::uint8_t group, std::uint8_t channel, std::uint8_t note) {
664 const bool emitted = utility_detail::emit_ump(
665 output.ump(),
666 {routing_detail::note_off_packet(midi2, group, channel, note), 0});
667 if (!emitted) {
668 ++report.dropped;
669 report.complete = false;
670 }
671 return emitted;
672 });
673 if (!midi_drained || !ump_drained) {
674 held_notes_->retain_unprocessed_ownership(input);
675 report.dropped += input.size();
676 report.complete = false;
677 return report;
678 }
679 for (const auto& event : input) {
680 if (held_notes_->consume_suppressed_release(event))
681 continue;
683 (event.note() < spec_.lowest || event.note() > spec_.highest)) {
684 held_notes_->record(event, false);
685 continue;
686 }
687 if (!held_notes_->can_forward(event)) {
688 held_notes_->record(event, false);
689 ++report.dropped;
690 report.complete = false;
691 continue;
692 }
693 const bool forwarded = utility_detail::emit(output, event, report);
694 held_notes_->record(event, forwarded);
695 }
696 bool copied_sidecars = utility_detail::copy_sysex_sidecar(input, output);
697 if (const auto* source = input.ump()) {
698 for (const auto& event : *source) {
699 if (held_notes_->consume_suppressed_release(event.packet))
700 continue;
701 if (utility_detail::is_note_addressed(event.packet) &&
702 (event.packet.note_number() < spec_.lowest ||
703 event.packet.note_number() > spec_.highest)) {
704 held_notes_->record(event.packet, false);
705 continue;
706 }
707 if (!held_notes_->can_forward(event.packet)) {
708 held_notes_->record(event.packet, false);
709 ++report.dropped;
710 report.complete = false;
711 continue;
712 }
713 const bool forwarded = utility_detail::emit_ump(output.ump(), event);
714 held_notes_->record(event.packet, forwarded);
715 copied_sidecars = forwarded && copied_sidecars;
716 }
717 }
718 if (!copied_sidecars) {
719 ++report.dropped;
720 report.complete = false;
721 }
722 return report;
723 }
724
725 private:
726 NoteRangeSpec spec_{};
727 bool valid_ = true;
728 std::unique_ptr<routing_detail::HeldNoteLedger> held_notes_;
729};
730
731// Stack-safe by construction: the ledger is heap-owned. A bound here fails
732// every consumer's compile if a large array is ever inlined again.
733static_assert(sizeof(NoteRangeFilter) <= 4096);
734
736 std::uint8_t split_note = 60;
738 std::uint8_t lower_channel = 0;
739 std::uint8_t upper_channel = 1;
740};
741
743 public:
749
750 static constexpr bool valid_spec(KeyboardSplitSpec spec) noexcept {
751 return spec.split_note <= 127 && spec.lower_channel <= 15 && spec.upper_channel <= 15;
752 }
753 // The ownership ledger is about 1.2 MB, so it lives on the heap: one
754 // allocation here, none in process(), flush(), reset() or replace_spec().
756 : spec_(spec), valid_(valid_spec(spec)),
757 held_notes_(std::make_unique<routing_detail::HeldNoteLedger>()) {}
758 // A moved-from object has no ledger and refuses every entry point.
759 bool valid() const noexcept {
760 return valid_ && held_notes_ != nullptr;
761 }
762 bool replace_spec(KeyboardSplitSpec spec) noexcept {
763 if (!held_notes_ || !valid_spec(spec) || !held_notes_->empty())
764 return false;
765 spec_ = spec;
766 valid_ = true;
767 return true;
768 }
769
770 MidiUtilityProcessReport flush(MidiBuffer& lower, MidiBuffer& upper) const noexcept {
773 if (!held_notes_)
774 return {0, 0, 0, false};
776 if (held_notes_->output_ownership_empty())
777 return report;
778 if (!utility_detail::ready(lower) || !utility_detail::ready(upper))
779 return {0, 0, 1, false};
780 report.complete = held_notes_->flush(
781 [&](std::uint8_t, std::uint8_t note) {
782 const bool is_upper =
783 note > spec_.split_note ||
784 (note == spec_.split_note && spec_.split_note_is_upper);
786 is_upper ? upper : lower,
788 is_upper ? spec_.upper_channel : spec_.lower_channel, note),
789 report);
790 },
791 [&](bool midi2, std::uint8_t group, std::uint8_t, std::uint8_t note) {
792 const bool is_upper =
793 note > spec_.split_note ||
794 (note == spec_.split_note && spec_.split_note_is_upper);
795 const bool emitted = utility_detail::emit_ump(
796 is_upper ? upper.ump() : lower.ump(),
797 {routing_detail::note_off_packet(
798 midi2, group,
799 is_upper ? spec_.upper_channel : spec_.lower_channel, note),
800 0});
801 if (!emitted) {
802 ++report.dropped;
803 report.complete = false;
804 }
805 return emitted;
806 });
807 if (!report.complete)
808 ++report.deferred;
809 return report;
810 }
811
812 MidiUtilityProcessReport reset(MidiBuffer& lower, MidiBuffer& upper) const noexcept {
813 auto report = flush(lower, upper);
814 if (report.complete)
815 held_notes_->discard_input_ownership();
816 return report;
817 }
818
820 MidiBuffer& upper) noexcept {
821 if (!valid_spec(spec)) {
824 return {0, 0, 0, false};
825 }
826 auto report = flush(lower, upper);
827 if (report.complete) {
828 spec_ = spec;
829 valid_ = true;
830 }
831 return report;
832 }
833
835 MidiBuffer& upper) const {
836 if (utility_detail::blocks_alias(input, lower) ||
837 utility_detail::blocks_alias(input, upper) ||
838 utility_detail::blocks_alias(lower, upper))
839 return {0, input.size(), 0, false};
842 if (!held_notes_)
843 return {0, input.size(), 0, false};
845 if (!utility_detail::ready(lower) || !utility_detail::ready(upper)) {
846 held_notes_->retain_unprocessed_ownership(input);
847 return {0, input.size(), 0, false};
848 }
849 if (!valid_) {
850 held_notes_->retain_unprocessed_ownership(input);
851 return {0, input.size(), 0, false};
852 }
853 const bool midi_drained =
854 held_notes_->drain_midi_releases([&](std::uint8_t channel, std::uint8_t note) {
855 const bool is_upper =
856 note > spec_.split_note ||
857 (note == spec_.split_note && spec_.split_note_is_upper);
859 is_upper ? upper : lower,
861 is_upper ? spec_.upper_channel : spec_.lower_channel, note),
862 report);
863 });
864 const bool ump_drained = held_notes_->drain_ump_releases(
865 [&](bool midi2, std::uint8_t group, std::uint8_t channel, std::uint8_t note) {
866 (void)channel;
867 const bool is_upper =
868 note > spec_.split_note ||
869 (note == spec_.split_note && spec_.split_note_is_upper);
870 const bool emitted = utility_detail::emit_ump(
871 is_upper ? upper.ump() : lower.ump(),
872 {routing_detail::note_off_packet(
873 midi2, group,
874 is_upper ? spec_.upper_channel : spec_.lower_channel, note),
875 0});
876 if (!emitted) {
877 ++report.dropped;
878 report.complete = false;
879 }
880 return emitted;
881 });
882 if (!midi_drained || !ump_drained) {
883 held_notes_->retain_unprocessed_ownership(input);
884 report.dropped += input.size();
885 report.complete = false;
886 return report;
887 }
888 for (const auto& event : input) {
890 utility_detail::emit(lower, event, report);
891 utility_detail::emit(upper, event, report);
892 continue;
893 }
895 const bool is_upper =
896 event.note() > spec_.split_note ||
897 (event.note() == spec_.split_note && spec_.split_note_is_upper);
898 auto routed = utility_detail::with_channel(event, is_upper ? spec_.upper_channel
899 : spec_.lower_channel);
900 if (held_notes_->consume_suppressed_release(event))
901 continue;
902 if (!held_notes_->can_forward(event)) {
903 held_notes_->record(event, false);
904 ++report.dropped;
905 report.complete = false;
906 continue;
907 }
908 const bool forwarded = utility_detail::emit(
909 is_upper ? upper : lower, routed, report);
910 held_notes_->record(event, forwarded);
911 continue;
912 }
913 utility_detail::emit(lower, utility_detail::with_channel(event, spec_.lower_channel),
914 report);
915 utility_detail::emit(upper, utility_detail::with_channel(event, spec_.upper_channel),
916 report);
917 }
918 bool copied_sidecars = utility_detail::copy_sysex_sidecar(input, lower);
919 copied_sidecars = utility_detail::copy_sysex_sidecar(input, upper) && copied_sidecars;
920 if (const auto* source = input.ump()) {
921 for (const auto& event : *source) {
922 if (!utility_detail::is_channel_voice(event.packet)) {
923 copied_sidecars =
924 utility_detail::emit_ump(lower.ump(), event) && copied_sidecars;
925 copied_sidecars =
926 utility_detail::emit_ump(upper.ump(), event) && copied_sidecars;
927 continue;
928 }
929 if (utility_detail::is_note_addressed(event.packet)) {
930 const bool is_upper = event.packet.note_number() > spec_.split_note ||
931 (event.packet.note_number() == spec_.split_note &&
932 spec_.split_note_is_upper);
933 auto routed = event;
934 routed.packet = utility_detail::with_channel(
935 event.packet, is_upper ? spec_.upper_channel : spec_.lower_channel);
936 if (held_notes_->consume_suppressed_release(event.packet))
937 continue;
938 if (!held_notes_->can_forward(event.packet)) {
939 held_notes_->record(event.packet, false);
940 ++report.dropped;
941 report.complete = false;
942 continue;
943 }
944 const bool forwarded = utility_detail::emit_ump(
945 is_upper ? upper.ump() : lower.ump(), routed);
946 held_notes_->record(event.packet, forwarded);
947 copied_sidecars = forwarded && copied_sidecars;
948 continue;
949 }
950 auto lower_event = event;
951 lower_event.packet =
952 utility_detail::with_channel(event.packet, spec_.lower_channel);
953 copied_sidecars =
954 utility_detail::emit_ump(lower.ump(), lower_event) && copied_sidecars;
955 auto upper_event = event;
956 upper_event.packet =
957 utility_detail::with_channel(event.packet, spec_.upper_channel);
958 copied_sidecars =
959 utility_detail::emit_ump(upper.ump(), upper_event) && copied_sidecars;
960 }
961 }
962 if (!copied_sidecars) {
963 ++report.dropped;
964 report.complete = false;
965 }
966 return report;
967 }
968
969 private:
970 KeyboardSplitSpec spec_{};
971 bool valid_ = true;
972 std::unique_ptr<routing_detail::HeldNoteLedger> held_notes_;
973};
974
975// Stack-safe by construction: the ledger is heap-owned. A bound here fails
976// every consumer's compile if a large array is ever inlined again.
977static_assert(sizeof(KeyboardSplit) <= 4096);
978
979} // namespace pulp::midi
static constexpr bool valid_spec(const ChannelRouteSpec &spec) noexcept
Definition routing_utility_kernels.hpp:374
bool replace_spec(ChannelRouteSpec spec) noexcept
Definition routing_utility_kernels.hpp:389
MidiUtilityProcessReport reset(MidiBuffer &output) const noexcept
Definition routing_utility_kernels.hpp:431
MidiUtilityProcessReport replace_spec(ChannelRouteSpec spec, MidiBuffer &output) noexcept
Definition routing_utility_kernels.hpp:438
MidiUtilityProcessReport process(const MidiBuffer &input, MidiBuffer &output) const
Definition routing_utility_kernels.hpp:451
MidiUtilityProcessReport flush(MidiBuffer &output) const noexcept
Definition routing_utility_kernels.hpp:397
bool valid() const noexcept
Definition routing_utility_kernels.hpp:386
static constexpr MidiUtilityContract contract() noexcept
Definition routing_utility_kernels.hpp:368
ChannelRouter(ChannelRouteSpec spec={})
Definition routing_utility_kernels.hpp:382
bool valid() const noexcept
Definition routing_utility_kernels.hpp:759
MidiUtilityProcessReport reset(MidiBuffer &lower, MidiBuffer &upper) const noexcept
Definition routing_utility_kernels.hpp:812
MidiUtilityProcessReport process(const MidiBuffer &input, MidiBuffer &lower, MidiBuffer &upper) const
Definition routing_utility_kernels.hpp:834
MidiUtilityProcessReport flush(MidiBuffer &lower, MidiBuffer &upper) const noexcept
Definition routing_utility_kernels.hpp:770
MidiUtilityProcessReport replace_spec(KeyboardSplitSpec spec, MidiBuffer &lower, MidiBuffer &upper) noexcept
Definition routing_utility_kernels.hpp:819
static constexpr bool valid_spec(KeyboardSplitSpec spec) noexcept
Definition routing_utility_kernels.hpp:750
KeyboardSplit(KeyboardSplitSpec spec={})
Definition routing_utility_kernels.hpp:755
static constexpr MidiUtilityContract contract() noexcept
Definition routing_utility_kernels.hpp:744
bool replace_spec(KeyboardSplitSpec spec) noexcept
Definition routing_utility_kernels.hpp:762
Collection of timestamped MIDI events within a single audio buffer period.
Definition buffer.hpp:31
std::size_t size() const
Definition buffer.hpp:123
const class UmpBuffer * ump() const
Attached UmpBuffer or nullptr.
Definition buffer.hpp:217
MidiUtilityProcessReport process(const MidiBuffer &input, MidiBuffer &output) const
Definition routing_utility_kernels.hpp:642
bool valid() const noexcept
Definition routing_utility_kernels.hpp:583
NoteRangeFilter(NoteRangeSpec spec={})
Definition routing_utility_kernels.hpp:579
MidiUtilityProcessReport reset(MidiBuffer &output) const noexcept
Definition routing_utility_kernels.hpp:622
MidiUtilityProcessReport replace_spec(NoteRangeSpec spec, MidiBuffer &output) noexcept
Definition routing_utility_kernels.hpp:629
static constexpr bool valid_spec(NoteRangeSpec spec) noexcept
Definition routing_utility_kernels.hpp:574
MidiUtilityProcessReport flush(MidiBuffer &output) const noexcept
Definition routing_utility_kernels.hpp:594
bool replace_spec(NoteRangeSpec spec) noexcept
Definition routing_utility_kernels.hpp:586
static constexpr MidiUtilityContract contract() noexcept
Definition routing_utility_kernels.hpp:568
Definition routing_utility_kernels.hpp:23
bool consume_suppressed_release(const MidiEvent &event) noexcept
Definition routing_utility_kernels.hpp:47
bool drain_midi_releases(Emit &&emit) const noexcept
Definition routing_utility_kernels.hpp:91
bool flush(EmitMidi &&emit_midi, EmitUmp &&emit_ump) const noexcept
Definition routing_utility_kernels.hpp:117
bool output_ownership_empty() const noexcept
Definition routing_utility_kernels.hpp:28
bool drain_ump_releases(Emit &&emit) const noexcept
Definition routing_utility_kernels.hpp:100
static constexpr std::size_t kMaxTrackedOwnership
Definition routing_utility_kernels.hpp:25
void discard_input_ownership() const noexcept
Definition routing_utility_kernels.hpp:34
void record(const UmpPacket &packet, bool forwarded) noexcept
Definition routing_utility_kernels.hpp:69
bool can_forward(const MidiEvent &event) const noexcept
Definition routing_utility_kernels.hpp:36
constexpr bool empty() const noexcept
Definition routing_utility_kernels.hpp:27
bool consume_suppressed_release(const UmpPacket &packet) noexcept
Definition routing_utility_kernels.hpp:53
bool can_forward(const UmpPacket &packet) const noexcept
Definition routing_utility_kernels.hpp:41
void retain_unprocessed_ownership(const MidiBuffer &input) noexcept
Definition routing_utility_kernels.hpp:79
void record(const MidiEvent &event, bool forwarded) noexcept
Definition routing_utility_kernels.hpp:60
Definition routing_utility_kernels.hpp:9
UmpPacket note_off_packet(bool midi2, std::uint8_t group, std::uint8_t channel, std::uint8_t note) noexcept
Definition routing_utility_kernels.hpp:11
bool is_note_addressed(const UmpPacket &packet) noexcept
Definition utility_contract.hpp:124
UmpPacket with_channel(UmpPacket packet, std::uint8_t channel) noexcept
Definition utility_contract.hpp:134
bool copy_sysex_sidecar(const MidiBuffer &input, MidiBuffer &output) noexcept
Definition utility_contract.hpp:104
bool is_channel_voice(const UmpPacket &packet) noexcept
Definition utility_contract.hpp:119
bool emit_ump(UmpBuffer *output, const UmpEvent &event) noexcept
Definition utility_contract.hpp:115
void clear_output(MidiBuffer &output) noexcept
Definition utility_contract.hpp:82
bool blocks_alias(const MidiBuffer &first, const MidiBuffer &second) noexcept
Definition utility_contract.hpp:92
bool ready(const MidiBuffer &output) noexcept
Definition utility_contract.hpp:86
bool emit(MidiBuffer &output, const MidiEvent &event, MidiUtilityProcessReport &report)
Definition utility_contract.hpp:67
int key_index(std::uint8_t channel, std::uint8_t note) noexcept
Definition utility_contract.hpp:55
Definition custom_node_events.hpp:26
@ None
Definition utility_contract.hpp:31
@ RetainReleaseDebt
Definition utility_contract.hpp:22
@ Midi2ChannelVoice
Definition ump.hpp:27
@ Midi1ChannelVoice
Definition ump.hpp:25
@ InputStable
Definition utility_contract.hpp:26
@ size
Definition config.hpp:216
@ i
Definition formant_filter_bank.hpp:20
SequencerStateChannel is the audio-safe transport for a plugin's structured NON-parameter state: the ...
Definition audio_device_manager.hpp:48
STL namespace.
Definition routing_utility_kernels.hpp:360
std::uint16_t accepted_channels
Definition routing_utility_kernels.hpp:361
std::array< std::uint8_t, 16 > output_channel
Definition routing_utility_kernels.hpp:362
Definition routing_utility_kernels.hpp:735
std::uint8_t upper_channel
Definition routing_utility_kernels.hpp:739
bool split_note_is_upper
Definition routing_utility_kernels.hpp:737
std::uint8_t lower_channel
Definition routing_utility_kernels.hpp:738
std::uint8_t split_note
Definition routing_utility_kernels.hpp:736
Timestamped MIDI event combining a choc::midi::ShortMessage with timing info.
Definition message.hpp:32
static MidiEvent note_off(uint8_t channel, uint8_t note, uint8_t velocity=0)
Create a Note Off event.
Definition message.hpp:54
Definition utility_contract.hpp:36
Definition utility_contract.hpp:46
bool complete
Definition utility_contract.hpp:50
std::size_t dropped
Definition utility_contract.hpp:48
std::size_t deferred
Definition utility_contract.hpp:49
Definition routing_utility_kernels.hpp:561
std::uint8_t lowest
Definition routing_utility_kernels.hpp:562
std::uint8_t highest
Definition routing_utility_kernels.hpp:563
Definition ump.hpp:84
static UmpPacket note_off_2(uint8_t group, uint8_t channel, uint8_t note, uint16_t velocity=0)
Definition ump.hpp:157
std::array< uint32_t, 4 > words
Definition ump.hpp:85
int word_count
Definition ump.hpp:86