Loading...
Searching...
No Matches
EntityToMidi.hpp
1#pragma once
2
3/* SPDX-License-Identifier: GPL-3.0-or-later */
4
5#include "PointTracker.hpp"
6
7#include <ossia/dataflow/exec_state_facade.hpp>
8#include <ossia/detail/flat_map.hpp>
9#include <ossia/math/filters.hpp>
10#include <ossia/network/base/device.hpp>
11#include <ossia/network/base/protocol.hpp>
12#include <ossia/protocols/midi/midi_protocol.hpp>
13
14#include <halp/audio.hpp>
15#include <halp/controls.enums.hpp>
16#include <halp/controls.hpp>
17#include <halp/layout.hpp>
18#include <halp/meta.hpp>
19#include <halp/midi.hpp>
20
21#include <algorithm>
22#include <array>
23#include <atomic>
24#include <bitset>
25#include <cmath>
26#include <cstdint>
27#include <limits>
28#include <string>
29#include <vector>
30
31namespace avnd_tools
32{
33
34// Entity to MIDI - turns the identified, tracked entities produced by
35// Point Tracker 2D / 3D into musically usable MIDI.
36//
37// The central design decision: THE NOTE IS THE ENTITY, NOT AN EVENT.
38// One note-on when a track is confirmed, one note-off when it expires;
39// everything in between is per-note expression (MPE pitch bend, channel
40// pressure, CC74). Note spam is structurally impossible and stuck notes
41// reduce to a single invariant: every held voice has a live entity behind it.
42//
43// Three layers, each owning its failure modes:
44// Voice - allocation, stealing, stuck notes
45// Mapping - ranges, curves, smoothing, scale quantisation
46// Emission - rate, byte budget, ordering, panic
47//
48// The mapping layer never generates note events; only lifecycle transitions
49// (and the explicit Triggered mode) do.
50
51enum class E2MOutputMode
52{
53 MPE,
54 ChannelPerEntity,
55 SingleChannel
56};
57enum class E2MZone
58{
59 Lower,
60 Upper
61};
62enum class E2MChannelReuse
63{
64 LRU,
65 Immediate
66};
67enum class E2MNoteModel
68{
69 Sustained,
70 Triggered
71};
72enum class E2MTriggerOn
73{
74 Confirmed,
75 FirstDetection
76};
77enum class E2MPriority
78{
79 ConfidenceAge,
80 StealOldest,
81 StealNewest,
82 StealSlowest,
83 StealLeastConfident
84};
85enum class E2MCoast
86{
87 Freeze,
88 Follow,
89 Fade
90};
91enum class E2MAxis
92{
93 X,
94 Y,
95 Z
96};
103enum class E2MCoords
104{
105 TwoD,
106 ThreeD
107};
108enum class E2MPitchTracking
109{
110 ContinuousBend,
111 Latched,
112 Retrigger
113};
114enum class E2MScale
115{
116 None,
117 Chromatic,
118 Major,
119 NaturalMinor,
120 HarmonicMinor,
121 MajorPentatonic,
122 MinorPentatonic,
123 Blues,
124 Dorian,
125 Phrygian,
126 Lydian,
127 Mixolydian,
128 WholeTone
129};
130enum class E2MRoot
131{
132 C,
133 Cs,
134 D,
135 Ds,
136 E,
137 F,
138 Fs,
139 G,
140 Gs,
141 A,
142 As,
143 B
144};
145enum class E2MVelSource
146{
147 EntrySpeed,
148 Fixed,
149 Confidence
150};
158enum class E2MSource
159{
160 None,
161 // --- position ---
162 X,
163 Y,
164 Z,
165 Radius,
166 // --- motion ---
167 Speed,
168 VelocityX,
169 VelocityY,
170 VelocityZ,
171 SpeedX,
172 SpeedY,
173 SpeedZ,
174 Acceleration,
175 AccelX,
176 AccelY,
177 AccelZ,
178 Jerk,
179 // --- path shape ---
180 TurnRate,
181 Curvature,
182 HeadingSin,
183 HeadingCos,
184 Agitation,
185 // --- relations between entities ---
186 NearestNeighbour,
187 CentroidDistance,
188 Density,
189 // --- lifecycle ---
190 Age,
191 Confidence
192};
193
198{
199 float x{}, y{}, z{}, radius{};
200 float speed{};
201 float vx{}, vy{}, vz{}; // signed, 0.5 = still
202 float sx{}, sy{}, sz{}; // magnitudes
203 float accel{};
204 float ax{}, ay{}, az{}; // signed, 0.5 = no acceleration
205 float jerk{};
206 float turn_rate{}, curvature{};
207 float heading_sin{0.5f}, heading_cos{1.f};
208 float agitation{};
209 float nearest{1.f}, centroid_dist{}, density{};
210 float age{}, confidence{};
211};
212enum class E2MQuantTargets
213{
214 Off,
215 Onsets,
216 OnsetsAndOffsets
217};
218enum class E2MGrid
219{
220 Whole,
221 Half,
222 Quarter,
223 Eighth,
224 Sixteenth,
225 EighthTriplet,
226 SixteenthTriplet
227};
228
230{
231 halp_meta(name, "Entity To MIDI")
232 halp_meta(c_name, "avnd_entity_to_midi")
233 halp_meta(category, "Midi")
234 halp_meta(author, "ossia team")
235 halp_meta(
236 description,
237 "Turn tracked entities (people, blobs, hands) into MIDI: each entity "
238 "becomes one held MPE note whose pitch bend, pressure and timbre follow "
239 "its motion for its whole lifetime. No note spam, no stuck notes.")
240 halp_meta(uuid, "e977241d-439f-4316-9de8-e316a1651b2e")
241 halp_flag(process_exec);
242
243 // The exact record emitted by Point Tracker 2D / 3D. Shared on purpose so a
244 // cable needs no adapter: the 3D variant is used as the common shape; a 2D
245 // tracker's vec2f positions decode into it with z = 0.
246 using track_record = PointTrackerBase<3>::track_record;
247
248 struct ins
249 {
250 struct : halp::val_port<"Entities", std::vector<ossia::value>>
251 {
252 halp_meta(
253 description,
254 "One element per entity, in the same formats Point Tracker accepts on "
255 "its Points inlet: full track records (as emitted by Point Tracker's "
256 "Tracks output), {position, id, confidence, ...} maps, vec2f / vec3f / "
257 "vec4f, sub-lists [x, y(, z)(, confidence)], or one flat list of "
258 "numbers. Records carrying an id keep it; bare positions are "
259 "identified by their index in the list, so such a source must emit "
260 "entities in a stable order - otherwise cable Point Tracker in "
261 "between, which is what assigns persistent ids.")
262 void update(auto& self) { self.tracks_dirty = true; }
263 } tracks;
264
265 struct : halp::combobox_t<"Coordinates", E2MCoords>
266 {
267 halp_meta(
268 description,
269 "How to read bare vectors and flat number lists. 2D: (x, y), a third "
270 "component is confidence, flat lists stride by 2. 3D: (x, y, z), a "
271 "fourth component is confidence, flat lists stride by 3. Full records "
272 "are unaffected - they name their fields.")
273 struct range
274 {
275 std::string_view values[2]{"2D", "3D"};
276 E2MCoords init{E2MCoords::TwoD};
277 };
278 } coords;
279
280 // ------------------------------------------------------------- Output
281 struct : halp::combobox_t<"Output Mode", E2MOutputMode>
282 {
283 halp_meta(
284 description,
285 "MPE gives every entity its own channel with per-note pitch bend, "
286 "pressure and CC74 - use it with any MPE synth. Channel per entity "
287 "is the same channel pinning without the MPE handshake, for "
288 "hardware or DAW routing. Single channel puts every note on one "
289 "channel: per-note bend is impossible there, so pitch is latched "
290 "and pressure goes out as polyphonic aftertouch.")
291 struct range
292 {
293 std::string_view values[3]{"MPE", "Channel per entity", "Single channel"};
294 E2MOutputMode init{E2MOutputMode::MPE};
295 };
296 } output_mode;
297
298 struct : halp::combobox_t<"MPE Zone", E2MZone>
299 {
300 halp_meta(
301 description,
302 "Lower zone: channel 1 is the master, notes use channels 2 and up. "
303 "Upper zone: channel 16 is the master, notes go down from 15. Use "
304 "Upper only to share the port with another Lower-zone device.")
305 struct range
306 {
307 std::string_view values[2]{"Lower", "Upper"};
308 E2MZone init{E2MZone::Lower};
309 };
310 } mpe_zone;
311
312 struct : halp::spinbox_i32<"Member Channels", halp::irange{1, 15, 15}>
313 {
314 halp_meta(
315 description,
316 "How many note channels the MPE zone uses (sent in the MPE "
317 "configuration). Also the channel count in Channel-per-entity mode. "
318 "Lower it to leave channels free for other gear on the same port.")
319 } member_channels;
320
321 struct : halp::spinbox_i32<"Bend Range", halp::irange{1, 96, 48}>
322 {
323 halp_meta(
324 description,
325 "Per-note pitch bend range in semitones, sent to the receiver at "
326 "start. 48 is the MPE default and gives eight octaves of glide, so "
327 "a moving entity never needs a retrigger. Must match the receiver "
328 "in non-MPE modes - set its bend range to the same value.")
329 } bend_range;
330
331 struct : halp::toggle<"Send MPE Config", halp::default_on_toggle>
332 {
333 halp_meta(
334 description,
335 "On start, send the MPE configuration (RPN 6) and per-channel bend "
336 "range (RPN 0) so the receiver sets itself up. Turn off if the "
337 "synth is already configured or mangles RPNs.")
338 } send_config;
339
340 struct : halp::spinbox_i32<"Channel", halp::irange{1, 16, 1}>
341 {
342 halp_meta(
343 description,
344 "The MIDI channel used in Single-channel mode. Ignored in the "
345 "other modes.")
346 } single_channel;
347
348 struct : halp::combobox_t<"Channel Reuse", E2MChannelReuse>
349 {
350 halp_meta(
351 description,
352 "What happens when a note ends and its channel could serve a new "
353 "entity. Least recently used waits for release tails to fade "
354 "before a channel is reused, so a new note's bend reset does not "
355 "detune a still-ringing release. Immediate reuses right away.")
356 struct range
357 {
358 std::string_view values[2]{"Least recently used", "Immediate"};
359 E2MChannelReuse init{E2MChannelReuse::LRU};
360 };
361 } channel_reuse;
362
363 struct : halp::spinbox_f32<"Release Reserve", halp::range{0., 5000., 500.}>
364 {
365 halp_meta(
366 description,
367 "Milliseconds a freed channel stays reserved for the previous "
368 "note's release tail. Raise it for long synth releases; 0 to "
369 "disable the reservation entirely.")
370 } release_reserve;
371
372 // ------------------------------------------------------------- Voice
373 struct : halp::combobox_t<"Note Model", E2MNoteModel>
374 {
375 halp_meta(
376 description,
377 "Sustained: the note IS the entity - one note-on when it appears, "
378 "one note-off when it leaves, expression in between. Triggered: "
379 "notes are short events fired when an entity appears (and "
380 "re-fired on pitch change in Retrigger tracking), for percussive "
381 "material. The two have different failure profiles on purpose.")
382 struct range
383 {
384 std::string_view values[2]{"Sustained", "Triggered"};
385 E2MNoteModel init{E2MNoteModel::Sustained};
386 };
387 } note_model;
388
389 struct : halp::combobox_t<"Trigger On", E2MTriggerOn>
390 {
391 halp_meta(
392 description,
393 "Confirmed waits until the tracker has real evidence (~100 ms) - "
394 "no ghost notes from noise. First detection fires on the very "
395 "first sighting for the lowest possible onset latency, at the "
396 "price of occasional false starts.")
397 struct range
398 {
399 std::string_view values[2]{"Confirmed", "First detection"};
400 E2MTriggerOn init{E2MTriggerOn::Confirmed};
401 };
402 } trigger_on;
403
404 struct : halp::spinbox_i32<"Max Voices", halp::irange{1, 16, 8}>
405 {
406 halp_meta(
407 description,
408 "How many entities sound at once. Fewer audible voices than "
409 "trackable entities keeps the result legible - the audience can "
410 "follow one person. Kept below the MPE member count so channel "
411 "sharing (which fuses two people into one voice) never happens.")
412 } max_voices;
413
414 struct : halp::combobox_t<"Steal Policy", E2MPriority>
415 {
416 halp_meta(
417 description,
418 "Who loses their voice when a new entity arrives and all voices "
419 "are busy. Confidence x Age protects long-standing, well-tracked "
420 "entities - a newcomer is the least musically committed. The "
421 "others name the victim directly. A confirmed entity is never "
422 "stolen for a provisional one.")
423 struct range
424 {
425 std::string_view values[5]{
426 "Confidence x Age", "Steal oldest", "Steal newest", "Steal slowest",
427 "Steal least confident"};
428 E2MPriority init{E2MPriority::ConfidenceAge};
429 };
430 } priority;
431
432 struct : halp::toggle<"Allow Stealing", halp::default_on_toggle>
433 {
434 halp_meta(
435 description,
436 "Off: when every voice is busy, new entities are denied (and "
437 "counted on the Denied output) instead of interrupting a playing "
438 "note.")
439 } allow_steal;
440
441 struct : halp::knob_f32<"Steal Margin", halp::range{0., 1., 0.1}>
442 {
443 halp_meta(
444 description,
445 "A newcomer must beat the weakest voice's priority by this much "
446 "to steal it. Without the margin, two near-equal entities "
447 "ping-pong one voice into a stutter.")
448 } steal_margin;
449
450 struct : halp::spinbox_f32<"Lost Grace", halp::range{0., 5000., 250.}>
451 {
452 halp_meta(
453 description,
454 "Milliseconds a note is held after its entity vanishes from the "
455 "tracker. If the entity comes back within the grace it resumes "
456 "the same note on the same channel - no retrigger, no channel "
457 "jump. Raise it for flickery detectors.")
458 } lost_grace;
459
460 struct : halp::spinbox_f32<"Min Note", halp::range{0., 2000., 80.}>
461 {
462 halp_meta(
463 description,
464 "Shortest note the object will ever emit, in ms. An entity that "
465 "blinks out immediately still produces an audible note instead "
466 "of an unmusical click.")
467 } min_note;
468
469 struct : halp::spinbox_f32<"Max Note", halp::range{0., 60000., 0.}>
470 {
471 halp_meta(
472 description,
473 "Hard ceiling on note length in ms; the note is released even if "
474 "the entity stays. 0 = unlimited (the note lives as long as the "
475 "entity).")
476 } max_note;
477
478 struct : halp::spinbox_f32<"Trigger Length", halp::range{10., 5000., 200.}>
479 {
480 halp_meta(
481 description,
482 "Note duration in Triggered mode, in ms. Ignored in Sustained "
483 "mode, where the entity itself decides.")
484 } trigger_duration;
485
486 struct : halp::spinbox_f32<"Retrigger Lockout", halp::range{0., 2000., 120.}>
487 {
488 halp_meta(
489 description,
490 "Minimum ms between two note-ons for the same entity. The "
491 "machine-gun brake for Triggered mode and for entities that "
492 "flicker in and out beyond the lost grace.")
493 } retrig_lockout;
494
495 struct : halp::spinbox_f32<"Watchdog", halp::range{0., 10000., 1000.}>
496 {
497 halp_meta(
498 description,
499 "Independent safety net: if no tracking data at all arrives for a "
500 "held note for this many ms (tracker crashed, cable unplugged), "
501 "the note is released anyway. 0 disables it. This is deliberately "
502 "separate from the tracker's own lifecycle.")
503 } watchdog;
504
505 struct : halp::combobox_t<"While Coasting", E2MCoast>
506 {
507 halp_meta(
508 description,
509 "What expression does while the tracker is coasting (predicting "
510 "through an occlusion). Freeze holds the last real values - "
511 "nothing moves that no one moved. Follow trusts the prediction. "
512 "Fade lets pressure sink towards silence until the entity is "
513 "seen again.")
514 struct range
515 {
516 std::string_view values[3]{"Freeze", "Follow", "Fade"};
517 E2MCoast init{E2MCoast::Freeze};
518 };
519 } coast;
520
521 // ------------------------------------------------------------- Pitch
522 struct : halp::combobox_t<"Pitch Axis", E2MAxis>
523 {
524 halp_meta(
525 description,
526 "Which movement axis drives pitch. Vertical (Y) reads as "
527 "high-note/high-position without explanation - the most legible "
528 "default for an audience.")
529 struct range
530 {
531 std::string_view values[3]{"X", "Y", "Z"};
532 E2MAxis init{E2MAxis::Y};
533 };
534 } pitch_axis;
535
536 struct : halp::toggle<"Invert Axis", halp::default_on_toggle>
537 {
538 halp_meta(
539 description,
540 "Flip the axis. On by default because camera coordinates grow "
541 "downwards: inverted, standing tall plays high.")
542 } pitch_invert;
543
544 struct : halp::spinbox_f32<"Position Min", halp::range{-1000., 1000., 0.}>
545 {
546 halp_meta(
547 description,
548 "Axis value that maps to the lowest pitch. The computer-vision "
549 "chain normalises positions to 0..1, hence the default.")
550 } in_lo;
551
552 struct : halp::spinbox_f32<"Position Max", halp::range{-1000., 1000., 1.}>
553 {
554 halp_meta(
555 description,
556 "Axis value that maps to the highest pitch. Narrow the min/max "
557 "window to make a small stage area cover the whole range.")
558 } in_hi;
559
560 struct : halp::spinbox_i32<"Lowest Pitch", halp::irange{0, 127, 48}>
561 {
562 halp_meta(description, "Bottom of the pitch range (MIDI note, 48 = C3).")
563 } pitch_lo;
564
565 struct : halp::spinbox_i32<"Highest Pitch", halp::irange{0, 127, 84}>
566 {
567 halp_meta(description, "Top of the pitch range (MIDI note, 84 = C6).")
568 } pitch_hi;
569
570 struct : halp::combobox_t<"Pitch Tracking", E2MPitchTracking>
571 {
572 halp_meta(
573 description,
574 "Continuous bend: the note number is set once and per-note pitch "
575 "bend follows the entity - glissando or, with a scale, glides "
576 "that land exactly on scale notes. Latched: pitch is fixed at "
577 "note-on and never moves. Retrigger fires a new note on every "
578 "scale-step change and is only meaningful in Triggered mode.")
579 struct range
580 {
581 std::string_view values[3]{"Continuous bend", "Latched", "Retrigger"};
582 E2MPitchTracking init{E2MPitchTracking::ContinuousBend};
583 };
584 } pitch_tracking;
585
586 struct : halp::spinbox_f32<"Glide", halp::range{0., 2000., 60.}>
587 {
588 halp_meta(
589 description,
590 "Slew time in ms of the pitch bend towards its target. Short = "
591 "tight tracking; long = portamento. Per the MPE spec the slew "
592 "stops the instant the note is released.")
593 } glide;
594
595 struct : halp::combobox_t<"Scale", E2MScale>
596 {
597 halp_meta(
598 description,
599 "Snap pitch to a scale. None is a theremin: raw continuous "
600 "pitch. With a scale, position picks the nearest scale note and "
601 "the bend glides between them - continuous and quantised pitch "
602 "are the same mechanism. Pentatonic scales make any combination "
603 "of entities consonant, which is why installations use them.")
604 struct range
605 {
606 std::string_view values[13]{
607 "None", "Chromatic", "Major", "Natural minor", "Harmonic minor",
608 "Major pentatonic", "Minor pentatonic", "Blues", "Dorian", "Phrygian",
609 "Lydian", "Mixolydian", "Whole tone"};
610 E2MScale init{E2MScale::MinorPentatonic};
611 };
612 } scale;
613
614 struct : halp::combobox_t<"Root", E2MRoot>
615 {
616 halp_meta(description, "Root note of the scale.")
617 struct range
618 {
619 std::string_view values[12]{
620 "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};
621 E2MRoot init{E2MRoot::C};
622 };
623 } root;
624
625 struct : halp::knob_f32<"Snap Hysteresis", halp::range{0., 0.5, 0.15}>
626 {
627 halp_meta(
628 description,
629 "Fraction of a scale step an entity must travel past the "
630 "boundary before the quantiser switches notes. Stops a body "
631 "hovering on a boundary from machine-gunning between two "
632 "pitches - the hysteresis lives here at the quantiser, where the "
633 "chatter actually happens.")
634 } quant_hyst;
635
636 // ------------------------------------------------------------- Velocity
637 struct : halp::combobox_t<"Velocity From", E2MVelSource>
638 {
639 halp_meta(
640 description,
641 "What sets note velocity. Entry speed: how fast the entity was "
642 "moving when it appeared - a run onto the stage hits hard, a "
643 "drift in whispers. Fixed: constant. Confidence: the tracker's "
644 "certainty about the entity.")
645 struct range
646 {
647 std::string_view values[3]{"Entry speed", "Fixed", "Confidence"};
648 E2MVelSource init{E2MVelSource::EntrySpeed};
649 };
650 } vel_source;
651
652 struct : halp::spinbox_i32<"Fixed Velocity", halp::irange{1, 127, 100}>
653 {
654 halp_meta(description, "Velocity used when Velocity From is Fixed.")
655 } vel_fixed;
656
657 struct : halp::spinbox_i32<"Velocity Min", halp::irange{1, 127, 40}>
658 {
659 halp_meta(
660 description,
661 "Softest velocity. 40 keeps even the gentlest entrance audible.")
662 } vel_lo;
663
664 struct : halp::spinbox_i32<"Velocity Max", halp::irange{1, 127, 110}>
665 {
666 halp_meta(description, "Hardest velocity, reached at Speed Reference.")
667 } vel_hi;
668
669 struct : halp::spinbox_f32<"Speed Reference", halp::range{0.01, 100., 2.}>
670 {
671 halp_meta(
672 description,
673 "Speed (in tracker coordinate units/s) that maps to maximum "
674 "velocity and full Speed expression. In normalised camera space, "
675 "2.0 means crossing the whole frame in half a second.")
676 } speed_ref;
677
678 struct : halp::spinbox_f32<"Speed Window", halp::range{0., 500., 100.}>
679 {
680 halp_meta(
681 description,
682 "Entry-speed velocity uses the PEAK speed over this many ms "
683 "before the note starts. Measuring at the exact instant of "
684 "confirmation catches the tracker's smoother still converging "
685 "and every note comes out the same - the peak over a short "
686 "window is the real gesture.")
687 } preroll;
688
689 // ------------------------------------------------------------- Expression
690 struct : halp::combobox_t<"Pressure From", E2MSource>
691 {
692 halp_meta(
693 description,
694 "What drives per-note pressure (the loudness/intensity dimension "
695 "on MPE synths). Speed: moving fast presses hard - stillness is "
696 "silence, motion is sound. In Single-channel mode this goes out "
697 "as polyphonic aftertouch.")
698 struct range
699 {
700 // Order must match E2MSource exactly.
701 std::string_view values[27]{
702 "None", "X",
703 "Y", "Z",
704 "Radius", "Speed",
705 "Velocity X", "Velocity Y",
706 "Velocity Z", "|Velocity X|",
707 "|Velocity Y|", "|Velocity Z|",
708 "Acceleration", "Accel X",
709 "Accel Y", "Accel Z",
710 "Jerk", "Turn rate",
711 "Curvature", "Heading sin",
712 "Heading cos", "Agitation",
713 "Nearest neighbour", "Distance to centroid",
714 "Density", "Age",
715 "Confidence"};
716
717 E2MSource init{E2MSource::Speed};
718 };
719 } pressure_src;
720
721 struct : halp::combobox_t<"Timbre From", E2MSource>
722 {
723 halp_meta(
724 description,
725 "What drives the timbre control (CC74, the brightness dimension "
726 "on MPE synths). Defaults to X so left-right position colours "
727 "the sound while height plays pitch.")
728 struct range
729 {
730 // Order must match E2MSource exactly.
731 std::string_view values[27]{
732 "None", "X",
733 "Y", "Z",
734 "Radius", "Speed",
735 "Velocity X", "Velocity Y",
736 "Velocity Z", "|Velocity X|",
737 "|Velocity Y|", "|Velocity Z|",
738 "Acceleration", "Accel X",
739 "Accel Y", "Accel Z",
740 "Jerk", "Turn rate",
741 "Curvature", "Heading sin",
742 "Heading cos", "Agitation",
743 "Nearest neighbour", "Distance to centroid",
744 "Density", "Age",
745 "Confidence"};
746
747 E2MSource init{E2MSource::X};
748 };
749 } timbre_src;
750
751 struct : halp::spinbox_i32<"Timbre CC", halp::irange{0, 127, 74}>
752 {
753 halp_meta(
754 description,
755 "Controller number for the timbre dimension. 74 is the MPE "
756 "standard; change it only for non-MPE receivers.")
757 } timbre_cc;
758
759 struct : halp::spinbox_f32<"Rise", halp::range{0., 2000., 20.}>
760 {
761 halp_meta(
762 description,
763 "Smoothing time when expression values increase. Fast attack "
764 "with slow release is the difference between responsive and "
765 "twitchy - a symmetric filter cannot give you both.")
766 } rise;
767
768 struct : halp::spinbox_f32<"Fall", halp::range{0., 2000., 80.}>
769 {
770 halp_meta(
771 description,
772 "Smoothing time when expression values decrease. Longer than "
773 "Rise so gestures speak instantly and decay musically.")
774 } fall;
775
776 struct : halp::spinbox_f32<"Expression Rate", halp::range{1., 200., 50.}>
777 {
778 halp_meta(
779 description,
780 "Updates per second for each expression dimension of each note. "
781 "50 Hz is imperceptible from continuous; lower it for DIN ports "
782 "or receivers that choke on dense CC streams.")
783 } expr_rate;
784
785 struct : halp::knob_f32<"Deadband", halp::range{0., 0.1, 0.005}>
786 {
787 halp_meta(
788 description,
789 "Suppress expression changes smaller than this fraction of full "
790 "scale. Kills the byte stream of an entity standing still "
791 "without affecting real motion.")
792 } deadband;
793
794 // ------------------------------------------------------------- Rhythm
795 struct : halp::combobox_t<"Beat Quantize", E2MQuantTargets>
796 {
797 halp_meta(
798 description,
799 "Snap notes to the musical grid of the score (which a Beat "
800 "Tracker can be driving live from a drummer). Onsets delays each "
801 "note-on to the nearest grid point - at most half a division. "
802 "Adding offsets snaps releases too. A note-off is never allowed "
803 "to land before its note-on.")
804 struct range
805 {
806 std::string_view values[3]{"Off", "Onsets", "Onsets + offsets"};
807 E2MQuantTargets init{E2MQuantTargets::Off};
808 };
809 } quant_mode;
810
811 struct : halp::combobox_t<"Grid", E2MGrid>
812 {
813 halp_meta(
814 description,
815 "Grid division notes snap to. Finer grids mean less hold "
816 "latency: at 120 BPM, 1/8 holds at most 125 ms, 1/16 at most "
817 "62 ms.")
818 struct range
819 {
820 std::string_view values[7]{"1/1", "1/2", "1/4", "1/8",
821 "1/16", "1/8T", "1/16T"};
822 E2MGrid init{E2MGrid::Eighth};
823 };
824 } grid;
825
826 struct : halp::knob_f32<"Quantize Strength", halp::range{0., 1., 1.}>
827 {
828 halp_meta(
829 description,
830 "Blend between the gesture's own timing (0) and the grid (1). "
831 "Partial values keep the human feel while tightening it.")
832 } strength;
833
834 struct : halp::spinbox_f32<"Max Hold", halp::range{0., 2000., 250.}>
835 {
836 halp_meta(
837 description,
838 "Longest a note will wait for its grid point, in ms. If the "
839 "transport stalls or the beat tracker drops out, the note plays "
840 "anyway instead of being stranded forever.")
841 } max_hold;
842
843 // ------------------------------------------------------------- Safety
844 struct : halp::impulse_button<"Panic">
845 {
846 halp_meta(
847 description,
848 "Release every held note now: per-voice note-offs, then All "
849 "Sound Off / All Notes Off / Reset Controllers on every touched "
850 "channel, then bend/pressure/timbre reset - messages spaced so a "
851 "DIN receiver is not overrun at the exact moment it must work.")
852 void update(auto& self) { self.panic_requested = true; }
853 } panic;
854
855 struct : halp::toggle<"Panic On Stop", halp::default_on_toggle>
856 {
857 halp_meta(
858 description,
859 "Run the panic routine when the transport stops or jumps, so "
860 "stopping the score never leaves a synth droning. Leave on "
861 "unless you are debugging the routine itself.")
862 } panic_on_stop;
863
864 struct : halp::spinbox_f32<"Panic Spacing", halp::range{0., 20., 1.}>
865 {
866 halp_meta(
867 description,
868 "Milliseconds between consecutive panic and configuration "
869 "messages. 1 ms keeps a DIN receiver from being overrun by the "
870 "burst; 0 sends everything back-to-back.")
871 } spacing;
872
873 // ------------------------------------------------- Analysis references
874 //
875 // Declared last on purpose: the flattened input order is what .scp presets
876 // index into, so appending leaves every existing preset entry pointing at
877 // the control it was written for. Their place in the UI is set by the
878 // layout below, not by this order.
879
880 struct : halp::spinbox_f32<"Acceleration Ref", halp::range{0.01, 200., 8.}>
881 {
882 halp_meta(
883 description,
884 "Acceleration, in coordinate units per second squared, that maps to "
885 "the top of the range. Much larger than the speed reference: a "
886 "gesture that reverses within a tenth of a second is already tens of "
887 "units per second squared.")
888 } accel_ref;
889
890 struct : halp::spinbox_f32<"Jerk Ref", halp::range{0.1, 5000., 200.}>
891 {
892 halp_meta(
893 description,
894 "Jerk (units per second cubed) that maps to the top of the range. "
895 "Jerk is the sharpest onset cue there is, and also the noisiest "
896 "descriptor - raise Descriptor Smoothing with it.")
897 } jerk_ref;
898
899 struct : halp::spinbox_f32<"Turn Rate Ref", halp::range{0.1, 50., 6.28}>
900 {
901 halp_meta(
902 description,
903 "Turn rate, in radians per second, that maps to the top of the "
904 "range. The default is one full turn per second.")
905 } turn_ref;
906
907 struct : halp::spinbox_f32<"Neighbour Range", halp::range{0.001, 100., 0.25}>
908 {
909 halp_meta(
910 description,
911 "Radius, in coordinate units, within which another entity counts as "
912 "a neighbour. Sets the scale of both Nearest Neighbour and Density: "
913 "roughly how close two people have to be to read as together.")
914 } neighbour_range;
915
916 struct : halp::spinbox_f32<"Age Ref", halp::range{0.1, 600., 10.}>
917 {
918 halp_meta(
919 description,
920 "How long an entity must have been present, in seconds, for Age to "
921 "reach the top of the range - the length of the arc a mapping from "
922 "Age draws.")
923 } age_ref;
924
925 struct : halp::spinbox_f32<"Descriptor Smoothing", halp::range{0., 2000., 80.}>
926 {
927 halp_meta(
928 description,
929 "Time constant, in milliseconds, applied to the derivative-based "
930 "descriptors (acceleration, jerk, turn rate, agitation). Each "
931 "derivative multiplies the detector's position noise by about 1/dt, "
932 "so acceleration carries it squared and jerk cubed: without "
933 "smoothing they are noise. Raise it until the mapping is playable.")
934 } desc_smooth;
935 } inputs;
936
937 struct outs
938 {
939 struct : halp::midi_out_bus<"MIDI">
940 {
941 halp_meta(
942 description,
943 "The MIDI stream: notes, per-note expression, MPE configuration "
944 "and panic messages, sample-accurately timestamped. Cable it to "
945 "a MIDI device or another process.")
946 ossia::net::node_base* ossia_node{};
947 } midi;
948
949 struct
950 {
951 halp_meta(name, "Active Voices")
952 halp_meta(
953 description,
954 "Number of notes currently sounding (including notes held "
955 "through a tracking dropout).")
956 int value{};
957 } active;
958
959 struct
960 {
961 halp_meta(name, "Denied")
962 halp_meta(
963 description,
964 "Running count of entities that wanted a voice and were refused "
965 "(voices full and stealing off or not worth it). If this climbs "
966 "during a show, raise Max Voices or loosen the steal policy - "
967 "silent drops are how installations get debugged at 2 am.")
968 int value{};
969 } denied;
970
971 } outputs;
972
973 using tick = halp::tick_musical;
974
975 // ======================================================== implementation
976
977 static constexpr int k_max_voices = 16;
978
979 enum class vstate : uint8_t
980 {
981 off, // free slot
982 pending_on, // waiting for grid point / not yet emitted
983 sounding, // note-on on the wire
984 };
985
986 struct voice
987 {
988 int32_t id = -1;
989 int8_t channel = -1;
990 uint8_t note = 60;
991 vstate st = vstate::off;
992 bool on_wire = false; // note-on emitted, note-off not yet
993 bool entity_confirmed = false;
994 bool missing = false; // entity absent from the last track list
995 bool coasting = false; // tracker reports coasting
996 bool off_scheduled = false;
1000 bool retrig_pending = false;
1001 bool note_suppressed = false; // single-channel (channel,pitch) collision
1002 double missing_since = 0.;
1003 double last_seen = 0.; // watchdog: last time data for id arrived
1004 double on_time = 0.; // when the note-on was emitted
1005 double on_raw_t = 0.; // when the trigger happened (pre-quantise)
1006 double on_target_q = -1.; // grid target in quarters, < 0 = immediate
1007 double off_raw_t = 0.;
1008 double off_target_q = -1.;
1009 float priority = 0.f;
1010 // mapping state
1011 float pitch_target = 60.f; // quantised, continuous semitones
1012 float quant_prev = -1.f; // scale quantiser memory
1013 float bend_semis = 0.f; // current slewed bend offset
1014 float pressure_v = 0.f; // smoothed 0..1
1015 float timbre_v = 0.f;
1016 float pressure_t = 0.f; // targets 0..1
1017 float timbre_t = 0.f;
1018 int last_bend = -1; // last emitted values, for deadband
1019 int last_pressure = -1;
1020 int last_timbre = -1;
1021 double expr_acc = 1e9; // time since last expression send
1022 uint8_t velocity = 100;
1023 };
1024
1026 {
1027 bool in_use = false;
1028 double freed_at = -1e18;
1029 };
1030
1032 {
1033 double t;
1034 uint8_t n;
1035 uint8_t bytes[3];
1036 };
1037
1038 struct out_msg
1039 {
1040 int64_t ts;
1041 uint8_t prio; // 0 off/panic, 1 on, 2 expression, 3 global
1042 uint8_t n;
1043 uint8_t bytes[3];
1044 };
1045
1047 {
1048 static constexpr int N = 24;
1049 double t[N] = {};
1050 float s[N] = {};
1051 int head = 0;
1052 double last_t = 0.;
1053 void push(double time, float speed) noexcept
1054 {
1055 t[head] = time;
1056 s[head] = speed;
1057 head = (head + 1) % N;
1058 last_t = time;
1059 }
1060 float peak_since(double t0) const noexcept
1061 {
1062 float m = 0.f;
1063 for(int i = 0; i < N; i++)
1064 if(t[i] >= t0 && s[i] > m)
1065 m = s[i];
1066 return m;
1067 }
1068 };
1069
1073 {
1074 double time = 0.;
1075 double first_seen = 0.;
1079 double last_dt = 0.;
1080 decltype(track_record::position) pos{};
1081
1082 // --- analysis history ---
1083 //
1084 // Each derivative multiplies the detector's position noise by roughly
1085 // 1/dt, so acceleration carries it squared and jerk cubed. At 60 Hz that
1086 // turns a millimetre of jitter into metres per second squared. Every one
1087 // of them is therefore low-passed before it is offered as a mapping
1088 // source; the raw values would be unplayable.
1089 decltype(track_record::velocity) vel{};
1090 decltype(track_record::velocity) accel{};
1091 float heading = 0.f;
1092 bool has_heading = false;
1093
1094 ossia::one_pole_filter<float> f_accel, f_jerk, f_turn, f_agitation;
1095 ossia::one_pole_filter<float> f_ax, f_ay, f_az;
1098 ossia::one_pole_filter<float> f_vx, f_vy, f_vz;
1099 };
1100 ossia::flat_map<int, synth_state> m_synth;
1101 std::vector<track_record> m_recs;
1102
1105 ossia::flat_map<int, entity_descriptors> m_desc;
1106
1107 bool tracks_dirty = false;
1108 bool panic_requested = false;
1109
1110 // What the stop-time panic sent (or would have sent, if no device is
1111 // bound): raw 3-byte messages, in order. Public for tests and post-mortems.
1112 std::vector<std::array<uint8_t, 3>> last_direct_panic;
1113
1114 // exec-state hookup: lets the binding resolve outputs.midi.ossia_node so
1115 // the stop-time panic can reach the device even though ticks have ended.
1116 ossia::exec_state_facade ossia_state;
1117 std::atomic<ossia::net::midi::midi_protocol*> midi_out{};
1118
1119 void prepare(halp::setup s)
1120 {
1121 if(s.rate > 0)
1122 m_rate = s.rate;
1123 }
1124
1125 void start()
1126 {
1127 m_do_config = true;
1128 m_started = true;
1129 }
1130
1131 void stop()
1132 {
1133 if(inputs.panic_on_stop)
1134 panic_direct();
1135 hard_reset();
1136 m_started = false;
1137 }
1138
1139 void pause()
1140 {
1141 if(inputs.panic_on_stop)
1142 panic_direct();
1143 hard_reset();
1144 }
1145
1146 void resume() { m_do_config = true; }
1147
1148 void transport(auto flicks)
1149 {
1150 // A transport jump mid-performance: release everything cleanly on the
1151 // next tick; live entities immediately re-acquire their notes.
1152 if(inputs.panic_on_stop)
1153 panic_requested = true;
1154 }
1155
1156 void operator()(const halp::tick_musical& tk)
1157 {
1158 const int frames = std::max(tk.frames, 1);
1159 const double dt = frames / m_rate;
1160 const double now = m_now;
1161 const double t_end = now + dt;
1162
1163 resolve_protocol();
1164
1165 m_msgs.clear();
1166
1167 // Detect an output-config change while running: panic + reconfigure.
1168 const uint64_t sig = config_signature();
1169 if(sig != m_config_sig)
1170 {
1171 if(m_config_sig != 0 && held_count() > 0)
1172 queue_panic();
1173 m_config_sig = sig;
1174 m_do_config = true;
1175 }
1176
1177 if(m_do_config)
1178 {
1179 m_do_config = false;
1180 if(inputs.send_config && inputs.output_mode.value == E2MOutputMode::MPE)
1181 queue_mpe_config();
1182 }
1183
1184 if(panic_requested)
1185 {
1186 panic_requested = false;
1187 queue_panic();
1188 }
1189
1190 if(tracks_dirty)
1191 {
1192 tracks_dirty = false;
1193 parse_entities(now);
1194 analyse(now);
1195 ingest(now);
1196 }
1197
1198 lifecycle(now, tk);
1199 emit_pending_notes(now, dt, frames, tk);
1200 update_expression(now, dt, frames);
1201 flush_timed(now, dt, frames);
1202
1203 // Sort by timestamp; at equal timestamps note-offs go before note-ons
1204 // before expression, so a steal never produces on/on/off.
1205 std::stable_sort(m_msgs.begin(), m_msgs.end(), [](const out_msg& a, const out_msg& b) {
1206 return a.ts != b.ts ? a.ts < b.ts : a.prio < b.prio;
1207 });
1208 for(const auto& m : m_msgs)
1209 {
1210 auto& msg = outputs.midi.midi_messages.emplace_back();
1211 msg.bytes.assign(m.bytes, m.bytes + m.n);
1212 msg.timestamp = std::clamp<int64_t>(m.ts, 0, frames - 1);
1213 }
1214
1215 // Invariants 2 and 4, asserted on the wire itself: replay this tick's
1216 // messages, in their final order, into a persistent model of what the
1217 // receiver holds. A note-on to a (channel, pitch) still sounding - e.g.
1218 // to a voice whose note-off is later in the buffer - or an off for a
1219 // pitch not held is recorded and reported by check_invariants().
1220 for(const auto& m : m_msgs)
1221 {
1222 const uint8_t st = m.bytes[0] & 0xF0;
1223 const int ch = m.bytes[0] & 0x0F;
1224 if(st == 0x90 && m.bytes[2] > 0)
1225 {
1226 if(m_wire[ch].test(m.bytes[1]))
1227 m_wire_err = "note-on while the (channel, pitch) is still held on the wire";
1228 m_wire[ch].set(m.bytes[1]);
1229 }
1230 else if(st == 0x80 || (st == 0x90 && m.bytes[2] == 0))
1231 {
1232 if(!m_wire[ch].test(m.bytes[1]))
1233 m_wire_err = "note-off for a (channel, pitch) not held on the wire";
1234 m_wire[ch].reset(m.bytes[1]);
1235 }
1236 }
1237
1238 outputs.active.value = held_count();
1239 outputs.denied.value = m_denied;
1240
1241 m_now = t_end;
1242 }
1243
1244 // ------------------------------------------------------------- invariants
1245 // Callable from tests: returns false and fills err on the first violation.
1246 bool check_invariants(std::string* err = nullptr) const
1247 {
1248 // 2 & 4, wire form: the replayed receiver model caught an on to a
1249 // still-sounding (channel, pitch) or an off for a free one.
1250 if(!m_wire_err.empty())
1251 {
1252 if(err)
1253 *err = m_wire_err;
1254 return false;
1255 }
1256 // 1. every on-wire note belongs to a live voice, and the outstanding
1257 // counter matches.
1258 int on_wire = 0;
1259 for(const auto& v : m_voices)
1260 if(v.on_wire)
1261 {
1262 on_wire++;
1263 if(v.st == vstate::off)
1264 {
1265 if(err)
1266 *err = "note on wire for a dead voice";
1267 return false;
1268 }
1269 }
1270 if(on_wire != m_outstanding)
1271 {
1272 if(err)
1273 *err = "outstanding note count mismatch";
1274 return false;
1275 }
1276 // 2. at most one outstanding note per (channel, pitch)
1277 for(int i = 0; i < k_max_voices; i++)
1278 for(int j = i + 1; j < k_max_voices; j++)
1279 if(m_voices[i].on_wire && m_voices[j].on_wire
1280 && m_voices[i].channel == m_voices[j].channel
1281 && m_voices[i].note == m_voices[j].note)
1282 {
1283 if(err)
1284 *err = "duplicate (channel, pitch) on wire";
1285 return false;
1286 }
1287 return true;
1288 }
1289
1290 int held_count() const noexcept
1291 {
1292 int n = 0;
1293 for(const auto& v : m_voices)
1294 if(v.on_wire)
1295 n++;
1296 return n;
1297 }
1298
1299 int outstanding() const noexcept { return m_outstanding; }
1300
1304 const std::vector<track_record>& parsed_entities() const noexcept { return m_recs; }
1305
1309 const ossia::flat_map<int, entity_descriptors>& descriptors() const noexcept
1310 {
1311 return m_desc;
1312 }
1313
1314private:
1315 // ------------------------------------------------------------- channels
1316 bool is_mpe() const noexcept
1317 {
1318 return inputs.output_mode.value == E2MOutputMode::MPE;
1319 }
1320 bool is_single() const noexcept
1321 {
1322 return inputs.output_mode.value == E2MOutputMode::SingleChannel;
1323 }
1324
1325 int master_channel() const noexcept
1326 {
1327 return inputs.mpe_zone.value == E2MZone::Lower ? 0 : 15;
1328 }
1329
1330 int member_count() const noexcept
1331 {
1332 if(is_single())
1333 return 1;
1334 int n = std::clamp(inputs.member_channels.value, 1, is_mpe() ? 15 : 16);
1335 return n;
1336 }
1337
1338 // The i-th note channel (0-based MIDI channel number).
1339 int member_channel(int i) const noexcept
1340 {
1341 if(is_single())
1342 return std::clamp(inputs.single_channel.value - 1, 0, 15);
1343 if(is_mpe())
1344 return inputs.mpe_zone.value == E2MZone::Lower ? 1 + i : 14 - i;
1345 return i; // channel-per-entity: channels 1..N (0-based 0..N-1)
1346 }
1347
1348 int alloc_channel(double now) noexcept
1349 {
1350 if(is_single())
1351 return member_channel(0);
1352
1353 const int n = member_count();
1354 const bool lru = inputs.channel_reuse.value == E2MChannelReuse::LRU;
1355 const double reserve = inputs.release_reserve.value * 1e-3;
1356
1357 int best = -1;
1358 double best_score = 0.;
1359 bool best_ok = false;
1360 for(int i = 0; i < n; i++)
1361 {
1362 const int c = member_channel(i);
1363 const auto& ci = m_chans[c];
1364 if(ci.in_use)
1365 continue;
1366 const double idle = now - ci.freed_at;
1367 const bool ok = !lru || idle >= reserve;
1368 // Prefer channels past the release reserve; among those (or among
1369 // reserved ones if none qualify) pick the least recently freed.
1370 if(best < 0 || (ok && !best_ok) || (ok == best_ok && idle > best_score))
1371 {
1372 best = c;
1373 best_score = idle;
1374 best_ok = ok;
1375 }
1376 }
1377 if(best >= 0)
1378 m_chans[best].in_use = true;
1379 return best;
1380 }
1381
1382 void free_channel(int c, double t) noexcept
1383 {
1384 if(c >= 0 && c < 16)
1385 {
1386 m_chans[c].in_use = false;
1387 m_chans[c].freed_at = t;
1388 m_touched[c] = true;
1389 }
1390 }
1391
1392 // ------------------------------------------------------------- mapping
1393 static float axis_of(const track_record& r, E2MAxis a) noexcept
1394 {
1395 switch(a)
1396 {
1397 case E2MAxis::X:
1398 return r.position.x;
1399 case E2MAxis::Y:
1400 return r.position.y;
1401 case E2MAxis::Z:
1402 return r.position.z;
1403 }
1404 return 0.f;
1405 }
1406
1407 float norm_axis(const track_record& r, E2MAxis a, bool invert) const noexcept
1408 {
1409 const float lo = inputs.in_lo.value, hi = inputs.in_hi.value;
1410 float n = hi != lo ? (axis_of(r, a) - lo) / (hi - lo) : 0.f;
1411 n = std::clamp(n, 0.f, 1.f);
1412 return invert ? 1.f - n : n;
1413 }
1414
1415 float speed_of(const track_record& r) const noexcept
1416 {
1417 const float vx = r.velocity.x, vy = r.velocity.y, vz = r.velocity.z;
1418 return std::sqrt(vx * vx + vy * vy + vz * vz);
1419 }
1420
1421 float norm_speed(const track_record& r) const noexcept
1422 {
1423 const float ref = std::max(inputs.speed_ref.value, 1e-3f);
1424 return std::clamp(speed_of(r) / ref, 0.f, 1.f);
1425 }
1426
1427 float raw_pitch(const track_record& r) const noexcept
1428 {
1429 const float n = norm_axis(r, inputs.pitch_axis.value, inputs.pitch_invert.value);
1430 const float lo = float(inputs.pitch_lo.value);
1431 const float hi = float(inputs.pitch_hi.value);
1432 return lo + n * (hi - lo);
1433 }
1434
1435 static const uint16_t* scale_mask(E2MScale s) noexcept
1436 {
1437 // Bit i set = pitch class i (relative to root) is in the scale.
1438 static constexpr uint16_t masks[13] = {
1439 0, // None (unused)
1440 0b111111111111, // Chromatic
1441 0b101010110101, // Major: 0 2 4 5 7 9 11
1442 0b010110101101, // Natural minor: 0 2 3 5 7 8 10
1443 0b100110101101, // Harmonic minor: 0 2 3 5 7 8 11
1444 0b001010010101, // Major pentatonic: 0 2 4 7 9
1445 0b010010101001, // Minor pentatonic: 0 3 5 7 10
1446 0b010011101001, // Blues: 0 3 5 6 7 10
1447 0b011010101101, // Dorian: 0 2 3 5 7 9 10
1448 0b010110101011, // Phrygian: 0 1 3 5 7 8 10
1449 0b101011010101, // Lydian: 0 2 4 6 7 9 11
1450 0b011010110101, // Mixolydian: 0 2 4 5 7 9 10
1451 0b010101010101, // Whole tone: 0 2 4 6 8 10
1452 };
1453 return &masks[int(s)];
1454 }
1455
1456 bool in_scale(int note) const noexcept
1457 {
1458 const auto s = inputs.scale.value;
1459 if(s == E2MScale::None || s == E2MScale::Chromatic)
1460 return true;
1461 const int root = int(inputs.root.value);
1462 const int pc = ((note - root) % 12 + 12) % 12;
1463 return (*scale_mask(s) >> pc) & 1;
1464 }
1465
1466 float nearest_scale_note(float raw) const noexcept
1467 {
1468 const auto s = inputs.scale.value;
1469 if(s == E2MScale::None)
1470 return raw;
1471 if(s == E2MScale::Chromatic)
1472 return std::round(raw);
1473 float best = std::round(raw);
1474 float best_d = 1e9f;
1475 const int c = int(std::floor(raw));
1476 for(int k = c - 12; k <= c + 13; k++)
1477 {
1478 if(k < 0 || k > 127 || !in_scale(k))
1479 continue;
1480 const float d = std::abs(raw - float(k));
1481 if(d < best_d)
1482 {
1483 best_d = d;
1484 best = float(k);
1485 }
1486 }
1487 return best;
1488 }
1489
1490 // Scale quantiser with hysteresis at the quantiser (not at the sensor):
1491 // switching to a new scale note requires the raw pitch to travel past the
1492 // midpoint by quant_hyst * the distance between the two candidates.
1493 float quantize_pitch(float raw, voice& v) const noexcept
1494 {
1495 if(inputs.scale.value == E2MScale::None)
1496 {
1497 v.quant_prev = -1.f;
1498 return raw;
1499 }
1500 const float cand = nearest_scale_note(raw);
1501 if(v.quant_prev < 0.f)
1502 {
1503 v.quant_prev = cand;
1504 return cand;
1505 }
1506 if(cand != v.quant_prev)
1507 {
1508 const float step = std::abs(cand - v.quant_prev);
1509 const float mid = 0.5f * (cand + v.quant_prev);
1510 const float h = inputs.quant_hyst.value * step;
1511 if((cand > v.quant_prev && raw > mid + h) || (cand < v.quant_prev && raw < mid - h))
1512 v.quant_prev = cand;
1513 }
1514 return v.quant_prev;
1515 }
1516
1526 static constexpr std::size_t max_pairwise = 64;
1527
1530 void analyse(double now)
1531 {
1532 m_desc.clear();
1533 if(m_recs.empty())
1534 return;
1535
1536 const float in_lo = inputs.in_lo.value, in_hi = inputs.in_hi.value;
1537 const float half = std::max(0.5f * (in_hi - in_lo), 1e-6f);
1538 // The Position Min/Max window applies to every axis, so one centre and one
1539 // half-extent describe the field.
1540 const float centre = 0.5f * (in_lo + in_hi);
1541 const bool is_3d = inputs.coords.value == E2MCoords::ThreeD;
1542
1543 const float sref = std::max(inputs.speed_ref.value, 1e-6f);
1544 const float aref = std::max(inputs.accel_ref.value, 1e-6f);
1545 const float jref = std::max(inputs.jerk_ref.value, 1e-6f);
1546 const float tref = std::max(inputs.turn_ref.value, 1e-6f);
1547 const float nrange = std::max(inputs.neighbour_range.value, 1e-6f);
1548 const float tau = std::max(inputs.desc_smooth.value * 1e-3f, 1e-4f);
1549
1550 // Centre of mass, for CentroidDistance.
1551 float gx = 0.f, gy = 0.f, gz = 0.f;
1552 for(const auto& r : m_recs)
1553 {
1554 gx += r.position.x;
1555 gy += r.position.y;
1556 gz += r.position.z;
1557 }
1558 const float inv_n = 1.f / float(m_recs.size());
1559 gx *= inv_n;
1560 gy *= inv_n;
1561 gz *= inv_n;
1562
1563 const bool pairwise = m_recs.size() <= max_pairwise;
1564
1565 for(std::size_t i = 0; i < m_recs.size(); i++)
1566 {
1567 const auto& r = m_recs[i];
1568 entity_descriptors d;
1569
1570 auto st_it = m_synth.find(r.id);
1571 const double dt = (st_it != m_synth.end()) ? st_it->second.last_dt : 0.;
1572 const float fdt = float(dt > 1e-6 ? dt : 0.);
1573 const float alpha = fdt > 0.f ? ossia::lag_alpha(tau, fdt) : 1.f;
1574
1575 // --- position ---
1576 d.x = norm_axis(r, E2MAxis::X, false);
1577 d.y = norm_axis(r, E2MAxis::Y, false);
1578 d.z = norm_axis(r, E2MAxis::Z, false);
1579 {
1580 // Only over the axes the source actually uses: in a 2D scene z is 0,
1581 // which is not the centre of the Position Min/Max window, so folding
1582 // it in would put every entity at full radius no matter where it is.
1583 const float dx = r.position.x - centre, dy = r.position.y - centre;
1584 float r2 = dx * dx + dy * dy;
1585 if(is_3d)
1586 {
1587 const float dz = r.position.z - centre;
1588 r2 += dz * dz;
1589 }
1590 d.radius = std::clamp(std::sqrt(r2) / half, 0.f, 1.f);
1591 }
1592 d.confidence = std::clamp(r.confidence, 0.f, 1.f);
1593 d.age = std::clamp(r.age / std::max(inputs.age_ref.value, 1e-6f), 0.f, 1.f);
1594
1595 // --- velocity ---
1596 const float vx = r.velocity.x, vy = r.velocity.y, vz = r.velocity.z;
1597 const float sp = std::sqrt(vx * vx + vy * vy + vz * vz);
1598 d.speed = std::clamp(sp / sref, 0.f, 1.f);
1599 d.vx = bipolar(vx / sref);
1600 d.vy = bipolar(vy / sref);
1601 d.vz = bipolar(vz / sref);
1602 d.sx = std::clamp(std::abs(vx) / sref, 0.f, 1.f);
1603 d.sy = std::clamp(std::abs(vy) / sref, 0.f, 1.f);
1604 d.sz = std::clamp(std::abs(vz) / sref, 0.f, 1.f);
1605
1606 // --- acceleration, jerk, heading: all need a previous frame ---
1607 if(st_it != m_synth.end() && fdt > 0.f)
1608 {
1609 auto& st = st_it->second;
1610
1611 const float ax = (vx - st.vel.x) / fdt;
1612 const float ay = (vy - st.vel.y) / fdt;
1613 const float az = (vz - st.vel.z) / fdt;
1614 const float amag = std::sqrt(ax * ax + ay * ay + az * az);
1615
1616 d.accel = std::clamp(st.f_accel(amag, alpha) / aref, 0.f, 1.f);
1617 d.ax = bipolar(st.f_ax(ax, alpha) / aref);
1618 d.ay = bipolar(st.f_ay(ay, alpha) / aref);
1619 d.az = bipolar(st.f_az(az, alpha) / aref);
1620
1621 const float jx = (ax - st.accel.x) / fdt;
1622 const float jy = (ay - st.accel.y) / fdt;
1623 const float jz = (az - st.accel.z) / fdt;
1624 const float jmag = std::sqrt(jx * jx + jy * jy + jz * jz);
1625 d.jerk = std::clamp(st.f_jerk(jmag, alpha) / jref, 0.f, 1.f);
1626
1627 // Heading on the ground plane: that is the direction an audience
1628 // reads, and it stays defined when an entity only moves in x/y.
1629 if(sp > 1e-5f)
1630 {
1631 const float raw = std::atan2(vy, vx);
1632 const float unwrapped
1633 = st.has_heading ? ossia::unwrap_angle(raw, st.heading) : raw;
1634 const float rate = st.has_heading ? (unwrapped - st.heading) / fdt : 0.f;
1635
1636 d.turn_rate = std::clamp(std::abs(st.f_turn(rate, alpha)) / tref, 0.f, 1.f);
1637 // Curvature is turn per distance travelled rather than per second,
1638 // so a slow careful arc and a fast one read the same.
1639 d.curvature = std::clamp(std::abs(rate) / (sp * tref) * sref, 0.f, 1.f);
1640 d.heading_sin = bipolar(std::sin(raw));
1641 d.heading_cos = bipolar(std::cos(raw));
1642
1643 st.heading = unwrapped;
1644 st.has_heading = true;
1645 }
1646 else
1647 {
1648 d.heading_sin = bipolar(std::sin(st.heading));
1649 d.heading_cos = bipolar(std::cos(st.heading));
1650 }
1651
1652 // Agitation: how far the velocity strays from its own average. Speed
1653 // alone cannot tell a purposeful walk from a jitter.
1654 const float mvx = st.f_vx(vx, alpha), mvy = st.f_vy(vy, alpha),
1655 mvz = st.f_vz(vz, alpha);
1656 const float ddx = vx - mvx, ddy = vy - mvy, ddz = vz - mvz;
1657 const float dev = std::sqrt(ddx * ddx + ddy * ddy + ddz * ddz);
1658 d.agitation = std::clamp(st.f_agitation(dev, alpha) / sref, 0.f, 1.f);
1659
1660 st.vel = r.velocity;
1661 st.accel = {ax, ay, az};
1662 }
1663
1664 // --- relations to the other entities ---
1665 {
1666 const float dx = r.position.x - gx, dy = r.position.y - gy,
1667 dz = r.position.z - gz;
1668 d.centroid_dist
1669 = std::clamp(std::sqrt(dx * dx + dy * dy + dz * dz) / half, 0.f, 1.f);
1670 }
1671
1672 if(pairwise && m_recs.size() > 1)
1673 {
1674 float best = std::numeric_limits<float>::max();
1675 int within = 0;
1676 for(std::size_t j = 0; j < m_recs.size(); j++)
1677 {
1678 if(j == i)
1679 continue;
1680 const auto& o = m_recs[j];
1681 const float dx = r.position.x - o.position.x;
1682 const float dy = r.position.y - o.position.y;
1683 const float dz = r.position.z - o.position.z;
1684 const float dist = std::sqrt(dx * dx + dy * dy + dz * dz);
1685 best = std::min(best, dist);
1686 if(dist <= nrange)
1687 within++;
1688 }
1689 d.nearest = std::clamp(best / nrange, 0.f, 1.f);
1690 // Normalised against the frame: 1 means everyone else is inside the
1691 // radius, which is what "crowded" means here.
1692 d.density = std::clamp(float(within) / float(m_recs.size() - 1), 0.f, 1.f);
1693 }
1694
1695 m_desc[r.id] = d;
1696 }
1697 }
1698
1702 static float bipolar(float v) noexcept
1703 {
1704 return std::clamp(0.5f + 0.5f * v, 0.f, 1.f);
1705 }
1706
1707 float source_value(const track_record& r, E2MSource s) const noexcept
1708 {
1709 switch(s)
1710 {
1711 case E2MSource::None:
1712 return 0.f;
1713 case E2MSource::X:
1714 return norm_axis(r, E2MAxis::X, false);
1715 case E2MSource::Y:
1716 return norm_axis(r, E2MAxis::Y, false);
1717 case E2MSource::Z:
1718 return norm_axis(r, E2MAxis::Z, false);
1719 case E2MSource::Confidence:
1720 return std::clamp(r.confidence, 0.f, 1.f);
1721 default:
1722 break;
1723 }
1724
1725 const auto it = m_desc.find(r.id);
1726 if(it == m_desc.end())
1727 return 0.f;
1728 const entity_descriptors& d = it->second;
1729
1730 switch(s)
1731 {
1732 case E2MSource::Radius:
1733 return d.radius;
1734 case E2MSource::Speed:
1735 return d.speed;
1736 case E2MSource::VelocityX:
1737 return d.vx;
1738 case E2MSource::VelocityY:
1739 return d.vy;
1740 case E2MSource::VelocityZ:
1741 return d.vz;
1742 case E2MSource::SpeedX:
1743 return d.sx;
1744 case E2MSource::SpeedY:
1745 return d.sy;
1746 case E2MSource::SpeedZ:
1747 return d.sz;
1748 case E2MSource::Acceleration:
1749 return d.accel;
1750 case E2MSource::AccelX:
1751 return d.ax;
1752 case E2MSource::AccelY:
1753 return d.ay;
1754 case E2MSource::AccelZ:
1755 return d.az;
1756 case E2MSource::Jerk:
1757 return d.jerk;
1758 case E2MSource::TurnRate:
1759 return d.turn_rate;
1760 case E2MSource::Curvature:
1761 return d.curvature;
1762 case E2MSource::HeadingSin:
1763 return d.heading_sin;
1764 case E2MSource::HeadingCos:
1765 return d.heading_cos;
1766 case E2MSource::Agitation:
1767 return d.agitation;
1768 case E2MSource::NearestNeighbour:
1769 return d.nearest;
1770 case E2MSource::CentroidDistance:
1771 return d.centroid_dist;
1772 case E2MSource::Density:
1773 return d.density;
1774 case E2MSource::Age:
1775 return d.age;
1776 default:
1777 return 0.f;
1778 }
1779 }
1780
1781 float entity_priority(const track_record& r) const noexcept
1782 {
1783 switch(inputs.priority.value)
1784 {
1785 case E2MPriority::ConfidenceAge:
1786 return std::clamp(r.confidence, 0.f, 1.f)
1787 * std::clamp(r.age / 1.f, 0.f, 1.f);
1788 case E2MPriority::StealOldest:
1789 return -r.age;
1790 case E2MPriority::StealNewest:
1791 return r.age;
1792 case E2MPriority::StealSlowest:
1793 return speed_of(r);
1794 case E2MPriority::StealLeastConfident:
1795 return r.confidence;
1796 }
1797 return 0.f;
1798 }
1799
1800 // ------------------------------------------------------------- ingest
1801 voice* find_voice(int32_t id) noexcept
1802 {
1803 for(auto& v : m_voices)
1804 if(v.st != vstate::off && v.id == id)
1805 return &v;
1806 return nullptr;
1807 }
1808
1809 voice* free_voice() noexcept
1810 {
1811 int used = 0;
1812 voice* free = nullptr;
1813 for(auto& v : m_voices)
1814 {
1815 if(v.st == vstate::off)
1816 {
1817 if(!free)
1818 free = &v;
1819 }
1820 else
1821 used++;
1822 }
1823 return used < std::min(inputs.max_voices.value, k_max_voices) ? free : nullptr;
1824 }
1825
1826 static bool number_like(const ossia::value& v) noexcept
1827 {
1828 const auto t = v.get_type();
1829 return t == ossia::val_type::FLOAT || t == ossia::val_type::INT
1830 || t == ossia::val_type::BOOL;
1831 }
1832 static float to_float(const ossia::value& v) noexcept
1833 {
1834 return ossia::convert<float>(v);
1835 }
1836
1839 bool third_is_confidence() const noexcept
1840 {
1841 return inputs.coords.value == E2MCoords::TwoD;
1842 }
1843
1845 bool parse_entity(const ossia::value& v, bool third_conf, track_record& out) noexcept
1846 {
1847 switch(v.get_type())
1848 {
1849 case ossia::val_type::VEC2F: {
1850 const auto& a = *v.target<ossia::vec2f>();
1851 out.position = {a[0], a[1], 0.f};
1852 return true;
1853 }
1854 case ossia::val_type::VEC3F: {
1855 const auto& a = *v.target<ossia::vec3f>();
1856 if(third_conf)
1857 {
1858 out.position = {a[0], a[1], 0.f};
1859 out.confidence = a[2];
1860 }
1861 else
1862 out.position = {a[0], a[1], a[2]};
1863 return true;
1864 }
1865 case ossia::val_type::VEC4F: {
1866 const auto& a = *v.target<ossia::vec4f>();
1867 out.position = {a[0], a[1], a[2]};
1868 out.confidence = a[3];
1869 return true;
1870 }
1871 case ossia::val_type::LIST: {
1872 const auto& l = *v.target<std::vector<ossia::value>>();
1873 if(l.size() < 2 || !number_like(l[0]) || !number_like(l[1]))
1874 return false;
1875 out.position = {to_float(l[0]), to_float(l[1]), 0.f};
1876 if(l.size() >= 3 && number_like(l[2]))
1877 {
1878 if(third_conf)
1879 out.confidence = to_float(l[2]);
1880 else
1881 out.position.z = to_float(l[2]);
1882 }
1883 if(l.size() >= 4 && number_like(l[3]))
1884 out.confidence = to_float(l[3]);
1885 return true;
1886 }
1887 case ossia::val_type::MAP: {
1888 const auto& m = *v.target<ossia::value_map_type>();
1889 bool has_pos = false;
1890 for(const auto& [k, val] : m)
1891 {
1892 if(k == "position" || k == "pos" || k == "centroid" || k == "point")
1893 {
1894 track_record sub;
1895 if(parse_entity(val, third_conf, sub))
1896 {
1897 out.position = sub.position;
1898 has_pos = true;
1899 }
1900 }
1901 else if(k == "position_raw")
1902 {
1903 track_record sub;
1904 if(parse_entity(val, third_conf, sub))
1905 out.position_raw = sub.position;
1906 }
1907 else if(k == "velocity" || k == "vel")
1908 {
1909 track_record sub;
1910 if(parse_entity(val, false, sub))
1911 out.velocity = sub.position;
1912 }
1913 else if(k == "id")
1914 {
1915 if(number_like(val))
1916 out.id = int(to_float(val));
1917 }
1918 else if(k == "slot")
1919 {
1920 if(number_like(val))
1921 out.slot = int(to_float(val));
1922 }
1923 else if(k == "confidence" || k == "conf" || k == "score")
1924 {
1925 if(number_like(val))
1926 out.confidence = to_float(val);
1927 }
1928 else if(k == "age")
1929 {
1930 if(number_like(val))
1931 out.age = to_float(val);
1932 }
1933 else if(k == "time_since_seen")
1934 {
1935 if(number_like(val))
1936 out.time_since_seen = to_float(val);
1937 }
1938 else if(k == "creation_time")
1939 {
1940 if(number_like(val))
1941 out.creation_time = to_float(val);
1942 }
1943 else if(k == "state")
1944 {
1945 if(auto st = val.target<std::string>())
1946 out.state = *st;
1947 }
1948 else if(k == "provisional")
1949 {
1950 if(number_like(val))
1951 out.provisional = to_float(val) != 0.f;
1952 }
1953 else if(k == "reacquired")
1954 {
1955 if(number_like(val))
1956 out.reacquired = to_float(val) != 0.f;
1957 }
1958 }
1959 return has_pos;
1960 }
1961 default:
1962 return false;
1963 }
1964 }
1965
1970 void parse_entities(double now)
1971 {
1972 m_recs.clear();
1973 const auto& in = inputs.tracks.value;
1974 if(in.empty())
1975 {
1976 m_synth.clear();
1977 return;
1978 }
1979
1980 const bool third_conf = third_is_confidence();
1981
1982 // One flat frame of plain numbers: [x, y(, z), x, y(, z), ...], strided by
1983 // the declared dimension.
1984 if(number_like(in[0]))
1985 {
1986 const std::size_t stride = inputs.coords.value == E2MCoords::ThreeD ? 3 : 2;
1987 for(std::size_t i = 0; i + stride <= in.size(); i += stride)
1988 {
1989 track_record r;
1990 r.position
1991 = {to_float(in[i]), to_float(in[i + 1]),
1992 stride == 3 ? to_float(in[i + 2]) : 0.f};
1993 r.confidence = 1.f;
1994 m_recs.push_back(std::move(r));
1995 if(m_recs.size() >= 512)
1996 break;
1997 }
1998 }
1999 else
2000 {
2001 for(const auto& v : in)
2002 {
2003 track_record r;
2004 r.confidence = 1.f;
2005 if(parse_entity(v, third_conf, r))
2006 m_recs.push_back(std::move(r));
2007 if(m_recs.size() >= 512)
2008 break;
2009 }
2010 }
2011
2012 // Identity and the derived quantities. A source that supplies ids keeps
2013 // them; otherwise the index is the identity, which is why the inlet
2014 // documents that the order must be stable.
2015 ossia::flat_map<int, synth_state> next;
2016 for(std::size_t i = 0; i < m_recs.size(); i++)
2017 {
2018 auto& r = m_recs[i];
2019 if(r.id < 0)
2020 r.id = int(i);
2021 if(r.slot < 0)
2022 r.slot = r.id;
2023 if(r.state.empty())
2024 r.state = r.provisional ? "provisional" : "confirmed";
2025 if(!std::isfinite(r.position_raw.x))
2026 r.position_raw = r.position;
2027
2028 auto& st = next[r.id];
2029 if(auto it = m_synth.find(r.id); it != m_synth.end())
2030 {
2031 const auto& prev = it->second;
2032 const double d = now - prev.time;
2033 // Only synthesise what the source did not provide: a full record
2034 // already carries a filtered velocity, and differencing it again
2035 // would fight the tracker's own smoothing.
2036 if(d > 1e-6 && r.velocity.x == 0.f && r.velocity.y == 0.f
2037 && r.velocity.z == 0.f)
2038 {
2039 r.velocity
2040 = {float((r.position.x - prev.pos.x) / d),
2041 float((r.position.y - prev.pos.y) / d),
2042 float((r.position.z - prev.pos.z) / d)};
2043 }
2044 // Carry the whole previous state, not just first_seen: the analysis
2045 // history and the filter memories the descriptors are built from live
2046 // here too, and rebuilding them every frame would reset every
2047 // derivative to its first-sample value.
2048 const double since = now - prev.time;
2049 st = prev;
2050 st.last_dt = since;
2051 }
2052 else
2053 {
2054 st.first_seen = now;
2055 st.last_dt = 0.;
2056 // A newly seen entity has no history to difference against, so it
2057 // enters with zero velocity rather than an arbitrary jump.
2058 }
2059 st.time = now;
2060 st.pos = r.position;
2061 if(r.age == 0.f)
2062 r.age = float(now - st.first_seen);
2063 }
2064 m_synth = std::move(next);
2065 }
2066
2067 void ingest(double now)
2068 {
2069 const auto& recs = m_recs;
2070 const bool triggered = inputs.note_model.value == E2MNoteModel::Triggered;
2071
2072 m_present.clear();
2073 for(const auto& r : recs)
2074 {
2075 if(r.id < 0)
2076 continue;
2077 m_present.push_back(r.id);
2078
2079 // Pre-roll speed history, kept from the very first (provisional)
2080 // sighting so entry velocity measures the approach, not the arrival.
2081 auto& h = m_hist[r.id];
2082 h.push(now, speed_of(r));
2083
2084 voice* v = find_voice(r.id);
2085 const bool eligible
2086 = inputs.trigger_on.value == E2MTriggerOn::FirstDetection || !r.provisional;
2087
2088 if(v)
2089 {
2090 refresh_voice(*v, r, now);
2091 }
2092 else if(eligible)
2093 {
2094 if(triggered)
2095 try_start_triggered(r, now);
2096 else
2097 try_start_sustained(r, now);
2098 }
2099 }
2100
2101 // Mark voices whose entity vanished from this update.
2102 for(auto& v : m_voices)
2103 {
2104 if(v.st == vstate::off)
2105 continue;
2106 bool present = false;
2107 for(auto id : m_present)
2108 if(id == v.id)
2109 {
2110 present = true;
2111 break;
2112 }
2113 if(!present && !v.missing)
2114 {
2115 v.missing = true;
2116 v.missing_since = now;
2117 }
2118 }
2119
2120 // Prune stale speed history.
2121 if(m_hist.size() > 128)
2122 for(auto it = m_hist.begin(); it != m_hist.end();)
2123 it = (now - it->second.last_t > 2.) ? m_hist.erase(it) : std::next(it);
2124 }
2125
2126 void refresh_voice(voice& v, const track_record& r, double now)
2127 {
2128 v.last_seen = now;
2129 v.missing = false;
2130 v.coasting = r.state == "coasting";
2131 v.entity_confirmed = v.entity_confirmed || !r.provisional;
2132 v.priority = entity_priority(r);
2133
2134 const bool freeze = v.coasting && inputs.coast.value == E2MCoast::Freeze;
2135 if(!freeze)
2136 {
2137 const float rp = raw_pitch(r);
2138 v.pitch_target = quantize_pitch(rp, v);
2139 v.pressure_t = source_value(r, inputs.pressure_src.value);
2140 v.timbre_t = source_value(r, inputs.timbre_src.value);
2141 }
2142 if(v.coasting && inputs.coast.value == E2MCoast::Fade)
2143 v.pressure_t = 0.f;
2144
2145 // Triggered + Retrigger tracking: a scale-step change fires a new note.
2146 //
2147 // This cannot test off_scheduled: try_start_triggered sets it on every
2148 // note, up front, because a triggered note has a fixed lifetime. Testing
2149 // it made the whole branch unreachable, so Retrigger behaved exactly like
2150 // Latched. What has to be excluded instead is a note that is already
2151 // waiting for its follow-up.
2152 if(inputs.note_model.value == E2MNoteModel::Triggered
2153 && inputs.pitch_tracking.value == E2MPitchTracking::Retrigger
2154 && v.st == vstate::sounding && !v.retrig_pending)
2155 {
2156 const int new_note = pitch_to_note(v.pitch_target);
2157 if(new_note != v.note && can_retrigger(v.id, now)
2158 && now - v.on_time >= inputs.min_note.value * 1e-3)
2159 {
2160 // Bring the fixed-lifetime note-off forward rather than calling
2161 // schedule_off, which returns early once off_scheduled is set and so
2162 // would leave the note sounding under the follow-up.
2163 v.off_raw_t = std::min(v.off_raw_t, now);
2164 v.off_target_q = -1.;
2165 v.retrig_pending = true;
2166 // The follow-up note starts once the off is emitted; remember intent.
2167 m_retrig_ids.push_back(v.id);
2168 }
2169 }
2170 }
2171
2172 static int pitch_to_note(float target) noexcept
2173 {
2174 return std::clamp(int(std::lround(target)), 0, 127);
2175 }
2176
2177 bool can_retrigger(int32_t id, double now) const noexcept
2178 {
2179 auto it = m_last_on.find(id);
2180 return it == m_last_on.end()
2181 || now - it->second >= inputs.retrig_lockout.value * 1e-3;
2182 }
2183
2184 // Steal or deny. Returns a voice ready to be initialised, or nullptr.
2185 voice* obtain_voice(const track_record& r, double now)
2186 {
2187 if(auto* v = free_voice())
2188 return v;
2189
2190 if(!inputs.allow_steal)
2191 {
2192 m_denied++;
2193 return nullptr;
2194 }
2195
2196 // Never steal a confirmed entity's voice for a provisional newcomer.
2197 voice* victim = nullptr;
2198 for(auto& v : m_voices)
2199 {
2200 if(v.st == vstate::off)
2201 continue;
2202 if(r.provisional && v.entity_confirmed)
2203 continue;
2204 if(!victim || v.priority < victim->priority)
2205 victim = &v;
2206 }
2207 const float p_new = entity_priority(r);
2208 if(!victim || p_new <= victim->priority + inputs.steal_margin.value)
2209 {
2210 m_denied++;
2211 return nullptr;
2212 }
2213
2214 // Emit the victim's off immediately (prio 0, same-timestamp offs sort
2215 // before ons) and recycle the slot.
2216 kill_voice(*victim, now);
2217 return victim;
2218 }
2219
2220 void init_voice(voice& v, const track_record& r, double now)
2221 {
2222 v = voice{};
2223 v.id = r.id;
2224 v.st = vstate::pending_on;
2225 v.entity_confirmed = !r.provisional;
2226 v.last_seen = now;
2227 v.priority = entity_priority(r);
2228 v.on_raw_t = now;
2229 v.pitch_target = quantize_pitch(raw_pitch(r), v);
2230 v.pressure_t = source_value(r, inputs.pressure_src.value);
2231 v.timbre_t = source_value(r, inputs.timbre_src.value);
2232 v.pressure_v = v.pressure_t;
2233 v.timbre_v = v.timbre_t;
2234 v.velocity = compute_velocity(r, now);
2235
2236 if(inputs.quant_mode.value != E2MQuantTargets::Off)
2237 v.on_target_q = -2.; // to be filled from the tick's musical position
2238 else
2239 v.on_target_q = -1.; // immediate
2240 }
2241
2242 void try_start_sustained(const track_record& r, double now)
2243 {
2244 // While panic or MPE-configuration messages are still queued, starting a
2245 // note would let its note-on race a pending CC 123 and be silently
2246 // killed. Wait; the entity is retried on the next track update.
2247 if(!m_timed.empty())
2248 return;
2249 if(!can_retrigger(r.id, now))
2250 return;
2251 voice* v = obtain_voice(r, now);
2252 if(!v)
2253 return;
2254 init_voice(*v, r, now);
2255 const int c = alloc_channel(now);
2256 if(c < 0)
2257 {
2258 // No channel free (all reserved / in use): deny rather than share.
2259 v->st = vstate::off;
2260 m_denied++;
2261 return;
2262 }
2263 v->channel = int8_t(c);
2264 }
2265
2266 void try_start_triggered(const track_record& r, double now)
2267 {
2268 if(!m_timed.empty())
2269 return;
2270 if(!can_retrigger(r.id, now))
2271 return;
2272 voice* v = obtain_voice(r, now);
2273 if(!v)
2274 return;
2275 init_voice(*v, r, now);
2276 const int c = alloc_channel(now);
2277 if(c < 0)
2278 {
2279 v->st = vstate::off;
2280 m_denied++;
2281 return;
2282 }
2283 v->channel = int8_t(c);
2284 // Triggered notes get a fixed lifetime, scheduled up front.
2285 v->off_scheduled = true;
2286 v->off_raw_t = now + inputs.trigger_duration.value * 1e-3;
2287 v->off_target_q = -1.;
2288 }
2289
2290 uint8_t compute_velocity(const track_record& r, double now) const
2291 {
2292 const int lo = std::min(inputs.vel_lo.value, inputs.vel_hi.value);
2293 const int hi = std::max(inputs.vel_lo.value, inputs.vel_hi.value);
2294 switch(inputs.vel_source.value)
2295 {
2296 case E2MVelSource::Fixed:
2297 return uint8_t(std::clamp(inputs.vel_fixed.value, 1, 127));
2298 case E2MVelSource::Confidence: {
2299 const float n = std::clamp(r.confidence, 0.f, 1.f);
2300 return uint8_t(std::clamp(int(std::lround(lo + n * (hi - lo))), 1, 127));
2301 }
2302 case E2MVelSource::EntrySpeed: {
2303 // Peak speed over the pre-roll window before the note starts:
2304 // measuring at the instant of confirmation catches the tracker's
2305 // smoother still converging and yields the same middling value for
2306 // every entrance.
2307 float peak = 0.f;
2308 auto it = m_hist.find(r.id);
2309 if(it != m_hist.end())
2310 peak = it->second.peak_since(now - inputs.preroll.value * 1e-3);
2311 else
2312 peak = speed_of(r);
2313 const float ref = std::max(inputs.speed_ref.value, 1e-3f);
2314 const float n = std::clamp(peak / ref, 0.f, 1.f);
2315 return uint8_t(std::clamp(int(std::lround(lo + n * (hi - lo))), 1, 127));
2316 }
2317 }
2318 return 100;
2319 }
2320
2321 // ------------------------------------------------------------- lifecycle
2322 void lifecycle(double now, const halp::tick_musical& tk)
2323 {
2324 const bool sustained = inputs.note_model.value == E2MNoteModel::Sustained;
2325 const double grace = inputs.lost_grace.value * 1e-3;
2326 const double wd = inputs.watchdog.value * 1e-3;
2327 const double maxn = inputs.max_note.value * 1e-3;
2328
2329 for(auto& v : m_voices)
2330 {
2331 if(v.st == vstate::off)
2332 continue;
2333
2334 // Entity gone past the grace window -> release.
2335 if(sustained && v.missing && !v.off_scheduled && now - v.missing_since >= grace)
2336 schedule_off(v, now, true);
2337
2338 // A pending note whose entity vanished never sounds.
2339 if(v.st == vstate::pending_on && v.missing && now - v.missing_since >= grace)
2340 {
2341 free_channel(v.channel, now);
2342 v.st = vstate::off;
2343 continue;
2344 }
2345
2346 // Watchdog: no data at all for too long, independent of the tracker's
2347 // own lifecycle. Catches a dead tracker / unplugged camera.
2348 if(wd > 0. && v.st == vstate::sounding && !v.off_scheduled
2349 && now - v.last_seen >= wd)
2350 schedule_off(v, now, false);
2351
2352 // Hard note-length ceiling.
2353 if(maxn > 0. && v.st == vstate::sounding && !v.off_scheduled
2354 && now - v.on_time >= maxn)
2355 schedule_off(v, now, true);
2356 }
2357 }
2358
2359 void schedule_off(voice& v, double now, bool allow_quantise)
2360 {
2361 if(v.st == vstate::pending_on)
2362 {
2363 // Never sounded: cancel silently.
2364 free_channel(v.channel, now);
2365 v.st = vstate::off;
2366 return;
2367 }
2368 if(v.off_scheduled)
2369 return;
2370 v.off_scheduled = true;
2371 v.off_raw_t = now;
2372 v.off_target_q
2373 = (allow_quantise
2374 && inputs.quant_mode.value == E2MQuantTargets::OnsetsAndOffsets)
2375 ? -2.
2376 : -1.;
2377 }
2378
2379 // Immediate, unquantised kill (stealing, panic): off at current tick start.
2380 void kill_voice(voice& v, double now)
2381 {
2382 if(v.on_wire)
2383 push_note_off(v, 0);
2384 free_channel(v.channel, now);
2385 v.st = vstate::off;
2386 }
2387
2388 // ------------------------------------------------------------- rhythm
2389 double grid_quarters() const noexcept
2390 {
2391 switch(inputs.grid.value)
2392 {
2393 case E2MGrid::Whole:
2394 return 4.;
2395 case E2MGrid::Half:
2396 return 2.;
2397 case E2MGrid::Quarter:
2398 return 1.;
2399 case E2MGrid::Eighth:
2400 return 0.5;
2401 case E2MGrid::Sixteenth:
2402 return 0.25;
2403 case E2MGrid::EighthTriplet:
2404 return 1. / 3.;
2405 case E2MGrid::SixteenthTriplet:
2406 return 1. / 6.;
2407 }
2408 return 0.5;
2409 }
2410
2411 // Compute the quantised emission point in quarters for an event raised at
2412 // musical position raw_q. Nearest grid point, lerped by strength.
2413 double quantise_target(double raw_q) const noexcept
2414 {
2415 const double g = grid_quarters();
2416 const double grid_q = std::round(raw_q / g) * g;
2417 return raw_q + inputs.strength.value * (grid_q - raw_q);
2418 }
2419
2420 // If target_q falls inside this tick, return the sample offset; -1 if it
2421 // is in the future; 0 (emit now) if it is already past.
2422 static int64_t
2423 quarters_to_frame(double target_q, const halp::tick_musical& tk) noexcept
2424 {
2425 const double a = tk.start_position_in_quarters;
2426 const double b = tk.end_position_in_quarters;
2427 if(target_q <= a)
2428 return 0;
2429 if(b <= a)
2430 return -1; // transport not advancing
2431 if(target_q >= b)
2432 return -1;
2433 return int64_t((target_q - a) / (b - a) * std::max(tk.frames, 1));
2434 }
2435
2436 void emit_pending_notes(double now, double dt, int frames, const halp::tick_musical& tk)
2437 {
2438 const double max_hold = inputs.max_hold.value * 1e-3;
2439 const double min_note = inputs.min_note.value * 1e-3;
2440
2441 for(auto& v : m_voices)
2442 {
2443 if(v.st == vstate::pending_on)
2444 {
2445 // First tick after the trigger: resolve the musical target.
2446 if(v.on_target_q == -2.)
2447 v.on_target_q = quantise_target(tk.start_position_in_quarters);
2448
2449 int64_t off = -1;
2450 bool fire = false;
2451 if(v.on_target_q < 0.)
2452 {
2453 fire = true;
2454 off = 0;
2455 }
2456 else
2457 {
2458 const int64_t f = quarters_to_frame(v.on_target_q, tk);
2459 if(f >= 0)
2460 {
2461 fire = true;
2462 off = f;
2463 }
2464 else if(now - v.on_raw_t >= max_hold)
2465 {
2466 // Beat tracker dropout / transport stall: play anyway.
2467 fire = true;
2468 off = 0;
2469 }
2470 }
2471 if(fire)
2472 emit_note_on(v, now, off, frames);
2473 }
2474
2475 if(v.st == vstate::sounding && v.off_scheduled)
2476 {
2477 // Resolve the off's musical target on the tick after scheduling.
2478 if(v.off_target_q == -2.)
2479 v.off_target_q = quantise_target(tk.start_position_in_quarters);
2480
2481 // Earliest legal off: on_time + min_note (also guards quantised
2482 // off landing before its on).
2483 const double earliest = v.on_time + min_note;
2484
2485 int64_t off = -1;
2486 bool fire = false;
2487 if(v.off_target_q >= 0.)
2488 {
2489 const int64_t f = quarters_to_frame(v.off_target_q, tk);
2490 if(f >= 0)
2491 {
2492 fire = true;
2493 off = f;
2494 }
2495 else if(now - v.off_raw_t >= max_hold)
2496 {
2497 fire = true;
2498 off = 0;
2499 }
2500 }
2501 else if(v.off_raw_t <= now + dt)
2502 {
2503 fire = true;
2504 off = std::clamp<int64_t>(
2505 int64_t((v.off_raw_t - now) / dt * frames), 0, frames - 1);
2506 }
2507
2508 if(fire)
2509 {
2510 const double t_emit = now + double(off) / m_rate;
2511 if(t_emit < earliest)
2512 {
2513 // Too early: push the off to the earliest legal time, unquantised.
2514 if(earliest <= now + dt)
2515 {
2516 fire = true;
2517 off = std::clamp<int64_t>(
2518 int64_t((earliest - now) / dt * frames), 0, frames - 1);
2519 }
2520 else
2521 fire = false;
2522 }
2523 if(fire)
2524 emit_note_off(v, off, now + double(off) / m_rate);
2525 }
2526 }
2527 }
2528
2529 // Retrigger follow-ups (triggered mode): start the new note for ids
2530 // whose old note was just released.
2531 if(!m_retrig_ids.empty())
2532 {
2533 for(auto id : m_retrig_ids)
2534 {
2535 for(const auto& r : m_recs)
2536 if(r.id == id)
2537 {
2538 try_start_triggered(r, now);
2539 break;
2540 }
2541 }
2542 m_retrig_ids.clear();
2543 }
2544 }
2545
2546 void emit_note_on(voice& v, double tick_start, int64_t frame_off, int frames)
2547 {
2548 const int note = pitch_to_note(v.pitch_target);
2549
2550 // Invariant 2: never a second note-on for a held (channel, pitch).
2551 // Structural in MPE / channel-per-entity (one note per channel); it
2552 // bites in single-channel mode when two entities converge on the same
2553 // quantised pitch - which happens constantly, because bodies cluster.
2554 for(const auto& o : m_voices)
2555 if(&o != &v && o.on_wire && o.channel == v.channel && o.note == note)
2556 {
2557 // Hold the voice pending; it will fire when the pitch diverges or
2558 // the other note releases.
2559 if(!v.note_suppressed)
2560 {
2561 v.note_suppressed = true;
2562 m_denied++;
2563 }
2564 return;
2565 }
2566
2567 // Invariant 4, intra-tick form: the (channel, pitch) may still be
2568 // occupied ON THE WIRE even though no voice holds it - a note-off
2569 // already pushed this tick can carry a later mid-buffer timestamp
2570 // (triggered-mode duration, min-note delay, quantised off). Emitting
2571 // the new on earlier in the buffer would invert their order for the
2572 // receiver and hang the note. Start the new note at the off's
2573 // timestamp instead: at equal timestamps offs sort before ons.
2574 for(const auto& m : m_msgs)
2575 if(m.n == 3 && (m.bytes[0] & 0xF0) == 0x80
2576 && (m.bytes[0] & 0x0F) == (uint8_t(v.channel) & 0x0F)
2577 && m.bytes[1] == uint8_t(note) && m.ts > frame_off)
2578 frame_off = m.ts;
2579 frame_off = std::clamp<int64_t>(frame_off, 0, frames - 1);
2580 const double t = tick_start + double(frame_off) / m_rate;
2581
2582 v.note = uint8_t(note);
2583 v.note_suppressed = false;
2584 v.bend_semis = v.pitch_target - float(v.note);
2585
2586 const uint8_t ch = uint8_t(v.channel);
2587 const bool member_dims = !is_single();
2588
2589 if(member_dims)
2590 {
2591 // Set the expressive state BEFORE the note-on so it starts in tune.
2592 if(inputs.pitch_tracking.value == E2MPitchTracking::ContinuousBend)
2593 {
2594 push_bend(v, frame_off, /*prio*/ 1);
2595 }
2596 else
2597 {
2598 v.bend_semis = 0.f;
2599 push_bend_value(ch, 8192, frame_off, 1);
2600 v.last_bend = 8192;
2601 }
2602 if(inputs.pressure_src.value != E2MSource::None)
2603 push_pressure(v, frame_off, 1);
2604 if(inputs.timbre_src.value != E2MSource::None)
2605 push_timbre(v, frame_off, 1);
2606 }
2607
2608 push(frame_off, 1, uint8_t(0x90 | ch), v.note, v.velocity);
2609 m_outstanding++;
2610 v.on_wire = true;
2611 v.st = vstate::sounding;
2612 v.on_time = t;
2613 v.expr_acc = 0.;
2614 m_touched[ch] = true;
2615 m_last_on[v.id] = t;
2616 prune_last_on(t);
2617 }
2618
2619 void emit_note_off(voice& v, int64_t frame_off, double t)
2620 {
2621 if(v.on_wire)
2622 {
2623 push(frame_off, 0, uint8_t(0x80 | uint8_t(v.channel)), v.note, 64);
2624 m_outstanding--;
2625 v.on_wire = false;
2626 }
2627 free_channel(v.channel, t);
2628 v.st = vstate::off;
2629 }
2630
2631 void push_note_off(voice& v, int64_t frame_off)
2632 {
2633 if(v.on_wire)
2634 {
2635 push(frame_off, 0, uint8_t(0x80 | uint8_t(v.channel)), v.note, 64);
2636 m_outstanding--;
2637 v.on_wire = false;
2638 }
2639 }
2640
2641 void prune_last_on(double now)
2642 {
2643 if(m_last_on.size() > 256)
2644 for(auto it = m_last_on.begin(); it != m_last_on.end();)
2645 it = (now - it->second > 10.) ? m_last_on.erase(it) : std::next(it);
2646 }
2647
2648 // ------------------------------------------------------------- expression
2649 void update_expression(double now, double dt, int frames)
2650 {
2651 const bool single = is_single();
2652 const double period = 1. / std::max(inputs.expr_rate.value, 1.f);
2653 const float db = inputs.deadband.value;
2654 const double tau_rise = std::max(inputs.rise.value, 0.f) * 1e-3;
2655 const double tau_fall = std::max(inputs.fall.value, 0.f) * 1e-3;
2656 const double tau_glide = std::max(inputs.glide.value, 0.f) * 1e-3;
2657 const bool cont
2658 = inputs.pitch_tracking.value == E2MPitchTracking::ContinuousBend && !single;
2659
2660 // Round-robin so no entity starves when the budget is tight.
2661 const int n = k_max_voices;
2662 const int start = m_rr++ % n;
2663 for(int k = 0; k < n; k++)
2664 {
2665 voice& v = m_voices[(start + k) % n];
2666 if(v.st != vstate::sounding || !v.on_wire)
2667 continue;
2668
2669 // Slew the mapping state every tick regardless of emission rate.
2670 const float bend_target = cont ? (v.pitch_target - float(v.note)) : 0.f;
2671 if(tau_glide <= 0.)
2672 v.bend_semis = bend_target;
2673 else
2674 v.bend_semis += (bend_target - v.bend_semis)
2675 * float(ossia::lag_alpha(tau_glide, dt));
2676 smooth_asym(v.pressure_v, v.pressure_t, tau_rise, tau_fall, dt);
2677 smooth_asym(v.timbre_v, v.timbre_t, tau_rise, tau_fall, dt);
2678
2679 v.expr_acc += dt;
2680 if(v.expr_acc < period)
2681 continue;
2682
2683 v.expr_acc = 0.;
2684
2685 if(cont)
2686 {
2687 const int bend = bend_to_14bit(v);
2688 if(v.last_bend < 0 || std::abs(bend - v.last_bend) >= int(db * 16384.f))
2689 if(bend != v.last_bend)
2690 {
2691 push_bend_value(uint8_t(v.channel), bend, 0, 2);
2692 v.last_bend = bend;
2693 }
2694 }
2695 if(inputs.pressure_src.value != E2MSource::None)
2696 {
2697 const int p = std::clamp(int(std::lround(v.pressure_v * 127.f)), 0, 127);
2698 if(v.last_pressure < 0
2699 || std::abs(p - v.last_pressure) >= std::max(1, int(db * 127.f)))
2700 if(p != v.last_pressure)
2701 {
2702 if(single)
2703 push(0, 2, uint8_t(0xA0 | uint8_t(v.channel)), v.note, uint8_t(p));
2704 else
2705 push2(0, 2, uint8_t(0xD0 | uint8_t(v.channel)), uint8_t(p));
2706 v.last_pressure = p;
2707 }
2708 }
2709 if(!single && inputs.timbre_src.value != E2MSource::None)
2710 {
2711 const int t = std::clamp(int(std::lround(v.timbre_v * 127.f)), 0, 127);
2712 if(v.last_timbre < 0
2713 || std::abs(t - v.last_timbre) >= std::max(1, int(db * 127.f)))
2714 if(t != v.last_timbre)
2715 {
2716 push(
2717 0, 2, uint8_t(0xB0 | uint8_t(v.channel)),
2718 uint8_t(std::clamp(inputs.timbre_cc.value, 0, 127)), uint8_t(t));
2719 v.last_timbre = t;
2720 }
2721 }
2722 }
2723 }
2724
2725 static void
2726 smooth_asym(float& state, float target, double tau_rise, double tau_fall, double dt)
2727 {
2728 const double tau = target > state ? tau_rise : tau_fall;
2729 if(tau <= 0.)
2730 state = target;
2731 else
2732 state += (target - state) * float(ossia::lag_alpha(tau, dt));
2733 }
2734
2735 int bend_to_14bit(const voice& v) const noexcept
2736 {
2737 const float range = float(std::max(inputs.bend_range.value, 1));
2738 const float n = std::clamp(v.bend_semis / range, -1.f, 1.f);
2739 return std::clamp(8192 + int(std::lround(n * 8191.f)), 0, 16383);
2740 }
2741
2742 // ------------------------------------------------------------- emission
2743 void push(int64_t ts, uint8_t prio, uint8_t b0, uint8_t b1, uint8_t b2)
2744 {
2745 m_msgs.push_back({ts, prio, 3, {b0, b1, b2}});
2746 }
2747 void push2(int64_t ts, uint8_t prio, uint8_t b0, uint8_t b1)
2748 {
2749 m_msgs.push_back({ts, prio, 2, {b0, b1, 0}});
2750 }
2751
2752 void push_bend(voice& v, int64_t ts, uint8_t prio)
2753 {
2754 const int bend = bend_to_14bit(v);
2755 push_bend_value(uint8_t(v.channel), bend, ts, prio);
2756 v.last_bend = bend;
2757 }
2758 void push_bend_value(uint8_t ch, int bend, int64_t ts, uint8_t prio)
2759 {
2760 push(ts, prio, uint8_t(0xE0 | ch), uint8_t(bend & 0x7F), uint8_t((bend >> 7) & 0x7F));
2761 }
2762 void push_pressure(voice& v, int64_t ts, uint8_t prio)
2763 {
2764 const int p = std::clamp(int(std::lround(v.pressure_v * 127.f)), 0, 127);
2765 push2(ts, prio, uint8_t(0xD0 | uint8_t(v.channel)), uint8_t(p));
2766 v.last_pressure = p;
2767 }
2768 void push_timbre(voice& v, int64_t ts, uint8_t prio)
2769 {
2770 const int t = std::clamp(int(std::lround(v.timbre_v * 127.f)), 0, 127);
2771 push(
2772 ts, prio, uint8_t(0xB0 | uint8_t(v.channel)),
2773 uint8_t(std::clamp(inputs.timbre_cc.value, 0, 127)), uint8_t(t));
2774 v.last_timbre = t;
2775 }
2776
2777 // Timed queue: panic + configuration, spaced spacing ms apart, drained
2778 // across as many ticks as needed.
2779 void queue_timed(uint8_t b0, uint8_t b1, uint8_t b2, uint8_t n = 3)
2780 {
2781 m_timed_tail = std::max(m_timed_tail, m_now);
2782 m_timed.push_back({m_timed_tail, n, {b0, b1, b2}});
2783 m_timed_tail += std::max(inputs.spacing.value, 0.f) * 1e-3;
2784 }
2785
2786 void flush_timed(double now, double dt, int frames)
2787 {
2788 if(m_timed.empty())
2789 return;
2790 std::size_t taken = 0;
2791 for(const auto& m : m_timed)
2792 {
2793 if(m.t >= now + dt)
2794 break;
2795 const int64_t off = std::clamp<int64_t>(
2796 int64_t((m.t - now) / dt * frames), 0, frames - 1);
2797 // Panic/config are class 0/3; use 0 so note traffic cannot precede a
2798 // pending panic.
2799 m_msgs.push_back({off, 0, m.n, {m.bytes[0], m.bytes[1], m.bytes[2]}});
2800 taken++;
2801 }
2802 if(taken)
2803 m_timed.erase(m_timed.begin(), m_timed.begin() + taken);
2804 }
2805
2806 void queue_rpn(uint8_t ch, uint8_t rpn_msb, uint8_t rpn_lsb, uint8_t value)
2807 {
2808 queue_timed(uint8_t(0xB0 | ch), 101, rpn_msb);
2809 queue_timed(uint8_t(0xB0 | ch), 100, rpn_lsb);
2810 queue_timed(uint8_t(0xB0 | ch), 6, value);
2811 queue_timed(uint8_t(0xB0 | ch), 101, 127);
2812 queue_timed(uint8_t(0xB0 | ch), 100, 127);
2813 }
2814
2815 void queue_mpe_config()
2816 {
2817 const uint8_t master = uint8_t(master_channel());
2818 const uint8_t members = uint8_t(member_count());
2819 // RPN 6: MPE configuration - number of member channels.
2820 queue_rpn(master, 0, 6, members);
2821 // RPN 0: pitch bend sensitivity, per member channel.
2822 const uint8_t range = uint8_t(std::clamp(inputs.bend_range.value, 1, 96));
2823 for(int i = 0; i < members; i++)
2824 queue_rpn(uint8_t(member_channel(i)), 0, 0, range);
2825 }
2826
2827 // Panic through the normal (timestamped, spaced) path.
2828 void queue_panic()
2829 {
2830 const double now = m_now;
2831 // 1. Explicit note-off per held voice - CC 123 alone is widely ignored.
2832 for(auto& v : m_voices)
2833 {
2834 if(v.st == vstate::off)
2835 continue;
2836 if(v.on_wire)
2837 {
2838 queue_timed(uint8_t(0x80 | uint8_t(v.channel)), v.note, 64);
2839 m_outstanding--;
2840 v.on_wire = false;
2841 }
2842 free_channel(v.channel, now);
2843 v.st = vstate::off;
2844 }
2845 // 2. All sound off / all notes off / reset controllers per touched
2846 // channel; 3. reset the expressive state.
2847 for(int c = 0; c < 16; c++)
2848 {
2849 if(!m_touched[c])
2850 continue;
2851 queue_timed(uint8_t(0xB0 | c), 120, 0);
2852 queue_timed(uint8_t(0xB0 | c), 123, 0);
2853 queue_timed(uint8_t(0xB0 | c), 121, 0);
2854 queue_timed(uint8_t(0xE0 | c), 0x00, 0x40); // bend centre
2855 queue_timed(uint8_t(0xD0 | c), 0, 0, 2); // pressure 0
2856 queue_timed(uint8_t(0xB0 | c), 74, 64); // timbre centre
2857 }
2858 }
2859
2860 // Stop-time panic: ticks have ended, so push straight to the MIDI device
2861 // protocol if the output is bound to one. Ordering is preserved; the 1 ms
2862 // spacing is not achievable here without blocking the caller. Every byte
2863 // also lands in last_direct_panic so tests (and post-mortems) can see
2864 // exactly what went out.
2865 void panic_direct()
2866 {
2867 last_direct_panic.clear();
2868 auto* proto = midi_out.load();
2869 for(auto& v : m_voices)
2870 {
2871 if(v.st == vstate::off)
2872 continue;
2873 if(v.on_wire)
2874 {
2875 send_direct(proto, uint8_t(0x80 | uint8_t(v.channel)), v.note, 64);
2876 m_outstanding--;
2877 m_wire[uint8_t(v.channel) & 0x0F].reset(v.note);
2878 }
2879 v.on_wire = false;
2880 v.st = vstate::off;
2881 }
2882 for(int c = 0; c < 16; c++)
2883 {
2884 if(!m_touched[c])
2885 continue;
2886 send_direct(proto, uint8_t(0xB0 | c), 120, 0);
2887 send_direct(proto, uint8_t(0xB0 | c), 123, 0);
2888 send_direct(proto, uint8_t(0xB0 | c), 121, 0);
2889 send_direct(proto, uint8_t(0xE0 | c), 0x00, 0x40);
2890 }
2891 }
2892
2893 void send_direct(
2894 ossia::net::midi::midi_protocol* proto, uint8_t b0, uint8_t b1, uint8_t b2)
2895 {
2896 if(last_direct_panic.size() < 4096)
2897 last_direct_panic.push_back({b0, b1, b2});
2898 if(proto)
2899 proto->push_value(libremidi::message{libremidi::midi_bytes{b0, b1, b2}, 0});
2900 }
2901
2902 void resolve_protocol()
2903 {
2904 if(outputs.midi.ossia_node)
2905 {
2906 auto& proto = outputs.midi.ossia_node->get_device().get_protocol();
2907 if(auto mp = dynamic_cast<ossia::net::midi::midi_protocol*>(&proto))
2908 midi_out.store(mp);
2909 }
2910 }
2911
2912 void hard_reset()
2913 {
2914 for(auto& v : m_voices)
2915 {
2916 if(v.on_wire)
2917 m_outstanding--;
2918 v = voice{};
2919 }
2920 for(auto& c : m_chans)
2921 c = chan_info{};
2922 m_timed.clear();
2923 m_timed_tail = 0.;
2924 m_retrig_ids.clear();
2925 // Fresh wire model for the next run. m_wire_err deliberately survives:
2926 // a violation is a bug, and resetting it would let tests miss it.
2927 for(auto& w : m_wire)
2928 w.reset();
2929 }
2930
2931 uint64_t config_signature() const noexcept
2932 {
2933 return uint64_t(inputs.output_mode.value)
2934 | (uint64_t(inputs.mpe_zone.value) << 4)
2935 | (uint64_t(std::clamp(inputs.member_channels.value, 1, 16)) << 8)
2936 | (uint64_t(std::clamp(inputs.bend_range.value, 1, 96)) << 16)
2937 | (uint64_t(std::clamp(inputs.single_channel.value, 1, 16)) << 24)
2938 | (uint64_t(1) << 32); // never 0, so the first tick configures
2939 }
2940
2941private:
2942 std::array<voice, k_max_voices> m_voices{};
2943 std::array<chan_info, 16> m_chans{};
2944 std::array<bool, 16> m_touched{};
2945 // Receiver-side wire model for invariants 2/4: which (channel, pitch) is
2946 // sounding, updated by replaying each tick's sorted output. m_wire_err is
2947 // sticky: once a violation is seen it stays reported - it is a bug.
2948 std::array<std::bitset<128>, 16> m_wire{};
2949 std::string m_wire_err;
2950 std::vector<out_msg> m_msgs;
2951 std::vector<timed_msg> m_timed;
2952 std::vector<int32_t> m_present;
2953 std::vector<int32_t> m_retrig_ids;
2954 ossia::flat_map<int32_t, speed_hist> m_hist;
2955 ossia::flat_map<int32_t, double> m_last_on;
2956
2957 double m_rate = 48000.;
2958 double m_now = 0.;
2959 double m_timed_tail = 0.;
2960 int m_denied = 0;
2961 int m_outstanding = 0;
2962 unsigned m_rr = 0;
2963 uint64_t m_config_sig = 0;
2964 bool m_do_config = false;
2965 bool m_started = false;
2966
2967public:
2968 // ------------------------------------------------------------- UI
2969 struct ui
2970 {
2971 halp_meta(name, "Entity To MIDI")
2972 halp_meta(layout, halp::layouts::tabs)
2973 halp_meta(background, halp::colors::background_mid)
2974
2975 struct
2976 {
2977 halp_meta(name, "Output")
2978 halp_meta(layout, halp::layouts::hbox)
2979
2980 struct
2981 {
2982 halp_meta(name, "Mode")
2983 halp_meta(layout, halp::layouts::vbox)
2984 halp::item<&ins::output_mode> output_mode;
2985 halp::item<&ins::single_channel> single_channel;
2986 } mode;
2987
2988 halp::spacing sp1{.width = 12, .height = 1};
2989
2990 struct
2991 {
2992 halp_meta(name, "MPE")
2993 halp_meta(layout, halp::layouts::vbox)
2994 halp::item<&ins::mpe_zone> mpe_zone;
2995 halp::item<&ins::member_channels> member_channels;
2996 halp::item<&ins::bend_range> bend_range;
2997 halp::item<&ins::send_config> send_config;
2998 } mpe;
2999
3000 halp::spacing sp2{.width = 12, .height = 1};
3001
3002 struct
3003 {
3004 halp_meta(name, "Channels")
3005 halp_meta(layout, halp::layouts::vbox)
3006 halp::item<&ins::channel_reuse> channel_reuse;
3007 halp::item<&ins::release_reserve> release_reserve;
3008 } chans;
3009 } output_tab;
3010
3011 struct
3012 {
3013 halp_meta(name, "Voice")
3014 halp_meta(layout, halp::layouts::hbox)
3015
3016 struct
3017 {
3018 halp_meta(name, "Model")
3019 halp_meta(layout, halp::layouts::vbox)
3020 halp::item<&ins::note_model> note_model;
3021 halp::item<&ins::trigger_on> trigger_on;
3022 halp::item<&ins::trigger_duration> trigger_duration;
3023 halp::item<&ins::coast> coast;
3024 } model;
3025
3026 halp::spacing sp1{.width = 12, .height = 1};
3027
3028 struct
3029 {
3030 halp_meta(name, "Allocation")
3031 halp_meta(layout, halp::layouts::vbox)
3032 halp::item<&ins::max_voices> max_voices;
3033 halp::item<&ins::priority> priority;
3034 halp::item<&ins::allow_steal> allow_steal;
3035 halp::item<&ins::steal_margin> steal_margin;
3036 } alloc;
3037
3038 halp::spacing sp2{.width = 12, .height = 1};
3039
3040 struct
3041 {
3042 halp_meta(name, "Timing")
3043 halp_meta(layout, halp::layouts::vbox)
3044 halp::item<&ins::lost_grace> lost_grace;
3045 halp::item<&ins::min_note> min_note;
3046 halp::item<&ins::max_note> max_note;
3047 halp::item<&ins::retrig_lockout> retrig_lockout;
3048 halp::item<&ins::watchdog> watchdog;
3049 } timing;
3050 } voice_tab;
3051
3052 struct
3053 {
3054 halp_meta(name, "Pitch")
3055 halp_meta(layout, halp::layouts::hbox)
3056
3057 struct
3058 {
3059 halp_meta(name, "Source")
3060 halp_meta(layout, halp::layouts::vbox)
3061 halp::item<&ins::coords> coords;
3062 halp::item<&ins::pitch_axis> pitch_axis;
3063 halp::item<&ins::pitch_invert> pitch_invert;
3064 halp::item<&ins::in_lo> in_lo;
3065 halp::item<&ins::in_hi> in_hi;
3066 } source;
3067
3068 halp::spacing sp1{.width = 12, .height = 1};
3069
3070 struct
3071 {
3072 halp_meta(name, "Range")
3073 halp_meta(layout, halp::layouts::vbox)
3074 halp::item<&ins::pitch_lo> pitch_lo;
3075 halp::item<&ins::pitch_hi> pitch_hi;
3076 halp::item<&ins::pitch_tracking> pitch_tracking;
3077 halp::item<&ins::glide> glide;
3078 } range;
3079
3080 halp::spacing sp2{.width = 12, .height = 1};
3081
3082 struct
3083 {
3084 halp_meta(name, "Scale")
3085 halp_meta(layout, halp::layouts::vbox)
3086 // note: the member must not be called `scale` - the layout engine
3087 // reads a `scale` member as a QGraphicsItem transform property.
3088 halp::item<&ins::scale> scale_sel;
3089 halp::item<&ins::root> root;
3090 halp::item<&ins::quant_hyst> quant_hyst;
3091 } scl;
3092 } pitch_tab;
3093
3094 struct
3095 {
3096 halp_meta(name, "Expression")
3097 halp_meta(layout, halp::layouts::hbox)
3098
3099 struct
3100 {
3101 halp_meta(name, "Velocity")
3102 halp_meta(layout, halp::layouts::vbox)
3103 halp::item<&ins::vel_source> vel_source;
3104 halp::item<&ins::vel_fixed> vel_fixed;
3105 halp::item<&ins::vel_lo> vel_lo;
3106 halp::item<&ins::vel_hi> vel_hi;
3107 halp::item<&ins::preroll> preroll;
3108 } vel;
3109
3110 halp::spacing sp1{.width = 12, .height = 1};
3111
3112 struct
3113 {
3114 halp_meta(name, "Dimensions")
3115 halp_meta(layout, halp::layouts::vbox)
3116 halp::item<&ins::pressure_src> pressure_src;
3117 halp::item<&ins::timbre_src> timbre_src;
3118 halp::item<&ins::timbre_cc> timbre_cc;
3119 } dims;
3120
3121 halp::spacing sp_an{.width = 12, .height = 1};
3122
3123 // The full-scale value for each family of descriptor. Grouped together
3124 // because they are only ever touched when a mapping pins at 0 or 127,
3125 // and then it is obvious which one is to blame.
3126 struct
3127 {
3128 halp_meta(name, "Analysis range")
3129 halp_meta(layout, halp::layouts::vbox)
3130 halp::item<&ins::speed_ref> speed_ref;
3131 halp::item<&ins::accel_ref> accel_ref;
3132 halp::item<&ins::jerk_ref> jerk_ref;
3133 halp::item<&ins::turn_ref> turn_ref;
3134 halp::item<&ins::neighbour_range> neighbour_range;
3135 halp::item<&ins::age_ref> age_ref;
3136 halp::item<&ins::desc_smooth> desc_smooth;
3137 } analysis;
3138
3139 halp::spacing sp2{.width = 12, .height = 1};
3140
3141 struct
3142 {
3143 halp_meta(name, "Smoothing")
3144 halp_meta(layout, halp::layouts::vbox)
3145 halp::item<&ins::rise> rise;
3146 halp::item<&ins::fall> fall;
3147 halp::item<&ins::expr_rate> expr_rate;
3148 halp::item<&ins::deadband> deadband;
3149 } smooth;
3150 } expr_tab;
3151
3152 struct
3153 {
3154 halp_meta(name, "Rhythm")
3155 halp_meta(layout, halp::layouts::hbox)
3156
3157 struct
3158 {
3159 halp_meta(name, "Quantize")
3160 halp_meta(layout, halp::layouts::vbox)
3161 halp::item<&ins::quant_mode> quant_mode;
3162 halp::item<&ins::grid> grid;
3163 } quant;
3164
3165 halp::spacing sp1{.width = 12, .height = 1};
3166
3167 struct
3168 {
3169 halp_meta(name, "Feel")
3170 halp_meta(layout, halp::layouts::vbox)
3171 halp::item<&ins::strength> strength;
3172 halp::item<&ins::max_hold> max_hold;
3173 } feel;
3174 } rhythm_tab;
3175
3176 struct
3177 {
3178 halp_meta(name, "Safety")
3179 halp_meta(layout, halp::layouts::hbox)
3180
3181 struct
3182 {
3183 halp_meta(name, "Panic")
3184 halp_meta(layout, halp::layouts::vbox)
3185 halp::item<&ins::panic> panic;
3186 halp::item<&ins::panic_on_stop> panic_on_stop;
3187 halp::item<&ins::spacing> spacing;
3188 } pan;
3189 } safety_tab;
3190 };
3191};
3192
3193}
Definition EntityToMidi.hpp:1026
Definition EntityToMidi.hpp:249
Definition EntityToMidi.hpp:1039
Definition EntityToMidi.hpp:938
Definition EntityToMidi.hpp:1047
Definition EntityToMidi.hpp:1073
decltype(track_record::velocity) accel
previous acceleration
Definition EntityToMidi.hpp:1090
float heading
unwrapped, so it can be smoothed
Definition EntityToMidi.hpp:1091
ossia::one_pole_filter< float > f_vx
Definition EntityToMidi.hpp:1098
double last_dt
Definition EntityToMidi.hpp:1079
decltype(track_record::velocity) vel
previous velocity
Definition EntityToMidi.hpp:1089
Definition EntityToMidi.hpp:1032
Definition EntityToMidi.hpp:2970
Definition EntityToMidi.hpp:987
bool retrig_pending
Definition EntityToMidi.hpp:1000
Definition EntityToMidi.hpp:230
const std::vector< track_record > & parsed_entities() const noexcept
Definition EntityToMidi.hpp:1304
ossia::flat_map< int, entity_descriptors > m_desc
Definition EntityToMidi.hpp:1105
const ossia::flat_map< int, entity_descriptors > & descriptors() const noexcept
Definition EntityToMidi.hpp:1309
Definition PointTracker.hpp:103
Definition EntityToMidi.hpp:198
Definition MIDISync.hpp:126