Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
step_player.hpp
Go to the documentation of this file.
1#pragma once
2
9
10#include <algorithm>
11#include <array>
12#include <cmath>
13#include <cstddef>
14#include <cstdint>
15#include <limits>
16#include <optional>
17#include <span>
18
19namespace pulp::midi {
20
21enum class StepPlayerDirection : std::uint8_t {
26};
27
35
48
57 bool on = false;
58 std::int8_t pitch_offset = 0;
59 std::uint8_t velocity = 100;
60 std::uint8_t gate_percent = 100;
61 std::uint8_t probability_percent = 100;
62 std::uint8_t ratchet_count = 1;
81 std::int32_t timing_offset_ticks = 0;
82 bool tie = false;
83 bool slide = false;
84 constexpr auto operator<=>(const StepPlayerStep&) const = default;
85};
86
88 std::uint8_t channel = 0;
89 std::uint8_t base_note = 60;
90 std::uint8_t step_count = 0;
93 std::uint8_t choke_group = 0;
94 bool enabled = true;
95 constexpr auto operator<=>(const StepPlayerLaneSpec&) const = default;
96};
97
115
135
137
138constexpr std::int64_t floor_div(std::int64_t numerator, std::int64_t denominator) noexcept {
139 const auto quotient = numerator / denominator;
140 const auto remainder = numerator % denominator;
141 return remainder != 0 && numerator < 0 ? quotient - 1 : quotient;
142}
143
144constexpr std::int64_t saturating_multiply(std::int64_t lhs, std::int64_t rhs) noexcept {
145 constexpr auto maximum = std::numeric_limits<std::int64_t>::max();
146 constexpr auto minimum = std::numeric_limits<std::int64_t>::min();
147 if (lhs == 0 || rhs == 0)
148 return 0;
149 if (lhs == -1)
150 return rhs == minimum ? maximum : -rhs;
151 if (rhs == -1)
152 return lhs == minimum ? maximum : -lhs;
153 if (lhs > 0) {
154 if (rhs > 0 && lhs > maximum / rhs)
155 return maximum;
156 if (rhs < 0 && rhs < minimum / lhs)
157 return minimum;
158 } else {
159 if (rhs > 0 && lhs < minimum / rhs)
160 return minimum;
161 if (rhs < 0 && lhs < maximum / rhs)
162 return maximum;
163 }
164 return lhs * rhs;
165}
166
167constexpr std::size_t positive_mod(std::int64_t value, std::size_t modulus) noexcept {
168 auto result = value % static_cast<std::int64_t>(modulus);
169 if (result < 0)
170 result += static_cast<std::int64_t>(modulus);
171 return static_cast<std::size_t>(result);
172}
173
174constexpr bool distance_exceeds(std::int64_t lhs, std::int64_t rhs,
175 std::uint64_t tolerance) noexcept {
176 const auto magnitude = [](std::int64_t value) constexpr {
177 return value >= 0 ? static_cast<std::uint64_t>(value)
178 : static_cast<std::uint64_t>(-(value + 1)) + 1;
179 };
180 if ((lhs < 0) == (rhs < 0)) {
181 const auto left = magnitude(lhs);
182 const auto right = magnitude(rhs);
183 return (left >= right ? left - right : right - left) > tolerance;
184 }
185 const auto left = magnitude(lhs);
186 const auto right = magnitude(rhs);
187 return left > std::numeric_limits<std::uint64_t>::max() - right ||
188 left + right > tolerance;
189}
190
191constexpr long double difference_as_long_double(std::int64_t lhs, std::int64_t rhs) noexcept {
192 // Subtract before conversion when both values have the same sign. Their
193 // difference is representable, and this preserves adjacent ticks at the
194 // int64 limits on platforms where long double has only 53 mantissa bits.
195 if ((lhs < 0) == (rhs < 0))
196 return static_cast<long double>(lhs - rhs);
197 return static_cast<long double>(lhs) - static_cast<long double>(rhs);
198}
199
200inline std::int64_t saturating_round(long double value) noexcept {
201 constexpr auto minimum = static_cast<long double>(std::numeric_limits<std::int64_t>::min());
202 constexpr auto maximum = static_cast<long double>(std::numeric_limits<std::int64_t>::max());
203 if (!std::isfinite(value) || value >= maximum)
204 return value < 0 ? std::numeric_limits<std::int64_t>::min()
205 : std::numeric_limits<std::int64_t>::max();
206 if (value <= minimum)
207 return std::numeric_limits<std::int64_t>::min();
208 return static_cast<std::int64_t>(std::llround(value));
209}
210
211constexpr bool valid_direction(StepPlayerDirection direction) noexcept {
212 return direction >= StepPlayerDirection::Forward && direction <= StepPlayerDirection::Random;
213}
214
215} // namespace step_player_detail
216
227 std::int64_t ordinal = 0;
228 std::uint64_t cycle = 0;
229 std::size_t index = 0;
230 constexpr auto operator<=>(const StepPlayerCoordinate&) const = default;
231};
232
250template <std::size_t MaxLanes = 8, std::size_t MaxSteps = 32, std::size_t MaxRatchetHits = 8,
251 std::size_t MaximumStepsPerBlock = 128>
253 public:
254 static_assert(MaxLanes > 0);
255 static_assert(MaxSteps > 0);
256 static_assert(MaxRatchetHits > 0);
257 static_assert(MaximumStepsPerBlock > 0);
258 static_assert(MaxLanes <= std::numeric_limits<std::size_t>::max() / MaxSteps);
259 static_assert(MaxLanes <= std::numeric_limits<std::size_t>::max() / (MaxRatchetHits + 1));
260 static_assert(MaxLanes <= 32, "lane parking uses a 32-bit mask");
261
262 static constexpr std::size_t kMaximumCells = MaxLanes * MaxSteps;
265 static constexpr std::size_t kMaximumSoundingNotes = MaxLanes * (MaxRatchetHits + 1);
273 static constexpr std::int32_t kMaximumTimingOffsetTicks =
274 static_cast<std::int32_t>(timebase::kTicksPerQuarter);
275
276 struct Spec {
277 std::size_t lane_count = 0;
278 std::array<StepPlayerLaneSpec, MaxLanes> lanes{};
279 std::uint64_t random_seed = 0;
280 constexpr auto operator<=>(const Spec&) const = default;
281 };
282
283 static constexpr StepPlayerCapacityContract contract() noexcept {
284 constexpr auto burst_events = 2 * MaxRatchetHits;
285 constexpr auto generated =
286 MaximumStepsPerBlock > (std::numeric_limits<std::size_t>::max() -
288 burst_events
289 ? std::numeric_limits<std::size_t>::max()
290 : MaximumStepsPerBlock * burst_events + 2 * kMaximumSoundingNotes;
291 return {MaxLanes,
292 MaxSteps,
293 MaximumStepsPerBlock,
294 generated,
298 true,
299 true};
300 }
301
302 static constexpr bool valid_lane_spec(const StepPlayerLaneSpec& lane) noexcept {
303 constexpr auto kMaximumStepDuration = timebase::kTicksPerQuarter * 64;
304 return lane.channel <= 15 && lane.base_note <= 127 && lane.step_count >= 1 &&
305 lane.step_count <= MaxSteps && lane.step_duration.value > 0 &&
306 lane.step_duration.value <= kMaximumStepDuration &&
308 }
309
310 static constexpr bool valid_spec(const Spec& spec) noexcept {
311 if (spec.lane_count < 1 || spec.lane_count > MaxLanes)
312 return false;
313 for (std::size_t lane = 0; lane < spec.lane_count; ++lane)
314 if (!valid_lane_spec(spec.lanes[lane]))
315 return false;
316 return true;
317 }
318
319 explicit constexpr StepPlayer(Spec spec = {}) noexcept
320 : spec_(spec), valid_(valid_spec(spec)) {}
321
322 constexpr bool valid() const noexcept {
323 return valid_;
324 }
325 constexpr const Spec& spec() const noexcept {
326 return spec_;
327 }
328
329 static constexpr StepPlayerError validate_step(const StepPlayerStep& step) noexcept {
330 if (step.velocity < 1 || step.velocity > 127)
332 if (step.gate_percent < 1 || step.gate_percent > 100)
334 if (step.probability_percent > 100)
336 if (step.ratchet_count < 1 || step.ratchet_count > MaxRatchetHits)
338 if (step.pitch_offset < -48 || step.pitch_offset > 48)
340 if (step.timing_offset_ticks < -kMaximumTimingOffsetTicks ||
341 step.timing_offset_ticks > kMaximumTimingOffsetTicks)
344 }
345
346 constexpr StepPlayerError set_step(std::size_t lane, std::size_t index,
347 StepPlayerStep step) noexcept {
348 if (!valid_)
350 if (lane >= spec_.lane_count)
352 if (index >= spec_.lanes[lane].step_count)
354 if (const auto error = validate_step(step); error != StepPlayerError::None)
355 return error;
356 const auto interval = spec_.lanes[lane].step_duration.value;
357 if (step.timing_offset_ticks < 0 &&
358 static_cast<std::int64_t>(-step.timing_offset_ticks) >= interval)
360 steps_[lane * MaxSteps + index] = step;
362 }
363
364 constexpr const StepPlayerStep* get_step(std::size_t lane, std::size_t index) const noexcept {
365 if (!valid_ || lane >= spec_.lane_count || index >= spec_.lanes[lane].step_count)
366 return nullptr;
367 return &steps_[lane * MaxSteps + index];
368 }
369
372 constexpr void clear_steps() noexcept {
373 for (auto& step : steps_)
374 step = {};
375 }
376
377 std::size_t sounding_note_count() const noexcept {
378 std::size_t count = 0;
379 for (const auto& note : sounding_)
380 if (note.active)
381 ++count;
382 return count;
383 }
384
386 const StepPlayerBlock& block) noexcept {
387 if (utility_detail::blocks_alias(input, output))
388 return {0, input.size(), 0, false};
391 if (!ordered(input)) {
392 // Generation interleaves by sample offset, so unordered input
393 // disables it; the input itself still passes through.
394 passthrough(input, output, report);
395 output.sort();
396 ++report.dropped;
397 report.complete = false;
398 finish_block_without_generation();
399 return report;
400 }
401 if (!valid_) {
402 // An unconfigured generator is a bypass, not a broken unit: input
403 // passes through and nothing is generated.
404 passthrough(input, output, report);
405 output.sort();
406 return report;
407 }
408 if (!valid_block(block) || !utility_detail::ready(output)) {
409 passthrough(input, output, report);
410 output.sort();
411 report.complete = false;
412 return report;
413 }
414
415 current_block_ = &block;
416 update_anchor(block);
417 steps_this_block_ = 0;
418 const bool discontinuity = detects_discontinuity(block);
419 if (discontinuity) {
420 for (auto& clock : clocks_)
421 clock = {};
422 if (!release_all(output, report, 0)) {
423 passthrough(input, output, report);
424 output.sort();
425 finish_block(block);
426 return report;
427 }
428 }
429
430 const bool should_play =
431 block.playing && block.transport_event != StepPlayerTransportEvent::Stopped;
432 if (!should_play) {
433 if (!release_all(output, report, 0)) {
434 passthrough(input, output, report);
435 output.sort();
436 finish_block(block);
437 return report;
438 }
439 for (auto& clock : clocks_)
440 clock = {};
441 } else {
442 align_clocks(block.tick_start);
443 }
444
445 for (const auto& event : input) {
446 const auto offset = event.sample_offset;
447 if (offset < 0 || offset >= block.sample_count) {
448 ++report.dropped;
449 report.complete = false;
450 continue;
451 }
452 if (!render_until(offset, false, should_play, output, report)) {
453 finish_block(block);
454 return report;
455 }
456 if (!utility_detail::emit(output, event, report)) {
457 finish_block(block);
458 return report;
459 }
460 }
461
462 if (!render_until(block.sample_count, false, should_play, output, report)) {
463 finish_block(block);
464 return report;
465 }
466 utility_detail::copy_sidecars(input, output, report);
467 output.sort();
468 finish_block(block);
469 return report;
470 }
471
477 if (!utility_detail::ready(output))
478 return {0, 0, sounding_note_count(), false};
479 release_all(output, report, 0);
480 for (auto& clock : clocks_)
481 clock = {};
482 output.sort();
483 return report;
484 }
485
488 auto report = flush_transport(output);
489 for (auto& note : sounding_)
490 note = {};
491 last_block_valid_ = false;
492 return report;
493 }
494
501 std::optional<StepPlayerCoordinate> coordinate_at(std::size_t lane,
502 timebase::TickPosition tick) const noexcept {
503 if (!valid_ || lane >= spec_.lane_count || spec_.lanes[lane].step_count == 0 ||
504 spec_.lanes[lane].step_duration.value <= 0)
505 return std::nullopt;
506 return coordinate_for(spec_.lanes[lane], lane, tick);
507 }
508
511 std::optional<StepPlayerCoordinate> upcoming_coordinate(std::size_t lane) const noexcept {
512 if (!valid_ || lane >= spec_.lane_count || spec_.lanes[lane].step_count == 0 ||
513 spec_.lanes[lane].step_duration.value <= 0 || !clocks_[lane].initialized)
514 return std::nullopt;
515 return coordinate_for(spec_.lanes[lane], lane, clocks_[lane].next_tick);
516 }
517
518 private:
519 struct LaneClock {
520 timebase::TickPosition next_tick{};
521 // Hits of the in-flight step already emitted. A ratchet burst can span
522 // a block or render boundary; the remaining hits are re-projected from
523 // the step's tick, never carried as sample state.
524 std::size_t hits_done = 0;
525 bool initialized = false;
526 };
527
528 struct SoundingNote {
529 std::uint8_t channel = 0;
530 std::uint8_t note = 0;
531 std::uint8_t lane = 0;
532 std::int64_t release_sample = 0;
533 bool active = false;
534 };
535
536 static bool valid_block(const StepPlayerBlock& block) noexcept {
537 if (block.sample_count < 0)
538 return false;
539 if (block.tempo_map != nullptr)
540 return block.tempo_map->sample_rate().valid();
541 return block.sample_rate.valid() && timebase::valid_tempo(block.tempo_bpm);
542 }
543
544 static bool ordered(const MidiBuffer& input) noexcept {
545 std::int32_t previous = std::numeric_limits<std::int32_t>::min();
546 for (const auto& event : input) {
547 if (event.sample_offset < previous)
548 return false;
549 previous = event.sample_offset;
550 }
551 return true;
552 }
553
554 void passthrough(const MidiBuffer& input, MidiBuffer& output,
555 MidiUtilityProcessReport& report) noexcept {
556 for (const auto& event : input)
557 if (!utility_detail::emit(output, event, report))
558 return;
559 }
560
561 void finish_block_without_generation() noexcept {
562 last_block_valid_ = false;
563 }
564
565 bool detects_discontinuity(const StepPlayerBlock& block) const noexcept {
566 if (block.transport_event != StepPlayerTransportEvent::Continuous)
567 return true;
568 if (!last_block_valid_)
569 return false;
570 if (block.sample_start.value != last_sample_end_ || block.playing != last_playing_)
571 return true;
572 return step_player_detail::distance_exceeds(block.tick_start.value, projected_tick_end_,
573 2);
574 }
575
576 void finish_block(const StepPlayerBlock& block) noexcept {
577 last_sample_end_ =
578 utility_detail::saturating_sample_add(block.sample_start.value, block.sample_count);
579 if (block.tempo_map != nullptr) {
580 const auto anchor = block.tempo_map->fractional_ticks_to_samples(
581 static_cast<long double>(block.tick_start.value));
582 projected_tick_end_ = step_player_detail::saturating_round(
583 block.tempo_map->fractional_samples_to_ticks(anchor +
584 static_cast<long double>(
585 block.sample_count)));
586 } else {
587 const auto ticks = static_cast<long double>(block.sample_count) *
588 static_cast<long double>(block.tempo_bpm) *
589 static_cast<long double>(timebase::kTicksPerQuarter) /
590 (60.0L * block.sample_rate.as_long_double());
591 projected_tick_end_ = utility_detail::saturating_sample_add(
592 block.tick_start.value, step_player_detail::saturating_round(ticks));
593 }
594 last_playing_ = block.playing;
595 last_block_valid_ = true;
596 current_block_ = nullptr;
597 }
598
599 void align_clocks(timebase::TickPosition tick) noexcept {
600 for (std::size_t lane = 0; lane < spec_.lane_count; ++lane) {
601 auto& clock = clocks_[lane];
602 if (clock.initialized || !spec_.lanes[lane].enabled)
603 continue;
604 const auto duration = spec_.lanes[lane].step_duration.value;
605 auto ordinal = step_player_detail::floor_div(tick.value, duration);
606 auto candidate = step_player_detail::saturating_multiply(ordinal, duration);
607 if (candidate < tick.value && ordinal != std::numeric_limits<std::int64_t>::max())
608 candidate = step_player_detail::saturating_multiply(ordinal + 1, duration);
609 clock.next_tick = {candidate};
610 clock.initialized = true;
611 }
612 }
613
614 void advance_clock(std::size_t lane) noexcept {
615 auto& clock = clocks_[lane];
616 clock.next_tick = {utility_detail::saturating_sample_add(
617 clock.next_tick.value, spec_.lanes[lane].step_duration.value)};
618 clock.hits_done = 0;
619 }
620
621 std::int64_t sample_for_tick(timebase::TickPosition tick) const noexcept {
622 return saturating_sample_of(static_cast<long double>(anchor_sample_) +
623 (tick_axis(*current_block_, tick.value) - anchor_axis_));
624 }
625
629 long double tick_axis(const StepPlayerBlock& block, std::int64_t tick) const noexcept {
630 if (block.tempo_map != nullptr)
631 return block.tempo_map->fractional_ticks_to_samples(static_cast<long double>(tick));
633 block.sample_rate.as_long_double() * 60.0L /
634 (static_cast<long double>(block.tempo_bpm) *
635 static_cast<long double>(timebase::kTicksPerQuarter));
636 }
637
651 void update_anchor(const StepPlayerBlock& block) noexcept {
652 const auto axis = tick_axis(block, block.tick_start.value);
653 const auto offset = static_cast<long double>(block.sample_start.value) - axis;
654 if (anchored_ &&
655 std::fabs(offset - (static_cast<long double>(anchor_sample_) - anchor_axis_)) <= 0.5L)
656 return;
657 anchor_sample_ = block.sample_start.value;
658 anchor_axis_ = axis;
659 anchored_ = true;
660 }
661
662 static std::int64_t saturating_sample_of(long double absolute) noexcept {
663 constexpr auto minimum = static_cast<long double>(std::numeric_limits<std::int64_t>::min());
664 constexpr auto maximum = static_cast<long double>(std::numeric_limits<std::int64_t>::max());
665 if (absolute <= minimum)
666 return std::numeric_limits<std::int64_t>::min();
667 if (absolute >= maximum)
668 return std::numeric_limits<std::int64_t>::max();
669 return static_cast<std::int64_t>(std::llround(absolute));
670 }
671
672 timebase::TickPosition tick_at_block_offset(std::int32_t offset) const noexcept {
673 const auto& block = *current_block_;
674 if (block.tempo_map != nullptr) {
675 const auto anchor = block.tempo_map->fractional_ticks_to_samples(
676 static_cast<long double>(block.tick_start.value));
677 const auto tick = block.tempo_map->fractional_samples_to_ticks(
678 anchor + static_cast<long double>(offset));
680 }
681 const auto ticks = static_cast<long double>(offset) *
682 static_cast<long double>(block.tempo_bpm) *
683 static_cast<long double>(timebase::kTicksPerQuarter) /
684 (60.0L * block.sample_rate.as_long_double());
686 block.tick_start.value, step_player_detail::saturating_round(ticks))};
687 }
688
689 std::int32_t block_offset(std::int64_t sample) const noexcept {
690 if (current_block_ == nullptr)
691 return 0;
692 const auto start = current_block_->sample_start.value;
693 if (sample <= start)
694 return 0;
695 const auto maximum_offset = std::max<std::int32_t>(0, current_block_->sample_count - 1);
696 const auto difference =
697 static_cast<std::uint64_t>(sample) - static_cast<std::uint64_t>(start);
698 if (difference >= static_cast<std::uint64_t>(maximum_offset))
699 return maximum_offset;
700 return static_cast<std::int32_t>(difference);
701 }
702
703 bool render_until(std::int32_t offset, bool include_step_at_offset, bool playing,
704 MidiBuffer& output, MidiUtilityProcessReport& report) noexcept {
705 const auto absolute_limit =
706 utility_detail::saturating_sample_add(current_block_->sample_start.value, offset);
707 const bool limit_is_inside_block = offset < current_block_->sample_count;
708 if (!playing)
709 return release_due(absolute_limit, limit_is_inside_block, output, report);
710
711 // A lane whose remaining burst hits land past the render limit is
712 // parked for this call only; other lanes with due steps still fire.
713 std::uint32_t parked = 0;
714 for (;;) {
715 // Steps fire in sample order across all lanes; ties resolve to the
716 // lowest lane so the stream is deterministic.
717 std::size_t best_lane = spec_.lane_count;
718 std::int64_t best_sample = std::numeric_limits<std::int64_t>::max();
719 for (std::size_t lane = 0; lane < spec_.lane_count; ++lane) {
720 if (!clocks_[lane].initialized || !spec_.lanes[lane].enabled ||
721 (parked & (std::uint32_t{1} << lane)) != 0)
722 continue;
723 const auto sample = next_onset_sample(lane);
724 if (best_lane == spec_.lane_count || sample < best_sample) {
725 best_lane = lane;
726 best_sample = sample;
727 }
728 }
729 if (best_lane == spec_.lane_count)
730 break;
731 const bool due =
732 include_step_at_offset ? best_sample <= absolute_limit : best_sample < absolute_limit;
733 if (!due)
734 break;
735 // A step boundary behind the block start is stale, unless the step
736 // is mid-burst: then its onset legitimately predates this block
737 // and only its remaining hits matter.
738 if (best_sample < current_block_->sample_start.value &&
739 clocks_[best_lane].hits_done == 0) {
740 advance_clock(best_lane);
741 report.complete = false;
742 ++report.dropped;
743 continue;
744 }
745 if (steps_this_block_ == MaximumStepsPerBlock) {
746 report.complete = false;
747 ++report.dropped;
748 realign_clocks_past(offset);
749 break;
750 }
751 const auto hits_before = clocks_[best_lane].hits_done;
752 if (!fire_step(best_lane, best_sample, absolute_limit, output, report))
753 return false;
754 if (clocks_[best_lane].hits_done == 0) {
755 advance_clock(best_lane);
756 ++steps_this_block_;
757 } else if (clocks_[best_lane].hits_done == hits_before) {
758 // The burst's remaining hits land past the render limit; the
759 // step stays armed and resumes at the next render call.
760 parked |= std::uint32_t{1} << best_lane;
761 }
762 }
763 return release_due(absolute_limit, limit_is_inside_block, output, report);
764 }
765
766 void realign_clocks_past(std::int32_t offset) noexcept {
767 const auto tick = tick_at_block_offset(offset);
768 for (std::size_t lane = 0; lane < spec_.lane_count; ++lane) {
769 auto& clock = clocks_[lane];
770 if (!clock.initialized)
771 continue;
772 const auto duration = spec_.lanes[lane].step_duration.value;
773 auto ordinal = step_player_detail::floor_div(tick.value, duration);
774 auto candidate = step_player_detail::saturating_multiply(ordinal, duration);
775 if (candidate <= tick.value && ordinal != std::numeric_limits<std::int64_t>::max())
776 candidate = step_player_detail::saturating_multiply(ordinal + 1, duration);
777 clock.next_tick = {candidate};
778 clock.hits_done = 0;
779 }
780 }
781
782 std::size_t walk_index(const StepPlayerLaneSpec& lane, std::int64_t ordinal,
783 timebase::TickPosition tick, std::size_t lane_index,
784 std::uint64_t cycle) const noexcept {
785 const auto length = static_cast<std::size_t>(lane.step_count);
786 switch (lane.direction) {
788 return length - 1 - step_player_detail::positive_mod(ordinal, length);
790 if (length == 1)
791 return 0;
792 const auto period = length * 2 - 2;
793 const auto position = step_player_detail::positive_mod(ordinal, period);
794 return position < length ? position : period - position;
795 }
797 // The walk is a pure function of the grid coordinate, so any block
798 // partition of the same transport span selects the same step.
799 return static_cast<std::size_t>(
801 spec_.random_seed,
802 {tick, static_cast<std::uint64_t>(lane_index), cycle, 1}) %
803 length);
805 return step_player_detail::positive_mod(ordinal, length);
806 }
807 return 0;
808 }
809
814 StepPlayerCoordinate coordinate_for(const StepPlayerLaneSpec& lane, std::size_t lane_index,
815 timebase::TickPosition tick) const noexcept {
816 StepPlayerCoordinate coordinate{};
817 coordinate.step_tick = tick;
818 coordinate.following_tick = timebase::TickPosition{
819 utility_detail::saturating_sample_add(tick.value, lane.step_duration.value)};
820 coordinate.ordinal = step_player_detail::floor_div(tick.value, lane.step_duration.value);
821 coordinate.cycle = static_cast<std::uint64_t>(step_player_detail::floor_div(
822 coordinate.ordinal, static_cast<std::int64_t>(lane.step_count)));
823 coordinate.index = walk_index(lane, coordinate.ordinal, tick, lane_index, coordinate.cycle);
824 return coordinate;
825 }
826
835 std::int64_t bounded_timing_offset(const StepPlayerStep& step,
836 std::int64_t interval_ticks) const noexcept {
837 if (!step.on || step.timing_offset_ticks == 0)
838 return 0;
839 const auto limit = std::max<std::int64_t>(0, interval_ticks - 1);
840 return std::clamp(static_cast<std::int64_t>(step.timing_offset_ticks), -limit, limit);
841 }
842
849 std::int64_t next_onset_sample(std::size_t lane_index) const noexcept {
850 const auto& lane = spec_.lanes[lane_index];
851 const auto tick = clocks_[lane_index].next_tick;
852 if (lane.step_count == 0 || lane.step_duration.value <= 0)
853 return sample_for_tick(tick);
854 const auto coordinate = coordinate_for(lane, lane_index, tick);
855 const auto offset = bounded_timing_offset(
856 steps_[lane_index * MaxSteps + coordinate.index], lane.step_duration.value);
857 return sample_for_tick({utility_detail::saturating_sample_add(tick.value, offset)});
858 }
859
860 bool fire_step(std::size_t lane_index, std::int64_t step_sample, std::int64_t absolute_limit,
861 MidiBuffer& output, MidiUtilityProcessReport& report) noexcept {
862 const auto& lane = spec_.lanes[lane_index];
863 auto& clock = clocks_[lane_index];
864 const auto coordinate = coordinate_for(lane, lane_index, clock.next_tick);
865 const auto step_tick = coordinate.step_tick;
866 const auto following_tick = coordinate.following_tick;
867 const auto cycle = coordinate.cycle;
868 const auto index = coordinate.index;
869 const auto& step = steps_[lane_index * MaxSteps + index];
870 if (!step.on)
871 return true;
872
873 if (step.probability_percent < 100) {
874 if (step.probability_percent == 0)
875 return true;
876 const auto chance = timebase::coordinate_chance(
877 spec_.random_seed,
878 {step_tick, static_cast<std::uint64_t>(lane_index), cycle, 0},
879 step.probability_percent, 100);
880 if (!chance.has_value() || !chance.value())
881 return true;
882 }
883
884 const auto pitch = static_cast<std::uint8_t>(std::clamp(
885 static_cast<int>(lane.base_note) + static_cast<int>(step.pitch_offset), 0, 127));
886 const auto interval_ticks = following_tick.value - step_tick.value;
887 // The step's grid coordinate stays where it was authored — the walk
888 // index, the cycle and the probability draw above all read it — and
889 // only the moment it speaks moves. A displacement therefore cannot
890 // accumulate into drift.
891 const auto onset_tick = timebase::TickPosition{utility_detail::saturating_sample_add(
892 step_tick.value, bounded_timing_offset(step, interval_ticks))};
893 // The gate is measured from the interval the step was authored on, not
894 // from what a displacement leaves of it, so a nudged step keeps the
895 // length it was written with.
896 const auto gate_ticks = std::max<std::int64_t>(
897 1, step_player_detail::saturating_multiply(interval_ticks, step.gate_percent) / 100);
898 const auto step_release_tick =
899 timebase::TickPosition{utility_detail::saturating_sample_add(onset_tick.value,
900 gate_ticks)};
901
902 // Tie, slide, and choke are step-onset policies; they apply exactly
903 // once, when the burst's first hit fires.
904 if (clock.hits_done == 0) {
905 // Tie extends a still-sounding note of the same pitch; it must win
906 // over the due-release sweep below or the note would be released at
907 // the boundary before we could extend it.
908 if (step.tie) {
909 if (auto* match = find_sounding(lane_index, lane.channel, pitch)) {
910 match->release_sample =
911 std::max(match->release_sample, sample_for_tick(step_release_tick));
912 return release_due(step_sample, true, output, report);
913 }
914 }
915
916 // Slide keeps a sounding note of a different pitch ringing one tick
917 // past the new attack so a downstream voice can glide between them.
918 if (step.slide) {
919 const auto overlap_sample =
920 std::max(step_sample + 1,
922 onset_tick.value, 1)}));
923 for (auto& sounding : sounding_)
924 if (sounding.active && sounding.lane == lane_index && sounding.note != pitch)
925 sounding.release_sample =
926 std::max(sounding.release_sample, overlap_sample);
927 }
928
929 if (lane.choke_group != 0 &&
930 !emit_choke(lane_index, lane.choke_group, step_sample, output, report))
931 return false;
932 }
933
934 std::array<timebase::TickPosition, MaxRatchetHits> hits{};
935 std::size_t hit_count = 1;
936 hits[0] = onset_tick;
937 if (step.ratchet_count > 1) {
938 // A burst subdivides what the displacement leaves of the interval,
939 // so every hit stays inside the step that owns it however far the
940 // onset was nudged.
942 onset_tick, following_tick, step.ratchet_count, onset_tick, following_tick,
943 std::span<timebase::TickPosition>(hits.data(), hits.size()));
944 if (projection && projection.event_count > 0)
945 hit_count = projection.event_count;
946 // A step interval too short to subdivide degrades to a single hit
947 // rather than an error: the step still speaks.
948 }
949 const auto hit_gate_ticks =
950 std::max<std::int64_t>(1, gate_ticks / static_cast<std::int64_t>(hit_count));
951
952 for (std::size_t hit = clock.hits_done; hit < hit_count; ++hit) {
953 const auto hit_tick = hits[hit];
954 auto release_tick = timebase::TickPosition{utility_detail::saturating_sample_add(
955 hit_tick.value, hit_gate_ticks)};
956 if (hit + 1 < hit_count) {
957 const auto next_tick = hits[hit + 1].value;
958 if (next_tick <= release_tick.value)
959 release_tick = {std::max(next_tick - 1, hit_tick.value)};
960 }
961 const auto hit_sample = sample_for_tick(hit_tick);
962 // A hit past the render limit belongs to a later render call or
963 // block; it is re-projected from its tick, never clamped into this
964 // block.
965 if (hit_sample >= absolute_limit)
966 break;
967 if (!release_due(hit_sample, true, output, report))
968 return false;
969 auto* slot = free_sounding_slot();
970 if (slot == nullptr) {
971 // Drop the newest attack, never its release: audible thinning
972 // under an exhausted ledger beats a stuck note.
973 ++report.dropped;
974 report.complete = false;
975 continue;
976 }
977 auto attack = MidiEvent::note_on(lane.channel, pitch, step.velocity);
978 attack.sample_offset = block_offset(hit_sample);
979 if (!utility_detail::emit(output, attack, report))
980 return false;
981 *slot = {lane.channel,
982 pitch,
983 static_cast<std::uint8_t>(lane_index),
984 sample_for_tick(release_tick),
985 true};
986 clock.hits_done = hit + 1;
987 }
988 if (clock.hits_done == hit_count)
989 clock.hits_done = 0;
990 return true;
991 }
992
993 SoundingNote* find_sounding(std::size_t lane, std::uint8_t channel, std::uint8_t note) noexcept {
994 for (auto& sounding : sounding_)
995 if (sounding.active && sounding.lane == lane && sounding.channel == channel &&
996 sounding.note == note)
997 return &sounding;
998 return nullptr;
999 }
1000
1001 SoundingNote* free_sounding_slot() noexcept {
1002 for (auto& note : sounding_)
1003 if (!note.active)
1004 return &note;
1005 return nullptr;
1006 }
1007
1008 bool emit_choke(std::size_t firing_lane, std::uint8_t group, std::int64_t sample,
1009 MidiBuffer& output, MidiUtilityProcessReport& report) noexcept {
1010 for (auto& sounding : sounding_) {
1011 if (!sounding.active || sounding.lane == firing_lane)
1012 continue;
1013 if (spec_.lanes[sounding.lane].choke_group != group)
1014 continue;
1015 if (!emit_release(sounding, sample, output, report))
1016 return false;
1017 }
1018 return true;
1019 }
1020
1021 bool release_due(std::int64_t sample, bool include_at_sample, MidiBuffer& output,
1022 MidiUtilityProcessReport& report) noexcept {
1023 for (auto& sounding : sounding_) {
1024 const bool due = sounding.release_sample < sample ||
1025 (include_at_sample && sounding.release_sample == sample);
1026 if (sounding.active && due &&
1027 !emit_release(sounding, sounding.release_sample, output, report))
1028 return false;
1029 }
1030 return true;
1031 }
1032
1033 bool release_all(MidiBuffer& output, MidiUtilityProcessReport& report,
1034 std::int32_t offset) noexcept {
1035 const auto sample =
1036 current_block_ != nullptr
1037 ? utility_detail::saturating_sample_add(current_block_->sample_start.value, offset)
1038 : 0;
1039 for (auto& sounding : sounding_)
1040 if (sounding.active && !emit_release(sounding, sample, output, report))
1041 return false;
1042 return true;
1043 }
1044
1045 bool emit_release(SoundingNote& sounding, std::int64_t sample, MidiBuffer& output,
1046 MidiUtilityProcessReport& report) noexcept {
1047 auto release = MidiEvent::note_off(sounding.channel, sounding.note);
1048 release.sample_offset = block_offset(sample);
1049 if (!utility_detail::emit(output, release, report)) {
1050 sounding.release_sample = sample;
1051 ++report.deferred;
1052 return false;
1053 }
1054 sounding = {};
1055 return true;
1056 }
1057
1058 Spec spec_{};
1059 bool valid_ = true;
1060 std::array<StepPlayerStep, kMaximumCells> steps_{};
1061 std::array<LaneClock, MaxLanes> clocks_{};
1062 std::array<SoundingNote, kMaximumSoundingNotes> sounding_{};
1063 const StepPlayerBlock* current_block_ = nullptr;
1064 bool anchored_ = false;
1065 std::int64_t anchor_sample_ = 0;
1066 long double anchor_axis_ = 0.0L;
1067 std::int64_t last_sample_end_ = 0;
1068 std::int64_t projected_tick_end_ = 0;
1069 bool last_playing_ = false;
1070 bool last_block_valid_ = false;
1071 std::size_t steps_this_block_ = 0;
1072};
1073
1074} // namespace pulp::midi
Collection of timestamped MIDI events within a single audio buffer period.
Definition buffer.hpp:31
std::optional< StepPlayerCoordinate > upcoming_coordinate(std::size_t lane) const noexcept
The coordinate a lane will fire next, or std::nullopt before the lane's clock has been placed by a tr...
Definition step_player.hpp:511
std::optional< StepPlayerCoordinate > coordinate_at(std::size_t lane, timebase::TickPosition tick) const noexcept
The coordinate a lane's grid selects at tick.
Definition step_player.hpp:501
static constexpr bool valid_spec(const Spec &spec) noexcept
Definition step_player.hpp:310
static constexpr std::int32_t kMaximumTimingOffsetTicks
A micro-timing nudge is bounded to one quarter note: far wider than any swing or groove needs,...
Definition step_player.hpp:273
constexpr bool valid() const noexcept
Definition step_player.hpp:322
static constexpr StepPlayerError validate_step(const StepPlayerStep &step) noexcept
Definition step_player.hpp:329
constexpr const StepPlayerStep * get_step(std::size_t lane, std::size_t index) const noexcept
Definition step_player.hpp:364
static constexpr std::size_t kMaximumCells
Definition step_player.hpp:262
MidiUtilityProcessReport flush_transport(MidiBuffer &output) noexcept
Release every owned note and resync lane clocks.
Definition step_player.hpp:474
MidiUtilityProcessReport reset(MidiBuffer &output) noexcept
Release every owned note and discard all playback state.
Definition step_player.hpp:487
static constexpr std::size_t kMaximumSoundingNotes
A lane can hold one ratchet burst plus one note kept overlapping by a slide, so the ledger never need...
Definition step_player.hpp:265
constexpr void clear_steps() noexcept
Authored-step edits are control-thread work; playback state is untouched so an in-flight note still r...
Definition step_player.hpp:372
std::size_t sounding_note_count() const noexcept
Definition step_player.hpp:377
static constexpr bool valid_lane_spec(const StepPlayerLaneSpec &lane) noexcept
Definition step_player.hpp:302
constexpr StepPlayer(Spec spec={}) noexcept
Definition step_player.hpp:319
static constexpr StepPlayerCapacityContract contract() noexcept
Definition step_player.hpp:283
MidiUtilityProcessReport process(const MidiBuffer &input, MidiBuffer &output, const StepPlayerBlock &block) noexcept
Definition step_player.hpp:385
constexpr const Spec & spec() const noexcept
Definition step_player.hpp:325
constexpr StepPlayerError set_step(std::size_t lane, std::size_t index, StepPlayerStep step) noexcept
Definition step_player.hpp:346
Definition compiled_tempo_map.hpp:91
@ release
Release only: voices enter their normal release and tails ring out (MIDI CC 123 semantic).
Definition panic.hpp:39
Definition step_player.hpp:136
constexpr std::int64_t floor_div(std::int64_t numerator, std::int64_t denominator) noexcept
Definition step_player.hpp:138
constexpr bool distance_exceeds(std::int64_t lhs, std::int64_t rhs, std::uint64_t tolerance) noexcept
Definition step_player.hpp:174
constexpr std::int64_t saturating_multiply(std::int64_t lhs, std::int64_t rhs) noexcept
Definition step_player.hpp:144
constexpr std::size_t positive_mod(std::int64_t value, std::size_t modulus) noexcept
Definition step_player.hpp:167
constexpr long double difference_as_long_double(std::int64_t lhs, std::int64_t rhs) noexcept
Definition step_player.hpp:191
constexpr bool valid_direction(StepPlayerDirection direction) noexcept
Definition step_player.hpp:211
std::int64_t saturating_round(long double value) noexcept
Definition step_player.hpp:200
void copy_sidecars(const MidiBuffer &input, MidiBuffer &output, MidiUtilityProcessReport &report) noexcept
Definition utility_contract.hpp:96
void clear_output(MidiBuffer &output) noexcept
Definition utility_contract.hpp:82
bool blocks_alias(const MidiBuffer &first, const MidiBuffer &second) noexcept
Definition utility_contract.hpp:92
constexpr std::int64_t saturating_sample_add(std::int64_t position, std::int64_t offset) noexcept
Definition utility_contract.hpp:59
bool ready(const MidiBuffer &output) noexcept
Definition utility_contract.hpp:86
bool emit(MidiBuffer &output, const MidiEvent &event, MidiUtilityProcessReport &report)
Definition utility_contract.hpp:67
Definition custom_node_events.hpp:26
StepPlayerDirection
Definition step_player.hpp:21
@ Random
Definition step_player.hpp:25
@ Forward
Definition step_player.hpp:22
@ Reverse
Definition step_player.hpp:23
@ PingPong
Definition step_player.hpp:24
@ None
Definition ble_midi.hpp:86
MidiUtilityTransportRequirement
Definition utility_contract.hpp:30
@ FlushOnDiscontinuity
Definition utility_contract.hpp:33
MidiUtilityOverflowPolicy
Definition utility_contract.hpp:19
@ DropUnstarted
Definition utility_contract.hpp:20
@ LoopWrapped
Definition arpeggiator.hpp:44
@ Continuous
Definition arpeggiator.hpp:40
@ Started
Definition arpeggiator.hpp:41
@ Stopped
Definition arpeggiator.hpp:42
@ Seeked
Definition arpeggiator.hpp:43
StepPlayerTransportEvent
Definition step_player.hpp:28
@ Continuous
Definition step_player.hpp:29
@ Stopped
Definition step_player.hpp:31
MidiUtilitySameSampleOrder
Definition utility_contract.hpp:25
@ ReleaseBeforeAttack
Definition utility_contract.hpp:27
StepPlayerError
Definition step_player.hpp:36
@ InvalidStepIndex
Definition step_player.hpp:40
@ NotConfigured
Definition step_player.hpp:38
@ InvalidRatchetCount
Definition step_player.hpp:44
@ InvalidPitchOffset
Definition step_player.hpp:45
@ None
Definition step_player.hpp:37
@ InvalidTimingOffset
Definition step_player.hpp:46
@ InvalidLane
Definition step_player.hpp:39
@ InvalidGate
Definition step_player.hpp:42
@ InvalidProbability
Definition step_player.hpp:43
@ InvalidVelocity
Definition step_player.hpp:41
@ Random
Definition arpeggiator.hpp:25
@ anchor
Mandatory structural event that density operations always retain.
Definition pattern_development.hpp:20
@ difference
Include events found only in the left input.
Definition pattern_development.hpp:275
@ index
Definition gpu_pass.hpp:140
@ attack
Definition envelope.hpp:58
float minimum(const float *data, std::size_t count) noexcept
float maximum(const float *data, std::size_t count) noexcept
@ match
Measured delay agrees with the report, within tolerance.
Definition latency_evidence.hpp:93
bool valid_tempo(double tempo_bpm) noexcept
Definition quantize.hpp:17
constexpr std::int64_t kTicksPerQuarter
Definition tick.hpp:54
runtime::Result< bool, ProbabilityError > coordinate_chance(std::uint64_t seed, RandomCoordinate coordinate, std::uint64_t numerator, std::uint64_t denominator) noexcept
Definition coordinate_random.hpp:63
constexpr RatchetProjectionResult project_ratchet_interval(TickPosition interval_begin, TickPosition interval_end, std::size_t hit_count, TickPosition window_begin, TickPosition window_end, std::span< TickPosition > output) noexcept
Projects a bounded ratchet burst into an exact clock interval.
Definition ratchet.hpp:87
constexpr std::uint64_t coordinate_random(std::uint64_t seed, RandomCoordinate coordinate) noexcept
Definition coordinate_random.hpp:47
@ block
Filled glyph cell; the covered glyph is drawn in the bg color.
Definition caret.hpp:66
@ candidate
Definition waveform_editor_primitives.hpp:193
@ start
Definition geometry.hpp:370
static MidiEvent note_on(uint8_t channel, uint8_t note, uint8_t velocity)
Create a Note On event.
Definition message.hpp:46
static MidiEvent note_off(uint8_t channel, uint8_t note, uint8_t velocity=0)
Create a Note Off event.
Definition message.hpp:54
Definition utility_contract.hpp:46
bool complete
Definition utility_contract.hpp:50
std::size_t dropped
Definition utility_contract.hpp:48
Block timing for one sample-accurate step-player call.
Definition step_player.hpp:105
double tempo_bpm
Definition step_player.hpp:110
std::int32_t sample_count
Definition step_player.hpp:108
const timebase::CompiledTempoMap * tempo_map
Definition step_player.hpp:111
bool playing
Definition step_player.hpp:112
StepPlayerTransportEvent transport_event
Definition step_player.hpp:113
timebase::RationalRate sample_rate
Definition step_player.hpp:109
timebase::SamplePosition sample_start
Definition step_player.hpp:106
timebase::TickPosition tick_start
Definition step_player.hpp:107
Definition step_player.hpp:116
MidiUtilityOverflowPolicy overflow
Definition step_player.hpp:121
constexpr std::size_t required_output_events(std::size_t input_events) const noexcept
Definition step_player.hpp:128
std::size_t maximum_steps_per_lane
Definition step_player.hpp:118
bool requires_reserved_capacity_limited_output
Definition step_player.hpp:125
std::size_t maximum_lanes
Definition step_player.hpp:117
bool requires_distinct_input_output
Definition step_player.hpp:126
MidiUtilityTransportRequirement transport
Definition step_player.hpp:123
MidiUtilitySameSampleOrder same_sample_order
Definition step_player.hpp:122
std::size_t maximum_steps_per_block
Definition step_player.hpp:119
std::size_t maximum_generated_events_per_block
Definition step_player.hpp:120
A lane's authored coordinate at one grid tick.
Definition step_player.hpp:224
constexpr auto operator<=>(const StepPlayerCoordinate &) const =default
std::int64_t ordinal
Definition step_player.hpp:227
std::size_t index
Definition step_player.hpp:229
timebase::TickPosition step_tick
Definition step_player.hpp:225
timebase::TickPosition following_tick
Definition step_player.hpp:226
std::uint64_t cycle
Definition step_player.hpp:228
Definition step_player.hpp:87
std::uint8_t choke_group
Definition step_player.hpp:93
bool enabled
Definition step_player.hpp:94
StepPlayerDirection direction
Definition step_player.hpp:92
timebase::TickDuration step_duration
Definition step_player.hpp:91
std::uint8_t channel
Definition step_player.hpp:88
std::uint8_t base_note
Definition step_player.hpp:89
constexpr auto operator<=>(const StepPlayerLaneSpec &) const =default
std::uint8_t step_count
Definition step_player.hpp:90
The authored data at one lane and step coordinate.
Definition step_player.hpp:56
constexpr auto operator<=>(const StepPlayerStep &) const =default
std::int8_t pitch_offset
Definition step_player.hpp:58
bool slide
Definition step_player.hpp:83
bool on
Definition step_player.hpp:57
std::uint8_t velocity
Definition step_player.hpp:59
bool tie
Definition step_player.hpp:82
std::uint8_t probability_percent
Definition step_player.hpp:61
std::uint8_t gate_percent
Definition step_player.hpp:60
std::uint8_t ratchet_count
Definition step_player.hpp:62
std::int32_t timing_offset_ticks
Micro-timing nudge in ticks, applied to this step's hits and to their releases together so the gate l...
Definition step_player.hpp:81
Definition step_player.hpp:276
constexpr auto operator<=>(const Spec &) const =default
std::uint64_t random_seed
Definition step_player.hpp:279
std::array< StepPlayerLaneSpec, MaxLanes > lanes
Definition step_player.hpp:278
std::size_t lane_count
Definition step_player.hpp:277
Definition rational_time.hpp:14
Definition rational_time.hpp:9
Definition tick.hpp:56
Definition tick.hpp:61