Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
note_delay.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <pulp/midi/detail/note_schedule.hpp>
4
5#include <algorithm>
6#include <array>
7#include <cstddef>
8#include <cstdint>
9#include <optional>
10
11namespace pulp::midi {
12
13enum class NoteDelaySync : std::uint8_t {
16};
17
29 std::int64_t milliseconds = 250;
30 std::uint8_t repeats = 0;
35 std::uint8_t gate_percent = 0;
36 std::uint8_t velocity_decay_percent = 70;
37 std::int8_t transpose_semitones = 0;
38 constexpr auto operator<=>(const NoteDelaySpec&) const = default;
39};
40
45template <std::size_t MaximumScheduledNotes = 128, std::size_t MaximumHeldSources = 64>
46class NoteDelay {
47 public:
48 using Block = note_schedule::Block;
49
55
56 static constexpr bool valid_spec(NoteDelaySpec spec) noexcept {
57 if (spec.gate_percent > 100 || spec.velocity_decay_percent > 100)
58 return false;
59 return spec.sync == NoteDelaySync::Division ? spec.interval.value > 0
60 : spec.milliseconds > 0;
61 }
62
65 static constexpr bool is_identity(NoteDelaySpec spec) noexcept {
66 return valid_spec(spec) && spec.repeats == 0;
67 }
68
72 static std::int64_t repeat_stride(NoteDelaySpec spec, const Block& block) noexcept {
73 return spec.sync == NoteDelaySync::Division
74 ? note_schedule::samples_for_duration(block, spec.interval)
75 : note_schedule::samples_for_milliseconds(block, spec.milliseconds);
76 }
77
81 static std::int64_t repeat_gate(NoteDelaySpec spec, const Block& block) noexcept {
82 if (spec.gate_percent == 0)
83 return 0;
84 const auto stride = repeat_stride(spec, block);
85 return std::max<std::int64_t>(1, (stride * static_cast<std::int64_t>(spec.gate_percent)) /
86 100);
87 }
88
90 static std::int64_t repeat_sample(NoteDelaySpec spec, const Block& block,
91 std::int64_t attack_absolute, std::size_t index) noexcept {
93 attack_absolute, repeat_stride(spec, block) * static_cast<std::int64_t>(index));
94 }
95
97 static constexpr std::uint8_t repeat_velocity(NoteDelaySpec spec, std::uint8_t velocity,
98 std::size_t index) noexcept {
99 return note_schedule::decayed_velocity(velocity, spec.velocity_decay_percent, index);
100 }
101
106 static constexpr std::optional<std::uint8_t>
107 repeat_note(NoteDelaySpec spec, std::uint8_t note, std::size_t index) noexcept {
108 const auto shifted = static_cast<std::int32_t>(note) +
109 static_cast<std::int32_t>(spec.transpose_semitones) *
110 static_cast<std::int32_t>(index);
111 if (shifted < 0 || shifted > 127)
112 return std::nullopt;
113 return static_cast<std::uint8_t>(shifted);
114 }
115
116 explicit constexpr NoteDelay(NoteDelaySpec spec = {}) noexcept
117 : spec_(spec), valid_(valid_spec(spec)) {}
118
119 constexpr bool valid() const noexcept {
120 return valid_;
121 }
122
123 constexpr NoteDelaySpec spec() const noexcept {
124 return spec_;
125 }
126
128 const Block& block) noexcept {
129 if (utility_detail::blocks_alias(input, output))
130 return {0, input.size(), 0, false};
133 if (!utility_detail::ready(output) || !valid_ || !note_schedule::valid_block(block)) {
134 report.dropped = input.size();
135 report.complete = false;
136 return report;
137 }
138 const std::int64_t block_end =
139 utility_detail::saturating_sample_add(block.sample_start.value, block.sample_count);
140 for (const auto& event : input) {
141 const auto absolute =
142 utility_detail::saturating_sample_add(block.sample_start.value, event.sample_offset);
143 emit_due_before(absolute, block, output, report);
144 const bool is_attack = event.is_note_on() && event.velocity() != 0;
145 const bool is_release =
146 event.is_note_off() || (event.is_note_on() && event.velocity() == 0);
147 // The dry note always passes through: this kernel is a send.
148 utility_detail::emit(output, event, report);
149 if (is_identity(spec_) || (!is_attack && !is_release))
150 continue;
151 if (is_attack)
152 schedule_echoes(event, absolute, block, report);
153 else
154 roll_back_armed(event, absolute, block, output, report);
155 }
156 emit_due_before(block_end, block, output, report);
157 utility_detail::copy_sidecars(input, output, report);
158 output.sort();
159 return report;
160 }
161
164 MidiUtilityProcessReport flush(MidiBuffer& output, const Block& block) noexcept {
167 if (!utility_detail::ready(output)) {
168 report.complete = false;
169 return report;
170 }
171 report.deferred += queue_.cancel_unstarted();
172 // An armed end is `kArmedEnd`; clamping it here is what lets the drain
173 // below finish notes whose authored release never arrived.
174 queue_.release_sounding_at(block.sample_start.value);
175 sources_.fill({});
176 const bool drained = queue_.drain(
177 [&](const note_schedule::ScheduledNote& slot, bool attack, std::int64_t at) {
178 return emit_edge(slot, attack, at, block, output, report);
179 });
180 if (!drained) {
181 ++report.deferred;
182 report.complete = false;
183 return report;
184 }
185 report.complete = empty();
186 if (report.complete && pending_spec_) {
187 spec_ = *pending_spec_;
188 valid_ = true;
189 pending_spec_.reset();
190 }
191 return report;
192 }
193
194 MidiUtilityProcessReport reset(MidiBuffer& output, const Block& block) noexcept {
195 return flush(output, block);
196 }
197
199 const Block& block) noexcept {
200 if (!valid_spec(spec)) {
202 return {0, 0, 0, false};
203 }
204 auto report = flush(output, block);
205 if (report.complete) {
206 spec_ = spec;
207 valid_ = true;
208 pending_spec_.reset();
209 } else {
210 pending_spec_ = spec;
211 }
212 return report;
213 }
214
215 bool empty() const noexcept {
216 return queue_.empty();
217 }
218
219 std::size_t scheduled() const noexcept {
220 return queue_.size();
221 }
222
223 std::size_t sounding() const noexcept {
224 return queue_.sounding();
225 }
226
227 private:
230 struct Source {
231 std::int64_t attack = 0;
232 std::uint16_t group = 0;
233 bool armed = false;
234 };
235
236 Source* find_source(std::uint16_t group) noexcept {
237 for (auto& source : sources_)
238 if (source.armed && source.group == group)
239 return &source;
240 return nullptr;
241 }
242
243 Source* claim_source(std::uint16_t group) noexcept {
244 if (auto* existing = find_source(group); existing != nullptr)
245 return existing;
246 for (auto& source : sources_)
247 if (!source.armed)
248 return &source;
249 return nullptr;
250 }
251
252 static constexpr std::uint16_t group_for(std::uint8_t channel, std::uint8_t note) noexcept {
253 return static_cast<std::uint16_t>(utility_detail::key_index(channel, note) + 1);
254 }
255
256 void schedule_echoes(const MidiEvent& event, std::int64_t absolute, const Block& block,
257 MidiUtilityProcessReport& report) noexcept {
258 const auto stride = repeat_stride(spec_, block);
259 const auto gate = repeat_gate(spec_, block);
260 const auto group = group_for(event.channel(), event.note());
261 // A re-attack before the previous release ends the earlier press's
262 // echoes here, using the length that press had actually been held. They
263 // cannot wait for the next release: that release describes a different
264 // note, and leaving them armed would sustain them until flush.
265 close_group(group, absolute);
266 auto* source = claim_source(group);
267 if (source == nullptr) {
268 // With nothing to record the arm against, the echoes could never be
269 // rolled back to a real length, so none is scheduled.
270 ++report.dropped;
271 report.complete = false;
272 return;
273 }
274 *source = {absolute, group, true};
275 for (std::size_t index = 1; index <= spec_.repeats; ++index) {
276 const auto pitch = repeat_note(spec_, event.note(), index);
277 if (!pitch)
278 continue;
279 auto* slot = queue_.acquire();
280 if (slot == nullptr) {
281 // DropUnstarted: an echo that cannot be scheduled never sounds,
282 // so it can never leave an unbalanced note behind.
283 ++report.dropped;
284 report.complete = false;
285 return;
286 }
288 absolute, stride * static_cast<std::int64_t>(index));
289 slot->end = gate == 0 ? note_schedule::kArmedEnd
290 : utility_detail::saturating_sample_add(slot->start, gate);
291 slot->channel = event.channel();
292 slot->note = *pitch;
293 slot->velocity = repeat_velocity(spec_, event.velocity(), index);
294 slot->group = group;
295 slot->step = static_cast<std::uint16_t>(index);
296 }
297 }
298
303 void roll_back_armed(const MidiEvent& event, std::int64_t absolute, const Block& block,
304 MidiBuffer& output, MidiUtilityProcessReport& report) noexcept {
305 const auto group = group_for(event.channel(), event.note());
306 if (!close_group(group, absolute))
307 return;
308 emit_due_before(absolute, block, output, report);
309 }
310
315 bool close_group(std::uint16_t group, std::int64_t absolute) noexcept {
316 auto* source = find_source(group);
317 if (source == nullptr)
318 return false;
319 const auto held = std::max<std::int64_t>(1, absolute - source->attack);
320 queue_.visit([&](note_schedule::ScheduledNote& slot) {
321 if (slot.group != group || slot.end != note_schedule::kArmedEnd)
322 return;
323 const auto end = utility_detail::saturating_sample_add(slot.start, held);
324 // An echo already sounding cannot end before now, or its note-off
325 // would be emitted ahead of the note-on that started it.
326 slot.end = slot.sounding ? std::max(end, absolute) : end;
327 });
328 *source = {};
329 return true;
330 }
331
332 void emit_due_before(std::int64_t boundary, const Block& block, MidiBuffer& output,
333 MidiUtilityProcessReport& report) noexcept {
334 queue_.emit_due_before(
335 boundary, [&](const note_schedule::ScheduledNote& slot, bool attack, std::int64_t at) {
336 return emit_edge(slot, attack, at, block, output, report);
337 });
338 }
339
340 static bool emit_edge(const note_schedule::ScheduledNote& slot, bool attack, std::int64_t at,
341 const Block& block, MidiBuffer& output,
342 MidiUtilityProcessReport& report) noexcept {
343 const auto offset = note_schedule::offset_in_block(block, at);
344 const auto event = attack ? MidiEvent::note_on(slot.channel, slot.note, slot.velocity)
345 : MidiEvent::note_off(slot.channel, slot.note);
346 return utility_detail::emit(output, utility_detail::at(event, offset), report);
347 }
348
349 NoteDelaySpec spec_{};
350 bool valid_ = true;
351 std::optional<NoteDelaySpec> pending_spec_{};
352 note_schedule::ScheduleQueue<MaximumScheduledNotes> queue_{};
353 std::array<Source, MaximumHeldSources> sources_{};
354};
355
356} // namespace pulp::midi
Collection of timestamped MIDI events within a single audio buffer period.
Definition buffer.hpp:31
constexpr NoteDelaySpec spec() const noexcept
Definition note_delay.hpp:123
constexpr NoteDelay(NoteDelaySpec spec={}) noexcept
Definition note_delay.hpp:116
static constexpr bool is_identity(NoteDelaySpec spec) noexcept
True when the kernel cannot change the stream, which is its declared bypass identity.
Definition note_delay.hpp:65
bool empty() const noexcept
Definition note_delay.hpp:215
MidiUtilityProcessReport replace_spec(NoteDelaySpec spec, MidiBuffer &output, const Block &block) noexcept
Definition note_delay.hpp:198
static std::int64_t repeat_stride(NoteDelaySpec spec, const Block &block) noexcept
Sample stride between consecutive repeats under whichever clock the spec selects.
Definition note_delay.hpp:72
static std::int64_t repeat_sample(NoteDelaySpec spec, const Block &block, std::int64_t attack_absolute, std::size_t index) noexcept
Absolute sample of repeat index (1-based; index 0 is the dry note).
Definition note_delay.hpp:90
constexpr bool valid() const noexcept
Definition note_delay.hpp:119
static constexpr std::uint8_t repeat_velocity(NoteDelaySpec spec, std::uint8_t velocity, std::size_t index) noexcept
Velocity of repeat index under the shipped decay law.
Definition note_delay.hpp:97
MidiUtilityProcessReport process(const MidiBuffer &input, MidiBuffer &output, const Block &block) noexcept
Definition note_delay.hpp:127
static constexpr std::optional< std::uint8_t > repeat_note(NoteDelaySpec spec, std::uint8_t note, std::size_t index) noexcept
Pitch of repeat index, or nothing when the cumulative transposition has walked the repeat off the MID...
Definition note_delay.hpp:107
MidiUtilityProcessReport reset(MidiBuffer &output, const Block &block) noexcept
Definition note_delay.hpp:194
std::size_t sounding() const noexcept
Definition note_delay.hpp:223
static constexpr bool valid_spec(NoteDelaySpec spec) noexcept
Definition note_delay.hpp:56
static constexpr MidiUtilityContract contract() noexcept
Definition note_delay.hpp:50
note_schedule::Block Block
Definition note_delay.hpp:48
MidiUtilityProcessReport flush(MidiBuffer &output, const Block &block) noexcept
Cancel unstarted echoes and release every echo the kernel still owns, including the ones whose end is...
Definition note_delay.hpp:164
static std::int64_t repeat_gate(NoteDelaySpec spec, const Block &block) noexcept
Length of an authorable echo gate.
Definition note_delay.hpp:81
std::size_t scheduled() const noexcept
Definition note_delay.hpp:219
@ group
Definition scene_node.hpp:92
@ end
Definition editor_host.hpp:354
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
@ DropUnstarted
Definition utility_contract.hpp:20
NoteDelaySync
Definition note_delay.hpp:13
@ Division
Definition note_delay.hpp:14
@ Milliseconds
Definition note_delay.hpp:15
@ ReleaseBeforeAttack
Definition utility_contract.hpp:27
@ index
Definition gpu_pass.hpp:140
@ attack
Definition envelope.hpp:58
constexpr std::int64_t kTicksPerQuarter
Definition tick.hpp:54
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
MIDI note delay / echo.
Definition note_delay.hpp:26
timebase::TickDuration interval
Definition note_delay.hpp:28
std::int8_t transpose_semitones
Definition note_delay.hpp:37
NoteDelaySync sync
Definition note_delay.hpp:27
std::int64_t milliseconds
Definition note_delay.hpp:29
std::uint8_t repeats
Definition note_delay.hpp:30
constexpr auto operator<=>(const NoteDelaySpec &) const =default
std::uint8_t gate_percent
Gate length for generated echoes as a percentage of their delay stride.
Definition note_delay.hpp:35
std::uint8_t velocity_decay_percent
Definition note_delay.hpp:36
Definition tick.hpp:56