44template <std::
size_t MaximumActiveTriggers = 16>
class ChordMemory {
73 for (
const auto& slot : per_key_)
78 return !single_.has_value();
83 bool learn(std::span<const std::uint8_t> notes)
noexcept {
84 const auto formula = formula_from(notes);
93 bool learn_for(std::uint8_t trigger, std::span<const std::uint8_t> notes)
noexcept {
94 const auto formula = formula_from(notes);
97 per_key_[
static_cast<std::size_t
>(trigger % 12)] = formula;
103 per_key_.fill(std::nullopt);
104 last_voicing_size_ = 0;
112 std::array<std::uint8_t, kMaxChordNotes>& out)
const noexcept {
113 const auto* formula = formula_for(trigger);
114 if (formula ==
nullptr)
117 ? scale_chord(*formula, trigger, out)
118 : parallel_chord(*formula, trigger, out);
123 return {0, input.size(), 0,
false};
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);
140 open_chord(event, output, report);
142 close_chord(event, output, report);
157 for (
auto& trigger : triggers_) {
160 if (!release_trigger(trigger, 0, output, report)) {
166 last_voicing_size_ = 0;
172 return flush(output);
180 return {0, 0, 0,
false};
182 auto report =
flush(output);
183 if (report.complete) {
191 for (
const auto& trigger : triggers_)
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;
206 static std::optional<music::ChordFormula>
207 formula_from(std::span<const std::uint8_t> notes)
noexcept {
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()));
222 const music::ChordFormula* formula_for(std::uint8_t trigger)
const noexcept {
224 const auto& slot = per_key_[
static_cast<std::size_t
>(trigger % 12)];
225 return slot ? &*slot :
nullptr;
227 return single_ ? &*single_ :
nullptr;
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);
237 const int pitch =
static_cast<int>(trigger) + *interval;
242 out[
count++] =
static_cast<std::uint8_t
>(pitch);
249 std::size_t scale_chord(
const music::ChordFormula& formula, std::uint8_t trigger,
250 std::array<std::uint8_t, kMaxChordNotes>& out)
const noexcept {
253 return parallel_chord(formula, trigger, out);
255 std::size_t
count = 0;
256 for (std::size_t tone = 0; tone < formula.size(); ++tone) {
257 const auto interval = formula.interval(tone);
262 const int degree = scale_steps_in(*scale, root, *interval);
263 const auto pitch = scale_note_above(*scale, root, degree);
266 out[
count++] = *pitch;
271 static int scale_steps_in(
const music::Scale& scale,
int root,
int semitones)
noexcept {
273 for (
int offset = 1; offset <= semitones; ++offset) {
283 static std::optional<std::uint8_t> scale_note_above(
const music::Scale& scale,
int root,
284 int steps)
noexcept {
286 return static_cast<std::uint8_t
>(std::clamp(root, 0, 127));
287 int remaining = steps;
291 if (--remaining == 0)
292 return static_cast<std::uint8_t
>(
candidate);
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)
304 std::array<int, kMaxChordNotes> previous{};
306 previous[index] =
static_cast<int>(last_voicing_[index]);
308 if (previous[index - 1] >= previous[index])
311 std::span<const int>(previous.data(), count),
313 if (!voicing || voicing->size() != count)
315 const auto pitches = voicing->pitches();
317 notes[index] = pitches[index];
321 void open_chord(
const MidiEvent& event, MidiBuffer& output,
322 MidiUtilityProcessReport& report)
noexcept {
323 std::array<std::uint8_t, kMaxChordNotes> notes{};
329 if (
const auto* formula = formula_for(event.note()); formula !=
nullptr)
330 count = revoiced(*formula, event.note(), notes, count);
335 close_key(event.channel(), event.note(), event.sample_offset, output, report);
336 Trigger* slot = free_trigger();
337 if (slot ==
nullptr) {
341 report.complete =
false;
344 std::uint8_t emitted = 0;
350 slot->notes[emitted++] = notes[
index];
353 report.complete =
false;
356 slot->count = emitted;
357 slot->channel =
event.channel();
358 slot->note =
event.note();
360 for (std::size_t index = 0;
index < emitted; ++
index)
361 last_voicing_[index] = slot->notes[index];
362 last_voicing_size_ = emitted;
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))
371 bool close_key(std::uint8_t channel, std::uint8_t note, std::int32_t sample_offset,
372 MidiBuffer& output, MidiUtilityProcessReport& report)
noexcept {
374 for (
auto& trigger : triggers_) {
375 if (!trigger.active || trigger.channel != channel || trigger.note != note)
377 release_trigger(trigger, sample_offset, output, report);
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];
396 Trigger* free_trigger() noexcept {
397 for (
auto& trigger : triggers_)
403 ChordMemorySpec spec_{};
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;
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::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