Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
latch.hpp
Go to the documentation of this file.
1#pragma once
2
4
5#include <array>
6#include <cstddef>
7#include <cstdint>
8#include <limits>
9#include <optional>
10
11namespace pulp::midi {
12
19enum class LatchMode : std::uint8_t {
23};
24
25struct LatchSpec {
27 constexpr auto operator<=>(const LatchSpec&) const = default;
28};
29
38class Latch {
39 public:
40 static constexpr std::size_t kKeySpace = 16 * 128;
41
47
48 static constexpr bool valid_spec(LatchSpec spec) noexcept {
49 return spec.mode == LatchMode::Off || spec.mode == LatchMode::Hold ||
50 spec.mode == LatchMode::Toggle;
51 }
52
53 explicit constexpr Latch(LatchSpec spec = {}) noexcept
54 : spec_(spec), valid_(valid_spec(spec)) {}
55
56 constexpr bool valid() const noexcept {
57 return valid_;
58 }
59
60 constexpr LatchSpec spec() const noexcept {
61 return spec_;
62 }
63
64 MidiUtilityProcessReport process(const MidiBuffer& input, MidiBuffer& output) noexcept {
65 if (utility_detail::blocks_alias(input, output))
66 return {0, input.size(), 0, false};
69 if (!utility_detail::ready(output) || !valid_) {
70 report.dropped = input.size();
71 report.complete = false;
72 return report;
73 }
74 drain_pending_releases(output, report);
75 for (const auto& event : input) {
76 const bool is_attack = event.is_note_on() && event.velocity() != 0;
77 const bool is_release =
78 event.is_note_off() || (event.is_note_on() && event.velocity() == 0);
79 if (!is_attack && !is_release) {
80 utility_detail::emit(output, event, report);
81 continue;
82 }
83 const int key = utility_detail::key_index(event.channel(), event.note());
84 if (is_attack)
85 handle_attack(key, event, output, report);
86 else
87 handle_release(key, event, output, report);
88 }
89 utility_detail::copy_sidecars(input, output, report);
90 output.sort();
91 return report;
92 }
93
99 if (!utility_detail::ready(output)) {
100 report.complete = false;
101 return report;
102 }
103 drain_pending_releases(output, report);
104 if (pending_release_total_ != 0)
105 return report;
106 for (std::size_t key = 0; key < kKeySpace && owned_total_ != 0; ++key) {
107 while (owned_depth_[key] != 0) {
108 if (!utility_detail::emit(output, note_off_for(key), report)) {
109 ++report.deferred;
110 report.complete = false;
111 return report;
112 }
113 --owned_depth_[key];
114 --owned_total_;
115 }
116 }
117 physical_depth_.fill(0);
118 physical_down_ = 0;
119 report.complete = empty();
120 if (report.complete && pending_spec_) {
121 spec_ = *pending_spec_;
122 valid_ = true;
123 pending_spec_.reset();
124 }
125 return report;
126 }
127
129 return flush(output);
130 }
131
136 if (!valid_spec(spec)) {
138 return {0, 0, 0, false};
139 }
140 auto report = flush(output);
141 if (report.complete) {
142 spec_ = spec;
143 valid_ = true;
144 pending_spec_.reset();
145 } else {
146 pending_spec_ = spec;
147 }
148 return report;
149 }
150
151 bool empty() const noexcept {
152 return owned_total_ == 0;
153 }
154
156 std::uint32_t owned_depth(std::uint8_t channel, std::uint8_t note) const noexcept {
157 return owned_depth_[static_cast<std::size_t>(utility_detail::key_index(channel, note))];
158 }
159
160 private:
161 void handle_attack(int key, const MidiEvent& event, MidiBuffer& output,
162 MidiUtilityProcessReport& report) noexcept {
163 if (spec_.mode == LatchMode::Off) {
164 if (utility_detail::emit(output, event, report))
165 acquire(key);
166 increment(physical_depth_[static_cast<std::size_t>(key)]);
167 increment(physical_down_);
168 return;
169 }
170 if (spec_.mode == LatchMode::Hold && physical_down_ == 0)
171 release_all_owned(event.sample_offset, output, report);
172 if (spec_.mode == LatchMode::Hold && physical_depth_[static_cast<std::size_t>(key)] == 0 &&
173 owned_depth_[static_cast<std::size_t>(key)] != 0) {
174 // Hold mode keeps one sounding voice per key. A re-press while
175 // another physical key is held updates physical ownership but
176 // does not retrigger the already latched voice.
177 increment(physical_depth_[static_cast<std::size_t>(key)]);
178 increment(physical_down_);
179 return;
180 }
181 if (spec_.mode == LatchMode::Toggle &&
182 owned_depth_[static_cast<std::size_t>(key)] != 0) {
183 // A second press on a latched key releases it and consumes the
184 // attack, so the key stops sounding instead of retriggering.
185 release_key(key, event.sample_offset, output, report);
186 increment(physical_depth_[static_cast<std::size_t>(key)]);
187 increment(physical_down_);
188 return;
189 }
190 if (utility_detail::emit(output, event, report))
191 acquire(key);
192 increment(physical_depth_[static_cast<std::size_t>(key)]);
193 increment(physical_down_);
194 }
195
196 void handle_release(int key, const MidiEvent& event, MidiBuffer& output,
197 MidiUtilityProcessReport& report) noexcept {
198 auto& physical = physical_depth_[static_cast<std::size_t>(key)];
199 if (physical != 0) {
200 --physical;
201 if (physical_down_ != 0)
202 --physical_down_;
203 }
204 if (spec_.mode == LatchMode::Off) {
205 if (utility_detail::emit(output, event, report))
206 surrender(key);
207 else
208 queue_pending_release(key);
209 return;
210 }
211 // Retained: the latch, not the player, decides when this note ends.
212 ++report.deferred;
213 }
214
215 void release_key(int key, std::int32_t sample_offset, MidiBuffer& output,
216 MidiUtilityProcessReport& report) noexcept {
217 while (owned_depth_[static_cast<std::size_t>(key)] != 0) {
218 auto off =
219 utility_detail::at(note_off_for(static_cast<std::size_t>(key)), sample_offset);
220 if (!utility_detail::emit(output, off, report)) {
221 ++report.deferred;
222 report.complete = false;
223 return;
224 }
225 surrender(key);
226 }
227 }
228
232 void acquire(int key) noexcept {
233 auto& depth = owned_depth_[static_cast<std::size_t>(key)];
234 if (depth == std::numeric_limits<std::uint32_t>::max())
235 return;
236 ++depth;
237 increment(owned_total_);
238 }
239
240 void surrender(int key) noexcept {
241 auto& depth = owned_depth_[static_cast<std::size_t>(key)];
242 if (depth == 0)
243 return;
244 --depth;
245 if (owned_total_ != 0)
246 --owned_total_;
247 }
248
249 void release_all_owned(std::int32_t sample_offset, MidiBuffer& output,
250 MidiUtilityProcessReport& report) noexcept {
251 // The retention table spans the whole key space so retention can never
252 // overflow, which makes the scan the price of that guarantee. Counting
253 // owned notes keeps the common case — a phrase start with nothing
254 // retained — off the audio thread's critical path entirely.
255 if (owned_total_ == 0)
256 return;
257 for (std::size_t key = 0; key < kKeySpace; ++key)
258 release_key(static_cast<int>(key), sample_offset, output, report);
259 }
260
261 void queue_pending_release(int key) noexcept {
262 auto& pending = pending_release_[static_cast<std::size_t>(key)];
263 if (pending != std::numeric_limits<std::uint32_t>::max()) {
264 ++pending;
265 ++pending_release_total_;
266 }
267 }
268
269 void drain_pending_releases(MidiBuffer& output, MidiUtilityProcessReport& report) noexcept {
270 for (std::size_t key = 0; key < kKeySpace && pending_release_total_ != 0; ++key) {
271 auto& pending = pending_release_[key];
272 while (pending != 0) {
273 if (!utility_detail::emit(output, note_off_for(key), report))
274 return;
275 --pending;
276 --pending_release_total_;
277 surrender(static_cast<int>(key));
278 }
279 }
280 }
281
282 static MidiEvent note_off_for(std::size_t key) noexcept {
283 return MidiEvent::note_off(static_cast<std::uint8_t>(key / 128),
284 static_cast<std::uint8_t>(key % 128));
285 }
286
287 template <typename Counter> static void increment(Counter& value) noexcept {
288 if (value != std::numeric_limits<Counter>::max())
289 ++value;
290 }
291
292 LatchSpec spec_{};
293 bool valid_ = true;
294 std::optional<LatchSpec> pending_spec_{};
295 std::array<std::uint32_t, kKeySpace> owned_depth_{};
296 std::array<std::uint32_t, kKeySpace> physical_depth_{};
297 std::array<std::uint32_t, kKeySpace> pending_release_{};
298 std::uint32_t pending_release_total_ = 0;
299 std::uint32_t physical_down_ = 0;
300 std::uint32_t owned_total_ = 0;
301};
302
303} // namespace pulp::midi
static constexpr bool valid_spec(LatchSpec spec) noexcept
Definition latch.hpp:48
static constexpr std::size_t kKeySpace
Definition latch.hpp:40
constexpr bool valid() const noexcept
Definition latch.hpp:56
MidiUtilityProcessReport process(const MidiBuffer &input, MidiBuffer &output) noexcept
Definition latch.hpp:64
constexpr LatchSpec spec() const noexcept
Definition latch.hpp:60
MidiUtilityProcessReport replace_spec(LatchSpec spec, MidiBuffer &output) noexcept
Swap the retention mode.
Definition latch.hpp:135
MidiUtilityProcessReport reset(MidiBuffer &output) noexcept
Definition latch.hpp:128
constexpr Latch(LatchSpec spec={}) noexcept
Definition latch.hpp:53
MidiUtilityProcessReport flush(MidiBuffer &output) noexcept
Release every note the latch still owns.
Definition latch.hpp:96
std::uint32_t owned_depth(std::uint8_t channel, std::uint8_t note) const noexcept
Number of note-offs the latch still owes for channel/note.
Definition latch.hpp:156
static constexpr MidiUtilityContract contract() noexcept
Definition latch.hpp:42
bool empty() const noexcept
Definition latch.hpp:151
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
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
LatchMode
How a latch retains the notes a player releases.
Definition latch.hpp:19
@ Toggle
Definition latch.hpp:22
@ Hold
Definition latch.hpp:21
@ Off
Definition latch.hpp:20
@ ReleaseBeforeAttack
Definition utility_contract.hpp:27
@ pending
Definition gpu_pass.hpp:556
@ off
Definition nonlinear_shaping.hpp:24
@ value
the widget's primary value (Knob, Fader, RangeSlider, Toggle, ProgressBar)
Definition binding_diagnostics.hpp:19
Definition latch.hpp:25
LatchMode mode
Definition latch.hpp:26
constexpr auto operator<=>(const LatchSpec &) const =default
Timestamped MIDI event combining a choc::midi::ShortMessage with timing info.
Definition message.hpp:32
static MidiEvent note_off(uint8_t channel, uint8_t note, uint8_t velocity=0)
Create a Note Off event.
Definition message.hpp:54
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