Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
inline_groove_projector.hpp
Go to the documentation of this file.
1#pragma once
2
6
7#include <array>
8#include <cstddef>
9#include <cstdint>
10#include <limits>
11#include <span>
12#include <type_traits>
13
14namespace pulp::timebase {
15
16// Sixteen entries hold one repeat of a sixteenth-grid groove, which is the
17// granularity a groove table is authored at. The capacity is deliberately two
18// orders of magnitude below kMaximumGrooveKernelSteps: the whole point of this
19// type is to fit inside a value a realtime consumer copies wholesale, and an
20// inline table of a thousand entries is large enough to be the reason it does
21// not. A table needing more repeats than this belongs in the canonical model,
22// which owns persistence and has no such bound.
23inline constexpr std::size_t kMaximumInlineGrooveSteps = 16;
24
25// Construction input. The numeric domains are the canonical groove domains --
26// timing strength and velocity strength in per-mille, velocity scale in
27// per-mille up to kMaximumGrooveKernelVelocityScale, offsets in ticks -- so a
28// table authored against timeline::GrooveTemplate converts across losslessly.
30 // Zero disables swing. Swing and table grids are deliberately independent.
33 // Zero exactly when steps is empty.
35 std::span<const GrooveKernelStep> steps{};
38};
39
50
51// A trivially copyable projection of the canonical authored groove model onto a
52// realtime path: timeline::GrooveTemplate's arithmetic carried in the
53// fixed-capacity inline storage that a spec struct can hold and a realtime
54// consumer can hot-swap by value.
55//
56// ORDER PRESERVATION IS EXPLICITLY NOT PROVIDED, AND THAT IS THE CONTRACT.
57// Adjacent entries may lean in opposite directions, so material at the end of
58// one entry can be displaced past material at the start of the next. That is
59// what a groove table is, and the canonical model states the same thing:
60// offsets are bounded to less than one entry rather than constrained into
61// monotonicity. Callers that genuinely require a non-reordering feel want
62// OrderPreservingGrooveKernel, which is the strict subset of this model and
63// refuses at create() any table whose scaled offsets fall by more than one tick
64// between adjacent entries. Both types exist because those are two different
65// questions: a sequencer emitting by displaced position needs no ordering
66// guarantee, while a consumer that walks events in authored order and cannot
67// re-sort them does. Asking the strict kernel for an ordinary groove gets an
68// error rather than a feel, and asking this type for an ordering guarantee gets
69// silence rather than one.
70//
71// Swing and the table are two independent displacements of the same authored
72// position, and the table is indexed by where a note was written rather than by
73// where swing moved it, so changing the swing setting never re-assigns material
74// to a different entry.
75//
76// A timing strength of zero is exact identity on every position, swing
77// included, which is what lets a consumer define bypass as the zero-strength
78// result rather than as a separate code path.
79//
80// Validation happens once in create(), on the control thread. Projection reads
81// inline storage and allocates nothing.
83 public:
84 // The default value states no feel: it projects every position onto itself
85 // and leaves every velocity unscaled, so a spec struct holding one needs no
86 // separate "groove absent" flag.
87 constexpr InlineGrooveProjector() noexcept = default;
88
90 create(InlineGrooveInput input) noexcept {
91 if (input.swing_grid.value < 0 ||
92 (input.swing_grid.value != 0 && !valid_swing_grid(input.swing_grid)))
94 if (!valid_swing_ratio(input.swing))
96 // A table width and a table imply each other: a width with no entries
97 // names nothing, and entries with no width have no position to be read
98 // at.
99 if ((input.table_grid.value != 0) != !input.steps.empty() || input.table_grid.value < 0 ||
100 input.table_grid.value > kMaxSwingGridTicks)
102 if (input.steps.size() > kMaximumInlineGrooveSteps)
104 if (input.timing_strength < 0 || input.timing_strength > kGrooveKernelUnitScale ||
105 input.velocity_strength < 0 || input.velocity_strength > kGrooveKernelUnitScale)
107 for (const auto& step : input.steps) {
108 // An offset of a whole entry or more would move material past the
109 // entry beyond its neighbour, which is a different table written
110 // wrong rather than an extreme feel. Anything inside that bound is
111 // admitted, reordering included.
112 if (step.timing_offset.value <= -input.table_grid.value ||
113 step.timing_offset.value >= input.table_grid.value)
114 return runtime::Err(InlineGrooveError::InvalidTable);
115 if (step.velocity_scale < 0 || step.velocity_scale > kMaximumGrooveKernelVelocityScale)
116 return runtime::Err(InlineGrooveError::InvalidVelocityScale);
117 }
118 return runtime::Ok(InlineGrooveProjector(input));
119 }
120
121 constexpr TickDuration swing_grid() const noexcept {
122 return swing_grid_;
123 }
124 constexpr SwingRatio swing() const noexcept {
125 return swing_;
126 }
127 constexpr TickDuration table_grid() const noexcept {
128 return table_grid_;
129 }
130 constexpr std::span<const GrooveKernelStep> steps() const noexcept {
131 return {steps_.data(), size_};
132 }
133 constexpr std::int32_t timing_strength() const noexcept {
134 return timing_strength_;
135 }
136 constexpr std::int32_t velocity_strength() const noexcept {
137 return velocity_strength_;
138 }
139
140 // Whether timing and velocity projection are both the identity.
141 constexpr bool states_no_feel() const noexcept {
142 return swing_grid_.value == 0 && size_ == 0;
143 }
144
145 // The sounding position for an authored one. Swing and table offsets can
146 // oppose one another, so their bounded deltas combine before the single
147 // position add and cancellation is preserved.
148 //
149 // The result is checked rather than saturating: a position close enough to
150 // the signed limit for the displacement to leave the domain reports
151 // RangeExceeded instead of clamping onto the rail, which is the same
152 // contract OrderPreservingGrooveKernel states. This is the one place the
153 // projection differs from timeline::GrooveTemplate, whose position add
154 // saturates; every position where that add does not rail projects
155 // identically in both.
157 apply_timing(TickPosition authored) const noexcept {
158 std::int64_t displacement = 0;
159 if (swing_grid_.value != 0)
160 displacement = scaled_by_strength(
161 swing_displacement(authored, swing_grid_, swing_).value, timing_strength_);
162 if (const auto* step = step_at(authored))
163 displacement += scaled_by_strength(step->timing_offset.value, timing_strength_);
164 std::int64_t transformed = 0;
165 if (!checked_add(authored.value, displacement, transformed))
167 return runtime::Ok(TickPosition{transformed});
168 }
169
170 // The accent multiplier at an authored position, in per-mille.
171 constexpr std::int32_t velocity_scale_at(TickPosition authored) const noexcept {
172 const auto* step = step_at(authored);
173 if (step == nullptr)
175 const auto deviation =
176 static_cast<std::int64_t>(step->velocity_scale) - kGrooveKernelUnitScale;
177 return static_cast<std::int32_t>(kGrooveKernelUnitScale +
178 scaled_by_strength(deviation, velocity_strength_));
179 }
180
181 private:
182 explicit constexpr InlineGrooveProjector(InlineGrooveInput input) noexcept
183 : swing_grid_(input.swing_grid), swing_(input.swing), table_grid_(input.table_grid),
184 size_(input.steps.size()), timing_strength_(input.timing_strength),
185 velocity_strength_(input.velocity_strength) {
186 for (std::size_t index = 0; index < size_; ++index)
187 steps_[index] = input.steps[index];
188 }
189
190 static constexpr bool checked_add(std::int64_t lhs, std::int64_t rhs,
191 std::int64_t& result) noexcept {
192 constexpr auto min = std::numeric_limits<std::int64_t>::min();
193 constexpr auto max = std::numeric_limits<std::int64_t>::max();
194 if ((rhs > 0 && lhs > max - rhs) || (rhs < 0 && lhs < min - rhs))
195 return false;
196 result = lhs + rhs;
197 return true;
198 }
199
200 // Scale by a per-mille strength, rounding halves away from zero so a
201 // positive and a negative offset of the same size are attenuated equally;
202 // truncation would bias every groove toward zero displacement. A strength
203 // of zero therefore scales every offset to exactly zero, which is what
204 // makes the zero-strength projection the exact identity.
205 //
206 // create() bounds both call sites -- an offset smaller than a table entry,
207 // a swing displacement smaller than a swing grid, each at most
208 // kMaxSwingGridTicks, or a velocity deviation smaller than the scale
209 // ceiling -- so the product with a strength of at most
210 // kGrooveKernelUnitScale stays far inside the signed domain.
211 static constexpr std::int64_t scaled_by_strength(std::int64_t value,
212 std::int32_t strength) noexcept {
213 const auto magnitude = value < 0 ? -value : value;
214 const auto scaled =
215 (magnitude * strength + kGrooveKernelUnitScale / 2) / kGrooveKernelUnitScale;
216 return value < 0 ? -scaled : scaled;
217 }
218
219 // The entry an authored position falls in, or null when there is no table.
220 // The table repeats in both directions, so the index floors toward negative
221 // infinity rather than inheriting the sign of the dividend.
222 constexpr const GrooveKernelStep* step_at(TickPosition authored) const noexcept {
223 if (size_ == 0 || table_grid_.value <= 0)
224 return nullptr;
225 auto index = authored.value / table_grid_.value;
226 if (authored.value % table_grid_.value < 0)
227 --index;
228 auto slot = index % static_cast<std::int64_t>(size_);
229 if (slot < 0)
230 slot += static_cast<std::int64_t>(size_);
231 return &steps_[static_cast<std::size_t>(slot)];
232 }
233
234 TickDuration swing_grid_{};
235 SwingRatio swing_{};
236 TickDuration table_grid_{};
237 std::array<GrooveKernelStep, kMaximumInlineGrooveSteps> steps_{};
238 std::size_t size_ = 0;
239 std::int32_t timing_strength_ = kGrooveKernelUnitScale;
240 std::int32_t velocity_strength_ = kGrooveKernelUnitScale;
241};
242
243// The reason this type exists rather than the canonical model: a realtime
244// consumer hot-swaps its configuration as one trivially copyable value, and
245// timeline::GrooveTemplate holds a shared_ptr over heap storage, so it cannot
246// live in such a value at any size.
247static_assert(std::is_trivially_copyable_v<InlineGrooveProjector>);
248
249} // namespace pulp::timebase
Wrap a value as a failure result.
Definition result.hpp:58
Wrap a value as a success result.
Definition result.hpp:42
Result<T, E> — either holds a T or an E, never both.
Definition result.hpp:76
static runtime::Result< InlineGrooveProjector, InlineGrooveError > create(InlineGrooveInput input) noexcept
Definition inline_groove_projector.hpp:90
constexpr TickDuration table_grid() const noexcept
Definition inline_groove_projector.hpp:127
constexpr std::int32_t velocity_scale_at(TickPosition authored) const noexcept
Definition inline_groove_projector.hpp:171
constexpr std::span< const GrooveKernelStep > steps() const noexcept
Definition inline_groove_projector.hpp:130
constexpr bool states_no_feel() const noexcept
Definition inline_groove_projector.hpp:141
constexpr SwingRatio swing() const noexcept
Definition inline_groove_projector.hpp:124
constexpr std::int32_t timing_strength() const noexcept
Definition inline_groove_projector.hpp:133
runtime::Result< TickPosition, InlineGrooveError > apply_timing(TickPosition authored) const noexcept
Definition inline_groove_projector.hpp:157
constexpr InlineGrooveProjector() noexcept=default
constexpr std::int32_t velocity_strength() const noexcept
Definition inline_groove_projector.hpp:136
constexpr TickDuration swing_grid() const noexcept
Definition inline_groove_projector.hpp:121
@ index
Definition gpu_pass.hpp:140
Definition generated_event_source.hpp:33
Err(E) -> Err< E >
@ Ok
No error; bytes is authoritative.
Definition stream.hpp:32
Definition beat_division.hpp:11
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
@ InvalidSwing
Definition groove_kernel.hpp:43
@ InvalidTable
Definition groove_kernel.hpp:44
@ TooManySteps
Definition groove_kernel.hpp:45
@ InvalidStrength
Definition groove_kernel.hpp:46
@ InvalidSwingGrid
Definition groove_kernel.hpp:42
@ InvalidVelocityScale
Definition groove_kernel.hpp:47
constexpr std::size_t kMaximumInlineGrooveSteps
Definition inline_groove_projector.hpp:23
constexpr std::int64_t kMaxSwingGridTicks
Definition quantize.hpp:69
@ RangeExceeded
Definition beat_division.hpp:49
InlineGrooveError
Definition inline_groove_projector.hpp:40
@ InvalidSwing
Definition inline_groove_projector.hpp:43
@ InvalidTable
Definition inline_groove_projector.hpp:44
@ TooManySteps
Definition inline_groove_projector.hpp:45
@ RangeExceeded
Definition inline_groove_projector.hpp:48
@ InvalidStrength
Definition inline_groove_projector.hpp:46
@ InvalidSwingGrid
Definition inline_groove_projector.hpp:42
@ None
Definition grid_projection.hpp:97
constexpr std::int32_t kGrooveKernelUnitScale
Definition groove_kernel.hpp:18
constexpr SwingRatio kStraightSwing
Definition quantize.hpp:61
Result<T, E> — success / failure union for fallible operations.
Definition inline_groove_projector.hpp:29
TickDuration swing_grid
Definition inline_groove_projector.hpp:31
std::span< const GrooveKernelStep > steps
Definition inline_groove_projector.hpp:35
std::int32_t timing_strength
Definition inline_groove_projector.hpp:36
std::int32_t velocity_strength
Definition inline_groove_projector.hpp:37
SwingRatio swing
Definition inline_groove_projector.hpp:32
TickDuration table_grid
Definition inline_groove_projector.hpp:34
Definition quantize.hpp:53
Definition tick.hpp:56
Definition tick.hpp:61