Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
strum.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <pulp/midi/detail/note_schedule.hpp>
5
6#include <algorithm>
7#include <array>
8#include <cmath>
9#include <cstddef>
10#include <cstdint>
11#include <optional>
12
13namespace pulp::midi {
14
15enum class StrumDirection : std::uint8_t {
24};
25
27enum class StrumShape : std::uint8_t {
33};
34
35enum class StrumSpacingSync : std::uint8_t {
38};
39
65
66template <std::size_t MaximumClusterNotes = 16> class Strum {
67 public:
68 using Block = note_schedule::Block;
69 static constexpr std::uint64_t kTimingStream = 5;
70 static constexpr std::uint64_t kVelocityStream = 6;
71 static constexpr std::uint64_t kOrderStream = 7;
72
78
79 static constexpr bool valid_spec(StrumSpec spec) noexcept {
80 if (spec.window_samples < 0 || spec.timing_jitter_samples < 0 ||
81 spec.velocity_jitter > 127 || !std::isfinite(spec.shape_exponent) ||
82 spec.shape_exponent <= 0.0 || spec.velocity_tilt < -127 || spec.velocity_tilt > 127)
83 return false;
84 return spec.sync == StrumSpacingSync::Division ? spec.spacing_interval.value > 0
85 : spec.spacing_milliseconds > 0;
86 }
87
89 static std::int64_t spacing_samples(StrumSpec spec, const Block& block) noexcept {
90 return spec.sync == StrumSpacingSync::Division
91 ? note_schedule::samples_for_duration(block, spec.spacing_interval)
92 : note_schedule::samples_for_milliseconds(block, spec.spacing_milliseconds);
93 }
94
98 static std::int64_t shaped_step(StrumShape shape, double exponent, std::size_t index,
99 std::size_t count) noexcept {
100 if (count <= 1 || index == 0)
101 return 0;
102 const auto last = static_cast<std::int64_t>(count - 1);
103 const auto position = static_cast<std::int64_t>(index);
104 switch (shape) {
106 exponent = std::max(0.0, exponent);
107 return static_cast<std::int64_t>(std::llround(
108 std::pow(static_cast<double>(position) / static_cast<double>(last), exponent) *
109 static_cast<double>(last)));
111 exponent = std::max(0.0, exponent);
112 return last -
113 static_cast<std::int64_t>(std::llround(
114 std::pow(static_cast<double>(last - position) / static_cast<double>(last),
115 exponent) *
116 static_cast<double>(last)));
118 break;
119 }
120 return position;
121 }
122
124 static std::int64_t shaped_step(StrumShape shape, std::size_t index,
125 std::size_t count) noexcept {
126 return shaped_step(shape, 2.0, index, count);
127 }
128
129 explicit constexpr Strum(StrumSpec spec = {}) noexcept
130 : spec_(spec), valid_(valid_spec(spec)) {}
131
132 constexpr bool valid() const noexcept {
133 return valid_;
134 }
135
136 constexpr StrumSpec spec() const noexcept {
137 return spec_;
138 }
139
141 const Block& block) noexcept {
142 if (utility_detail::blocks_alias(input, output))
143 return {0, input.size(), 0, false};
146 if (!utility_detail::ready(output) || !valid_ || !note_schedule::valid_block(block)) {
147 report.dropped = input.size();
148 report.complete = false;
149 return report;
150 }
151 const std::int64_t block_end =
152 utility_detail::saturating_sample_add(block.sample_start.value, block.sample_count);
153 for (const auto& event : input) {
154 const auto absolute =
155 utility_detail::saturating_sample_add(block.sample_start.value, event.sample_offset);
156 advance_to(absolute, block, output, report);
157 const bool is_attack = event.is_note_on() && event.velocity() != 0;
158 const bool is_release =
159 event.is_note_off() || (event.is_note_on() && event.velocity() == 0);
160 if (!is_attack && !is_release) {
161 utility_detail::emit(output, event, report);
162 continue;
163 }
164 if (is_attack) {
165 admit(event, absolute, block, output, report);
166 continue;
167 }
168 // A release can outrun its own spread note. Emitting the attack
169 // first keeps the pair ordered and the note balanced.
170 force_out(event.channel(), event.note(), absolute, block, output, report);
171 utility_detail::emit(output, event, report);
172 }
173 advance_to(block_end, block, output, report);
174 utility_detail::copy_sidecars(input, output, report);
175 output.sort();
176 return report;
177 }
178
180 MidiUtilityProcessReport flush(MidiBuffer& output, const Block& block) noexcept {
183 if (!utility_detail::ready(output)) {
184 report.complete = false;
185 return report;
186 }
187 if (open_)
188 close_cluster(block);
189 for (auto& slot : cluster_) {
190 if (!slot.active)
191 continue;
192 if (!emit_slot(slot, block.sample_start.value, block, output, report)) {
193 ++report.deferred;
194 report.complete = false;
195 return report;
196 }
197 }
198 count_ = 0;
199 open_ = false;
200 report.complete = empty();
201 if (report.complete && pending_spec_) {
202 spec_ = *pending_spec_;
203 valid_ = true;
204 pending_spec_.reset();
205 }
206 return report;
207 }
208
209 MidiUtilityProcessReport reset(MidiBuffer& output, const Block& block) noexcept {
210 return flush(output, block);
211 }
212
214 const Block& block) noexcept {
215 if (!valid_spec(spec)) {
217 return {0, 0, 0, false};
218 }
219 auto report = flush(output, block);
220 if (report.complete) {
221 spec_ = spec;
222 valid_ = true;
223 pending_spec_.reset();
224 } else {
225 pending_spec_ = spec;
226 }
227 return report;
228 }
229
230 bool empty() const noexcept {
231 for (const auto& slot : cluster_)
232 if (slot.active)
233 return false;
234 return true;
235 }
236
237 private:
238 struct Slot {
239 MidiEvent event{};
240 std::int64_t arrival = 0;
241 std::int64_t scheduled = 0;
242 std::uint8_t order = 0;
243 bool scheduled_yet = false;
244 bool active = false;
245 };
246
247 static std::uint64_t draw(std::uint64_t seed, std::int64_t coordinate, std::uint64_t lane,
248 std::uint64_t stream, std::uint64_t span) noexcept {
249 if (span == 0)
250 return 0;
251 const timebase::RandomCoordinate point{timebase::TickPosition{coordinate}, lane, 0, stream};
253 }
254
255 void admit(const MidiEvent& event, std::int64_t absolute, const Block& block,
256 MidiBuffer& output, MidiUtilityProcessReport& report) noexcept {
257 if (open_ && absolute - cluster_start_ > spec_.window_samples)
258 close_cluster(block);
259 if (!open_) {
260 cluster_start_ = absolute;
261 open_ = true;
262 }
263 Slot* slot = free_slot();
264 if (slot == nullptr) {
265 // FailOpenBalanced: a cluster that cannot grow forwards the note
266 // untouched rather than swallowing it, so the player's note always
267 // sounds and its release always matches something.
268 close_cluster(block);
269 flush_scheduled(std::numeric_limits<std::int64_t>::max(), block, output, report);
270 utility_detail::emit(output, event, report);
271 return;
272 }
273 *slot = {event, absolute, 0, 0, false, true};
274 ++count_;
275 }
276
279 void close_cluster(const Block& block) noexcept {
280 open_ = false;
281 std::array<Slot*, MaximumClusterNotes> members{};
282 std::size_t members_count = 0;
283 for (auto& slot : cluster_)
284 if (slot.active && !slot.scheduled_yet && members_count < MaximumClusterNotes)
285 members[members_count++] = &slot;
286 if (members_count == 0)
287 return;
288 order_members(members, members_count);
289 const auto spacing = spacing_samples(spec_, block);
290 const auto base =
291 utility_detail::saturating_sample_add(cluster_start_, spec_.window_samples);
292 for (std::size_t index = 0; index < members_count; ++index) {
293 auto* slot = members[index];
294 const auto step = shaped_step(spec_.shape, spec_.shape_exponent, index, members_count);
295 auto at = utility_detail::saturating_sample_add(base, spacing * step);
296 if (spec_.timing_jitter_samples > 0) {
297 const auto lane = static_cast<std::uint64_t>(
298 utility_detail::key_index(slot->event.channel(), slot->event.note()));
300 at, static_cast<std::int64_t>(
301 draw(spec_.seed, slot->arrival, lane, kTimingStream,
302 static_cast<std::uint64_t>(spec_.timing_jitter_samples) + 1)));
303 }
304 slot->scheduled = std::max(at, slot->arrival);
305 slot->order = static_cast<std::uint8_t>(index);
306 slot->scheduled_yet = true;
307 if (spec_.velocity_jitter != 0)
308 apply_velocity_jitter(*slot);
309 if (spec_.velocity_tilt != 0)
310 apply_velocity_tilt(*slot, index, members_count);
311 }
312 alternate_flip_ = !alternate_flip_;
313 }
314
315 void apply_velocity_jitter(Slot& slot) noexcept {
316 const auto lane = static_cast<std::uint64_t>(
317 utility_detail::key_index(slot.event.channel(), slot.event.note()));
318 const auto span = static_cast<std::uint64_t>(spec_.velocity_jitter) * 2 + 1;
319 const auto value = static_cast<std::int32_t>(
320 draw(spec_.seed, slot.arrival, lane, kVelocityStream, span));
321 const auto offset = value - static_cast<std::int32_t>(spec_.velocity_jitter);
322 const auto velocity = static_cast<std::uint8_t>(
323 std::clamp(static_cast<std::int32_t>(slot.event.velocity()) + offset, 1, 127));
324 auto shaped = MidiEvent::note_on(slot.event.channel(), slot.event.note(), velocity);
325 shaped.timestamp = slot.event.timestamp;
326 slot.event = shaped;
327 }
328
329 void apply_velocity_tilt(Slot& slot, std::size_t index, std::size_t count) noexcept {
330 if (count <= 1)
331 return;
332 const auto numerator = static_cast<std::int32_t>(index);
333 const auto denominator = static_cast<std::int32_t>(count - 1);
334 const auto tilt = static_cast<std::int32_t>(spec_.velocity_tilt);
335 const auto offset = (tilt * numerator) / denominator;
336 const auto velocity = static_cast<std::uint8_t>(
337 std::clamp(static_cast<std::int32_t>(slot.event.velocity()) + offset, 1, 127));
338 auto shaped = MidiEvent::note_on(slot.event.channel(), slot.event.note(), velocity);
339 shaped.timestamp = slot.event.timestamp;
340 slot.event = shaped;
341 }
342
351 template <typename Less>
352 static void stable_order(std::array<Slot*, MaximumClusterNotes>& members, std::size_t count,
353 Less less) noexcept {
354 for (std::size_t i = 1; i < count; ++i) {
355 Slot* const held = members[i];
356 std::size_t j = i;
357 // Strictly-less keeps equal keys in their original order.
358 while (j > 0 && less(held, members[j - 1])) {
359 members[j] = members[j - 1];
360 --j;
361 }
362 members[j] = held;
363 }
364 }
365
366 void order_members(std::array<Slot*, MaximumClusterNotes>& members,
367 std::size_t count) noexcept {
368 switch (effective_direction()) {
370 // Arrival order is already the buffer order; ties keep input order.
371 stable_order(members, count,
372 [](const Slot* a, const Slot* b) { return a->arrival < b->arrival; });
373 return;
375 stable_order(members, count, [](const Slot* a, const Slot* b) {
376 return a->event.note() < b->event.note();
377 });
378 return;
380 stable_order(members, count, [](const Slot* a, const Slot* b) {
381 return a->event.note() > b->event.note();
382 });
383 return;
385 break;
387 break;
388 }
389 // Random: a seeded key per member gives a stable permutation for this
390 // cluster coordinate without any mutable generator state.
391 stable_order(members, count, [this](const Slot* a, const Slot* b) {
392 return random_key(*a) < random_key(*b);
393 });
394 }
395
396 StrumDirection effective_direction() const noexcept {
397 if (spec_.direction != StrumDirection::Alternate)
398 return spec_.direction;
399 return alternate_flip_ ? StrumDirection::Down : StrumDirection::Up;
400 }
401
402 std::uint64_t random_key(const Slot& slot) const noexcept {
403 const auto lane =
404 static_cast<std::uint64_t>(utility_detail::key_index(slot.event.channel(), slot.event.note()));
405 const timebase::RandomCoordinate point{timebase::TickPosition{cluster_start_}, lane, 0,
407 return timebase::coordinate_random(spec_.seed, point);
408 }
409
410 void advance_to(std::int64_t boundary, const Block& block, MidiBuffer& output,
411 MidiUtilityProcessReport& report) noexcept {
412 if (open_ && boundary - cluster_start_ > spec_.window_samples)
413 close_cluster(block);
414 flush_scheduled(boundary, block, output, report);
415 }
416
417 void flush_scheduled(std::int64_t boundary, const Block& block, MidiBuffer& output,
418 MidiUtilityProcessReport& report) noexcept {
419 while (true) {
420 Slot* earliest = nullptr;
421 for (auto& slot : cluster_) {
422 if (!slot.active || !slot.scheduled_yet || slot.scheduled >= boundary)
423 continue;
424 if (earliest == nullptr || slot.scheduled < earliest->scheduled ||
425 (slot.scheduled == earliest->scheduled && slot.order < earliest->order))
426 earliest = &slot;
427 }
428 if (earliest == nullptr)
429 return;
430 if (!emit_slot(*earliest, earliest->scheduled, block, output, report))
431 return;
432 }
433 }
434
435 void force_out(std::uint8_t channel, std::uint8_t note, std::int64_t before, const Block& block,
436 MidiBuffer& output, MidiUtilityProcessReport& report) noexcept {
437 for (auto& slot : cluster_) {
438 if (!slot.active || slot.event.channel() != channel || slot.event.note() != note)
439 continue;
440 const auto at = slot.scheduled_yet ? std::min(slot.scheduled, before) : before;
441 emit_slot(slot, at, block, output, report);
442 }
443 }
444
445 bool emit_slot(Slot& slot, std::int64_t at, const Block& block, MidiBuffer& output,
446 MidiUtilityProcessReport& report) noexcept {
447 const auto offset = note_schedule::offset_in_block(block, at);
448 if (!utility_detail::emit(output, utility_detail::at(slot.event, offset), report))
449 return false;
450 slot = {};
451 if (count_ != 0)
452 --count_;
453 return true;
454 }
455
456 Slot* free_slot() noexcept {
457 for (auto& slot : cluster_)
458 if (!slot.active)
459 return &slot;
460 return nullptr;
461 }
462
463 StrumSpec spec_{};
464 bool valid_ = true;
465 std::optional<StrumSpec> pending_spec_{};
466 std::array<Slot, MaximumClusterNotes> cluster_{};
467 std::size_t count_ = 0;
468 std::int64_t cluster_start_ = 0;
469 bool open_ = false;
470 bool alternate_flip_ = false;
471};
472
473} // namespace pulp::midi
Collection of timestamped MIDI events within a single audio buffer period.
Definition buffer.hpp:31
note_schedule::Block Block
Definition strum.hpp:68
constexpr Strum(StrumSpec spec={}) noexcept
Definition strum.hpp:129
constexpr StrumSpec spec() const noexcept
Definition strum.hpp:136
static constexpr std::uint64_t kVelocityStream
Definition strum.hpp:70
MidiUtilityProcessReport reset(MidiBuffer &output, const Block &block) noexcept
Definition strum.hpp:209
MidiUtilityProcessReport process(const MidiBuffer &input, MidiBuffer &output, const Block &block) noexcept
Definition strum.hpp:140
static std::int64_t shaped_step(StrumShape shape, double exponent, std::size_t index, std::size_t count) noexcept
Position of the note at ordered index index in a cluster of count, expressed in whole spacing steps.
Definition strum.hpp:98
bool empty() const noexcept
Definition strum.hpp:230
constexpr bool valid() const noexcept
Definition strum.hpp:132
MidiUtilityProcessReport flush(MidiBuffer &output, const Block &block) noexcept
Emit every buffered and scheduled attack immediately.
Definition strum.hpp:180
static constexpr std::uint64_t kOrderStream
Definition strum.hpp:71
static constexpr bool valid_spec(StrumSpec spec) noexcept
Definition strum.hpp:79
static std::int64_t shaped_step(StrumShape shape, std::size_t index, std::size_t count) noexcept
Legacy shape-only helper; preserves the quadratic curve law.
Definition strum.hpp:124
static constexpr std::uint64_t kTimingStream
Definition strum.hpp:69
static constexpr MidiUtilityContract contract() noexcept
Definition strum.hpp:73
static std::int64_t spacing_samples(StrumSpec spec, const Block &block) noexcept
Sample stride between adjacent notes of a linear spread.
Definition strum.hpp:89
MidiUtilityProcessReport replace_spec(StrumSpec spec, MidiBuffer &output, const Block &block) noexcept
Definition strum.hpp:213
@ count
sentinel: number of real capabilities (keep LAST)
Definition canvas_capability.hpp:38
MidiEvent at(MidiEvent event, std::int32_t sample_offset) noexcept
Definition utility_contract.hpp:140
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
StrumShape
How the spacing is distributed across the spread.
Definition strum.hpp:27
@ Linear
Definition strum.hpp:28
@ Accelerate
Notes bunch at the start and open out.
Definition strum.hpp:30
@ Decelerate
Notes open wide and close up.
Definition strum.hpp:32
@ FlushOnDiscontinuity
Definition utility_contract.hpp:33
@ FailOpenBalanced
Definition utility_contract.hpp:21
StrumDirection
Definition strum.hpp:15
@ Down
Definition strum.hpp:17
@ Up
Definition strum.hpp:16
@ Random
Seeded shuffle, stable for a given cluster coordinate.
Definition strum.hpp:23
@ Alternate
Flip between up and down on each successive cluster.
Definition strum.hpp:19
@ AsPlayed
Keep the order the notes arrived in.
Definition strum.hpp:21
@ Division
Definition note_delay.hpp:14
@ Milliseconds
Definition note_delay.hpp:15
StrumSpacingSync
Definition strum.hpp:35
@ Division
Definition strum.hpp:36
@ Milliseconds
Definition strum.hpp:37
@ InputStable
Definition utility_contract.hpp:26
@ Down
Definition arpeggiator.hpp:22
@ Up
Definition arpeggiator.hpp:21
@ Random
Definition arpeggiator.hpp:25
@ AsPlayed
Definition arpeggiator.hpp:24
@ a
Definition pitch.hpp:29
@ b
Definition pitch.hpp:31
@ less
Definition gpu_pass.hpp:287
@ index
Definition gpu_pass.hpp:140
@ i
Definition formant_filter_bank.hpp:20
constexpr std::uint64_t multiply_high(std::uint64_t lhs, std::uint64_t rhs) noexcept
Definition coordinate_random.hpp:31
constexpr std::int64_t kTicksPerQuarter
Definition tick.hpp:54
constexpr std::uint64_t coordinate_random(std::uint64_t seed, RandomCoordinate coordinate) noexcept
Definition coordinate_random.hpp:47
@ value
the widget's primary value (Knob, Fader, RangeSlider, Toggle, ProgressBar)
Definition binding_diagnostics.hpp:19
@ j
Definition input_events.hpp:40
@ spacing
spacing: map — dimension values.
Definition token_lock.hpp:64
Timestamped MIDI event combining a choc::midi::ShortMessage with timing info.
Definition message.hpp:32
static MidiEvent note_on(uint8_t channel, uint8_t note, uint8_t velocity)
Create a Note On event.
Definition message.hpp:46
Definition utility_contract.hpp:36
Definition utility_contract.hpp:46
bool complete
Definition utility_contract.hpp:50
std::size_t dropped
Definition utility_contract.hpp:48
std::size_t deferred
Definition utility_contract.hpp:49
Spread a near-simultaneous cluster of notes across time.
Definition strum.hpp:47
std::int64_t timing_jitter_samples
Forward-only timing jitter per note, in samples.
Definition strum.hpp:58
std::int64_t spacing_milliseconds
Definition strum.hpp:56
StrumSpacingSync sync
Definition strum.hpp:52
timebase::TickDuration spacing_interval
Definition strum.hpp:55
std::int64_t window_samples
Cluster window in samples: how close two notes must be to be one chord.
Definition strum.hpp:54
double shape_exponent
Continuous spread exponent. Two preserves the legacy curved shapes.
Definition strum.hpp:51
std::int16_t velocity_tilt
Signed velocity tilt across the ordered cluster, in MIDI velocity units.
Definition strum.hpp:61
constexpr auto operator<=>(const StrumSpec &) const =default
std::uint8_t velocity_jitter
Definition strum.hpp:59
std::uint64_t seed
Definition strum.hpp:62
StrumShape shape
Definition strum.hpp:49
StrumDirection direction
Definition strum.hpp:48
Definition tick.hpp:56