Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
chord_memory.hpp
Go to the documentation of this file.
1#pragma once
2
6
7#include <algorithm>
8#include <array>
9#include <cstddef>
10#include <cstdint>
11#include <optional>
12#include <span>
13
14namespace pulp::midi {
15
17enum class ChordMemoryMode : std::uint8_t {
25};
26
38
44template <std::size_t MaximumActiveTriggers = 16> class ChordMemory {
45 public:
46 static constexpr std::size_t kMaxChordNotes = music::ChordFormula::kMaxNotes;
47
53
54 static constexpr bool valid_spec(ChordMemorySpec spec) noexcept {
55 return spec.range.valid() && music::is_valid(spec.scale_root);
56 }
57
58 explicit ChordMemory(ChordMemorySpec spec = {}) noexcept
59 : spec_(spec), valid_(valid_spec(spec)) {}
60
61 bool valid() const noexcept {
62 return valid_;
63 }
64
65 ChordMemorySpec spec() const noexcept {
66 return spec_;
67 }
68
71 bool memory_empty() const noexcept {
72 if (spec_.mode == ChordMemoryMode::PerKey) {
73 for (const auto& slot : per_key_)
74 if (slot.has_value())
75 return false;
76 return true;
77 }
78 return !single_.has_value();
79 }
80
83 bool learn(std::span<const std::uint8_t> notes) noexcept {
84 const auto formula = formula_from(notes);
85 if (!formula)
86 return false;
87 single_ = formula;
88 return true;
89 }
90
93 bool learn_for(std::uint8_t trigger, std::span<const std::uint8_t> notes) noexcept {
94 const auto formula = formula_from(notes);
95 if (!formula)
96 return false;
97 per_key_[static_cast<std::size_t>(trigger % 12)] = formula;
98 return true;
99 }
100
101 void clear_memory() noexcept {
102 single_.reset();
103 per_key_.fill(std::nullopt);
104 last_voicing_size_ = 0;
105 }
106
111 std::size_t chord_for(std::uint8_t trigger,
112 std::array<std::uint8_t, kMaxChordNotes>& out) const noexcept {
113 const auto* formula = formula_for(trigger);
114 if (formula == nullptr)
115 return 0;
116 return spec_.mode == ChordMemoryMode::ScaleDegree
117 ? scale_chord(*formula, trigger, out)
118 : parallel_chord(*formula, trigger, out);
119 }
120
121 MidiUtilityProcessReport process(const MidiBuffer& input, MidiBuffer& output) noexcept {
122 if (utility_detail::blocks_alias(input, output))
123 return {0, input.size(), 0, false};
126 if (!utility_detail::ready(output) || !valid_) {
127 report.dropped = input.size();
128 report.complete = false;
129 return report;
130 }
131 for (const auto& event : input) {
132 const bool is_attack = event.is_note_on() && event.velocity() != 0;
133 const bool is_release =
134 event.is_note_off() || (event.is_note_on() && event.velocity() == 0);
135 if (memory_empty() || (!is_attack && !is_release)) {
136 utility_detail::emit(output, event, report);
137 continue;
138 }
139 if (is_attack)
140 open_chord(event, output, report);
141 else
142 close_chord(event, output, report);
143 }
144 utility_detail::copy_sidecars(input, output, report);
145 output.sort();
146 return report;
147 }
148
153 if (!utility_detail::ready(output)) {
154 report.complete = false;
155 return report;
156 }
157 for (auto& trigger : triggers_) {
158 if (!trigger.active)
159 continue;
160 if (!release_trigger(trigger, 0, output, report)) {
161 ++report.deferred;
162 report.complete = false;
163 return report;
164 }
165 }
166 last_voicing_size_ = 0;
167 report.complete = empty();
168 return report;
169 }
170
172 return flush(output);
173 }
174
178 if (!valid_spec(spec)) {
180 return {0, 0, 0, false};
181 }
182 auto report = flush(output);
183 if (report.complete) {
184 spec_ = spec;
185 valid_ = true;
186 }
187 return report;
188 }
189
190 bool empty() const noexcept {
191 for (const auto& trigger : triggers_)
192 if (trigger.active)
193 return false;
194 return true;
195 }
196
197 private:
198 struct Trigger {
199 std::array<std::uint8_t, kMaxChordNotes> notes{};
200 std::uint8_t count = 0;
201 std::uint8_t channel = 0;
202 std::uint8_t note = 0;
203 bool active = false;
204 };
205
206 static std::optional<music::ChordFormula>
207 formula_from(std::span<const std::uint8_t> notes) noexcept {
208 if (notes.empty() || notes.size() > kMaxChordNotes)
209 return std::nullopt;
210 std::array<int, kMaxChordNotes> sorted{};
211 for (std::size_t index = 0; index < notes.size(); ++index)
212 sorted[index] = static_cast<int>(notes[index]);
213 std::sort(sorted.begin(), sorted.begin() + static_cast<std::ptrdiff_t>(notes.size()));
214 const int lowest = sorted[0];
215 std::array<int, kMaxChordNotes> intervals{};
216 for (std::size_t index = 0; index < notes.size(); ++index)
217 intervals[index] = sorted[index] - lowest;
219 std::span<const int>(intervals.data(), notes.size()));
220 }
221
222 const music::ChordFormula* formula_for(std::uint8_t trigger) const noexcept {
223 if (spec_.mode == ChordMemoryMode::PerKey) {
224 const auto& slot = per_key_[static_cast<std::size_t>(trigger % 12)];
225 return slot ? &*slot : nullptr;
226 }
227 return single_ ? &*single_ : nullptr;
228 }
229
230 static std::size_t parallel_chord(const music::ChordFormula& formula, std::uint8_t trigger,
231 std::array<std::uint8_t, kMaxChordNotes>& out) noexcept {
232 std::size_t count = 0;
233 for (std::size_t tone = 0; tone < formula.size(); ++tone) {
234 const auto interval = formula.interval(tone);
235 if (!interval)
236 continue;
237 const int pitch = static_cast<int>(trigger) + *interval;
238 // A chord that would run off the top is truncated rather than
239 // folded, because folding invents a voicing nobody captured.
240 if (pitch > 127)
241 break;
242 out[count++] = static_cast<std::uint8_t>(pitch);
243 }
244 return count;
245 }
246
249 std::size_t scale_chord(const music::ChordFormula& formula, std::uint8_t trigger,
250 std::array<std::uint8_t, kMaxChordNotes>& out) const noexcept {
251 const auto scale = music::Scale::named(spec_.scale_root, spec_.scale);
252 if (!scale)
253 return parallel_chord(formula, trigger, out);
254 const int root = utility_detail::nearest_scale_note(static_cast<int>(trigger), *scale);
255 std::size_t count = 0;
256 for (std::size_t tone = 0; tone < formula.size(); ++tone) {
257 const auto interval = formula.interval(tone);
258 if (!interval)
259 continue;
260 // The captured semitone interval names the degree by counting the
261 // scale members it spans, which is what makes the replay diatonic.
262 const int degree = scale_steps_in(*scale, root, *interval);
263 const auto pitch = scale_note_above(*scale, root, degree);
264 if (!pitch)
265 break;
266 out[count++] = *pitch;
267 }
268 return count;
269 }
270
271 static int scale_steps_in(const music::Scale& scale, int root, int semitones) noexcept {
272 int steps = 0;
273 for (int offset = 1; offset <= semitones; ++offset) {
274 const int candidate = root + offset;
275 if (candidate > 127)
276 break;
277 if (scale.contains(static_cast<music::PitchClass>(candidate % 12)))
278 ++steps;
279 }
280 return steps;
281 }
282
283 static std::optional<std::uint8_t> scale_note_above(const music::Scale& scale, int root,
284 int steps) noexcept {
285 if (steps == 0)
286 return static_cast<std::uint8_t>(std::clamp(root, 0, 127));
287 int remaining = steps;
288 for (int candidate = root + 1; candidate <= 127; ++candidate) {
289 if (!scale.contains(static_cast<music::PitchClass>(candidate % 12)))
290 continue;
291 if (--remaining == 0)
292 return static_cast<std::uint8_t>(candidate);
293 }
294 return std::nullopt;
295 }
296
299 std::size_t revoiced(const music::ChordFormula& formula, std::uint8_t trigger,
300 std::array<std::uint8_t, kMaxChordNotes>& notes,
301 std::size_t count) noexcept {
302 if (!spec_.revoice || last_voicing_size_ != count || count == 0)
303 return count;
304 std::array<int, kMaxChordNotes> previous{};
305 for (std::size_t index = 0; index < count; ++index)
306 previous[index] = static_cast<int>(last_voicing_[index]);
307 for (std::size_t index = 1; index < count; ++index)
308 if (previous[index - 1] >= previous[index])
309 return count;
310 const auto voicing = music::minimum_motion_voice_leading(
311 std::span<const int>(previous.data(), count),
312 static_cast<music::PitchClass>(trigger % 12), formula, spec_.range);
313 if (!voicing || voicing->size() != count)
314 return count;
315 const auto pitches = voicing->pitches();
316 for (std::size_t index = 0; index < count; ++index)
317 notes[index] = pitches[index];
318 return count;
319 }
320
321 void open_chord(const MidiEvent& event, MidiBuffer& output,
322 MidiUtilityProcessReport& report) noexcept {
323 std::array<std::uint8_t, kMaxChordNotes> notes{};
324 auto count = chord_for(event.note(), notes);
325 if (count == 0) {
326 utility_detail::emit(output, event, report);
327 return;
328 }
329 if (const auto* formula = formula_for(event.note()); formula != nullptr)
330 count = revoiced(*formula, event.note(), notes, count);
331 // Release first: a retrigger of the same key gives up the chord it
332 // already owns, so a key can never own two chords at once AND the slot
333 // it frees is available to the press that freed it. Claiming a slot
334 // before this ran would drop a retrigger at capacity that had room.
335 close_key(event.channel(), event.note(), event.sample_offset, output, report);
336 Trigger* slot = free_trigger();
337 if (slot == nullptr) {
338 // DropUnstarted: with no slot to record ownership the chord would
339 // have no way to be released, so it must not sound at all.
340 ++report.dropped;
341 report.complete = false;
342 return;
343 }
344 std::uint8_t emitted = 0;
345 for (std::size_t index = 0; index < count; ++index) {
346 auto on = MidiEvent::note_on(event.channel(), notes[index], event.velocity());
347 on = utility_detail::at(on, event.sample_offset);
348 if (!utility_detail::emit(output, on, report))
349 break;
350 slot->notes[emitted++] = notes[index];
351 }
352 if (emitted == 0) {
353 report.complete = false;
354 return;
355 }
356 slot->count = emitted;
357 slot->channel = event.channel();
358 slot->note = event.note();
359 slot->active = true;
360 for (std::size_t index = 0; index < emitted; ++index)
361 last_voicing_[index] = slot->notes[index];
362 last_voicing_size_ = emitted;
363 }
364
365 void close_chord(const MidiEvent& event, MidiBuffer& output,
366 MidiUtilityProcessReport& report) noexcept {
367 if (!close_key(event.channel(), event.note(), event.sample_offset, output, report))
368 utility_detail::emit(output, event, report);
369 }
370
371 bool close_key(std::uint8_t channel, std::uint8_t note, std::int32_t sample_offset,
372 MidiBuffer& output, MidiUtilityProcessReport& report) noexcept {
373 bool closed = false;
374 for (auto& trigger : triggers_) {
375 if (!trigger.active || trigger.channel != channel || trigger.note != note)
376 continue;
377 release_trigger(trigger, sample_offset, output, report);
378 closed = true;
379 }
380 return closed;
381 }
382
383 bool release_trigger(Trigger& trigger, std::int32_t sample_offset, MidiBuffer& output,
384 MidiUtilityProcessReport& report) noexcept {
385 while (trigger.count != 0) {
386 const auto note = trigger.notes[trigger.count - 1];
387 auto off = utility_detail::at(MidiEvent::note_off(trigger.channel, note), sample_offset);
388 if (!utility_detail::emit(output, off, report))
389 return false;
390 --trigger.count;
391 }
392 trigger = {};
393 return true;
394 }
395
396 Trigger* free_trigger() noexcept {
397 for (auto& trigger : triggers_)
398 if (!trigger.active)
399 return &trigger;
400 return nullptr;
401 }
402
403 ChordMemorySpec spec_{};
404 bool valid_ = true;
405 std::optional<music::ChordFormula> single_{};
406 std::array<std::optional<music::ChordFormula>, 12> per_key_{};
407 std::array<Trigger, MaximumActiveTriggers> triggers_{};
408 std::array<std::uint8_t, kMaxChordNotes> last_voicing_{};
409 std::size_t last_voicing_size_ = 0;
410};
411
412} // namespace pulp::midi
MidiUtilityProcessReport flush(MidiBuffer &output) noexcept
Release every note the kernel owns.
Definition chord_memory.hpp:150
static constexpr MidiUtilityContract contract() noexcept
Definition chord_memory.hpp:48
void clear_memory() noexcept
Definition chord_memory.hpp:101
MidiUtilityProcessReport reset(MidiBuffer &output) noexcept
Definition chord_memory.hpp:171
bool empty() const noexcept
Definition chord_memory.hpp:190
ChordMemorySpec spec() const noexcept
Definition chord_memory.hpp:65
MidiUtilityProcessReport replace_spec(ChordMemorySpec spec, MidiBuffer &output) noexcept
Swap the spec.
Definition chord_memory.hpp:177
static constexpr std::size_t kMaxChordNotes
Definition chord_memory.hpp:46
bool valid() const noexcept
Definition chord_memory.hpp:61
static constexpr bool valid_spec(ChordMemorySpec spec) noexcept
Definition chord_memory.hpp:54
bool memory_empty() const noexcept
True while nothing is captured, which is the kernel's declared bypass identity: every event passes th...
Definition chord_memory.hpp:71
bool learn(std::span< const std::uint8_t > notes) noexcept
Capture notes as an interval shape relative to its own lowest note.
Definition chord_memory.hpp:83
ChordMemory(ChordMemorySpec spec={}) noexcept
Definition chord_memory.hpp:58
std::size_t chord_for(std::uint8_t trigger, std::array< std::uint8_t, kMaxChordNotes > &out) const noexcept
The chord trigger produces, written into out.
Definition chord_memory.hpp:111
MidiUtilityProcessReport process(const MidiBuffer &input, MidiBuffer &output) noexcept
Definition chord_memory.hpp:121
bool learn_for(std::uint8_t trigger, std::span< const std::uint8_t > notes) noexcept
Capture notes as the shape trigger's pitch class plays.
Definition chord_memory.hpp:93
Collection of timestamped MIDI events within a single audio buffer period.
Definition buffer.hpp:31
static constexpr std::size_t kMaxNotes
Definition chord.hpp:89
static constexpr std::optional< ChordFormula > from_intervals(std::span< const int > intervals) noexcept
Definition chord.hpp:93
static constexpr std::optional< Scale > named(PitchClass root, NamedScale name) noexcept
Definition pitch.hpp:275
@ count
sentinel: number of real capabilities (keep LAST)
Definition canvas_capability.hpp:38
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
int nearest_scale_note(int note, const music::Scale &scale) noexcept
Definition utility_contract.hpp:165
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
Definition custom_node_events.hpp:26
@ FlushOnDiscontinuity
Definition utility_contract.hpp:33
ChordMemoryMode
How a captured chord is addressed when a single note triggers it.
Definition chord_memory.hpp:17
@ PerKey
Each trigger pitch class owns its own captured chord.
Definition chord_memory.hpp:21
@ ScaleDegree
The captured shape is stored as scale degrees, so a trigger anywhere in the scale produces the diaton...
Definition chord_memory.hpp:24
@ Single
One captured chord answers every trigger, transposed to the trigger.
Definition chord_memory.hpp:19
@ DropUnstarted
Definition utility_contract.hpp:20
@ ReleaseBeforeAttack
Definition utility_contract.hpp:27
constexpr bool is_valid(PitchClass pitch_class) noexcept
Definition pitch.hpp:34
std::optional< Voicing > minimum_motion_voice_leading(std::span< const int > previous, PitchClass target_root, const ChordFormula &formula, MidiRange range={}) noexcept
Definition voicing.hpp:295
NamedScale
Definition pitch.hpp:147
@ major
Definition pitch.hpp:148
@ closed
Definition voicing.hpp:19
PitchClass
Definition pitch.hpp:19
@ c
Definition pitch.hpp:20
@ index
Definition gpu_pass.hpp:140
@ off
Definition nonlinear_shaping.hpp:24
void scale(const float *a, float scalar, float *dst, std::size_t count) noexcept
@ candidate
Definition waveform_editor_primitives.hpp:193
@ on
Definition accessibility.hpp:132
Definition chord_memory.hpp:27
bool revoice
Pick the voicing closest to the previous chord instead of the literal transposition.
Definition chord_memory.hpp:32
music::PitchClass scale_root
Definition chord_memory.hpp:34
music::NamedScale scale
Definition chord_memory.hpp:35
constexpr auto operator<=>(const ChordMemorySpec &) const =default
music::MidiRange range
Definition chord_memory.hpp:33
ChordMemoryMode mode
Definition chord_memory.hpp:28
static MidiEvent note_on(uint8_t channel, uint8_t note, uint8_t velocity)
Create a Note On event.
Definition message.hpp:46
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
Definition voicing.hpp:26