Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
arpeggiator.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <pulp/midi/detail/arpeggiator_math.hpp>
8
9#include <algorithm>
10#include <array>
11#include <cmath>
12#include <compare>
13#include <cstddef>
14#include <cstdint>
15#include <limits>
16#include <optional>
17
18namespace pulp::midi {
19
20enum class ArpeggiatorOrder : std::uint8_t {
27};
28
29enum class ArpeggiatorRepeatedNotePolicy : std::uint8_t {
32};
33
38
46
48 std::uint32_t numerator = 1;
49 std::uint32_t denominator = 2;
50 constexpr auto operator<=>(const ArpeggiatorGate&) const = default;
51};
52
65
83
103
105
106constexpr bool valid_order(ArpeggiatorOrder order) noexcept {
107 return order >= ArpeggiatorOrder::Up && order <= ArpeggiatorOrder::Chord;
108}
109
114
119
120} // namespace arpeggiator_detail
121
131template <std::size_t MaximumHeldNotes = 32, std::size_t MaximumOctaves = 4,
132 std::size_t MaximumStepsPerBlock = 64, std::size_t MaximumOwnershipRuns = 256>
134 public:
135 static_assert(MaximumHeldNotes > 0);
136 static_assert(MaximumOctaves > 0);
137 static_assert(MaximumStepsPerBlock > 0);
138 static_assert(MaximumOwnershipRuns >= MaximumHeldNotes);
139 static_assert(MaximumHeldNotes <= std::numeric_limits<std::size_t>::max() / MaximumOctaves);
140
141 static constexpr std::size_t kMaximumPatternNotes = MaximumHeldNotes * MaximumOctaves;
142 static_assert(MaximumStepsPerBlock <=
143 (std::numeric_limits<std::size_t>::max() / kMaximumPatternNotes - 1) / 2);
144
145 static constexpr ArpeggiatorCapacityContract contract() noexcept {
146 constexpr auto generated = (MaximumStepsPerBlock * 2 + 1) * kMaximumPatternNotes;
147 return {MaximumHeldNotes,
149 MaximumStepsPerBlock,
150 generated,
154 true,
155 true};
156 }
157
158 static constexpr bool valid_spec(const ArpeggiatorSpec& spec) noexcept {
159 constexpr auto kMaximumRate = timebase::kTicksPerQuarter * 16;
160 return spec.rate.value > 0 && spec.rate.value <= kMaximumRate && spec.octave_count > 0 &&
161 spec.octave_count <= MaximumOctaves && spec.gate.numerator > 0 &&
162 spec.gate.denominator > 0 && spec.gate.numerator <= spec.gate.denominator &&
163 spec.gate.denominator <= 1'000'000 && timebase::valid_swing_grid(spec.rate) &&
168 }
169
170 explicit constexpr Arpeggiator(ArpeggiatorSpec spec = {}) noexcept
171 : spec_(spec), valid_(valid_spec(spec)) {}
172
173 constexpr bool valid() const noexcept {
174 return valid_;
175 }
176 constexpr const ArpeggiatorSpec& spec() const noexcept {
177 return spec_;
178 }
179
180 std::size_t held_note_count() const noexcept {
181 std::size_t count = 0;
182 for (const auto& note : held_)
183 if (note.active)
184 ++count;
185 return count;
186 }
187
188 std::size_t physical_note_count() const noexcept {
189 return physical_count_;
190 }
191 std::size_t sounding_note_count() const noexcept {
192 std::size_t count = 0;
193 for (const auto& note : sounding_)
194 if (note.active)
195 ++count;
196 return count;
197 }
198
200 const ArpeggiatorBlock& block) noexcept {
201 if (utility_detail::blocks_alias(input, output))
202 return {0, input.size(), 0, false};
205 if (!valid_ || !valid_block(block) || !ordered(input) || !utility_detail::ready(output)) {
206 retain_unprocessed_input(input);
207 return {0, input.size(), 0, false};
208 }
209
210 current_block_ = &block;
211 steps_this_block_ = 0;
212 const bool discontinuity = detects_discontinuity(block);
213 if (discontinuity) {
214 clock_initialized_ = false;
215 if (!release_all(output, report, 0)) {
216 retain_unprocessed_input(input);
217 finish_block(block);
218 return report;
219 }
220 }
221 if (!drain_pending_spec(output, report)) {
222 retain_unprocessed_input(input);
223 finish_block(block);
224 return report;
225 }
226
227 const bool should_play =
228 block.playing && block.transport_event != ArpeggiatorTransportEvent::Stopped;
229 if (!should_play) {
230 if (!release_all(output, report, 0)) {
231 retain_unprocessed_input(input);
232 finish_block(block);
233 return report;
234 }
235 clock_initialized_ = false;
236 } else if (!clock_initialized_) {
237 align_clock(block.tick_start);
238 }
239
240 std::size_t input_index = 0;
241 while (input_index < input.size()) {
242 const auto offset = input[input_index].sample_offset;
243 if (offset < 0 || offset >= block.sample_count) {
244 ++report.dropped;
245 report.complete = false;
246 retain_input_event(input[input_index]);
247 ++input_index;
248 continue;
249 }
250 if (!render_until(offset,
251 spec_.boundary_order == ArpeggiatorBoundaryOrder::StepBeforeInput,
252 should_play, output, report)) {
253 retain_unprocessed_input(input, input_index);
254 finish_block(block);
255 return report;
256 }
257 const auto group_end = equal_offset_end(input, input_index, offset);
258 for (; input_index < group_end; ++input_index) {
259 if (!consume_input_event(input[input_index], output, report)) {
260 retain_unprocessed_input(input, input_index + 1);
261 finish_block(block);
262 return report;
263 }
264 }
265 if (should_play && spec_.boundary_order == ArpeggiatorBoundaryOrder::InputBeforeStep &&
266 !render_until(offset, true, true, output, report)) {
267 retain_unprocessed_input(input, input_index);
268 finish_block(block);
269 return report;
270 }
271 }
272
273 if (!render_until(block.sample_count, false, should_play, output, report)) {
274 finish_block(block);
275 return report;
276 }
277 utility_detail::copy_sidecars(input, output, report);
278 output.sort();
279 finish_block(block);
280 return report;
281 }
282
289 if (!utility_detail::ready(output))
290 return {0, 0, sounding_note_count(), false};
291 release_all(output, report, 0);
292 clock_initialized_ = false;
293 output.sort();
294 return report;
295 }
296
299 auto report = flush_transport(output);
300 clear_held_state();
301 last_block_valid_ = false;
302 pending_spec_.reset();
303 return report;
304 }
305
310 MidiBuffer& output) noexcept {
312 if (!valid_spec(spec))
313 return {0, 0, 0, false};
315 if (!utility_detail::ready(output)) {
316 pending_spec_ = spec;
317 return {0, 0, sounding_note_count(), false};
318 }
319 if (!release_all(output, report, 0)) {
320 pending_spec_ = spec;
321 return report;
322 }
323 apply_spec(spec);
324 output.sort();
325 return report;
326 }
327
328 private:
329 struct HeldNote {
330 std::uint8_t channel = 0;
331 std::uint8_t note = 0;
332 std::uint8_t velocity = 0;
333 std::uint64_t serial = 0;
334 bool physical = false;
335 bool active = false;
336 };
337
338 struct PatternNote {
339 std::uint8_t channel = 0;
340 std::uint8_t note = 0;
341 std::uint8_t velocity = 0;
342 std::uint64_t serial = 0;
343 };
344
345 struct SoundingNote {
346 std::uint8_t channel = 0;
347 std::uint8_t note = 0;
348 std::int64_t release_sample = 0;
349 bool active = false;
350 };
351
352 struct OwnershipRun {
353 std::uint16_t key = 0;
354 std::uint32_t count = 0;
355 bool accepted = false;
356 };
357
358 static constexpr bool is_attack(const MidiEvent& event) noexcept {
359 return event.is_note_on() && event.velocity() != 0;
360 }
361
362 static constexpr bool is_release(const MidiEvent& event) noexcept {
363 return event.is_note_off() || (event.is_note_on() && event.velocity() == 0);
364 }
365
366 static bool valid_block(const ArpeggiatorBlock& block) noexcept {
367 if (block.sample_count < 0)
368 return false;
369 if (block.tempo_map != nullptr)
370 return block.tempo_map->sample_rate().valid();
371 return block.sample_rate.valid() && timebase::valid_tempo(block.tempo_bpm);
372 }
373
374 static bool ordered(const MidiBuffer& input) noexcept {
375 std::int32_t previous = std::numeric_limits<std::int32_t>::min();
376 for (const auto& event : input) {
377 if (event.sample_offset < previous)
378 return false;
379 previous = event.sample_offset;
380 }
381 return true;
382 }
383
384 static std::size_t equal_offset_end(const MidiBuffer& input, std::size_t begin,
385 std::int32_t offset) noexcept {
386 auto end = begin;
387 while (end < input.size() && input[end].sample_offset == offset)
388 ++end;
389 return end;
390 }
391
392 bool detects_discontinuity(const ArpeggiatorBlock& block) const noexcept {
393 if (block.transport_event != ArpeggiatorTransportEvent::Continuous)
394 return true;
395 if (!last_block_valid_)
396 return false;
397 if (block.sample_start.value != last_sample_end_ || block.playing != last_playing_)
398 return true;
399 return arpeggiator_detail::distance_exceeds(block.tick_start.value, projected_tick_end_, 2);
400 }
401
402 void finish_block(const ArpeggiatorBlock& block) noexcept {
403 last_sample_end_ =
404 utility_detail::saturating_sample_add(block.sample_start.value, block.sample_count);
405 if (block.tempo_map != nullptr) {
406 const auto anchor = block.tempo_map->fractional_ticks_to_samples(
407 static_cast<long double>(block.tick_start.value));
408 projected_tick_end_ =
409 arpeggiator_detail::saturating_round(block.tempo_map->fractional_samples_to_ticks(
410 anchor + static_cast<long double>(block.sample_count)));
411 } else {
412 const auto ticks = static_cast<long double>(block.sample_count) *
413 static_cast<long double>(block.tempo_bpm) *
414 static_cast<long double>(timebase::kTicksPerQuarter) /
415 (60.0L * block.sample_rate.as_long_double());
416 projected_tick_end_ = utility_detail::saturating_sample_add(
417 block.tick_start.value, arpeggiator_detail::saturating_round(ticks));
418 }
419 last_playing_ = block.playing;
420 last_block_valid_ = true;
421 current_block_ = nullptr;
422 }
423
424 void align_clock(timebase::TickPosition tick) noexcept {
425 const auto step = spec_.rate.value;
426 const auto quotient = arpeggiator_detail::floor_div(tick.value, step);
427 auto index = quotient == std::numeric_limits<std::int64_t>::min() ? quotient : quotient - 1;
428 for (int attempts = 0; attempts < 4; ++attempts, ++index) {
429 const auto candidate = swung_boundary(index);
430 if (candidate.value >= tick.value) {
431 next_step_index_ = index;
432 next_step_tick_ = candidate;
433 clock_initialized_ = true;
434 return;
435 }
436 }
437 next_step_index_ = index;
438 next_step_tick_ = swung_boundary(index);
439 clock_initialized_ = true;
440 }
441
442 timebase::TickPosition swung_boundary(std::int64_t index) const noexcept {
443 const auto straight = timebase::TickPosition{
444 arpeggiator_detail::saturating_multiply(index, spec_.rate.value)};
445 const auto displacement = timebase::swing_displacement(straight, spec_.rate, spec_.swing);
446 return {utility_detail::saturating_sample_add(straight.value, displacement.value)};
447 }
448
449 void advance_clock() noexcept {
450 if (next_step_index_ != std::numeric_limits<std::int64_t>::max())
451 ++next_step_index_;
452 next_step_tick_ = swung_boundary(next_step_index_);
453 }
454
455 std::int64_t sample_for_tick(timebase::TickPosition tick) const noexcept {
456 const auto& block = *current_block_;
457 long double delta = 0.0L;
458 if (block.tempo_map != nullptr) {
459 const auto event_sample =
460 block.tempo_map->fractional_ticks_to_samples(static_cast<long double>(tick.value));
461 const auto anchor_sample = block.tempo_map->fractional_ticks_to_samples(
462 static_cast<long double>(block.tick_start.value));
463 delta = event_sample - anchor_sample;
464 } else {
465 const auto tick_delta = arpeggiator_detail::difference_as_long_double(
466 tick.value, block.tick_start.value);
467 delta = tick_delta * block.sample_rate.as_long_double() * 60.0L /
468 (static_cast<long double>(block.tempo_bpm) *
469 static_cast<long double>(timebase::kTicksPerQuarter));
470 }
471 constexpr auto minimum = static_cast<long double>(std::numeric_limits<std::int64_t>::min());
472 constexpr auto maximum = static_cast<long double>(std::numeric_limits<std::int64_t>::max());
473 const auto absolute = static_cast<long double>(block.sample_start.value) + delta;
474 if (absolute <= minimum)
475 return std::numeric_limits<std::int64_t>::min();
476 if (absolute >= maximum)
477 return std::numeric_limits<std::int64_t>::max();
478 return static_cast<std::int64_t>(std::llround(absolute));
479 }
480
481 bool render_until(std::int32_t offset, bool include_step_at_offset, bool playing,
482 MidiBuffer& output, MidiUtilityProcessReport& report) noexcept {
483 const auto absolute_limit =
484 utility_detail::saturating_sample_add(current_block_->sample_start.value, offset);
485 const bool limit_is_inside_block = offset < current_block_->sample_count;
486 const bool full_gate_tie = spec_.repeated_note == ArpeggiatorRepeatedNotePolicy::Tie &&
487 spec_.gate.numerator == spec_.gate.denominator;
488 if (!playing)
489 return release_due(absolute_limit, limit_is_inside_block, output, report);
490
491 while (clock_initialized_) {
492 const auto step_sample = sample_for_tick(next_step_tick_);
493 const bool due = include_step_at_offset ? step_sample <= absolute_limit
494 : step_sample < absolute_limit;
495 if (!due)
496 break;
497 if (!release_due(step_sample, !full_gate_tie, output, report))
498 return false;
499 if (step_sample < current_block_->sample_start.value) {
500 advance_clock();
501 report.complete = false;
502 ++report.dropped;
503 continue;
504 }
505 if (steps_this_block_ == MaximumStepsPerBlock) {
506 report.complete = false;
507 ++report.dropped;
508 const auto tick = tick_at_block_offset(offset);
509 align_clock({utility_detail::saturating_sample_add(tick.value, 1)});
510 break;
511 }
512 const auto following_tick =
513 swung_boundary(next_step_index_ == std::numeric_limits<std::int64_t>::max()
514 ? next_step_index_
515 : next_step_index_ + 1);
516 if (!fire_step(step_sample, next_step_tick_, following_tick, output, report))
517 return false;
518 advance_clock();
519 ++steps_this_block_;
520 }
521 const bool step_at_limit =
522 clock_initialized_ && sample_for_tick(next_step_tick_) == absolute_limit;
523 const bool include_releases_at_limit =
524 limit_is_inside_block && !(full_gate_tie && step_at_limit);
525 return release_due(absolute_limit, include_releases_at_limit, output, report);
526 }
527
528 timebase::TickPosition tick_at_block_offset(std::int32_t offset) const noexcept {
529 const auto& block = *current_block_;
530 if (block.tempo_map != nullptr) {
531 const auto anchor = block.tempo_map->fractional_ticks_to_samples(
532 static_cast<long double>(block.tick_start.value));
533 const auto tick = block.tempo_map->fractional_samples_to_ticks(
534 anchor + static_cast<long double>(offset));
535 return {arpeggiator_detail::saturating_round(tick)};
536 }
537 const auto ticks = static_cast<long double>(offset) *
538 static_cast<long double>(block.tempo_bpm) *
539 static_cast<long double>(timebase::kTicksPerQuarter) /
540 (60.0L * block.sample_rate.as_long_double());
541 return {utility_detail::saturating_sample_add(block.tick_start.value,
542 arpeggiator_detail::saturating_round(ticks))};
543 }
544
545 bool consume_input_event(const MidiEvent& event, MidiBuffer& output,
546 MidiUtilityProcessReport& report) noexcept {
547 if (is_attack(event)) {
548 const bool starts_latched_phrase =
549 spec_.latch && physical_count_ == 0 && held_note_count() != 0;
550 if (starts_latched_phrase) {
551 const bool releases_complete = release_all(output, report, event.sample_offset);
552 clear_latched_notes();
553 retain_attack(event, report);
554 return releases_complete;
555 }
556 retain_attack(event, report);
557 return true;
558 }
559 if (is_release(event)) {
560 consume_release(event.channel(), event.note());
561 if (!spec_.latch && held_note_count() == 0)
562 return release_all(output, report, event.sample_offset);
563 return true;
564 }
565 return utility_detail::emit(output, event, report);
566 }
567
568 void retain_input_event(const MidiEvent& event) noexcept {
569 if (is_attack(event)) {
570 const auto key = static_cast<std::uint16_t>(
571 utility_detail::key_index(event.channel(), event.note()));
572 record_ownership(key, false);
573 } else if (is_release(event)) {
574 consume_release(event.channel(), event.note());
575 }
576 }
577
578 void retain_attack(const MidiEvent& event, MidiUtilityProcessReport& report) noexcept {
579 const auto key =
580 static_cast<std::uint16_t>(utility_detail::key_index(event.channel(), event.note()));
581 const auto slot = free_held_slot();
582 const bool accepted = slot != nullptr && can_record_ownership(key, true);
583 if (!record_ownership(key, accepted) || !accepted) {
584 ++report.dropped;
585 report.complete = false;
586 return;
587 }
588 if (next_serial_ == std::numeric_limits<std::uint64_t>::max())
589 rebase_serials();
590 *slot = {event.channel(), event.note(), event.velocity(), next_serial_++, true, true};
591 ++physical_count_;
592 }
593
594 void retain_unprocessed_input(const MidiBuffer& input, std::size_t begin = 0) noexcept {
595 for (std::size_t index = begin; index < input.size(); ++index)
596 retain_input_event(input[index]);
597 }
598
599 HeldNote* free_held_slot() noexcept {
600 for (auto& note : held_)
601 if (!note.active)
602 return &note;
603 return nullptr;
604 }
605
606 bool can_record_ownership(std::uint16_t key, bool accepted) const noexcept {
607 if (overflow_suppressed_[key] != 0)
608 return !accepted;
609 if (ownership_size_ < ownership_.size())
610 return true;
611 return !accepted;
612 }
613
614 bool record_ownership(std::uint16_t key, bool accepted) noexcept {
615 if (overflow_suppressed_[key] != 0 || ownership_size_ == ownership_.size()) {
616 if (accepted || overflow_suppressed_[key] == std::numeric_limits<std::uint32_t>::max())
617 return false;
618 ++overflow_suppressed_[key];
619 return true;
620 }
621 for (std::size_t index = ownership_size_; index != 0; --index) {
622 auto& previous = ownership_[index - 1];
623 if (previous.key != key)
624 continue;
625 if (previous.accepted == accepted &&
626 previous.count != std::numeric_limits<std::uint32_t>::max()) {
627 ++previous.count;
628 return true;
629 }
630 break;
631 }
632 ownership_[ownership_size_++] = {key, 1, accepted};
633 return true;
634 }
635
636 void consume_release(std::uint8_t channel, std::uint8_t note) noexcept {
637 const auto key = static_cast<std::uint16_t>(utility_detail::key_index(channel, note));
638 for (std::size_t index = 0; index < ownership_size_; ++index) {
639 auto& run = ownership_[index];
640 if (run.key != key)
641 continue;
642 const bool accepted = run.accepted;
643 if (--run.count == 0)
644 erase_ownership(index);
645 if (accepted)
646 release_oldest_physical(key);
647 return;
648 }
649 if (overflow_suppressed_[key] != 0)
650 --overflow_suppressed_[key];
651 }
652
653 void release_oldest_physical(std::uint16_t key) noexcept {
654 HeldNote* oldest = nullptr;
655 for (auto& note : held_) {
656 if (!note.active || !note.physical ||
657 utility_detail::key_index(note.channel, note.note) != key)
658 continue;
659 if (oldest == nullptr || note.serial < oldest->serial)
660 oldest = &note;
661 }
662 if (oldest == nullptr)
663 return;
664 oldest->physical = false;
665 --physical_count_;
666 if (!spec_.latch)
667 *oldest = {};
668 }
669
670 void erase_ownership(std::size_t index) noexcept {
671 for (std::size_t next = index + 1; next < ownership_size_; ++next)
672 ownership_[next - 1] = ownership_[next];
673 --ownership_size_;
674 }
675
676 void clear_latched_notes() noexcept {
677 for (auto& note : held_)
678 if (note.active && !note.physical)
679 note = {};
680 }
681
682 void clear_held_state() noexcept {
683 held_.fill({});
684 ownership_.fill({});
685 overflow_suppressed_.fill(0);
686 ownership_size_ = 0;
687 physical_count_ = 0;
688 }
689
690 void rebase_serials() noexcept {
691 std::array<std::size_t, MaximumHeldNotes> indices{};
692 std::size_t size = 0;
693 for (std::size_t index = 0; index < held_.size(); ++index)
694 if (held_[index].active)
695 indices[size++] = index;
696 std::sort(indices.begin(), indices.begin() + size, [&](std::size_t lhs, std::size_t rhs) {
697 return held_[lhs].serial < held_[rhs].serial;
698 });
699 for (std::size_t serial = 0; serial < size; ++serial)
700 held_[indices[serial]].serial = serial;
701 next_serial_ = size;
702 }
703
704 std::size_t build_pattern() noexcept {
705 std::array<std::size_t, MaximumHeldNotes> held_indices{};
706 std::size_t held_size = 0;
707 for (std::size_t index = 0; index < held_.size(); ++index)
708 if (held_[index].active)
709 held_indices[held_size++] = index;
710 std::sort(held_indices.begin(), held_indices.begin() + held_size,
711 [&](std::size_t lhs, std::size_t rhs) {
712 return held_[lhs].serial < held_[rhs].serial;
713 });
714 std::size_t size = 0;
715 for (std::size_t octave = 0; octave < spec_.octave_count; ++octave) {
716 for (std::size_t held_index = 0; held_index < held_size; ++held_index) {
717 const auto& held = held_[held_indices[held_index]];
718 const auto pitch = static_cast<int>(held.note) +
719 static_cast<int>(octave) * music::kPitchClassesPerOctave;
720 if (pitch > 127)
721 continue;
722 const auto order_serial = static_cast<std::uint64_t>(size);
723 pattern_[size++] = {held.channel, static_cast<std::uint8_t>(pitch), held.velocity,
724 order_serial};
725 }
726 }
727 if (spec_.order != ArpeggiatorOrder::AsPlayed) {
728 std::sort(pattern_.begin(), pattern_.begin() + size,
729 [](const PatternNote& lhs, const PatternNote& rhs) {
730 if (lhs.note != rhs.note)
731 return lhs.note < rhs.note;
732 if (lhs.channel != rhs.channel)
733 return lhs.channel < rhs.channel;
734 return lhs.serial < rhs.serial;
735 });
736 }
737 return size;
738 }
739
740 std::size_t selected_pattern_index(std::size_t size) const noexcept {
741 const auto positive_mod = [size](std::int64_t value) {
742 auto result = value % static_cast<std::int64_t>(size);
743 if (result < 0)
744 result += static_cast<std::int64_t>(size);
745 return static_cast<std::size_t>(result);
746 };
747 switch (spec_.order) {
749 return size - 1 - positive_mod(next_step_index_);
751 if (size == 1)
752 return 0;
753 const auto period = size * 2 - 2;
754 auto position = next_step_index_ % static_cast<std::int64_t>(period);
755 if (position < 0)
756 position += static_cast<std::int64_t>(period);
757 const auto index = static_cast<std::size_t>(position);
758 return index < size ? index : period - index;
759 }
761 return static_cast<std::size_t>(
762 arpeggiator_detail::mix(spec_.random_seed ^
763 static_cast<std::uint64_t>(next_step_index_)) %
764 size);
768 return positive_mod(next_step_index_);
769 }
770 return 0;
771 }
772
773 bool fire_step(std::int64_t step_sample, timebase::TickPosition step_tick,
774 timebase::TickPosition following_tick, MidiBuffer& output,
775 MidiUtilityProcessReport& report) noexcept {
776 const auto pattern_size = build_pattern();
777 if (pattern_size == 0)
778 return release_all_at_sample(output, report, step_sample);
779
780 const auto unsigned_interval = following_tick.value <= step_tick.value
781 ? std::uint64_t{0}
782 : static_cast<std::uint64_t>(following_tick.value) -
783 static_cast<std::uint64_t>(step_tick.value);
784 const auto interval = std::max<std::int64_t>(
785 1, unsigned_interval >=
786 static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max())
787 ? std::numeric_limits<std::int64_t>::max()
788 : static_cast<std::int64_t>(unsigned_interval));
789 const auto gate_ticks = std::max<std::int64_t>(
790 1, arpeggiator_detail::saturating_round(
791 (static_cast<long double>(interval) * spec_.gate.numerator) /
792 spec_.gate.denominator));
793 const auto release_tick = timebase::TickPosition{
794 utility_detail::saturating_sample_add(step_tick.value, gate_ticks)};
795 const auto release_sample = sample_for_tick(release_tick);
796
797 selected_count_ = 0;
798 if (spec_.order == ArpeggiatorOrder::Chord) {
799 for (std::size_t index = 0; index < pattern_size; ++index)
800 selected_[selected_count_++] = pattern_[index];
801 } else {
802 selected_[selected_count_++] = pattern_[selected_pattern_index(pattern_size)];
803 }
804
805 const bool may_tie = spec_.repeated_note == ArpeggiatorRepeatedNotePolicy::Tie &&
806 spec_.gate.numerator == spec_.gate.denominator;
807 if (!release_for_step(selected_, selected_count_, may_tie, step_sample, output, report))
808 return false;
809
810 const auto offset = block_offset(step_sample);
811 for (std::size_t index = 0; index < selected_count_; ++index) {
812 const auto& note = selected_[index];
813 if (may_tie && sounding_contains(note.channel, note.note)) {
814 extend_sounding(note.channel, note.note, release_sample);
815 continue;
816 }
817 auto* slot = free_sounding_slot();
818 if (slot == nullptr) {
819 ++report.dropped;
820 report.complete = false;
821 return false;
822 }
823 auto attack = MidiEvent::note_on(note.channel, note.note, note.velocity);
824 attack.sample_offset = offset;
825 if (!utility_detail::emit(output, attack, report))
826 return false;
827 *slot = {note.channel, note.note, release_sample, true};
828 }
829 return true;
830 }
831
832 bool release_for_step(const std::array<PatternNote, kMaximumPatternNotes>& next,
833 std::size_t next_count, bool may_tie, std::int64_t sample,
834 MidiBuffer& output, MidiUtilityProcessReport& report) noexcept {
835 for (auto& sounding : sounding_) {
836 if (!sounding.active)
837 continue;
838 bool keep = false;
839 if (may_tie) {
840 for (std::size_t index = 0; index < next_count; ++index) {
841 if (next[index].channel == sounding.channel &&
842 next[index].note == sounding.note) {
843 keep = true;
844 break;
845 }
846 }
847 }
848 if (keep)
849 continue;
850 if (!emit_release(sounding, sample, output, report))
851 return false;
852 }
853 return true;
854 }
855
856 bool release_due(std::int64_t sample, bool include_at_sample, MidiBuffer& output,
857 MidiUtilityProcessReport& report) noexcept {
858 for (auto& sounding : sounding_) {
859 const bool due = sounding.release_sample < sample ||
860 (include_at_sample && sounding.release_sample == sample);
861 if (sounding.active && due &&
862 !emit_release(sounding, sounding.release_sample, output, report))
863 return false;
864 }
865 return true;
866 }
867
868 bool release_all(MidiBuffer& output, MidiUtilityProcessReport& report,
869 std::int32_t offset) noexcept {
870 const auto sample =
871 current_block_ != nullptr
872 ? utility_detail::saturating_sample_add(current_block_->sample_start.value, offset)
873 : 0;
874 return release_all_at_sample(output, report, sample);
875 }
876
877 bool release_all_at_sample(MidiBuffer& output, MidiUtilityProcessReport& report,
878 std::int64_t sample) noexcept {
879 for (auto& sounding : sounding_)
880 if (sounding.active && !emit_release(sounding, sample, output, report))
881 return false;
882 return true;
883 }
884
885 bool emit_release(SoundingNote& sounding, std::int64_t sample, MidiBuffer& output,
886 MidiUtilityProcessReport& report) noexcept {
887 auto release = MidiEvent::note_off(sounding.channel, sounding.note);
888 release.sample_offset = block_offset(sample);
889 if (!utility_detail::emit(output, release, report)) {
890 sounding.release_sample = sample;
891 ++report.deferred;
892 return false;
893 }
894 sounding = {};
895 return true;
896 }
897
898 std::int32_t block_offset(std::int64_t sample) const noexcept {
899 if (current_block_ == nullptr)
900 return 0;
901 const auto start = current_block_->sample_start.value;
902 if (sample <= start)
903 return 0;
904 const auto maximum_offset = std::max<std::int32_t>(0, current_block_->sample_count - 1);
905 const auto difference =
906 static_cast<std::uint64_t>(sample) - static_cast<std::uint64_t>(start);
907 if (difference >= static_cast<std::uint64_t>(maximum_offset))
908 return maximum_offset;
909 return static_cast<std::int32_t>(difference);
910 }
911
912 SoundingNote* free_sounding_slot() noexcept {
913 for (auto& note : sounding_)
914 if (!note.active)
915 return &note;
916 return nullptr;
917 }
918
919 bool sounding_contains(std::uint8_t channel, std::uint8_t note) const noexcept {
920 for (const auto& sounding : sounding_)
921 if (sounding.active && sounding.channel == channel && sounding.note == note)
922 return true;
923 return false;
924 }
925
926 void extend_sounding(std::uint8_t channel, std::uint8_t note,
927 std::int64_t release_sample) noexcept {
928 for (auto& sounding : sounding_)
929 if (sounding.active && sounding.channel == channel && sounding.note == note)
930 sounding.release_sample = std::max(sounding.release_sample, release_sample);
931 }
932
933 bool drain_pending_spec(MidiBuffer& output, MidiUtilityProcessReport& report) noexcept {
934 if (!pending_spec_)
935 return true;
936 if (!release_all(output, report, 0))
937 return false;
938 const auto replacement = *pending_spec_;
939 pending_spec_.reset();
940 apply_spec(replacement);
941 return true;
942 }
943
944 void apply_spec(const ArpeggiatorSpec& spec) noexcept {
945 const bool disabling_latch = spec_.latch && !spec.latch;
946 spec_ = spec;
947 valid_ = true;
948 clock_initialized_ = false;
949 if (disabling_latch)
950 clear_latched_notes();
951 }
952
953 ArpeggiatorSpec spec_{};
954 bool valid_ = true;
955 std::optional<ArpeggiatorSpec> pending_spec_;
956 std::array<HeldNote, MaximumHeldNotes> held_{};
957 std::array<PatternNote, kMaximumPatternNotes> pattern_{};
958 std::array<PatternNote, kMaximumPatternNotes> selected_{};
959 std::size_t selected_count_ = 0;
960 std::array<SoundingNote, kMaximumPatternNotes> sounding_{};
961 std::array<OwnershipRun, MaximumOwnershipRuns> ownership_{};
962 std::array<std::uint32_t, 16 * 128> overflow_suppressed_{};
963 std::size_t ownership_size_ = 0;
964 std::size_t physical_count_ = 0;
965 std::uint64_t next_serial_ = 0;
966 std::int64_t next_step_index_ = 0;
967 timebase::TickPosition next_step_tick_{};
968 bool clock_initialized_ = false;
969 const ArpeggiatorBlock* current_block_ = nullptr;
970 std::int64_t last_sample_end_ = 0;
971 std::int64_t projected_tick_end_ = 0;
972 bool last_playing_ = false;
973 bool last_block_valid_ = false;
974 std::size_t steps_this_block_ = 0;
975};
976
977} // namespace pulp::midi
std::size_t physical_note_count() const noexcept
Definition arpeggiator.hpp:188
MidiUtilityProcessReport replace_spec(const ArpeggiatorSpec &spec, MidiBuffer &output) noexcept
Flush old generated notes before adopting a new policy.
Definition arpeggiator.hpp:309
constexpr bool valid() const noexcept
Definition arpeggiator.hpp:173
static constexpr std::size_t kMaximumPatternNotes
Definition arpeggiator.hpp:141
static constexpr bool valid_spec(const ArpeggiatorSpec &spec) noexcept
Definition arpeggiator.hpp:158
constexpr Arpeggiator(ArpeggiatorSpec spec={}) noexcept
Definition arpeggiator.hpp:170
MidiUtilityProcessReport process(const MidiBuffer &input, MidiBuffer &output, const ArpeggiatorBlock &block) noexcept
Definition arpeggiator.hpp:199
MidiUtilityProcessReport reset(MidiBuffer &output) noexcept
Release generated notes and discard every held/latch ownership.
Definition arpeggiator.hpp:298
std::size_t sounding_note_count() const noexcept
Definition arpeggiator.hpp:191
MidiUtilityProcessReport flush_transport(MidiBuffer &output) noexcept
Release generated notes and reset their clock while preserving the held input ledger.
Definition arpeggiator.hpp:286
std::size_t held_note_count() const noexcept
Definition arpeggiator.hpp:180
constexpr const ArpeggiatorSpec & spec() const noexcept
Definition arpeggiator.hpp:176
static constexpr ArpeggiatorCapacityContract contract() noexcept
Definition arpeggiator.hpp:145
Collection of timestamped MIDI events within a single audio buffer period.
Definition buffer.hpp:31
Definition compiled_tempo_map.hpp:91
@ accepted
Definition realtime_time_stretch.hpp:31
void run(LV2_Handle handle, uint32_t n_samples)
Definition lv2_entry.hpp:430
@ release
Release only: voices enter their normal release and tails ring out (MIDI CC 123 semantic).
Definition panic.hpp:39
@ end
Definition editor_host.hpp:354
Definition arpeggiator.hpp:104
constexpr bool valid_repeated_note_policy(ArpeggiatorRepeatedNotePolicy policy) noexcept
Definition arpeggiator.hpp:110
constexpr bool valid_boundary_order(ArpeggiatorBoundaryOrder order) noexcept
Definition arpeggiator.hpp:115
constexpr bool valid_order(ArpeggiatorOrder order) noexcept
Definition arpeggiator.hpp:106
constexpr std::size_t positive_mod(std::int64_t value, std::size_t modulus) noexcept
Definition step_player.hpp:167
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
int key_index(std::uint8_t channel, std::uint8_t note) noexcept
Definition utility_contract.hpp:55
Definition custom_node_events.hpp:26
MidiUtilityTransportRequirement
Definition utility_contract.hpp:30
@ FlushOnDiscontinuity
Definition utility_contract.hpp:33
MidiUtilityOverflowPolicy
Definition utility_contract.hpp:19
@ DropUnstarted
Definition utility_contract.hpp:20
ArpeggiatorTransportEvent
Definition arpeggiator.hpp:39
@ 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
ArpeggiatorRepeatedNotePolicy
Definition arpeggiator.hpp:29
@ Tie
Definition arpeggiator.hpp:31
@ Retrigger
Definition arpeggiator.hpp:30
ArpeggiatorBoundaryOrder
Definition arpeggiator.hpp:34
@ InputBeforeStep
Definition arpeggiator.hpp:35
@ StepBeforeInput
Definition arpeggiator.hpp:36
MidiUtilitySameSampleOrder
Definition utility_contract.hpp:25
@ ReleaseBeforeAttack
Definition utility_contract.hpp:27
ArpeggiatorOrder
Definition arpeggiator.hpp:20
@ Down
Definition arpeggiator.hpp:22
@ Up
Definition arpeggiator.hpp:21
@ Random
Definition arpeggiator.hpp:25
@ UpDown
Definition arpeggiator.hpp:23
@ Chord
Definition arpeggiator.hpp:26
@ AsPlayed
Definition arpeggiator.hpp:24
@ 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
constexpr int kPitchClassesPerOctave
Definition pitch.hpp:16
@ octave
Definition chord.hpp:28
@ index
Definition gpu_pass.hpp:140
@ size
Definition config.hpp:216
@ attack
Definition envelope.hpp:58
@ oldest
Recycle the grain furthest through its window — the quietest one when durations match,...
Definition granular.hpp:123
float minimum(const float *data, std::size_t count) noexcept
float maximum(const float *data, std::size_t count) noexcept
constexpr bool valid_swing_ratio(SwingRatio ratio) noexcept
Definition quantize.hpp:79
constexpr bool valid_swing_grid(TickDuration grid) noexcept
Definition quantize.hpp:72
TickDuration swing_displacement(TickPosition position, TickDuration grid, SwingRatio ratio) noexcept
Definition quantize.hpp:114
bool valid_tempo(double tempo_bpm) noexcept
Definition quantize.hpp:17
constexpr std::int64_t kTicksPerQuarter
Definition tick.hpp:54
constexpr SwingRatio kStraightSwing
Definition quantize.hpp:61
@ block
Filled glyph cell; the covered glyph is drawn in the bg color.
Definition caret.hpp:66
@ value
the widget's primary value (Knob, Fader, RangeSlider, Toggle, ProgressBar)
Definition binding_diagnostics.hpp:19
@ candidate
Definition waveform_editor_primitives.hpp:193
@ start
Definition geometry.hpp:370
Block timing for one sample-accurate arpeggiator call.
Definition arpeggiator.hpp:73
timebase::RationalRate sample_rate
Definition arpeggiator.hpp:77
bool playing
Definition arpeggiator.hpp:80
const timebase::CompiledTempoMap * tempo_map
Definition arpeggiator.hpp:79
std::int32_t sample_count
Definition arpeggiator.hpp:76
timebase::SamplePosition sample_start
Definition arpeggiator.hpp:74
timebase::TickPosition tick_start
Definition arpeggiator.hpp:75
double tempo_bpm
Definition arpeggiator.hpp:78
ArpeggiatorTransportEvent transport_event
Definition arpeggiator.hpp:81
Definition arpeggiator.hpp:84
std::size_t maximum_held_notes
Definition arpeggiator.hpp:85
MidiUtilityTransportRequirement transport
Definition arpeggiator.hpp:91
bool requires_distinct_input_output
Definition arpeggiator.hpp:94
std::size_t maximum_steps_per_block
Definition arpeggiator.hpp:87
std::size_t maximum_pattern_notes
Definition arpeggiator.hpp:86
std::size_t maximum_generated_events_per_block
Definition arpeggiator.hpp:88
constexpr std::size_t required_output_events(std::size_t input_events) const noexcept
Definition arpeggiator.hpp:96
MidiUtilitySameSampleOrder same_sample_order
Definition arpeggiator.hpp:90
MidiUtilityOverflowPolicy overflow
Definition arpeggiator.hpp:89
bool requires_reserved_capacity_limited_output
Definition arpeggiator.hpp:93
Definition arpeggiator.hpp:47
std::uint32_t denominator
Definition arpeggiator.hpp:49
constexpr auto operator<=>(const ArpeggiatorGate &) const =default
std::uint32_t numerator
Definition arpeggiator.hpp:48
Definition arpeggiator.hpp:53
std::uint64_t random_seed
Definition arpeggiator.hpp:62
ArpeggiatorRepeatedNotePolicy repeated_note
Definition arpeggiator.hpp:59
constexpr auto operator<=>(const ArpeggiatorSpec &) const =default
std::uint8_t octave_count
Definition arpeggiator.hpp:56
timebase::TickDuration rate
Definition arpeggiator.hpp:54
ArpeggiatorBoundaryOrder boundary_order
Definition arpeggiator.hpp:60
bool latch
Definition arpeggiator.hpp:61
ArpeggiatorGate gate
Definition arpeggiator.hpp:57
ArpeggiatorOrder order
Definition arpeggiator.hpp:55
timebase::SwingRatio swing
Definition arpeggiator.hpp:58
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
Definition rational_time.hpp:14
Definition rational_time.hpp:9
Definition quantize.hpp:53
Definition tick.hpp:56
Definition tick.hpp:61