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