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")
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);
251 struct : halp::val_port<
"Entities", std::vector<ossia::value>>
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; }
266 struct : halp::combobox_t<
"Coordinates", E2MCoords>
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.")
276 std::string_view values[2]{
"2D",
"3D"};
277 E2MCoords init{E2MCoords::TwoD};
282 struct : halp::combobox_t<
"Output Mode", E2MOutputMode>
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.")
294 std::string_view values[3]{
"MPE",
"Channel per entity",
"Single channel"};
295 E2MOutputMode init{E2MOutputMode::MPE};
299 struct : halp::combobox_t<
"MPE Zone", E2MZone>
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.")
308 std::string_view values[2]{
"Lower",
"Upper"};
309 E2MZone init{E2MZone::Lower};
313 struct : halp::spinbox_i32<
"Member Channels", halp::irange{1, 15, 15}>
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.")
322 struct : halp::spinbox_i32<
"Bend Range", halp::irange{1, 96, 48}>
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.")
332 struct : halp::toggle<
"Send MPE Config", halp::default_on_toggle>
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.")
341 struct : halp::spinbox_i32<
"Channel", halp::irange{1, 16, 1}>
345 "The MIDI channel used in Single-channel mode. Ignored in the "
349 struct : halp::combobox_t<
"Channel Reuse", E2MChannelReuse>
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.")
359 std::string_view values[2]{
"Least recently used",
"Immediate"};
360 E2MChannelReuse init{E2MChannelReuse::LRU};
364 struct : halp::spinbox_f32<
"Release Reserve", halp::range{0., 5000., 500.}>
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.")
374 struct : halp::combobox_t<
"Note Model", E2MNoteModel>
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.")
385 std::string_view values[2]{
"Sustained",
"Triggered"};
386 E2MNoteModel init{E2MNoteModel::Sustained};
390 struct : halp::combobox_t<
"Trigger On", E2MTriggerOn>
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.")
400 std::string_view values[2]{
"Confirmed",
"First detection"};
401 E2MTriggerOn init{E2MTriggerOn::Confirmed};
405 struct : halp::spinbox_i32<
"Max Voices", halp::irange{1, 16, 8}>
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.")
415 struct : halp::combobox_t<
"Steal Policy", E2MPriority>
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.")
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};
433 struct : halp::toggle<
"Allow Stealing", halp::default_on_toggle>
437 "Off: when every voice is busy, new entities are denied (and "
438 "counted on the Denied output) instead of interrupting a playing "
442 struct : halp::knob_f32<
"Steal Margin", halp::range{0., 1., 0.1}>
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.")
451 struct : halp::spinbox_f32<
"Lost Grace", halp::range{0., 5000., 250.}>
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.")
461 struct : halp::time_chooser<
"Min Note", halp::range{0., 2., 0.08}>
464 static float upgrade_value(
float ms)
noexcept {
return ms / 1000.f; }
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.")
472 struct : halp::time_chooser<
"Max Note", halp::range{0., 60., 0.}>
475 static float upgrade_value(
float ms)
noexcept {
return ms / 1000.f; }
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 "
483 struct : halp::time_chooser<
"Trigger Length", halp::range{0.01, 5., 0.2}>
486 static float upgrade_value(
float ms)
noexcept {
return ms / 1000.f; }
489 "Note duration in Triggered mode. Ignored in Sustained "
490 "mode, where the entity itself decides.")
493 struct : halp::spinbox_f32<
"Retrigger Lockout", halp::range{0., 2000., 120.}>
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.")
502 struct : halp::spinbox_f32<
"Watchdog", halp::range{0., 10000., 1000.}>
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.")
512 struct : halp::combobox_t<
"While Coasting", E2MCoast>
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 "
523 std::string_view values[3]{
"Freeze",
"Follow",
"Fade"};
524 E2MCoast init{E2MCoast::Freeze};
529 struct : halp::combobox_t<
"Pitch Axis", E2MAxis>
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.")
538 std::string_view values[3]{
"X",
"Y",
"Z"};
539 E2MAxis init{E2MAxis::Y};
543 struct : halp::toggle<
"Invert Axis", halp::default_on_toggle>
547 "Flip the axis. On by default because camera coordinates grow "
548 "downwards: inverted, standing tall plays high.")
551 struct : halp::spinbox_f32<
"Position Min", halp::range{-1000., 1000., 0.}>
555 "Axis value that maps to the lowest pitch. The computer-vision "
556 "chain normalises positions to 0..1, hence the default.")
559 struct : halp::spinbox_f32<
"Position Max", halp::range{-1000., 1000., 1.}>
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.")
567 struct : halp::spinbox_i32<
"Lowest Pitch", halp::irange{0, 127, 48}>
569 halp_meta(description,
"Bottom of the pitch range (MIDI note, 48 = C3).")
572 struct : halp::spinbox_i32<
"Highest Pitch", halp::irange{0, 127, 84}>
574 halp_meta(description,
"Top of the pitch range (MIDI note, 84 = C6).")
577 struct : halp::combobox_t<
"Pitch Tracking", E2MPitchTracking>
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.")
588 std::string_view values[3]{
"Continuous bend",
"Latched",
"Retrigger"};
589 E2MPitchTracking init{E2MPitchTracking::ContinuousBend};
593 struct : halp::time_chooser<
"Glide", halp::range{0., 2., 0.06}>
596 static float upgrade_value(
float ms)
noexcept {
return ms / 1000.f; }
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.")
604 struct : halp::combobox_t<
"Scale", E2MScale>
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.")
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};
623 struct : halp::combobox_t<
"Root", E2MRoot>
625 halp_meta(description,
"Root note of the scale.")
628 std::string_view values[12]{
629 "C",
"C#",
"D",
"D#",
"E",
"F",
"F#",
"G",
"G#",
"A",
"A#",
"B"};
630 E2MRoot init{E2MRoot::C};
634 struct : halp::knob_f32<
"Snap Hysteresis", halp::range{0., 0.5, 0.15}>
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.")
646 struct : halp::combobox_t<
"Velocity From", E2MVelSource>
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.")
656 std::string_view values[3]{
"Entry speed",
"Fixed",
"Confidence"};
657 E2MVelSource init{E2MVelSource::EntrySpeed};
661 struct : halp::spinbox_i32<
"Fixed Velocity", halp::irange{1, 127, 100}>
663 halp_meta(description,
"Velocity used when Velocity From is Fixed.")
666 struct : halp::spinbox_i32<
"Velocity Min", halp::irange{1, 127, 40}>
670 "Softest velocity. 40 keeps even the gentlest entrance audible.")
673 struct : halp::spinbox_i32<
"Velocity Max", halp::irange{1, 127, 110}>
675 halp_meta(description,
"Hardest velocity, reached at Speed Reference.")
678 struct : halp::spinbox_f32<
"Speed Reference", halp::range{0.01, 100., 2.}>
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.")
687 struct : halp::spinbox_f32<
"Speed Window", halp::range{0., 500., 100.}>
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.")
699 struct : halp::combobox_t<
"Pressure From", E2MSource>
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.")
710 std::string_view values[27]{
714 "Velocity X",
"Velocity Y",
715 "Velocity Z",
"|Velocity X|",
716 "|Velocity Y|",
"|Velocity Z|",
717 "Acceleration",
"Accel X",
718 "Accel Y",
"Accel Z",
720 "Curvature",
"Heading sin",
721 "Heading cos",
"Agitation",
722 "Nearest neighbour",
"Distance to centroid",
726 E2MSource init{E2MSource::Speed};
730 struct : halp::combobox_t<
"Timbre From", E2MSource>
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.")
740 std::string_view values[27]{
744 "Velocity X",
"Velocity Y",
745 "Velocity Z",
"|Velocity X|",
746 "|Velocity Y|",
"|Velocity Z|",
747 "Acceleration",
"Accel X",
748 "Accel Y",
"Accel Z",
750 "Curvature",
"Heading sin",
751 "Heading cos",
"Agitation",
752 "Nearest neighbour",
"Distance to centroid",
756 E2MSource init{E2MSource::X};
760 struct : halp::spinbox_i32<
"Timbre CC", halp::irange{0, 127, 74}>
764 "Controller number for the timbre dimension. 74 is the MPE "
765 "standard; change it only for non-MPE receivers.")
768 struct : halp::spinbox_f32<
"Rise", halp::range{0., 2000., 20.}>
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.")
777 struct : halp::spinbox_f32<
"Fall", halp::range{0., 2000., 80.}>
781 "Smoothing time when expression values decrease. Longer than "
782 "Rise so gestures speak instantly and decay musically.")
785 struct : halp::spinbox_f32<
"Expression Rate", halp::range{1., 200., 50.}>
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.")
794 struct : halp::knob_f32<
"Deadband", halp::range{0., 0.1, 0.005}>
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.")
804 struct : halp::combobox_t<
"Beat Quantize", E2MQuantTargets>
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.")
815 std::string_view values[3]{
"Off",
"Onsets",
"Onsets + offsets"};
816 E2MQuantTargets init{E2MQuantTargets::Off};
820 struct : halp::combobox_t<
"Grid", E2MGrid>
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 "
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};
835 struct : halp::knob_f32<
"Quantize Strength", halp::range{0., 1., 1.}>
839 "Blend between the gesture's own timing (0) and the grid (1). "
840 "Partial values keep the human feel while tightening it.")
843 struct : halp::time_chooser<
"Max Hold", halp::range{0., 2., 0.25}>
846 static float upgrade_value(
float ms)
noexcept {
return ms / 1000.f; }
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.")
855 struct : halp::impulse_button<
"Panic">
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; }
866 struct : halp::toggle<
"Panic On Stop", halp::default_on_toggle>
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.")
875 struct : halp::spinbox_f32<
"Panic Spacing", halp::range{0., 20., 1.}>
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.")
891 struct : halp::spinbox_f32<
"Acceleration Ref", halp::range{0.01, 200., 8.}>
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.")
901 struct : halp::spinbox_f32<
"Jerk Ref", halp::range{0.1, 5000., 200.}>
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.")
910 struct : halp::spinbox_f32<
"Turn Rate Ref", halp::range{0.1, 50., 6.28}>
914 "Turn rate, in radians per second, that maps to the top of the "
915 "range. The default is one full turn per second.")
918 struct : halp::spinbox_f32<
"Neighbour Range", halp::range{0.001, 100., 0.25}>
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.")
927 struct : halp::spinbox_f32<
"Age Ref", halp::range{0.1, 600., 10.}>
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 "
936 struct : halp::spinbox_f32<
"Descriptor Smoothing", halp::range{0., 2000., 80.}>
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.")
950 struct : halp::midi_out_bus<
"MIDI">
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{};
962 halp_meta(name,
"Active Voices")
965 "Number of notes currently sounding (including notes held "
966 "through a tracking dropout).")
972 halp_meta(name,
"Denied")
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.")
984 using tick = halp::tick_musical;
988 static constexpr int k_max_voices = 16;
990 enum class vstate : uint8_t
1000 int8_t channel = -1;
1002 vstate st = vstate::off;
1003 bool on_wire =
false;
1004 bool entity_confirmed =
false;
1005 bool missing =
false;
1006 bool coasting =
false;
1007 bool off_scheduled =
false;
1012 bool note_suppressed =
false;
1013 double missing_since = 0.;
1014 double last_seen = 0.;
1015 double on_time = 0.;
1016 double on_raw_t = 0.;
1017 double on_target_q = -1.;
1018 double off_raw_t = 0.;
1019 double off_target_q = -1.;
1020 float priority = 0.f;
1022 float pitch_target = 60.f;
1023 float quant_prev = -1.f;
1024 float bend_semis = 0.f;
1025 float pressure_v = 0.f;
1026 float timbre_v = 0.f;
1027 float pressure_t = 0.f;
1028 float timbre_t = 0.f;
1030 int last_pressure = -1;
1031 int last_timbre = -1;
1032 double expr_acc = 1e9;
1033 uint8_t velocity = 100;
1038 bool in_use =
false;
1039 double freed_at = -1e18;
1059 static constexpr int N = 24;
1064 void push(
double time,
float speed)
noexcept
1068 head = (head + 1) % N;
1071 float peak_since(
double t0)
const noexcept
1074 for(
int i = 0; i < N; i++)
1075 if(t[i] >= t0 && s[i] > m)
1086 double first_seen = 0.;
1091 decltype(track_record::position) pos{};
1100 decltype(track_record::velocity)
vel{};
1101 decltype(track_record::velocity)
accel{};
1103 bool has_heading =
false;
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;
1111 ossia::flat_map<int, synth_state> m_synth;
1112 std::vector<track_record> m_recs;
1116 ossia::flat_map<int, entity_descriptors>
m_desc;
1118 bool tracks_dirty =
false;
1119 bool panic_requested =
false;
1123 std::vector<std::array<uint8_t, 3>> last_direct_panic;
1127 ossia::exec_state_facade ossia_state;
1128 std::atomic<ossia::net::midi::midi_stream*> midi_out{};
1130 void prepare(halp::setup s)
1144 if(inputs.panic_on_stop)
1152 if(inputs.panic_on_stop)
1157 void resume() { m_do_config =
true; }
1159 void transport(
auto flicks)
1163 if(inputs.panic_on_stop)
1164 panic_requested =
true;
1167 void operator()(
const halp::tick_musical& tk)
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;
1179 const uint64_t sig = config_signature();
1180 if(sig != m_config_sig)
1182 if(m_config_sig != 0 && held_count() > 0)
1190 m_do_config =
false;
1191 if(inputs.send_config && inputs.output_mode.value == E2MOutputMode::MPE)
1197 panic_requested =
false;
1203 tracks_dirty =
false;
1204 parse_entities(now);
1210 emit_pending_notes(now, dt, frames, tk);
1211 update_expression(now, dt, frames);
1212 flush_timed(now, dt, frames);
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;
1219 for(
const auto& m : m_msgs)
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);
1231 for(
const auto& m : m_msgs)
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)
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]);
1241 else if(st == 0x80 || (st == 0x90 && m.bytes[2] == 0))
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]);
1249 outputs.active.value = held_count();
1250 outputs.denied.value = m_denied;
1257 bool check_invariants(std::string* err =
nullptr)
const
1261 if(!m_wire_err.empty())
1270 for(
const auto& v : m_voices)
1274 if(v.st == vstate::off)
1277 *err =
"note on wire for a dead voice";
1281 if(on_wire != m_outstanding)
1284 *err =
"outstanding note count mismatch";
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)
1295 *err =
"duplicate (channel, pitch) on wire";
1301 int held_count() const noexcept
1304 for(
const auto& v : m_voices)
1310 int outstanding() const noexcept {
return m_outstanding; }
1320 const ossia::flat_map<int, entity_descriptors>&
descriptors() const noexcept
1327 bool is_mpe() const noexcept
1329 return inputs.output_mode.value == E2MOutputMode::MPE;
1331 bool is_single() const noexcept
1333 return inputs.output_mode.value == E2MOutputMode::SingleChannel;
1336 int master_channel() const noexcept
1338 return inputs.mpe_zone.value == E2MZone::Lower ? 0 : 15;
1341 int member_count() const noexcept
1345 int n = std::clamp(inputs.member_channels.value, 1, is_mpe() ? 15 : 16);
1350 int member_channel(
int i)
const noexcept
1353 return std::clamp(inputs.single_channel.value - 1, 0, 15);
1355 return inputs.mpe_zone.value == E2MZone::Lower ? 1 + i : 14 - i;
1359 int alloc_channel(
double now)
noexcept
1362 return member_channel(0);
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;
1369 double best_score = 0.;
1370 bool best_ok =
false;
1371 for(
int i = 0; i < n; i++)
1373 const int c = member_channel(i);
1374 const auto& ci = m_chans[c];
1377 const double idle = now - ci.freed_at;
1378 const bool ok = !lru || idle >= reserve;
1381 if(best < 0 || (ok && !best_ok) || (ok == best_ok && idle > best_score))
1389 m_chans[best].in_use =
true;
1393 void free_channel(
int c,
double t)
noexcept
1395 if(c >= 0 && c < 16)
1397 m_chans[c].in_use =
false;
1398 m_chans[c].freed_at = t;
1399 m_touched[c] =
true;
1404 static float axis_of(
const track_record& r, E2MAxis a)
noexcept
1409 return r.position.x;
1411 return r.position.y;
1413 return r.position.z;
1418 float norm_axis(
const track_record& r, E2MAxis a,
bool invert)
const noexcept
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;
1426 float speed_of(
const track_record& r)
const noexcept
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);
1432 float norm_speed(
const track_record& r)
const noexcept
1434 const float ref = std::max(inputs.speed_ref.value, 1e-3f);
1435 return std::clamp(speed_of(r) / ref, 0.f, 1.f);
1438 float raw_pitch(
const track_record& r)
const noexcept
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);
1446 static const uint16_t* scale_mask(E2MScale s)
noexcept
1449 static constexpr uint16_t masks[13] = {
1464 return &masks[int(s)];
1467 bool in_scale(
int note)
const noexcept
1469 const auto s = inputs.scale.value;
1470 if(s == E2MScale::None || s == E2MScale::Chromatic)
1472 const int root = int(inputs.root.value);
1473 const int pc = ((note - root) % 12 + 12) % 12;
1474 return (*scale_mask(s) >> pc) & 1;
1477 float nearest_scale_note(
float raw)
const noexcept
1479 const auto s = inputs.scale.value;
1480 if(s == E2MScale::None)
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++)
1489 if(k < 0 || k > 127 || !in_scale(k))
1491 const float d = std::abs(raw -
float(k));
1504 float quantize_pitch(
float raw, voice& v)
const noexcept
1506 if(inputs.scale.value == E2MScale::None)
1508 v.quant_prev = -1.f;
1511 const float cand = nearest_scale_note(raw);
1512 if(v.quant_prev < 0.f)
1514 v.quant_prev = cand;
1517 if(cand != v.quant_prev)
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;
1525 return v.quant_prev;
1537 static constexpr std::size_t max_pairwise = 64;
1541 void analyse(
double now)
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);
1551 const float centre = 0.5f * (in_lo + in_hi);
1552 const bool is_3d = inputs.coords.value == E2MCoords::ThreeD;
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);
1562 float gx = 0.f, gy = 0.f, gz = 0.f;
1563 for(
const auto& r : m_recs)
1569 const float inv_n = 1.f / float(m_recs.size());
1574 const bool pairwise = m_recs.size() <= max_pairwise;
1576 for(std::size_t i = 0; i < m_recs.size(); i++)
1578 const auto& r = m_recs[i];
1579 entity_descriptors d;
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;
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);
1594 const float dx = r.position.x - centre, dy = r.position.y - centre;
1595 float r2 = dx * dx + dy * dy;
1598 const float dz = r.position.z - centre;
1601 d.radius = std::clamp(std::sqrt(r2) / half, 0.f, 1.f);
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);
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);
1618 if(st_it != m_synth.end() && fdt > 0.f)
1620 auto& st = st_it->second;
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);
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);
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);
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;
1647 d.turn_rate = std::clamp(std::abs(st.f_turn(rate, alpha)) / tref, 0.f, 1.f);
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));
1654 st.heading = unwrapped;
1655 st.has_heading =
true;
1659 d.heading_sin = bipolar(std::sin(st.heading));
1660 d.heading_cos = bipolar(std::cos(st.heading));
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);
1671 st.vel = r.velocity;
1672 st.accel = {ax, ay, az};
1677 const float dx = r.position.x - gx, dy = r.position.y - gy,
1678 dz = r.position.z - gz;
1680 = std::clamp(std::sqrt(dx * dx + dy * dy + dz * dz) / half, 0.f, 1.f);
1683 if(pairwise && m_recs.size() > 1)
1685 float best = std::numeric_limits<float>::max();
1687 for(std::size_t j = 0; j < m_recs.size(); j++)
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);
1700 d.nearest = std::clamp(best / nrange, 0.f, 1.f);
1703 d.density = std::clamp(
float(within) /
float(m_recs.size() - 1), 0.f, 1.f);
1713 static float bipolar(
float v)
noexcept
1715 return std::clamp(0.5f + 0.5f * v, 0.f, 1.f);
1718 float source_value(
const track_record& r, E2MSource s)
const noexcept
1722 case E2MSource::None:
1725 return norm_axis(r, E2MAxis::X,
false);
1727 return norm_axis(r, E2MAxis::Y,
false);
1729 return norm_axis(r, E2MAxis::Z,
false);
1730 case E2MSource::Confidence:
1731 return std::clamp(r.confidence, 0.f, 1.f);
1736 const auto it =
m_desc.find(r.id);
1739 const entity_descriptors& d = it->second;
1743 case E2MSource::Radius:
1745 case E2MSource::Speed:
1747 case E2MSource::VelocityX:
1749 case E2MSource::VelocityY:
1751 case E2MSource::VelocityZ:
1753 case E2MSource::SpeedX:
1755 case E2MSource::SpeedY:
1757 case E2MSource::SpeedZ:
1759 case E2MSource::Acceleration:
1761 case E2MSource::AccelX:
1763 case E2MSource::AccelY:
1765 case E2MSource::AccelZ:
1767 case E2MSource::Jerk:
1769 case E2MSource::TurnRate:
1771 case E2MSource::Curvature:
1773 case E2MSource::HeadingSin:
1774 return d.heading_sin;
1775 case E2MSource::HeadingCos:
1776 return d.heading_cos;
1777 case E2MSource::Agitation:
1779 case E2MSource::NearestNeighbour:
1781 case E2MSource::CentroidDistance:
1782 return d.centroid_dist;
1783 case E2MSource::Density:
1785 case E2MSource::Age:
1792 float entity_priority(
const track_record& r)
const noexcept
1794 switch(inputs.priority.value)
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:
1801 case E2MPriority::StealNewest:
1803 case E2MPriority::StealSlowest:
1805 case E2MPriority::StealLeastConfident:
1806 return r.confidence;
1812 voice* find_voice(int32_t
id)
noexcept
1814 for(
auto& v : m_voices)
1815 if(v.st != vstate::off && v.id == id)
1820 voice* free_voice() noexcept
1823 voice* free =
nullptr;
1824 for(
auto& v : m_voices)
1826 if(v.st == vstate::off)
1834 return used < std::min(inputs.max_voices.value, k_max_voices) ? free :
nullptr;
1837 static bool number_like(
const ossia::value& v)
noexcept
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;
1843 static float to_float(
const ossia::value& v)
noexcept
1845 return ossia::convert<float>(v);
1850 bool third_is_confidence() const noexcept
1852 return inputs.coords.value == E2MCoords::TwoD;
1856 bool parse_entity(
const ossia::value& v,
bool third_conf, track_record& out)
noexcept
1858 switch(v.get_type())
1860 case ossia::val_type::VEC2F: {
1861 const auto& a = *v.target<ossia::vec2f>();
1862 out.position = {a[0], a[1], 0.f};
1865 case ossia::val_type::VEC3F: {
1866 const auto& a = *v.target<ossia::vec3f>();
1869 out.position = {a[0], a[1], 0.f};
1870 out.confidence = a[2];
1873 out.position = {a[0], a[1], a[2]};
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];
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]))
1886 out.position = {to_float(l[0]), to_float(l[1]), 0.f};
1887 if(l.size() >= 3 && number_like(l[2]))
1890 out.confidence = to_float(l[2]);
1892 out.position.z = to_float(l[2]);
1894 if(l.size() >= 4 && number_like(l[3]))
1895 out.confidence = to_float(l[3]);
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)
1903 if(k ==
"position" || k ==
"pos" || k ==
"centroid" || k ==
"point")
1906 if(parse_entity(val, third_conf, sub))
1908 out.position = sub.position;
1912 else if(k ==
"position_raw")
1915 if(parse_entity(val, third_conf, sub))
1916 out.position_raw = sub.position;
1918 else if(k ==
"velocity" || k ==
"vel")
1921 if(parse_entity(val,
false, sub))
1922 out.velocity = sub.position;
1926 if(number_like(val))
1927 out.id = int(to_float(val));
1929 else if(k ==
"slot")
1931 if(number_like(val))
1932 out.slot = int(to_float(val));
1934 else if(k ==
"confidence" || k ==
"conf" || k ==
"score")
1936 if(number_like(val))
1937 out.confidence = to_float(val);
1941 if(number_like(val))
1942 out.age = to_float(val);
1944 else if(k ==
"time_since_seen")
1946 if(number_like(val))
1947 out.time_since_seen = to_float(val);
1949 else if(k ==
"creation_time")
1951 if(number_like(val))
1952 out.creation_time = to_float(val);
1954 else if(k ==
"state")
1956 if(
auto st = val.target<std::string>())
1959 else if(k ==
"provisional")
1961 if(number_like(val))
1962 out.provisional = to_float(val) != 0.f;
1964 else if(k ==
"reacquired")
1966 if(number_like(val))
1967 out.reacquired = to_float(val) != 0.f;
1981 void parse_entities(
double now)
1984 const auto& in = inputs.tracks.value;
1991 const bool third_conf = third_is_confidence();
1995 if(number_like(in[0]))
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)
2002 = {to_float(in[i]), to_float(in[i + 1]),
2003 stride == 3 ? to_float(in[i + 2]) : 0.f};
2005 m_recs.push_back(std::move(r));
2006 if(m_recs.size() >= 512)
2012 for(
const auto& v : in)
2016 if(parse_entity(v, third_conf, r))
2017 m_recs.push_back(std::move(r));
2018 if(m_recs.size() >= 512)
2026 ossia::flat_map<int, synth_state> next;
2027 for(std::size_t i = 0; i < m_recs.size(); i++)
2029 auto& r = m_recs[i];
2035 r.state = r.provisional ?
"provisional" :
"confirmed";
2036 if(!std::isfinite(r.position_raw.x))
2037 r.position_raw = r.position;
2039 auto& st = next[r.id];
2040 if(
auto it = m_synth.find(r.id); it != m_synth.end())
2042 const auto& prev = it->second;
2043 const double d = now - prev.time;
2047 if(d > 1e-6 && r.velocity.x == 0.f && r.velocity.y == 0.f
2048 && r.velocity.z == 0.f)
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)};
2059 const double since = now - prev.time;
2065 st.first_seen = now;
2071 st.pos = r.position;
2073 r.age = float(now - st.first_seen);
2075 m_synth = std::move(next);
2078 void ingest(
double now)
2080 const auto& recs = m_recs;
2081 const bool triggered = inputs.note_model.value == E2MNoteModel::Triggered;
2084 for(
const auto& r : recs)
2088 m_present.push_back(r.id);
2092 auto& h = m_hist[r.id];
2093 h.push(now, speed_of(r));
2095 voice* v = find_voice(r.id);
2097 = inputs.trigger_on.value == E2MTriggerOn::FirstDetection || !r.provisional;
2101 refresh_voice(*v, r, now);
2106 try_start_triggered(r, now);
2108 try_start_sustained(r, now);
2113 for(
auto& v : m_voices)
2115 if(v.st == vstate::off)
2117 bool present =
false;
2118 for(
auto id : m_present)
2124 if(!present && !v.missing)
2127 v.missing_since = now;
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);
2137 void refresh_voice(voice& v,
const track_record& r,
double now)
2141 v.coasting = r.state ==
"coasting";
2142 v.entity_confirmed = v.entity_confirmed || !r.provisional;
2143 v.priority = entity_priority(r);
2145 const bool freeze = v.coasting && inputs.coast.value == E2MCoast::Freeze;
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);
2153 if(v.coasting && inputs.coast.value == E2MCoast::Fade)
2163 if(inputs.note_model.value == E2MNoteModel::Triggered
2164 && inputs.pitch_tracking.value == E2MPitchTracking::Retrigger
2165 && v.st == vstate::sounding && !v.retrig_pending)
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)
2174 v.off_raw_t = std::min(v.off_raw_t, now);
2175 v.off_target_q = -1.;
2176 v.retrig_pending =
true;
2178 m_retrig_ids.push_back(v.id);
2183 static int pitch_to_note(
float target)
noexcept
2185 return std::clamp(
int(std::lround(target)), 0, 127);
2188 bool can_retrigger(int32_t
id,
double now)
const noexcept
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;
2196 voice* obtain_voice(
const track_record& r,
double now)
2198 if(
auto* v = free_voice())
2201 if(!inputs.allow_steal)
2208 voice* victim =
nullptr;
2209 for(
auto& v : m_voices)
2211 if(v.st == vstate::off)
2213 if(r.provisional && v.entity_confirmed)
2215 if(!victim || v.priority < victim->priority)
2218 const float p_new = entity_priority(r);
2219 if(!victim || p_new <= victim->priority + inputs.steal_margin.value)
2227 kill_voice(*victim, now);
2231 void init_voice(voice& v,
const track_record& r,
double now)
2235 v.st = vstate::pending_on;
2236 v.entity_confirmed = !r.provisional;
2238 v.priority = entity_priority(r);
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);
2247 if(inputs.quant_mode.value != E2MQuantTargets::Off)
2248 v.on_target_q = -2.;
2250 v.on_target_q = -1.;
2253 void try_start_sustained(
const track_record& r,
double now)
2258 if(!m_timed.empty())
2260 if(!can_retrigger(r.id, now))
2262 voice* v = obtain_voice(r, now);
2265 init_voice(*v, r, now);
2266 const int c = alloc_channel(now);
2270 v->st = vstate::off;
2274 v->channel = int8_t(c);
2277 void try_start_triggered(
const track_record& r,
double now)
2279 if(!m_timed.empty())
2281 if(!can_retrigger(r.id, now))
2283 voice* v = obtain_voice(r, now);
2286 init_voice(*v, r, now);
2287 const int c = alloc_channel(now);
2290 v->st = vstate::off;
2294 v->channel = int8_t(c);
2296 v->off_scheduled =
true;
2297 v->off_raw_t = now + inputs.trigger_duration.value;
2298 v->off_target_q = -1.;
2301 uint8_t compute_velocity(
const track_record& r,
double now)
const
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)
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));
2313 case E2MVelSource::EntrySpeed: {
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);
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));
2333 void lifecycle(
double now,
const halp::tick_musical& tk)
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;
2340 for(
auto& v : m_voices)
2342 if(v.st == vstate::off)
2346 if(sustained && v.missing && !v.off_scheduled && now - v.missing_since >= grace)
2347 schedule_off(v, now,
true);
2350 if(v.st == vstate::pending_on && v.missing && now - v.missing_since >= grace)
2352 free_channel(v.channel, now);
2359 if(wd > 0. && v.st == vstate::sounding && !v.off_scheduled
2360 && now - v.last_seen >= wd)
2361 schedule_off(v, now,
false);
2364 if(maxn > 0. && v.st == vstate::sounding && !v.off_scheduled
2365 && now - v.on_time >= maxn)
2366 schedule_off(v, now,
true);
2370 void schedule_off(voice& v,
double now,
bool allow_quantise)
2372 if(v.st == vstate::pending_on)
2375 free_channel(v.channel, now);
2381 v.off_scheduled =
true;
2385 && inputs.quant_mode.value == E2MQuantTargets::OnsetsAndOffsets)
2391 void kill_voice(voice& v,
double now)
2394 push_note_off(v, 0);
2395 free_channel(v.channel, now);
2400 double grid_quarters() const noexcept
2402 switch(inputs.grid.value)
2404 case E2MGrid::Whole:
2408 case E2MGrid::Quarter:
2410 case E2MGrid::Eighth:
2412 case E2MGrid::Sixteenth:
2414 case E2MGrid::EighthTriplet:
2416 case E2MGrid::SixteenthTriplet:
2424 double quantise_target(
double raw_q)
const noexcept
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);
2434 quarters_to_frame(
double target_q,
const halp::tick_musical& tk)
noexcept
2436 const double a = tk.start_position_in_quarters;
2437 const double b = tk.end_position_in_quarters;
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);
2449 void emit_pending_notes(
double now,
double dt,
int frames,
const halp::tick_musical& tk)
2451 const double max_hold = inputs.max_hold.value;
2452 const double min_note = inputs.min_note.value;
2454 for(
auto& v : m_voices)
2456 if(v.st == vstate::pending_on)
2459 if(v.on_target_q == -2.)
2460 v.on_target_q = quantise_target(tk.start_position_in_quarters);
2464 if(v.on_target_q < 0.)
2471 const int64_t f = quarters_to_frame(v.on_target_q, tk);
2477 else if(now - v.on_raw_t >= max_hold)
2485 emit_note_on(v, now, off, frames);
2488 if(v.st == vstate::sounding && v.off_scheduled)
2491 if(v.off_target_q == -2.)
2492 v.off_target_q = quantise_target(tk.start_position_in_quarters);
2496 const double earliest = v.on_time + min_note;
2500 if(v.off_target_q >= 0.)
2502 const int64_t f = quarters_to_frame(v.off_target_q, tk);
2508 else if(now - v.off_raw_t >= max_hold)
2514 else if(v.off_raw_t <= now + dt)
2517 off = std::clamp<int64_t>(
2518 int64_t((v.off_raw_t - now) / dt * frames), 0, frames - 1);
2523 const double t_emit = now + double(off) / m_rate;
2524 if(t_emit < earliest)
2527 if(earliest <= now + dt)
2530 off = std::clamp<int64_t>(
2531 int64_t((earliest - now) / dt * frames), 0, frames - 1);
2537 emit_note_off(v, off, now +
double(off) / m_rate);
2544 if(!m_retrig_ids.empty())
2546 for(
auto id : m_retrig_ids)
2548 for(
const auto& r : m_recs)
2551 try_start_triggered(r, now);
2555 m_retrig_ids.clear();
2559 void emit_note_on(voice& v,
double tick_start, int64_t frame_off,
int frames)
2561 const int note = pitch_to_note(v.pitch_target);
2567 for(
const auto& o : m_voices)
2568 if(&o != &v && o.on_wire && o.channel == v.channel && o.note == note)
2572 if(!v.note_suppressed)
2574 v.note_suppressed =
true;
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)
2592 frame_off = std::clamp<int64_t>(frame_off, 0, frames - 1);
2593 const double t = tick_start + double(frame_off) / m_rate;
2595 v.note = uint8_t(note);
2596 v.note_suppressed =
false;
2597 v.bend_semis = v.pitch_target - float(v.note);
2599 const uint8_t ch = uint8_t(v.channel);
2600 const bool member_dims = !is_single();
2605 if(inputs.pitch_tracking.value == E2MPitchTracking::ContinuousBend)
2607 push_bend(v, frame_off, 1);
2612 push_bend_value(ch, 8192, frame_off, 1);
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);
2621 push(frame_off, 1, uint8_t(0x90 | ch), v.note, v.velocity);
2624 v.st = vstate::sounding;
2627 m_touched[ch] =
true;
2628 m_last_on[v.id] = t;
2632 void emit_note_off(voice& v, int64_t frame_off,
double t)
2636 push(frame_off, 0, uint8_t(0x80 | uint8_t(v.channel)), v.note, 64);
2640 free_channel(v.channel, t);
2644 void push_note_off(voice& v, int64_t frame_off)
2648 push(frame_off, 0, uint8_t(0x80 | uint8_t(v.channel)), v.note, 64);
2654 void prune_last_on(
double now)
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);
2662 void update_expression(
double now,
double dt,
int frames)
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);
2671 = inputs.pitch_tracking.value == E2MPitchTracking::ContinuousBend && !single;
2674 const int n = k_max_voices;
2675 const int start = m_rr++ % n;
2676 for(
int k = 0; k < n; k++)
2678 voice& v = m_voices[(start + k) % n];
2679 if(v.st != vstate::sounding || !v.on_wire)
2683 const float bend_target = cont ? (v.pitch_target - float(v.note)) : 0.f;
2685 v.bend_semis = bend_target;
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);
2693 if(v.expr_acc < period)
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)
2704 push_bend_value(uint8_t(v.channel), bend, 0, 2);
2708 if(inputs.pressure_src.value != E2MSource::None)
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)
2716 push(0, 2, uint8_t(0xA0 | uint8_t(v.channel)), v.note, uint8_t(p));
2718 push2(0, 2, uint8_t(0xD0 | uint8_t(v.channel)), uint8_t(p));
2719 v.last_pressure = p;
2722 if(!single && inputs.timbre_src.value != E2MSource::None)
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)
2730 0, 2, uint8_t(0xB0 | uint8_t(v.channel)),
2731 uint8_t(std::clamp(inputs.timbre_cc.value, 0, 127)), uint8_t(t));
2739 smooth_asym(
float& state,
float target,
double tau_rise,
double tau_fall,
double dt)
2741 const double tau = target > state ? tau_rise : tau_fall;
2745 state += (target - state) *
float(ossia::lag_alpha(tau, dt));
2748 int bend_to_14bit(
const voice& v)
const noexcept
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);
2756 void push(int64_t ts, uint8_t prio, uint8_t b0, uint8_t b1, uint8_t b2)
2758 m_msgs.push_back({ts, prio, 3, {b0, b1, b2}});
2760 void push2(int64_t ts, uint8_t prio, uint8_t b0, uint8_t b1)
2762 m_msgs.push_back({ts, prio, 2, {b0, b1, 0}});
2765 void push_bend(voice& v, int64_t ts, uint8_t prio)
2767 const int bend = bend_to_14bit(v);
2768 push_bend_value(uint8_t(v.channel), bend, ts, prio);
2771 void push_bend_value(uint8_t ch,
int bend, int64_t ts, uint8_t prio)
2773 push(ts, prio, uint8_t(0xE0 | ch), uint8_t(bend & 0x7F), uint8_t((bend >> 7) & 0x7F));
2775 void push_pressure(voice& v, int64_t ts, uint8_t prio)
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;
2781 void push_timbre(voice& v, int64_t ts, uint8_t prio)
2783 const int t = std::clamp(
int(std::lround(v.timbre_v * 127.f)), 0, 127);
2785 ts, prio, uint8_t(0xB0 | uint8_t(v.channel)),
2786 uint8_t(std::clamp(inputs.timbre_cc.value, 0, 127)), uint8_t(t));
2792 void queue_timed(uint8_t b0, uint8_t b1, uint8_t b2, uint8_t n = 3)
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;
2799 void flush_timed(
double now,
double dt,
int frames)
2803 std::size_t taken = 0;
2804 for(
const auto& m : m_timed)
2808 const int64_t off = std::clamp<int64_t>(
2809 int64_t((m.t - now) / dt * frames), 0, frames - 1);
2812 m_msgs.push_back({off, 0, m.n, {m.bytes[0], m.bytes[1], m.bytes[2]}});
2816 m_timed.erase(m_timed.begin(), m_timed.begin() + taken);
2819 void queue_rpn(uint8_t ch, uint8_t rpn_msb, uint8_t rpn_lsb, uint8_t value)
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);
2828 void queue_mpe_config()
2830 const uint8_t master = uint8_t(master_channel());
2831 const uint8_t members = uint8_t(member_count());
2833 queue_rpn(master, 0, 6, members);
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);
2843 const double now = m_now;
2845 for(
auto& v : m_voices)
2847 if(v.st == vstate::off)
2851 queue_timed(uint8_t(0x80 | uint8_t(v.channel)), v.note, 64);
2855 free_channel(v.channel, now);
2860 for(
int c = 0; c < 16; c++)
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);
2868 queue_timed(uint8_t(0xD0 | c), 0, 0, 2);
2869 queue_timed(uint8_t(0xB0 | c), 74, 64);
2880 last_direct_panic.clear();
2881 auto* proto = midi_out.load();
2882 for(
auto& v : m_voices)
2884 if(v.st == vstate::off)
2888 send_direct(proto, uint8_t(0x80 | uint8_t(v.channel)), v.note, 64);
2890 m_wire[uint8_t(v.channel) & 0x0F].reset(v.note);
2895 for(
int c = 0; c < 16; c++)
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);
2907 ossia::net::midi::midi_stream* proto, uint8_t b0, uint8_t b1, uint8_t b2)
2909 if(last_direct_panic.size() < 4096)
2910 last_direct_panic.push_back({b0, b1, b2});
2912 proto->push_value(libremidi::message{libremidi::midi_bytes{b0, b1, b2}, 0});
2915 void resolve_protocol()
2917 if(outputs.midi.ossia_node)
2919 auto& proto = outputs.midi.ossia_node->get_device().get_protocol();
2920 if(
auto mp =
dynamic_cast<ossia::net::midi::midi_stream*
>(&proto))
2927 for(
auto& v : m_voices)
2933 for(
auto& c : m_chans)
2937 m_retrig_ids.clear();
2940 for(
auto& w : m_wire)
2944 uint64_t config_signature() const noexcept
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);
2955 std::array<voice, k_max_voices> m_voices{};
2956 std::array<chan_info, 16> m_chans{};
2957 std::array<bool, 16> m_touched{};
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;
2970 double m_rate = 48000.;
2972 double m_timed_tail = 0.;
2974 int m_outstanding = 0;
2976 uint64_t m_config_sig = 0;
2977 bool m_do_config =
false;
2978 bool m_started =
false;
2984 halp_meta(name,
"Entity To MIDI")
2985 halp_meta(layout, halp::layouts::tabs)
2986 halp_meta(background, halp::colors::background_mid)
2990 halp_meta(name,
"Output")
2991 halp_meta(layout, halp::layouts::hbox)
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;
3001 halp::spacing sp1{.width = 12, .height = 1};
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;
3013 halp::spacing sp2{.width = 12, .height = 1};
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;
3026 halp_meta(name,
"Voice")
3027 halp_meta(layout, halp::layouts::hbox)
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;
3039 halp::spacing sp1{.width = 12, .height = 1};
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;
3051 halp::spacing sp2{.width = 12, .height = 1};
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;
3067 halp_meta(name,
"Pitch")
3068 halp_meta(layout, halp::layouts::hbox)
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;
3081 halp::spacing sp1{.width = 12, .height = 1};
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;
3093 halp::spacing sp2{.width = 12, .height = 1};
3097 halp_meta(name,
"Scale")
3098 halp_meta(layout, halp::layouts::vbox)
3101 halp::item<&ins::scale> scale_sel;
3102 halp::item<&ins::root> root;
3103 halp::item<&ins::quant_hyst> quant_hyst;
3109 halp_meta(name,
"Expression")
3110 halp_meta(layout, halp::layouts::hbox)
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;
3123 halp::spacing sp1{.width = 12, .height = 1};
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;
3134 halp::spacing sp_an{.width = 12, .height = 1};
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;
3152 halp::spacing sp2{.width = 12, .height = 1};
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;
3167 halp_meta(name,
"Rhythm")
3168 halp_meta(layout, halp::layouts::hbox)
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;
3178 halp::spacing sp1{.width = 12, .height = 1};
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;
3191 halp_meta(name,
"Safety")
3192 halp_meta(layout, halp::layouts::hbox)
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;