Loading...
Searching...
No Matches
VelToNoteCore.hpp
1#pragma once
2// SPDX-License-Identifier: GPL-3.0-or-later
3// Allocation-free, host-independent scheduling for Nodes::PulseToNote::Node.
4
5#include <cmath>
6
7#include <algorithm>
8#include <array>
9#include <bitset>
10#include <cstddef>
11#include <cstdint>
12#include <limits>
13#include <optional>
14#include <type_traits>
15#include <utility>
16
17namespace Nodes::PulseToNote::detail
18{
19using frame_t = std::int64_t;
20using position_t = long double;
21inline constexpr position_t flicks_per_second = 705600000.L;
22
23enum class EndMode
24{
25 quantized,
26 duration
27};
28enum class DurationUnit
29{
30 model_seconds,
31 quarters
32};
33enum class PitchDirection
34{
35 Both,
36 Higher,
37 Lower
38};
39
41{
42 std::uint8_t pitch{}; // Source identity, BEFORE transposition / randomization.
43 std::uint8_t velocity{}; // Zero is an explicit release.
44};
45
46inline std::optional<int> midi_number(double x) noexcept
47{
48 if(!std::isfinite(x))
49 return std::nullopt;
50 return static_cast<int>(std::clamp(x, 0., 127.));
51}
52
53// Float velocities are normalized; integer velocities use the MIDI range.
54// A positive normalized velocity remains a note-on, even below 1 / 127.
55inline std::optional<DecodedNote>
56decode_pair(double pitch, double velocity, bool normalized_velocity) noexcept
57{
58 const auto p = midi_number(pitch);
59 if(!p || !std::isfinite(velocity))
60 return std::nullopt;
61 const double v = normalized_velocity ? std::clamp(velocity, 0., 1.) * 127.
62 : std::clamp(velocity, 0., 127.);
63 const int iv = v > 0. ? std::max(1, static_cast<int>(v)) : 0;
64 return DecodedNote{static_cast<std::uint8_t>(*p), static_cast<std::uint8_t>(iv)};
65}
66
67// A duration bound in whichever unit the chooser was in. Zero disables it.
68// Each bound carries its own unit because the two choosers sync independently.
69struct Bound
70{
71 double value{};
72 DurationUnit unit{DurationUnit::quarters};
73
74 friend bool operator==(const Bound&, const Bound&) = default;
75};
76
78{
79 // The score selectors contain fractions of a whole note, NOT rates.
80 // For compatibility with score, 1 and above mean one bar and multiples
81 // of a bar. Fractions below 1 mean 4 * value quarter notes, reset per bar.
82 double start_quant{0.25};
83 double tightness{1.};
84 double end_quant{0.25}; // > 0: grid; == 0: one sample; < 0: explicit release.
85 // Quantized mode only: a floor and a ceiling on how long a note may hold.
86 // The floor extends to the first grid point at or after it, so a bounded
87 // note still lands on the grid. The ceiling clamps exactly -- snapping back
88 // to the previous grid point can land before the note started.
89 Bound min_duration{};
90 Bound max_duration{};
91 EndMode end_mode{EndMode::quantized};
92 DurationUnit duration_unit{DurationUnit::model_seconds};
93 double duration{}; // Already converted to the unit above by the host adapter.
94 int channel{1}; // User-facing, one-based.
95 int pitch_shift{};
96 int pitch_random{};
97 int velocity_random{};
98 PitchDirection pitch_direction{PitchDirection::Both};
99};
100
101struct Block
102{
103 int frames{};
104 frame_t first_frame{}; // Absolute AUDIO clock of this slice's first sample.
105 double quarters_begin{};
106 double quarters_end{}; // One-past-end musical position.
107 int numerator{4};
108 int denominator{4};
109 double bar_begin{}; // Last bar line at slice start.
110 double bar_end{}; // Last bar line at slice end.
111 double last_signature{};
112 std::int64_t model_begin{}; // Logical flicks; NOT the wall / sample clock.
113 std::int64_t model_end{};
114 bool discontinuity{};
115 bool end_discontinuity{};
116};
117
119{
120 int frame{}; // Slice-relative; always in [0, Block::frames).
121 std::uint8_t channel{}; // Wire format, ZERO-based.
122 std::uint8_t pitch{};
123 std::uint8_t velocity{}; // Release velocity is deliberately zero.
124 bool on{};
125 friend bool operator==(const MidiEvent&, const MidiEvent&) = default;
126};
127
129{
130 std::uint64_t input_overflows{};
131 std::uint64_t rejected_inputs{};
132 std::uint64_t dropped_triggers{};
133 std::uint64_t coalesced_starts{};
134 std::uint64_t retriggers{};
135 std::uint64_t resets{};
136 std::uint64_t invalid_blocks{};
137 std::uint64_t output_failures{};
138};
139
140inline double finite_clamp(double x, double lo, double hi, double fallback) noexcept
141{
142 return std::isfinite(x) ? std::clamp(x, lo, hi) : fallback;
143}
144
145inline Settings sanitize(Settings s) noexcept
146{
147 // Explicit lower bound prevents unrepresentable / denormal grid spacing.
148 auto quant = [](double q) {
149 if(!std::isfinite(q) || q <= 0.)
150 return 0.;
151 return std::clamp(q, 1. / 1024., 64.);
152 };
153 s.start_quant = quant(s.start_quant);
154 s.tightness = finite_clamp(s.tightness, 0., 1., 1.);
155 s.end_quant
156 = std::isfinite(s.end_quant) && s.end_quant < 0. ? -1. : quant(s.end_quant);
157 s.duration = finite_clamp(s.duration, 0., 86400., 0.);
158 s.min_duration.value = finite_clamp(s.min_duration.value, 0., 86400., 0.);
159 s.max_duration.value = finite_clamp(s.max_duration.value, 0., 86400., 0.);
160 s.channel = std::clamp(s.channel, 1, 16);
161 s.pitch_shift = std::clamp(s.pitch_shift, -127, 127);
162 s.pitch_random = std::clamp(s.pitch_random, 0, 127);
163 s.velocity_random = std::clamp(s.velocity_random, 0, 127);
164 return s;
165}
166
167// Correct only rounding noise at a sample boundary, never quantize a duration
168// to a grid. Interpolation uses wide arithmetic; endpoint inputs may be doubles.
169inline position_t clean_integer(position_t x) noexcept
170{
171 const auto n = std::round(x);
172 const auto eps = std::clamp(
173 64.L * std::numeric_limits<double>::epsilon() * std::max(1.L, std::abs(x)), 1e-9L,
174 1e-5L);
175 return std::abs(x - n) <= eps ? n : x;
176}
177template <typename T>
178inline int direction(T a, T z) noexcept
179{
180 return (z > a) - (z < a);
181}
182
183// Analytic grid: no per-block grid allocation and no arbitrary event-count cap.
184// Fractions < 1 are whole-note fractions, reset at every bar. Values >= 1
185// denote multiples of a bar counted from the last signature change, as in score.
186class Grid
187{
188public:
189 explicit Grid(const Block& b) noexcept
190 : length_{
191 b.numerator > 0 && b.denominator > 0 ? 4.L * b.numerator / b.denominator
192 : 4.L}
193 , near_{std::min(b.bar_begin, b.bar_end)}
194 , far_{std::max(b.bar_begin, b.bar_end)}
195 , origin_{b.last_signature}
196 {
197 const auto lo = std::min(b.quarters_begin, b.quarters_end);
198 const auto hi = std::max(b.quarters_begin, b.quarters_end);
199 // Defensive fallback only for missing/inconsistent bar metadata.
200 if(near_ > lo || near_ + length_ < lo)
201 near_ = origin_ + std::floor((lo - origin_) / length_) * length_;
202 if(far_ < near_ || far_ > hi)
203 far_ = near_;
204 }
205
206 [[nodiscard]] position_t
207 next(position_t q, double selection, int dir, bool strict = false) const noexcept
208 {
209 if(selection >= 1.)
210 {
211 const auto unit = length_ * selection;
212 const auto x = clean_integer((q - origin_) / unit);
213 const auto k = dir > 0 ? (strict ? std::floor(x) + 1.L : std::ceil(x))
214 : (strict ? std::ceil(x) - 1.L : std::floor(x));
215 return origin_ + k * unit;
216 }
217 const auto unit = 4.L * selection;
218 const auto anchor = q >= far_ ? far_ : near_;
219 const auto bar
220 = anchor + std::floor(clean_integer((q - anchor) / length_)) * length_;
221 auto end = bar + length_;
222 if(bar < far_ && end > far_)
223 end = far_; // An irregular reported bar line.
224 const auto x = clean_integer((q - bar) / unit);
225 if(dir > 0)
226 {
227 const auto k = strict ? std::floor(x) + 1.L : std::ceil(x);
228 return std::min(bar + k * unit, end);
229 }
230 const auto k = strict ? std::ceil(x) - 1.L : std::floor(x);
231 if(k >= 0.L)
232 return bar + k * unit;
233 // We crossed the start of this bar. The preceding bar can be truncated.
234 const auto previous
235 = bar == far_ && far_ > near_
236 ? near_
237 + (std::ceil(clean_integer((far_ - near_) / length_)) - 1.L)
238 * length_
239 : bar - length_;
240 const auto last = std::ceil(clean_integer((bar - previous) / unit)) - 1.L;
241 return previous + std::max(0.L, last) * unit;
242 }
243
244private:
245 position_t length_, near_, far_, origin_;
246};
247
248// Directed phase uses the actual adjacent points, including the short cell at
249// an odd-meter bar boundary. Every target is causal in either travel direction.
250inline position_t onset_target(
251 const Grid& grid, position_t arrival, double quant, double tightness,
252 int dir) noexcept
253{
254 if(quant <= 0. || tightness <= 0.)
255 return arrival;
256 const auto next = grid.next(arrival, quant, dir);
257 if(next == arrival)
258 return arrival;
259 if(tightness >= 1.)
260 return next;
261 const auto previous = grid.next(arrival, quant, -dir);
262 const auto phase = (arrival - previous) / (next - previous);
263 const auto grace = (1.L - tightness) / 2.L;
264 const auto x = std::clamp((phase - grace) / grace, 0.L, 1.L);
265 const auto weight = tightness * x * x * (3.L - 2.L * x);
266 return arrival + weight * (next - arrival);
267}
268
269// Not a source of cryptographic randomness. Instance-owned, reproducible, no
270// locking / allocation / process-global random generator on the audio thread.
272{
273public:
274 void seed(std::uint64_t value) noexcept { state_ = value; }
275 int between(int low, int high) noexcept
276 {
277 if(low == high)
278 return low;
279 state_ += 0x9e3779b97f4a7c15ULL;
280 auto z = state_;
281 z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9ULL;
282 z = (z ^ (z >> 27)) * 0x94d049bb133111ebULL;
283 z ^= z >> 31;
284 // At most 255 outcomes. The modulo bias is negligible (less than 2^-56).
285 return low + static_cast<int>(z % static_cast<unsigned>(high - low + 1));
286 }
287 int symmetric(int radius) noexcept { return between(-radius, radius); }
288
289private:
290 std::uint64_t state_{0x71cc53a295a3b2e1ULL};
291};
292
293// Single execution thread. The sink accepts only CURRENT-slice messages.
294// A void sink promises delivery; a bool sink can refuse a message. Refused
295// note-offs are retained locally and must succeed before any more note-ons.
296template <std::size_t MaxVoices = 256, std::size_t MaxInputs = 4096>
298{
299 static_assert(MaxVoices > 0 && MaxInputs > 0);
300 static constexpr frame_t frame_limit = std::numeric_limits<frame_t>::max() / 4;
301 enum class State : std::uint8_t
302 {
303 free,
304 pending,
305 active
306 };
307 enum class EndKind : std::uint8_t
308 {
309 grid,
310 sample,
311 model,
312 musical,
313 hold
314 };
315 struct Voice
316 {
317 State state{};
318 EndKind end_kind{};
319 std::uint8_t source{}, pitch{}, velocity{}, channel{};
320 std::uint64_t serial{};
321 frame_t arrival_frame{}, not_before{};
322 std::int64_t model_anchor{}; // end_target is relative to this for model durations.
323 position_t arrival_quarter{}, start_target{}, end_target{};
324 // Where the note actually began, and its bounds resolved into quarters.
325 // Both are fixed when the note starts: a bound asked for in seconds must
326 // not move later because the tempo did.
327 position_t activation_quarter{}, min_quarters{}, max_quarters{};
328 double start_quant{}, tightness{}, end_quant{}, duration{};
329 Bound min_duration{}, max_duration{};
330 int direction{};
331 int due{}; // Slice-local deadline; Block::frames means not in this slice.
332 };
333 struct Input
334 {
335 int frame{};
336 DecodedNote note{};
337 std::size_t order{};
338 };
339
340public:
341 static constexpr std::size_t max_voices = MaxVoices;
342 static constexpr std::size_t max_inputs = MaxInputs;
343 static constexpr std::size_t wire_notes = 16 * 128;
344 // Two messages per new/carried voice, plus every locally owed wire release.
345 static constexpr std::size_t max_output_events
346 = 2 * MaxInputs + 2 * MaxVoices + wire_notes;
347
348 void seed(std::uint64_t value) noexcept { random_.seed(value); }
349 [[nodiscard]] const Statistics& statistics() const noexcept { return stats_; }
350 [[nodiscard]] std::size_t active_count() const noexcept
351 {
352 return count(State::active);
353 }
354 [[nodiscard]] std::size_t pending_count() const noexcept
355 {
356 return count(State::pending);
357 }
358 [[nodiscard]] std::size_t deferred_release_count() const noexcept
359 {
360 return owed_offs_.count();
361 }
362 [[nodiscard]] bool needs_service() const noexcept
363 {
364 return active_count() || pending_count() || owed_offs_.any();
365 }
366
367 // NO port access here. Lifecycle, transport, and end-of-tick calls can safely
368 // retire voices even if no output sample currently exists. Repeated calls
369 // preserve the locally stored releases. Pending starts are discarded.
370 void request_reset() noexcept
371 {
372 for(auto& v : voices_)
373 {
374 if(v.state == State::active)
375 owe_off(v);
376 v.state = State::free;
377 }
378 have_previous_ = false;
379 ++stats_.resets;
380 }
381
382 void begin_input() noexcept
383 {
384 input_count_ = 0;
385 overflow_ = false;
386 }
387 bool push(int frame, DecodedNote note) noexcept
388 {
389 if(input_count_ == MaxInputs)
390 {
391 overflow_ = true;
392 return false;
393 }
394 inputs_[input_count_] = {frame, note, input_count_};
395 ++input_count_;
396 return true;
397 }
398 void input_overflow() noexcept { overflow_ = true; }
399
400 // Service a real writable window without triggering or advancing any notes.
401 // May be used by a host that gives stopped nodes a final cleanup tick.
402 template <typename Sink>
403 bool drain_releases(int frame, int frames, Sink&& sink) noexcept
404 {
405 if(frame < 0 || frame >= frames)
406 return !owed_offs_.any();
407 for(std::size_t k = 0; k < wire_notes; ++k)
408 {
409 if(!owed_offs_[k])
410 continue;
411 if(!send(
412 MidiEvent{
413 frame, static_cast<std::uint8_t>(k / 128),
414 static_cast<std::uint8_t>(k % 128), 0, false},
415 sink))
416 return false;
417 owed_offs_.reset(k);
418 }
419 return true;
420 }
421
422 template <typename Sink>
423 void process(const Block& b, Settings settings, Sink&& sink) noexcept
424 {
425 // Inspect transport even when there is no writable frame. Never emit an
426 // out-of-range message and never consume locally owed releases in this case.
427 if(b.frames <= 0)
428 {
429 if(b.frames < 0 || b.discontinuity || b.end_discontinuity
430 || b.model_begin != b.model_end || b.quarters_begin != b.quarters_end
431 || changed_position(b))
432 request_reset();
433 begin_input();
434 return;
435 }
436 if(!valid(b))
437 {
438 ++stats_.invalid_blocks;
439 request_reset();
440 drain_releases(0, b.frames, sink);
441 begin_input();
442 return;
443 }
444 const int md = direction(b.model_begin, b.model_end);
445 const int qd = direction(b.quarters_begin, b.quarters_end);
446 const bool reversed
447 = have_previous_
448 && ((md && previous_model_direction_ && md != previous_model_direction_)
449 || (qd && previous_quarter_direction_
450 && qd != previous_quarter_direction_));
451 if(b.discontinuity || changed_position(b) || reversed)
452 request_reset();
453
454 // Stationary scrubs audition no new triggers. Retire all previous voices.
455 // A continuous reverse tick, unlike a stationary tick, is fully playable.
456 if(md == 0)
457 request_reset();
458 if(!drain_releases(0, b.frames, sink))
459 {
460 begin_input();
461 return; // Backpressure: do not permit a new on before its owed off.
462 }
463 if(md == 0)
464 {
465 begin_input();
466 remember(b);
467 return;
468 }
469 if(overflow_)
470 {
471 ++stats_.input_overflows;
472 request_reset(); // A discarded input suffix could contain explicit offs.
473 drain_releases(0, b.frames, sink);
474 begin_input();
475 remember(b);
476 return;
477 }
478
479 settings = sanitize(settings);
480 std::size_t kept = 0;
481 for(std::size_t i = 0; i < input_count_; ++i)
482 if(inputs_[i].frame >= 0 && inputs_[i].frame < b.frames
483 && inputs_[i].note.pitch <= 127 && inputs_[i].note.velocity <= 127)
484 inputs_[kept++] = inputs_[i];
485 else
486 ++stats_.rejected_inputs;
487 input_count_ = kept;
488 std::sort(
489 inputs_.begin(), inputs_.begin() + input_count_,
490 [](const Input& a, const Input& c) {
491 return a.frame != c.frame ? a.frame < c.frame : a.order < c.order;
492 });
493
494 const Grid grid{b};
495 const bool meter_changed = have_previous_
496 && (b.numerator != previous_numerator_
497 || b.denominator != previous_denominator_
498 || b.last_signature != previous_signature_);
499 for(auto& v : voices_)
500 {
501 if(v.state == State::pending && meter_changed && v.start_quant > 0. && qd)
502 set_start_target(v, grid, b, qd);
503 if(v.state == State::active && v.end_kind == EndKind::grid && meter_changed && qd
504 && qd * (v.end_target - b.quarters_begin) > 0.L)
505 v.end_target = clamp_grid_end(
506 v, v.activation_quarter, grid.next(b.quarters_begin, v.end_quant, qd),
507 grid);
508 if(v.state != State::free)
509 v.due = deadline(v, b, grid);
510 }
511
512 bool ok = true;
513 std::size_t input = 0;
514 while(ok)
515 {
516 int now = input < input_count_ ? inputs_[input].frame : b.frames;
517 for(const auto& v : voices_)
518 if(v.state != State::free)
519 now = std::min(now, v.due);
520 if(now >= b.frames)
521 break;
522
523 // Automatic offs, then timestamp-stable inputs, then surviving ons.
524 for(auto& v : voices_)
525 if(v.state == State::active && v.due == now)
526 if(!release(v, now, sink))
527 {
528 ok = false;
529 break;
530 }
531 if(!ok)
532 break;
533 while(input < input_count_ && inputs_[input].frame == now)
534 {
535 const auto note = inputs_[input++].note;
536 if(note.velocity == 0)
537 ok = cancel_source(note.pitch, now, sink);
538 else
539 enqueue(note, now, settings, b, grid);
540 if(!ok)
541 break;
542 }
543 if(!ok)
544 break;
545
546 // One voice per wire (channel,pitch); last arrival wins on equal samples.
547 for(auto& v : voices_)
548 {
549 if(v.state != State::pending || v.due != now)
550 continue;
551 for(const auto& other : voices_)
552 if(other.state == State::pending && other.due == now
553 && other.channel == v.channel && other.pitch == v.pitch
554 && other.serial > v.serial)
555 {
556 v.state = State::free;
557 ++stats_.coalesced_starts;
558 break;
559 }
560 }
561 // Release collisions before ANY new on at this timestamp.
562 for(const auto& p : voices_)
563 {
564 if(p.state != State::pending || p.due != now)
565 continue;
566 for(auto& a : voices_)
567 if(a.state == State::active && a.channel == p.channel && a.pitch == p.pitch)
568 {
569 ok = release(a, now, sink);
570 ++stats_.retriggers;
571 if(!ok)
572 break;
573 }
574 if(!ok)
575 break;
576 }
577 if(!ok)
578 break;
579 for(;;)
580 {
581 Voice* next = nullptr;
582 for(auto& v : voices_)
583 if(v.state == State::pending && v.due == now
584 && (!next || v.serial < next->serial))
585 next = &v;
586 if(!next)
587 break;
588 if(!activate(*next, now, b, grid, sink))
589 {
590 ok = false;
591 break;
592 }
593 }
594 }
595 begin_input();
596 if(!ok)
597 {
598 request_reset();
599 return;
600 }
601 remember(b);
602 // The next tick owns this boundary. Do not emit at frame == b.frames or
603 // append a speculative future timestamp to the output port.
604 if(b.end_discontinuity)
605 request_reset();
606 }
607
608private:
609 [[nodiscard]] std::size_t count(State state) const noexcept
610 {
611 return std::count_if(voices_.begin(), voices_.end(), [state](const Voice& v) {
612 return v.state == state;
613 });
614 }
615 void owe_off(const Voice& v) noexcept
616 {
617 owed_offs_.set(std::size_t(v.channel) * 128 + v.pitch);
618 }
619 template <typename Sink>
620 bool send(const MidiEvent& e, Sink& sink) noexcept
621 {
622 if constexpr(std::is_void_v<std::invoke_result_t<Sink&, const MidiEvent&>>)
623 {
624 sink(e);
625 return true;
626 }
627 else
628 {
629 if(sink(e))
630 return true;
631 ++stats_.output_failures;
632 return false;
633 }
634 }
635 static bool valid(const Block& b) noexcept
636 {
637 auto position = [](double q) { return std::isfinite(q) && std::abs(q) <= 1e9; };
638 return b.first_frame >= -frame_limit && b.first_frame <= frame_limit - b.frames
639 && position(b.quarters_begin) && position(b.quarters_end)
640 && position(b.bar_begin) && position(b.bar_end) && position(b.last_signature)
641 && b.numerator >= 0 && b.numerator <= 1024 && b.denominator >= 0
642 && b.denominator <= 1024;
643 }
644 bool changed_position(const Block& b) const noexcept
645 {
646 return have_previous_
647 && (b.first_frame != previous_frame_ || b.model_begin != previous_model_
648 || std::abs(position_t(b.quarters_begin) - previous_quarter_) > 1e-8L);
649 }
650 static position_t at(position_t a, position_t z, int f, int frames) noexcept
651 {
652 return a + (z - a) * (position_t(f) / frames);
653 }
654 static position_t quarter_at(const Block& b, int f) noexcept
655 {
656 return at(b.quarters_begin, b.quarters_end, f, b.frames);
657 }
658 // A bound in quarters, or zero when it is disabled or cannot be resolved.
659 // A seconds bound needs a tempo, and the slice is the only place one is
660 // known; a slice that advances no musical time cannot supply it, so the
661 // bound is dropped rather than guessed.
662 static position_t resolve_bound(const Bound& bound, const Block& b) noexcept
663 {
664 if(!(bound.value > 0.))
665 return 0.L;
666 if(bound.unit == DurationUnit::quarters)
667 return position_t(bound.value);
668 const auto seconds
669 = std::abs(model_delta(b.model_end, b.model_begin)) / flicks_per_second;
670 const auto quarters = std::abs(position_t(b.quarters_end) - position_t(b.quarters_begin));
671 if(!(seconds > 0.L) || !(quarters > 0.L))
672 return 0.L;
673 return position_t(bound.value) * (quarters / seconds);
674 }
675 // Apply the quantized-mode bounds to a grid deadline. `from` is where the
676 // note began. Order matters: the floor moves the target out to a grid point,
677 // then the ceiling caps it, so a ceiling below the floor yields exactly the
678 // floor rather than cancelling the note.
679 static position_t clamp_grid_end(
680 const Voice& v, position_t from, position_t target, const Grid& grid) noexcept
681 {
682 const int dir = v.direction;
683 if(dir == 0)
684 return target;
685 if(v.min_quarters > 0.L)
686 {
687 const auto floor_at = from + dir * v.min_quarters;
688 if(dir * (floor_at - target) > 0.L)
689 target = grid.next(floor_at, v.end_quant, dir);
690 }
691 if(v.max_quarters > 0.L)
692 {
693 const auto ceil_at = from + dir * std::max(v.max_quarters, v.min_quarters);
694 if(dir * (target - ceil_at) > 0.L)
695 target = ceil_at;
696 }
697 return target;
698 }
699 // Cast AFTER unsigned subtraction. This handles the complete int64 range
700 // without signed overflow or losing small deltas at a large model origin on
701 // platforms where long double has only double precision (notably MSVC).
702 static position_t model_delta(std::int64_t to, std::int64_t from) noexcept
703 {
704 if(to >= from)
705 return position_t(std::uint64_t(to) - std::uint64_t(from));
706 return -position_t(std::uint64_t(from) - std::uint64_t(to));
707 }
708 static int
709 frame_of(position_t a, position_t z, position_t target, int frames, bool ceil) noexcept
710 {
711 if(a == z)
712 return frames;
713 const auto x = clean_integer((target - a) / (z - a) * frames);
714 if(!std::isfinite(x) || x >= frames)
715 return frames;
716 if(x <= 0.L)
717 return 0;
718 return static_cast<int>(ceil ? std::ceil(x) : std::floor(x));
719 }
720 static int local_frame(const Block& b, frame_t absolute) noexcept
721 {
722 if(absolute <= b.first_frame)
723 return 0;
724 if(absolute >= b.first_frame + b.frames)
725 return b.frames;
726 return static_cast<int>(absolute - b.first_frame);
727 }
728 static void
729 set_start_target(Voice& v, const Grid& grid, const Block& b, int dir) noexcept
730 {
731 const auto from = dir > 0
732 ? std::max(v.arrival_quarter, position_t(b.quarters_begin))
733 : std::min(v.arrival_quarter, position_t(b.quarters_begin));
734 v.start_target = onset_target(grid, from, v.start_quant, v.tightness, dir);
735 }
736 int deadline(const Voice& v, const Block& b, const Grid& grid) const noexcept
737 {
738 if(v.state == State::pending)
739 {
740 const int earliest = local_frame(b, v.arrival_frame);
741 if(v.start_quant == 0. || v.tightness == 0.)
742 return earliest;
743 return std::max(
744 earliest,
745 frame_of(b.quarters_begin, b.quarters_end, v.start_target, b.frames, true));
746 }
747 const int earliest = local_frame(b, v.not_before);
748 switch(v.end_kind)
749 {
750 case EndKind::sample:
751 return earliest;
752 case EndKind::model:
753 return std::max(
754 earliest, frame_of(
755 0.L, model_delta(b.model_end, b.model_begin),
756 model_delta(v.model_anchor, b.model_begin) + v.end_target,
757 b.frames, true));
758 case EndKind::musical:
759 return std::max(
760 earliest,
761 frame_of(b.quarters_begin, b.quarters_end, v.end_target, b.frames, true));
762 case EndKind::grid:
763 return std::max(
764 earliest,
765 frame_of(b.quarters_begin, b.quarters_end, v.end_target, b.frames, true));
766 case EndKind::hold:
767 return b.frames;
768 }
769 return b.frames;
770 }
771 template <typename Sink>
772 bool release(Voice& v, int frame, Sink& sink) noexcept
773 {
774 const bool ok = send(MidiEvent{frame, v.channel, v.pitch, 0, false}, sink);
775 if(!ok)
776 owe_off(v);
777 v.state = State::free;
778 return ok;
779 }
780 template <typename Sink>
781 bool cancel_source(std::uint8_t source, int frame, Sink& sink) noexcept
782 {
783 for(auto& v : voices_)
784 if(v.state != State::free && v.source == source)
785 {
786 if(v.state == State::active)
787 {
788 if(!release(v, frame, sink))
789 return false;
790 }
791 else
792 v.state = State::free;
793 }
794 return true; // Unmatched releases never stop notes owned by other processors.
795 }
796 void enqueue(
797 DecodedNote note, int frame, const Settings& s, const Block& b,
798 const Grid& grid) noexcept
799 {
800 // A full pool rejects the trigger; it never silently evicts an unrelated
801 // active note or forgets that note's eventual release.
802 Voice* free = nullptr;
803 for(auto& v : voices_)
804 {
805 if(v.state == State::free && !free)
806 free = &v;
807 }
808 if(!free)
809 {
810 ++stats_.dropped_triggers;
811 return;
812 }
813 auto& v = *free;
814 v = {};
815 v.state = State::pending;
816 v.source = note.pitch;
817 v.pitch = static_cast<std::uint8_t>(std::clamp(
818 int(note.pitch) + s.pitch_shift
819 + random_.between(
820 s.pitch_direction == PitchDirection::Higher ? 0 : -s.pitch_random,
821 s.pitch_direction == PitchDirection::Lower ? 0 : s.pitch_random),
822 0, 127));
823 v.velocity = static_cast<std::uint8_t>(
824 std::clamp(int(note.velocity) + random_.symmetric(s.velocity_random), 1, 127));
825 v.channel = static_cast<std::uint8_t>(s.channel - 1);
826 v.serial = ++serial_;
827 v.arrival_frame = b.first_frame + frame;
828 v.arrival_quarter = quarter_at(b, frame);
829 v.start_quant = s.start_quant;
830 v.tightness = s.tightness;
831 v.end_quant = s.end_quant;
832 v.duration = s.duration;
833 v.min_duration = s.min_duration;
834 v.max_duration = s.max_duration;
835 v.end_kind
836 = s.end_mode == EndMode::duration
837 ? (s.duration == 0. ? EndKind::sample
838 : s.duration_unit == DurationUnit::model_seconds ? EndKind::model
839 : EndKind::musical)
840 : (s.end_quant > 0. ? EndKind::grid
841 : s.end_quant < 0. ? EndKind::hold
842 : EndKind::sample);
843 const int qd = direction(b.quarters_begin, b.quarters_end);
844 v.direction = qd ? qd : direction(b.model_begin, b.model_end);
845 if(s.start_quant > 0.)
846 set_start_target(v, grid, b, v.direction);
847 v.due = deadline(v, b, grid);
848 }
849 template <typename Sink>
850 bool
851 activate(Voice& v, int frame, const Block& b, const Grid& grid, Sink& sink) noexcept
852 {
853 if(!send(MidiEvent{frame, v.channel, v.pitch, v.velocity, true}, sink))
854 {
855 v.state = State::free;
856 return false;
857 }
858 v.state = State::active;
859 v.not_before = b.first_frame + frame + 1;
860 if(v.end_kind == EndKind::model)
861 {
862 v.model_anchor = b.model_begin;
863 v.end_target
864 = model_delta(b.model_end, b.model_begin) * (position_t(frame) / b.frames)
865 + direction(b.model_begin, b.model_end) * v.duration * flicks_per_second;
866 }
867 else if(v.end_kind == EndKind::musical)
868 v.end_target = quarter_at(b, frame) + v.direction * v.duration;
869 else if(v.end_kind == EndKind::grid)
870 {
871 v.activation_quarter = quarter_at(b, frame);
872 v.min_quarters = resolve_bound(v.min_duration, b);
873 v.max_quarters = resolve_bound(v.max_duration, b);
874 v.end_target = clamp_grid_end(
875 v, v.activation_quarter,
876 grid.next(v.activation_quarter, v.end_quant, v.direction, true), grid);
877 }
878 v.due = deadline(v, b, grid);
879 return true;
880 }
881 void remember(const Block& b) noexcept
882 {
883 previous_frame_ = b.first_frame + b.frames;
884 previous_model_ = b.model_end;
885 previous_quarter_ = b.quarters_end;
886 previous_model_direction_ = direction(b.model_begin, b.model_end);
887 previous_quarter_direction_ = direction(b.quarters_begin, b.quarters_end);
888 previous_numerator_ = b.numerator;
889 previous_denominator_ = b.denominator;
890 previous_signature_ = b.last_signature;
891 have_previous_ = true;
892 }
893
894 std::array<Voice, MaxVoices> voices_{};
895 std::array<Input, MaxInputs> inputs_{};
896 // Owed MIDI bytes are exactly determined by this key; release velocity is 0.
897 // This is LOCAL STORAGE, not a retained output-port buffer or reference.
898 std::bitset<wire_notes> owed_offs_{};
899 std::size_t input_count_{};
900 std::uint64_t serial_{};
901 Random random_{};
902 Statistics stats_{};
903 frame_t previous_frame_{};
904 std::int64_t previous_model_{};
905 double previous_quarter_{}, previous_signature_{};
906 int previous_model_direction_{}, previous_quarter_direction_{};
907 int previous_numerator_{}, previous_denominator_{};
908 bool have_previous_{}, overflow_{};
909};
910} // namespace Nodes::PulseToNote::detail
Definition VelToNoteCore.hpp:187
Definition VelToNoteCore.hpp:272
Link of score with the OSSIA API execution engine.
Definition CurveConversion.hpp:8
Utilities for OSSIA data structures.
Definition DeviceInterface.hpp:35
Definition VelToNoteCore.hpp:102
Definition VelToNoteCore.hpp:70
Definition VelToNoteCore.hpp:41
Definition VelToNoteCore.hpp:119
Definition VelToNoteCore.hpp:78
Definition VelToNoteCore.hpp:129