Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
quantize.hpp
Go to the documentation of this file.
1#pragma once
2
4
5#include <algorithm>
6#include <cmath>
7#include <cstdint>
8
9namespace pulp::timebase {
10
11inline constexpr double kBoundaryEpsilonBeats = 1.0e-9;
12
13inline bool valid_sample_rate(double sample_rate) noexcept {
14 return sample_rate > 0.0 && std::isfinite(sample_rate);
15}
16
17inline bool valid_tempo(double tempo_bpm) noexcept {
18 return tempo_bpm > 0.0 && std::isfinite(tempo_bpm);
19}
20
21inline bool valid_grid(double grid_beats) noexcept {
22 return grid_beats > 0.0 && std::isfinite(grid_beats);
23}
24
25inline double beats_per_bar(int numerator, int denominator) noexcept {
26 if (numerator <= 0 || denominator <= 0)
27 return 0.0;
28 return static_cast<double>(numerator) * (4.0 / static_cast<double>(denominator));
29}
30
31inline double frames_to_beats(double frames, double sample_rate, double tempo_bpm) noexcept {
32 return (frames / sample_rate) * (tempo_bpm / 60.0);
33}
34
35inline double beats_to_frames(double beats, double sample_rate, double tempo_bpm) noexcept {
36 return (beats * 60.0 / tempo_bpm) * sample_rate;
37}
38
39inline double next_grid_boundary(double position_beats, double grid_beats) noexcept {
40 const auto index = std::ceil((position_beats - kBoundaryEpsilonBeats) / grid_beats);
41 return index * grid_beats;
42}
43
44// Swing displaces the interior boundary of every pair of grid cells. In a pair
45// spanning `2 * grid` ticks the boundary sits at `grid` when the feel is
46// straight; swing moves it to `ratio` of the pair and warps the material on
47// either side linearly onto the new halves. The whole passage leans, not only
48// the notes that happen to land exactly on the off-beat.
49//
50// The ratio is an exact rational rather than a double because the result is a
51// document-visible tick. Two machines must agree on that tick exactly, and a
52// double ratio would make the agreement depend on how a division rounds.
53struct SwingRatio {
54 std::int64_t numerator = 1;
55 std::int64_t denominator = 2;
56 constexpr auto operator<=>(const SwingRatio&) const = default;
57};
58
59// The straight feel. swing_position() with this ratio is the identity on every
60// tick, not merely close to it.
61inline constexpr SwingRatio kStraightSwing{1, 2};
62
63// The classic "hard" eighth-note shuffle: the off-beat lands on the last
64// triplet of the pair.
65inline constexpr SwingRatio kTripletSwing{2, 3};
66
67// The bounds keep every intermediate product inside the signed 64-bit domain.
68// The widest is `local * pivot`, and both factors are smaller than a pair.
69inline constexpr std::int64_t kMaxSwingGridTicks = 1'000'000'000;
70inline constexpr std::int64_t kMaxSwingDenominator = 1'000'000;
71
72inline constexpr bool valid_swing_grid(TickDuration grid) noexcept {
73 return grid.value > 0 && grid.value <= kMaxSwingGridTicks;
74}
75
76// A ratio of 0 or 1 collapses one half of every pair onto a single instant.
77// That is not an extreme feel but a loss of order, so the interval is open at
78// both ends rather than clamped into range.
79inline constexpr bool valid_swing_ratio(SwingRatio ratio) noexcept {
80 return ratio.denominator > 0 && ratio.denominator <= kMaxSwingDenominator &&
81 ratio.numerator > 0 && ratio.numerator < ratio.denominator;
82}
83
84namespace detail {
85
86constexpr std::int64_t floor_div(std::int64_t numerator, std::int64_t denominator) noexcept {
87 const auto quotient = numerator / denominator;
88 const auto remainder = numerator % denominator;
89 return (remainder != 0 && ((numerator < 0) != (denominator < 0))) ? quotient - 1 : quotient;
90}
91
92// Half-up rounding over non-negative operands. Every swing quantity is an
93// offset inside one pair, so the negative case does not arise here and no
94// policy is invented for it.
95constexpr std::int64_t rounded_scale(std::int64_t value, std::int64_t multiplier,
96 std::int64_t divisor) noexcept {
97 return (value * multiplier + divisor / 2) / divisor;
98}
99
100// Where the interior boundary lands, in ticks from the start of the pair.
101// Clamped away from both ends because a pair only has so many ticks to spend:
102// at a coarse tick resolution an extreme ratio would otherwise round onto a
103// pair boundary and erase one half of the warp.
104constexpr std::int64_t swing_pivot(std::int64_t pair, SwingRatio ratio) noexcept {
105 const auto pivot = rounded_scale(pair, ratio.numerator, ratio.denominator);
106 return std::clamp<std::int64_t>(pivot, 1, pair - 1);
107}
108
109} // namespace detail
110
111// The bounded displacement applied by swing_position(). Keeping the delta
112// available separately lets higher-level timing transforms combine it with
113// other authored displacements before saturating the final tick position.
115 SwingRatio ratio) noexcept {
116 if (!valid_swing_grid(grid) || !valid_swing_ratio(ratio))
117 return {};
118 const auto pair = grid.value * 2;
119 const auto pivot = detail::swing_pivot(pair, ratio);
120 auto local = position.value % pair;
121 if (local < 0)
122 local += pair;
123 const auto warped =
124 local < grid.value
125 ? detail::rounded_scale(local, pivot, grid.value)
126 : pivot + detail::rounded_scale(local - grid.value, pair - pivot, grid.value);
127 return {warped - local};
128}
129
130// Contract, for a valid grid and ratio:
131// * pair boundaries are exact fixed points, and the grid point inside a pair
132// maps exactly onto the pivot;
133// * the map is non-decreasing, so material is never reordered;
134// * with kStraightSwing it is the identity on every tick, bit for bit.
135// It is not injective: the compressed half of each pair has fewer ticks to
136// land on than the half it came from, so unswing_position() recovers a
137// position only to within the bound that half's slope implies. An invalid grid
138// or ratio is the identity — the caller validates, and a bad setting must not
139// silently move music.
141 SwingRatio ratio) noexcept {
142 // Apply the displacement at the original position. Materializing the pair
143 // boundary first can underflow below INT64_MIN for a negative, non-boundary
144 // tick even though the final displaced position is representable.
145 return position + swing_displacement(position, grid, ratio);
146}
147
148// The left inverse of swing_position() under the same grid and ratio, to
149// within the rounding the compressed half forces.
151 SwingRatio ratio) noexcept {
152 if (!valid_swing_grid(grid) || !valid_swing_ratio(ratio))
153 return position;
154 const auto pair = grid.value * 2;
155 const auto pivot = detail::swing_pivot(pair, ratio);
156 auto local = position.value % pair;
157 if (local < 0)
158 local += pair;
159 const auto straightened =
160 local < pivot ? detail::rounded_scale(local, grid.value, pivot)
161 : grid.value + detail::rounded_scale(local - pivot, grid.value, pair - pivot);
162 return {detail::saturating_add(position.value, straightened - local)};
163}
164
165} // namespace pulp::timebase
constexpr std::int64_t saturating_add(std::int64_t lhs, std::int64_t rhs) noexcept
Definition tick.hpp:11
constexpr std::int64_t floor_div(std::int64_t numerator, std::int64_t denominator) noexcept
Definition quantize.hpp:86
constexpr std::int64_t swing_pivot(std::int64_t pair, SwingRatio ratio) noexcept
Definition quantize.hpp:104
constexpr std::int64_t rounded_scale(std::int64_t value, std::int64_t multiplier, std::int64_t divisor) noexcept
Definition quantize.hpp:95
Definition beat_division.hpp:11
bool valid_grid(double grid_beats) noexcept
Definition quantize.hpp:21
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
TickPosition swing_position(TickPosition position, TickDuration grid, SwingRatio ratio) noexcept
Definition quantize.hpp:140
constexpr double kBoundaryEpsilonBeats
Definition quantize.hpp:11
constexpr SwingRatio kTripletSwing
Definition quantize.hpp:65
constexpr std::int64_t kMaxSwingDenominator
Definition quantize.hpp:70
double beats_per_bar(int numerator, int denominator) noexcept
Definition quantize.hpp:25
double next_grid_boundary(double position_beats, double grid_beats) noexcept
Definition quantize.hpp:39
double beats_to_frames(double beats, double sample_rate, double tempo_bpm) noexcept
Definition quantize.hpp:35
TickPosition unswing_position(TickPosition position, TickDuration grid, SwingRatio ratio) noexcept
Definition quantize.hpp:150
constexpr std::int64_t kMaxSwingGridTicks
Definition quantize.hpp:69
bool valid_sample_rate(double sample_rate) noexcept
Definition quantize.hpp:13
double frames_to_beats(double frames, double sample_rate, double tempo_bpm) noexcept
Definition quantize.hpp:31
constexpr SwingRatio kStraightSwing
Definition quantize.hpp:61
Definition quantize.hpp:53
std::int64_t numerator
Definition quantize.hpp:54
std::int64_t denominator
Definition quantize.hpp:55
constexpr auto operator<=>(const SwingRatio &) const =default
Definition tick.hpp:56
Definition tick.hpp:61