Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
humanize.hpp
Go to the documentation of this file.
1#pragma once
2
5
6#include <algorithm>
7#include <array>
8#include <cstddef>
9#include <cstdint>
10#include <optional>
11
12namespace pulp::midi {
13
27 std::int64_t timing_samples = 0;
28 std::uint8_t velocity_amount = 0;
29 std::uint64_t seed = 0;
30 std::int64_t minimum_timing_samples = 0;
31 constexpr auto operator<=>(const HumanizeSpec&) const = default;
32};
33
39template <std::size_t MaximumPendingAttacks = 128> class Humanize {
40 public:
43 static constexpr std::uint64_t kTimingStream = 1;
44 static constexpr std::uint64_t kVelocityStream = 2;
45
51
52 static constexpr bool valid_spec(HumanizeSpec spec) noexcept {
53 return spec.minimum_timing_samples >= 0 &&
54 spec.minimum_timing_samples <= spec.timing_samples && spec.velocity_amount <= 127;
55 }
56
59 static constexpr bool is_identity(HumanizeSpec spec) noexcept {
60 return valid_spec(spec) && spec.timing_samples == 0 && spec.velocity_amount == 0;
61 }
62
63 explicit constexpr Humanize(HumanizeSpec spec = {}) noexcept
64 : spec_(spec), valid_(valid_spec(spec)) {}
65
66 constexpr bool valid() const noexcept {
67 return valid_;
68 }
69
70 constexpr HumanizeSpec spec() const noexcept {
71 return spec_;
72 }
73
78 static std::int64_t jittered_position(HumanizeSpec spec, std::uint8_t channel,
79 std::uint8_t note, std::int64_t absolute) noexcept {
80 if (spec.timing_samples <= 0)
81 return absolute;
82 const auto width =
83 static_cast<std::uint64_t>(spec.timing_samples - spec.minimum_timing_samples) + 1;
84 const auto draw = draw_value(spec.seed, channel, note, absolute, kTimingStream, width);
85 return utility_detail::saturating_sample_add(absolute, spec.minimum_timing_samples +
86 static_cast<std::int64_t>(draw));
87 }
88
92 static std::uint8_t jittered_velocity(HumanizeSpec spec, std::uint8_t channel,
93 std::uint8_t note, std::int64_t absolute,
94 std::uint8_t velocity) noexcept {
95 if (spec.velocity_amount == 0)
96 return velocity;
97 const auto span = static_cast<std::uint64_t>(spec.velocity_amount) * 2 + 1;
98 const auto draw = draw_value(spec.seed, channel, note, absolute, kVelocityStream, span);
99 const auto offset =
100 static_cast<std::int32_t>(draw) - static_cast<std::int32_t>(spec.velocity_amount);
101 return static_cast<std::uint8_t>(std::clamp(static_cast<std::int32_t>(velocity) + offset, 1, 127));
102 }
103
105 timebase::SamplePosition block_start,
106 std::int32_t block_samples) noexcept {
107 if (utility_detail::blocks_alias(input, output))
108 return {0, input.size(), 0, false};
111 if (!utility_detail::ready(output) || !valid_ || block_samples < 0) {
112 report.dropped = input.size();
113 report.complete = false;
114 return report;
115 }
116 const std::int64_t block_end =
117 utility_detail::saturating_sample_add(block_start.value, block_samples);
118 for (const auto& event : input) {
119 const auto absolute =
120 utility_detail::saturating_sample_add(block_start.value, event.sample_offset);
121 emit_due_before(absolute, block_start, output, report);
122 const bool is_attack = event.is_note_on() && event.velocity() != 0;
123 const bool is_release =
124 event.is_note_off() || (event.is_note_on() && event.velocity() == 0);
125 if (is_attack) {
126 schedule_attack(event, absolute, block_start, output, report);
127 } else if (is_release) {
128 // An authored release can arrive before its own jittered attack
129 // has fired. Pulling the attack back to just before the release
130 // keeps the note audible and keeps the pair balanced instead of
131 // emitting an orphaned note-off.
132 const int key = utility_detail::key_index(event.channel(), event.note());
133 clamp_pending_against_release(key, absolute, block_start, output, report);
134 utility_detail::emit(output, event, report);
135 } else {
136 utility_detail::emit(output, event, report);
137 }
138 }
139 emit_due_before(block_end, block_start, output, report);
140 utility_detail::copy_sidecars(input, output, report);
141 output.sort();
142 return report;
143 }
144
151 if (!utility_detail::ready(output)) {
152 report.complete = false;
153 return report;
154 }
155 for (auto& slot : pending_) {
156 if (!slot.active)
157 continue;
158 if (!emit_pending(slot, slot.scheduled, block_start, output, report)) {
159 ++report.deferred;
160 report.complete = false;
161 return report;
162 }
163 }
164 report.complete = empty();
165 if (report.complete && pending_spec_) {
166 spec_ = *pending_spec_;
167 valid_ = true;
168 pending_spec_.reset();
169 }
170 return report;
171 }
172
174 timebase::SamplePosition block_start) noexcept {
175 return flush(output, block_start);
176 }
177
179 timebase::SamplePosition block_start) noexcept {
180 if (!valid_spec(spec)) {
182 return {0, 0, 0, false};
183 }
184 auto report = flush(output, block_start);
185 if (report.complete) {
186 spec_ = spec;
187 valid_ = true;
188 pending_spec_.reset();
189 } else {
190 pending_spec_ = spec;
191 }
192 return report;
193 }
194
200 if (!valid_spec(spec))
201 return false;
202 spec_ = spec;
203 valid_ = true;
204 pending_spec_.reset();
205 return true;
206 }
207
208 bool empty() const noexcept {
209 for (const auto& slot : pending_)
210 if (slot.active)
211 return false;
212 return true;
213 }
214
215 private:
216 struct Pending {
217 MidiEvent event{};
218 std::int64_t scheduled = 0;
219 int key = 0;
220 bool active = false;
221 };
222
223 Pending* find_pending(int key) noexcept {
224 for (auto& slot : pending_)
225 if (slot.active && slot.key == key)
226 return &slot;
227 return nullptr;
228 }
229
230 Pending* free_pending() noexcept {
231 for (auto& slot : pending_)
232 if (!slot.active)
233 return &slot;
234 return nullptr;
235 }
236
237 static std::uint64_t draw_value(std::uint64_t seed, std::uint8_t channel, std::uint8_t note,
238 std::int64_t absolute, std::uint64_t stream,
239 std::uint64_t span) noexcept {
240 const timebase::RandomCoordinate coordinate{
241 timebase::TickPosition{absolute},
242 static_cast<std::uint64_t>(utility_detail::key_index(channel, note)), 0, stream};
243 const auto value = timebase::coordinate_random(seed, coordinate);
244 return span == 0 ? 0 : timebase::detail::multiply_high(value, span);
245 }
246
247 void schedule_attack(const MidiEvent& event, std::int64_t absolute,
248 timebase::SamplePosition block_start, MidiBuffer& output,
249 MidiUtilityProcessReport& report) noexcept {
250 const auto key = utility_detail::key_index(event.channel(), event.note());
251 // One key holds at most one attack in flight; a re-attack forces the
252 // earlier one out first so the pair order is never inverted.
253 if (auto* existing = find_pending(key); existing != nullptr)
254 emit_pending(*existing, absolute, block_start, output, report);
255 auto* slot = free_pending();
256 if (slot == nullptr) {
257 // FailOpenBalanced: with no slot to hold the jitter, the attack is
258 // forwarded unchanged rather than dropped, so the player's note
259 // always sounds and its release always matches something.
260 utility_detail::emit(output, event, report);
261 ++report.deferred;
262 return;
263 }
264 const auto velocity =
265 jittered_velocity(spec_, event.channel(), event.note(), absolute, event.velocity());
266 auto shaped = MidiEvent::note_on(event.channel(), event.note(), velocity);
267 shaped.timestamp = event.timestamp;
268 *slot = {shaped, jittered_position(spec_, event.channel(), event.note(), absolute), key,
269 true};
270 }
271
272 void clamp_pending_against_release(int key, std::int64_t release_absolute,
273 timebase::SamplePosition block_start, MidiBuffer& output,
274 MidiUtilityProcessReport& report) noexcept {
275 auto* slot = find_pending(key);
276 if (slot == nullptr || slot->scheduled < release_absolute)
277 return;
278 const auto latest = release_absolute > 0 ? release_absolute - 1 : release_absolute;
279 emit_pending(*slot, std::min(slot->scheduled, latest), block_start, output, report);
280 }
281
282 void emit_due_before(std::int64_t boundary, timebase::SamplePosition block_start,
283 MidiBuffer& output, MidiUtilityProcessReport& report) noexcept {
284 for (auto& slot : pending_)
285 if (slot.active && slot.scheduled < boundary)
286 emit_pending(slot, slot.scheduled, block_start, output, report);
287 }
288
289 bool emit_pending(Pending& slot, std::int64_t at_absolute,
290 timebase::SamplePosition block_start, MidiBuffer& output,
291 MidiUtilityProcessReport& report) noexcept {
292 if (!slot.active)
293 return true;
294 const auto offset = at_absolute - block_start.value;
295 const auto clamped = static_cast<std::int32_t>(
296 std::clamp<std::int64_t>(offset, 0, std::numeric_limits<std::int32_t>::max()));
297 if (!utility_detail::emit(output, utility_detail::at(slot.event, clamped), report))
298 return false;
299 slot = {};
300 return true;
301 }
302
303 HumanizeSpec spec_{};
304 bool valid_ = true;
305 std::optional<HumanizeSpec> pending_spec_{};
306 std::array<Pending, MaximumPendingAttacks> pending_{};
307};
308
309} // namespace pulp::midi
MidiUtilityProcessReport flush(MidiBuffer &output, timebase::SamplePosition block_start) noexcept
Emit every attack still waiting in the jitter window.
Definition humanize.hpp:148
static std::int64_t jittered_position(HumanizeSpec spec, std::uint8_t channel, std::uint8_t note, std::int64_t absolute) noexcept
The absolute sample position at which absolute is emitted, derived only from the spec and the event's...
Definition humanize.hpp:78
static constexpr MidiUtilityContract contract() noexcept
Definition humanize.hpp:46
MidiUtilityProcessReport process(const MidiBuffer &input, MidiBuffer &output, timebase::SamplePosition block_start, std::int32_t block_samples) noexcept
Definition humanize.hpp:104
static constexpr bool is_identity(HumanizeSpec spec) noexcept
True when the spec cannot change any event, which is the kernel's declared bypass identity.
Definition humanize.hpp:59
bool update_spec_for_future_attacks(HumanizeSpec spec) noexcept
Change the spec used to schedule future attacks without disturbing attacks already materialized in th...
Definition humanize.hpp:199
constexpr bool valid() const noexcept
Definition humanize.hpp:66
static constexpr bool valid_spec(HumanizeSpec spec) noexcept
Definition humanize.hpp:52
constexpr HumanizeSpec spec() const noexcept
Definition humanize.hpp:70
static constexpr std::uint64_t kTimingStream
Random streams are separated so changing one amount cannot shift the other's draw for the same note.
Definition humanize.hpp:43
bool empty() const noexcept
Definition humanize.hpp:208
static constexpr std::uint64_t kVelocityStream
Definition humanize.hpp:44
MidiUtilityProcessReport replace_spec(HumanizeSpec spec, MidiBuffer &output, timebase::SamplePosition block_start) noexcept
Definition humanize.hpp:178
constexpr Humanize(HumanizeSpec spec={}) noexcept
Definition humanize.hpp:63
MidiUtilityProcessReport reset(MidiBuffer &output, timebase::SamplePosition block_start) noexcept
Definition humanize.hpp:173
static std::uint8_t jittered_velocity(HumanizeSpec spec, std::uint8_t channel, std::uint8_t note, std::int64_t absolute, std::uint8_t velocity) noexcept
The velocity velocity becomes, derived only from the spec and the event's own coordinate.
Definition humanize.hpp:92
Collection of timestamped MIDI events within a single audio buffer period.
Definition buffer.hpp:31
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
@ FlushOnDiscontinuity
Definition utility_contract.hpp:33
@ FailOpenBalanced
Definition utility_contract.hpp:21
@ InputStable
Definition utility_contract.hpp:26
constexpr std::uint64_t multiply_high(std::uint64_t lhs, std::uint64_t rhs) noexcept
Definition coordinate_random.hpp:31
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
Seeded timing and velocity jitter over note attacks.
Definition humanize.hpp:26
std::uint64_t seed
Definition humanize.hpp:29
std::uint8_t velocity_amount
Definition humanize.hpp:28
std::int64_t timing_samples
Definition humanize.hpp:27
std::int64_t minimum_timing_samples
Definition humanize.hpp:30
constexpr auto operator<=>(const HumanizeSpec &) const =default
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
Definition rational_time.hpp:9