Loading...
Searching...
No Matches
Chord.hpp
1#pragma once
2#include <Advanced/MidiScaler/MidiCommon/ChannelAllocator.hpp>
3#include <Advanced/MidiScaler/MidiCommon/EventQueue.hpp>
4#include <Advanced/MidiScaler/MidiCommon/Messages.hpp>
5#include <Advanced/MidiScaler/MidiCommon/NoteTracker.hpp>
6#include <Advanced/MidiScaler/MidiCommon/Theory.hpp>
7
8#include <halp/audio.hpp>
9#include <halp/channel_mask.hpp>
10#include <halp/controls.hpp>
11#include <halp/layout.hpp>
12#include <halp/meta.hpp>
13#include <halp/midi.hpp>
14#include <halp/string_list.hpp>
15#include <halp/value_list.hpp>
16#include <libremidi/message.hpp>
17
18#include <ossia/network/value/value.hpp>
19
20#include <algorithm>
21#include <array>
22#include <charconv>
23#include <cmath>
24#include <cstdint>
25#include <functional>
26#include <random>
27#include <string>
28#include <string_view>
29#include <vector>
30
31namespace Nodes
32{
33namespace Chord
34{
41template <int N>
43{
44 std::array<int, N> values{};
45 int count{};
46 bool valid{true};
47
48 void parse(std::string_view s, int min, int max) noexcept
49 {
50 count = 0;
51 valid = true;
52 std::size_t i = 0;
53 while(i < s.size())
54 {
55 const char c = s[i];
56 if(c == ' ' || c == ',' || c == '\t' || c == '\n' || c == '\r')
57 {
58 i++;
59 continue;
60 }
61 int v{};
62 const char* first = s.data() + i;
63 if(*first == '+')
64 first++;
65 auto [ptr, ec] = std::from_chars(first, s.data() + s.size(), v);
66 if(ec != std::errc{} || v < min || v > max || count >= N)
67 {
68 count = 0;
69 valid = false;
70 return;
71 }
72 values[count++] = v;
73 i = std::size_t(ptr - s.data());
74 }
75 }
76
78 void assign(const auto& values, int min, int max) noexcept
79 {
80 count = 0;
81 valid = true;
82 for(int v : values)
83 if(count < int(N))
84 this->values[count++] = std::clamp(v, min, max);
85 }
86
88 int at_or_last(int i, int fallback) const noexcept
89 {
90 if(count == 0)
91 return fallback;
92 return values[std::min(i, count - 1)];
93 }
94};
95
97template <int N, std::size_t MaxText = 256>
99{
100 int_list<N> list;
101
102 const int_list<N>& update(std::string_view text, int min, int max) noexcept
103 {
104 if(text.size() > MaxText)
105 text = text.substr(0, MaxText);
106 if(m_parsed && text == std::string_view{m_text.data(), m_size})
107 return list;
108 std::copy(text.begin(), text.end(), m_text.begin());
109 m_size = text.size();
110 m_parsed = true;
111 list.parse(text, min, max);
112 return list;
113 }
114
115private:
116 std::array<char, MaxText> m_text{};
117 std::size_t m_size{};
118 bool m_parsed{};
119};
120
121struct Node
122{
123 halp_meta(name, "Chord")
124 halp_meta(c_name, "Chord")
125 halp_meta(category, "Midi/Pitch")
126 halp_meta(author, "ossia score")
127 halp_meta(manual_url, "https://ossia.io/score-docs/processes/midi-utilities.html#chord")
128 halp_meta(description, "Generate a chord from a single note")
129 halp_meta(uuid, "F0904279-EA26-48DB-B0DF-F68FE3091DA1");
130 halp_flag(process_exec);
131 // MIDI packets the output keeps for all the ticks of one buffer
132 halp_meta(local_midi_tick_batch_reserve, 4096)
133
134
136 enum Chord : uint8_t
137 {
138 Maj,
139 Min,
140 Sus2,
141 Sus4,
142 Dim,
143 Aug,
144 T00, T01, T02, T03, T04, T05, T06, T07, T08, T09,
145 T10, T11, T12, T13, T14, T15, T16, T17, T18, T19,
146 T20, T21, T22, T23, T24, T25, T26, T27, T28, T29,
147 T30, T31, T32, T33, T34, T35, T36
148 };
149 static constexpr int legacy_chord_count = 6;
150 static constexpr int chord_count = legacy_chord_count + mtk::chord_type_count;
151 static_assert(chord_count == T36 + 1);
152
153 enum PlayMode : uint8_t
154 {
155 Table,
156 Harmonizer,
157 Memory
158 };
159
160 enum StrumDirection : uint8_t
161 {
162 Up,
163 Down,
164 Alternate
165 };
166
169 static constexpr int max_voices = 12;
170 static constexpr int max_intervals = 8;
173 static constexpr int max_memory_text = 128 * 64;
175 static constexpr int memory_row_id = 10000;
176
178 {
179 int8_t count{};
180 std::array<int8_t, max_voices> offsets{};
181 };
183 using memory_table = std::array<learned_chord, 128>;
184
185 using midi_out = halp::midi_out_bus<"out", libremidi::message>;
186 struct ins
187 {
188 halp::midi_bus<"in", libremidi::message> midi;
189 halp::hslider_i32<"Num. Notes", halp::irange{1, 5, 3}> num;
190 struct : halp::enum_t<Chord, "Chord">
191 {
192 // Names of the table that would only differ by case from another
193 // entry are spelled out: "sus2" is "sus2 triad" next to "Sus2", "M7+5"
194 // is "maj7+5" next to "m7+5". No name starts with a digit: an integer
195 // value is an index, and resolvers try names first.
196 struct range
197 {
198 std::string_view values[chord_count]
199 = {"Maj", "Min", "Sus2", "Sus4", "Dim",
200 "Aug", "maj triad", "min triad", "dim triad", "aug triad",
201 "Power", "sus2 triad", "sus4 triad", "maj6", "Dom7",
202 "Dom7-5", "Dom7+5", "Dom7sus4", "maj7", "maj7+5",
203 "add4", "add9", "sus4add9", "add11", "maj6/9",
204 "Dom9", "maj9", "Dom9sus4", "Dom7-9", "Dom7+11",
205 "m6", "m7", "m7-5", "m7+5", "dim7",
206 "mM7", "madd4", "madd9", "m69", "m9",
207 "m7b9b5", "m7add11", "mM7add11"};
208 Chord init = Maj;
209 };
210 } chord;
211
212 struct : halp::enum_t<PlayMode, "Mode">
213 {
214 halp_meta(
215 description,
216 "Chord plays the chosen chord type, Harmonizer the intervals, Memory "
217 "the chord learned for each key")
218 struct range
219 {
220 std::string_view values[3] = {"Chord", "Harmonizer", "Memory"};
221 PlayMode init = Table;
222 };
223 } mode;
224
225 struct : halp::value_list<"Intervals", int, max_intervals, 4, 7>
226 {
227 halp_meta(
228 description,
229 "Harmonizer: intervals in semitones from the played note, e.g. 4 7 or "
230 "-12 7. A repeated interval plays one note")
231 static constexpr std::array<halp::list_column, 1> columns()
232 {
233 return {halp::list_column{.name = "Semitones", .min = -48, .max = 48, .init = 7}};
234 }
235 } intervals;
236
237 struct : halp::toggle<"Play root", halp::toggle_setup{.init = true}>
238 {
239 halp_meta(description, "Harmonizer: also play the note itself")
240 } root;
241
242 struct : halp::spinbox_i32<"Inversion", halp::irange{-8, 8, 0}>
243 {
244 halp_meta(
245 description,
246 "Each step moves the lowest note an octave up (negative: the highest "
247 "an octave down). Past the number of notes, the whole chord moves by "
248 "octaves")
249 } inversion;
250
251 struct : halp::value_list<"Voice chance", float, max_voices, 1.f>
252 {
253 halp_meta(
254 description,
255 "Chance of each voice from 0 to 1, from the root up, e.g. 1 0.5 0.5. "
256 "The last value applies to the remaining voices")
257 static constexpr std::array<halp::list_column, 1> columns()
258 {
259 return {halp::list_column{
260 .name = "Chance", .min = 0., .max = 1., .init = 1., .integral = false}};
261 }
262 } chance;
263
264 struct : halp::value_list<"Voice velocity", float, max_voices, 1.f>
265 {
266 halp_meta(
267 description,
268 "Factor applied to the played velocity for each voice, from the root "
269 "up, e.g. 1 0.8 0.6. The last value applies to the remaining voices")
270 static constexpr std::array<halp::list_column, 1> columns()
271 {
272 return {halp::list_column{
273 .name = "Velocity", .min = 0., .max = 4., .init = 1., .integral = false}};
274 }
275 } velocity;
276
277 struct : halp::spinbox_i32<"Seed", halp::irange{0, 10000, 0}>
278 {
279 halp_meta(
280 description,
281 "0 draws freely; any other value gives a reproducible sequence of "
282 "chances, restarted on stop")
283 } seed;
284
285 struct : halp::time_chooser<"Strum", halp::range{0., 0.5, 0.}>
286 {
287 halp_meta(description, "Delay between successive notes of a chord")
288 } strum;
289
290 struct : halp::enum_t<StrumDirection, "Strum direction">
291 {
292 halp_meta(
293 description, "Order of the strum: low to high, high to low, or both in turn")
294 struct range
295 {
296 std::string_view values[3] = {"Up", "Down", "Alternate"};
297 StrumDirection init = Up;
298 };
299 } strum_direction;
300
301 struct : halp::knob_f32<"Tension", halp::range{-1., 1., 0.}>
302 {
303 halp_meta(
304 description,
305 "Above 0 the strum slows down across the chord, below 0 it speeds up")
306 } tension;
307
308 struct : halp::knob_f32<"Crescendo", halp::range{-1., 1., 0.}>
309 {
310 halp_meta(
311 description,
312 "Velocity change across the strum order: 1 doubles the last note, -1 "
313 "fades it out")
314 } crescendo;
315
316 struct : halp::toggle<"Learn">
317 {
318 halp_meta(
319 description,
320 "Memory learning: play a chord, release it, then press the key that "
321 "will trigger it")
322 } learn;
323
324 struct : halp::string_list<"Memory">
325 {
326 halp_meta(
327 description,
328 "Learned chords, one row per key: the key, then semitones from the "
329 "key, e.g. C4: 0 4 7 or 62: 0 3 7")
330 // Learning sends the table from the audio thread: the binding copies it
331 // into storage made beforehand, and controller_value makes the rows on
332 // the UI thread.
333 std::function<void(const memory_table&)> update_controller;
334 static halp::string_list_value controller_value(const memory_table& t)
335 {
336 return memory_rows(t);
337 }
338
341 static ossia::value upgrade_value(const ossia::value& old)
342 {
343 if(auto text = old.target<std::string>())
344 return rows_value(memory_rows_from_text(*text));
345 return old;
346 }
347 static ossia::value migrate_value(const ossia::value& v)
348 {
349 return upgrade_value(v);
350 }
351 } memory;
352
353 struct : halp::toggle<"MPE">
354 {
355 halp_meta(
356 description,
357 "Each voice on its own channel, with the pitch bend, pressure and "
358 "CC 74 of the note that started it")
359 } mpe;
360
361 struct : halp::channel_mask<"MPE channels", 0xFFFE>
362 {
363 halp_meta(description, "Channels the MPE voices use. None: 2 to 16")
364 } mpe_channels;
365 } inputs;
366
367 struct
368 {
369 midi_out midi;
370 } outputs;
371
372 struct ui
373 {
374 halp_meta(name, "Chord")
375 halp_meta(layout, halp::layouts::hbox)
376 halp_meta(background, halp::colors::background_dark)
377
378 struct
379 {
380 halp_meta(name, "Chord")
381 halp_meta(layout, halp::layouts::section)
382 halp_meta(background, halp::colors::background_mid)
383 halp::item<&ins::mode> mode;
384 struct
385 {
386 halp_meta(layout, halp::layouts::vbox)
387 static constexpr auto condition()
388 {
389 return halp::enabled_when<&ins::mode, PlayMode::Table>{};
390 }
391 halp::item<&ins::chord> chord;
392 halp::item<&ins::num> num;
393 } table;
394 halp::item<&ins::inversion> inversion;
395 } chord;
396
397 struct
398 {
399 halp_meta(name, "Strum")
400 halp_meta(layout, halp::layouts::section)
401 halp_meta(background, halp::colors::background_mid)
402 halp::item<&ins::strum> strum;
403 halp::item<&ins::strum_direction, halp::label_as("Direction")> strum_direction;
404 struct
405 {
406 halp_meta(layout, halp::layouts::hbox)
407 halp::item<&ins::tension> tension;
408 halp::item<&ins::crescendo> crescendo;
409 } shape;
410 } strum;
411
412 struct
413 {
414 halp_meta(name, "Settings")
415 halp_meta(layout, halp::layouts::tabs)
416 halp_meta(background, halp::colors::background_darker)
417
418 struct
419 {
420 halp_meta(name, "Harmonizer")
421 halp_meta(layout, halp::layouts::vbox)
422 static constexpr auto condition()
423 {
424 return halp::enabled_when<&ins::mode, PlayMode::Harmonizer>{};
425 }
426 halp::item<&ins::intervals> intervals;
427 halp::item<&ins::root> root;
428 } harmonizer;
429
430 struct
431 {
432 halp_meta(name, "Voices")
433 halp_meta(layout, halp::layouts::hbox)
434 halp::item<&ins::chance> chance;
435 halp::item<&ins::velocity> velocity;
436 halp::item<&ins::seed> seed;
437 } voices;
438
439 struct
440 {
441 halp_meta(name, "Memory")
442 halp_meta(layout, halp::layouts::vbox)
443 static constexpr auto condition()
444 {
445 return halp::enabled_when<&ins::mode, PlayMode::Memory>{};
446 }
447 halp::item<&ins::learn> learn;
448 halp::item<&ins::memory> memory;
449 } memory;
450
451 struct
452 {
453 halp_meta(name, "MPE")
454 halp_meta(layout, halp::layouts::vbox)
455 halp::item<&ins::mpe> mpe;
456 halp::item<&ins::mpe_channels, halp::label_as("Channels")> mpe_channels;
457 } mpe;
458 } settings;
459 };
460
461 // C C# D D# E F F# G G# A A# B
462 // 1 . . . 1 . . 1 . 1 . .
463 static const constexpr std::array<int, 5> major7{0, 4, 7, 11, 12};
464 static const constexpr std::array<int, 5> minor7{0, 3, 7, 10, 12};
465 static const constexpr std::array<int, 5> sus2{0, 2, 7, 9, 12};
466 static const constexpr std::array<int, 5> sus4{0, 5, 7, 9, 12};
467 static const constexpr std::array<int, 5> dim{0, 3, 6, 9, 12};
468 static const constexpr std::array<int, 5> aug{0, 4, 8, 10, 12};
469 static constexpr std::array<const std::array<int, 5>*, legacy_chord_count> legacy_chords{
470 &major7, &minor7, &sus2, &sus4, &dim, &aug};
471
472 Node()
473 {
474 std::random_device rd;
475 m_free_seed = (uint64_t(rd()) << 32) ^ uint64_t(rd());
476 reseed();
477 }
478
479 void prepare(halp::setup info) noexcept
480 {
481 if(info.rate > 0.)
482 m_rate = info.rate;
483 }
484
485 void stop() noexcept { m_stop_requested = true; }
486
487 using tick = halp::tick_musical;
488 void operator()(const halp::tick_musical& tk) noexcept
489 {
490 const int frames = tk.frames > 0 ? tk.frames : 0;
491 update_parameters();
492
493 if(m_stop_requested)
494 {
495 m_stop_requested = false;
496 panic(0);
497 }
498
499 for(const libremidi::message& m : inputs.midi)
500 {
501 const int64_t ts
502 = frames > 0 ? std::clamp<int64_t>(m.timestamp, 0, frames - 1) : 0;
503 release_queue(int(ts));
504 handle(m, ts);
505 }
506 release_queue(frames);
507 m_now += frames;
508 }
509
512 int voices(int pitch, std::array<int, max_voices>& out) noexcept
513 {
514 update_parameters();
515 std::array<voice, max_voices> v{};
516 const int n = build_voices(pitch, v);
517 for(int i = 0; i < n; i++)
518 out[i] = v[i].offset;
519 return n;
520 }
521
522private:
523 struct voice
524 {
525 int offset{};
526 int index{};
527 int velocity{};
528 int64_t delay{};
529 };
530
531 using tracker_type = mtk::note_tracker<128, max_voices>;
532
533 static uint32_t tag_of(int channel, int pitch) noexcept
534 {
535 return uint32_t(((channel - 1) << 7) | pitch) + 1;
536 }
537
538 // ------------------------------------------------------------ parameters
539
540 void update_parameters() noexcept
541 {
542 const int seed = inputs.seed;
543 if(seed != m_seed)
544 {
545 m_seed = seed;
546 reseed();
547 }
548
549 m_intervals.assign(inputs.intervals.value, -127, 127);
550 update_memory(inputs.memory.value);
551
552 const uint16_t mask = inputs.mpe_channels.mask();
553 m_alloc.set_channels(mask != 0 ? mask : uint16_t(0xFFFE));
554
555 if(!inputs.learn.value)
556 reset_learning();
557 }
558
559 void reseed() noexcept
560 {
561 m_rng = m_seed != 0 ? uint64_t(m_seed) * UINT64_C(0x9E3779B97F4A7C15) : m_free_seed;
562 }
563
566 static float voice_factor(const auto& list, int i, float max) noexcept
567 {
568 if(list.empty())
569 return 1.f;
570 const float v = list[std::min(std::size_t(i), list.size() - 1)];
571 return std::isfinite(v) ? std::clamp(v, 0.f, max) : 1.f;
572 }
573
575 double random01() noexcept
576 {
577 uint64_t z = (m_rng += UINT64_C(0x9E3779B97F4A7C15));
578 z = (z ^ (z >> 30)) * UINT64_C(0xBF58476D1CE4E5B9);
579 z = (z ^ (z >> 27)) * UINT64_C(0x94D049BB133111EB);
580 z = z ^ (z >> 31);
581 return double(z >> 11) * 0x1.0p-53;
582 }
583
584 // ---------------------------------------------------------------- memory
585
589 void update_memory(const halp::string_list_value& rows) noexcept
590 {
591 // The rows as one text, a row per line, compared with the last one
592 // without copying it.
593 std::size_t n = 0;
594 bool same = m_memory_parsed;
595 for_each_memory_char(rows, [&](char c) {
596 same = same && n < m_memory_size && m_memory_text[n] == c;
597 n++;
598 });
599 if(same && n == m_memory_size)
600 return;
601 m_memory_size = 0;
602 for_each_memory_char(rows, [&](char c) { m_memory_text[m_memory_size++] = c; });
603 m_memory_parsed = true;
604 parse_memory({m_memory_text.data(), m_memory_size}, m_memory);
605 }
606
608 static void
609 for_each_memory_char(const halp::string_list_value& rows, auto&& f) noexcept
610 {
611 std::size_t n = 0;
612 for(const auto& [id, text] : rows)
613 {
614 for(char c : text)
615 {
616 if(n++ >= max_memory_text)
617 return;
618 f(c);
619 }
620 if(n++ >= max_memory_text)
621 return;
622 f('\n');
623 }
624 }
625
626public:
629 static void parse_memory(std::string_view s, memory_table& table) noexcept
630 {
631 table = {};
632 std::size_t start = 0;
633 while(start <= s.size())
634 {
635 std::size_t end = start;
636 while(end < s.size() && s[end] != ';' && s[end] != '\n')
637 end++;
638 parse_memory_entry(s.substr(start, end - start), table);
639 start = end + 1;
640 }
641 }
642
645 static std::size_t print_memory_entry(int key, const learned_chord& c, char* buf) noexcept
646 {
647 static constexpr std::string_view names[12]
648 = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};
649 char* const end = buf + 64;
650 char* p = buf;
651 const auto name = names[key % 12];
652 p = std::copy(name.begin(), name.end(), p);
653 p = std::to_chars(p, end, key / 12 - 1).ptr;
654 *p++ = ':';
655 for(int i = 0; i < c.count; i++)
656 {
657 *p++ = ' ';
658 p = std::to_chars(p, end, int(c.offsets[i])).ptr;
659 }
660 return std::size_t(p - buf);
661 }
662
665 static halp::string_list_value memory_rows(const memory_table& table)
666 {
667 halp::string_list_value rows;
668 for(int key = 0; key < 128; key++)
669 {
670 if(table[key].count <= 0)
671 continue;
672 char buf[64];
673 rows.emplace_back(
674 memory_row_id + key, std::string(buf, print_memory_entry(key, table[key], buf)));
675 }
676 return rows;
677 }
678
681 static halp::string_list_value memory_rows_from_text(std::string_view s)
682 {
683 halp::string_list_value rows;
684 std::size_t start = 0;
685 while(start <= s.size())
686 {
687 std::size_t end = start;
688 while(end < s.size() && s[end] != ';' && s[end] != '\n')
689 end++;
690 auto entry = s.substr(start, end - start);
691 start = end + 1;
692 while(!entry.empty() && (entry.front() == ' ' || entry.front() == '\t'))
693 entry.remove_prefix(1);
694 while(!entry.empty()
695 && (entry.back() == ' ' || entry.back() == '\t' || entry.back() == '\r'))
696 entry.remove_suffix(1);
697 if(entry.empty())
698 continue;
699
700 const int id = memory_row_id + int(rows.size());
701 memory_table one{};
702 parse_memory_entry(entry, one);
703 auto it = std::find_if(one.begin(), one.end(), [](auto& c) { return c.count > 0; });
704 if(it != one.end())
705 {
706 char buf[64];
707 rows.emplace_back(
708 id, std::string(buf, print_memory_entry(int(it - one.begin()), *it, buf)));
709 }
710 else
711 {
712 rows.emplace_back(id, std::string(entry));
713 }
714 }
715 return rows;
716 }
717
718 static ossia::value rows_value(const halp::string_list_value& rows)
719 {
720 std::vector<ossia::value> res;
721 res.reserve(rows.size());
722 for(const auto& [id, text] : rows)
723 res.emplace_back(std::vector<ossia::value>{id, text});
724 return res;
725 }
726
727private:
728 static void parse_memory_entry(std::string_view e, memory_table& table) noexcept
729 {
730 const auto colon = e.find(':');
731 if(colon == std::string_view::npos)
732 return;
733 auto key_text = e.substr(0, colon);
734 while(!key_text.empty() && (key_text.front() == ' ' || key_text.front() == '\t'))
735 key_text.remove_prefix(1);
736 while(!key_text.empty() && (key_text.back() == ' ' || key_text.back() == '\t' || key_text.back() == '\r'))
737 key_text.remove_suffix(1);
738 if(key_text.empty())
739 return;
740
741 int key = -1;
742 if(key_text[0] >= '0' && key_text[0] <= '9')
743 {
744 auto [ptr, ec] = std::from_chars(key_text.data(), key_text.data() + key_text.size(), key);
745 if(ec != std::errc{} || ptr != key_text.data() + key_text.size())
746 return;
747 }
748 else if(auto n = mtk::parse_note_name(key_text); n && n->second == key_text.size())
749 {
750 key = n->first;
751 }
752 if(key < 0 || key > 127)
753 return;
754
755 int_list<max_voices> offsets;
756 offsets.parse(e.substr(colon + 1), -127, 127);
757 if(!offsets.valid || offsets.count == 0)
758 return;
759 auto& c = table[key];
760 c.count = int8_t(offsets.count);
761 for(int i = 0; i < offsets.count; i++)
762 c.offsets[i] = int8_t(offsets.values[i]);
763 }
764
765 // -------------------------------------------------------------- learning
766
767 void reset_learning() noexcept
768 {
769 m_learn_down = {};
770 m_learn_down_count = 0;
771 m_group_count = 0;
772 m_captured_count = 0;
773 m_trigger_down = false;
774 }
775
778 void learn_note_on(int pitch) noexcept
779 {
780 if(m_learn_down[pitch])
781 return;
782 if(m_learn_down_count == 0)
783 {
784 m_group_count = 0;
785 if(m_captured_count >= 2)
786 {
787 assign_learned(pitch);
788 m_captured_count = 0;
789 m_trigger_down = true;
790 }
791 }
792 m_learn_down[pitch] = true;
793 m_learn_down_count++;
794 if(!m_trigger_down && m_group_count < max_voices)
795 m_group[m_group_count++] = pitch;
796 }
797
798 void learn_note_off(int pitch) noexcept
799 {
800 if(!m_learn_down[pitch])
801 return;
802 m_learn_down[pitch] = false;
803 if(--m_learn_down_count > 0)
804 return;
805 if(!m_trigger_down && m_group_count >= 2)
806 {
807 m_captured = m_group;
808 m_captured_count = m_group_count;
809 std::sort(m_captured.begin(), m_captured.begin() + m_captured_count);
810 }
811 m_trigger_down = false;
812 m_group_count = 0;
813 }
814
815 void assign_learned(int key) noexcept
816 {
817 auto& c = m_memory[key];
818 c.count = int8_t(m_captured_count);
819 for(int i = 0; i < m_captured_count; i++)
820 c.offsets[i] = int8_t(m_captured[i] - key);
821
822 // The rows sent to the control are what saves the learned chord with the
823 // document.
824 if(inputs.memory.update_controller)
825 inputs.memory.update_controller(m_memory);
826 }
827
828 // ---------------------------------------------------------------- voices
829
830 int build_voices(int pitch, std::array<voice, max_voices>& v) noexcept
831 {
832 int n = 0;
833 auto add = [&](int offset) {
834 if(n < max_voices)
835 {
836 v[n].offset = offset;
837 v[n].index = n;
838 n++;
839 }
840 };
841
842 if(inputs.learn.value)
843 {
844 add(0);
845 return n;
846 }
847
848 switch(inputs.mode.value)
849 {
850 case Table:
851 default: {
852 const int chord = std::clamp(int(inputs.chord.value), 0, chord_count - 1);
853 const int num = std::clamp(int(inputs.num.value), 1, max_voices);
854 if(chord < legacy_chord_count)
855 {
856 const auto& c = *legacy_chords[chord];
857 for(int i = 0; i < std::min(num, int(c.size())); i++)
858 add(c[i]);
859 }
860 else
861 {
862 const auto notes = mtk::chord_types[chord - legacy_chord_count].notes();
863 for(int i = 0; i < std::min(num, int(notes.size())); i++)
864 add(notes[i]);
865 }
866 break;
867 }
868 case Harmonizer: {
869 if(inputs.root.value)
870 add(0);
871 const auto& iv = m_intervals;
872 for(int i = 0; i < std::min(iv.count, max_intervals); i++)
873 {
874 bool seen = false;
875 for(int k = 0; k < n; k++)
876 seen |= v[k].offset == iv.values[i];
877 if(!seen)
878 add(iv.values[i]);
879 }
880 break;
881 }
882 case Memory: {
883 const auto& c = m_memory[pitch & 127];
884 if(c.count > 0)
885 for(int i = 0; i < c.count; i++)
886 add(c.offsets[i]);
887 else
888 add(0);
889 break;
890 }
891 }
892
893 invert(v, n, inputs.inversion.value);
894 return n;
895 }
896
899 static void invert(std::array<voice, max_voices>& v, int& n, int steps) noexcept
900 {
901 if(n == 0)
902 return;
903 for(int s = 0; s < steps; s++)
904 {
905 int lo = 0;
906 for(int i = 1; i < n; i++)
907 if(v[i].offset < v[lo].offset)
908 lo = i;
909 v[lo].offset += 12;
910 }
911 for(int s = 0; s > steps; s--)
912 {
913 int hi = 0;
914 for(int i = 1; i < n; i++)
915 if(v[i].offset > v[hi].offset)
916 hi = i;
917 v[hi].offset -= 12;
918 }
919 int w = 0;
920 for(int r = 0; r < n; r++)
921 {
922 bool seen = false;
923 for(int k = 0; k < w; k++)
924 seen |= v[k].offset == v[r].offset;
925 if(!seen)
926 v[w++] = v[r];
927 }
928 n = w;
929 }
930
931 // -------------------------------------------------------------- handling
932
933 void handle(const libremidi::message& m, int64_t ts) noexcept
934 {
935 const int ch = mtk::channel_of(m);
936 switch(mtk::classify(m))
937 {
938 case mtk::midi_kind::note_on:
939 note_on(ch, m.bytes[1] & 127, m.bytes[2] & 127, ts);
940 return;
941 case mtk::midi_kind::note_off:
942 if(m.size() >= 3)
943 {
944 const int vel = (m.bytes[0] & 0xF0) == 0x80 ? (m.bytes[2] & 127) : 0;
945 note_off(ch, m.bytes[1] & 127, vel, ts);
946 return;
947 }
948 break;
949 case mtk::midi_kind::control_change:
950 if(mtk::is_all_notes_off(m))
951 {
952 release_channel(ch, ts);
953 break;
954 }
955 if(m.size() >= 3 && m.bytes[1] == 74)
956 {
957 m_in_timbre[ch - 1] = int16_t(m.bytes[2] & 127);
958 if(forward_expression(m, ch, -1))
959 return;
960 }
961 break;
962 case mtk::midi_kind::pitch_bend:
963 if(m.size() >= 3)
964 m_in_bend[ch - 1] = int16_t((m.bytes[2] << 7) | m.bytes[1]);
965 if(forward_expression(m, ch, -1))
966 return;
967 break;
968 case mtk::midi_kind::channel_pressure:
969 if(m.size() >= 2)
970 m_in_pressure[ch - 1] = int16_t(m.bytes[1] & 127);
971 if(forward_expression(m, ch, -1))
972 return;
973 break;
974 case mtk::midi_kind::poly_pressure:
975 if(m.size() >= 3 && forward_expression(m, ch, m.bytes[1] & 127))
976 return;
977 break;
978 default:
979 break;
980 }
981 outputs.midi.push_back(m);
982 }
983
984 void note_on(int ch, int pitch, int vel, int64_t ts) noexcept
985 {
986 if(inputs.learn.value)
987 learn_note_on(pitch);
988
989 // A key pressed again without a release ends its previous chord first.
990 if(m_tracker.holds(ch, pitch))
991 end_input(ch, pitch, 0, ts);
992
993 if(m_tracker.size() == 128)
994 {
995 const auto& oldest = m_tracker[0];
996 cancel_pending(oldest.channel, oldest.pitch);
997 }
998 m_tracker.begin(ch, pitch, ts, [this](libremidi::message&& m) { emit(std::move(m)); });
999
1000 std::array<voice, max_voices> v{};
1001 int n = build_voices(pitch, v);
1002
1003 // Chance and velocity per voice index; the draw happens for every voice
1004 // so that the sequence of a seed does not depend on the values.
1005 int kept = 0;
1006 for(int i = 0; i < n; i++)
1007 {
1008 const double draw = random01();
1009 const float chance = voice_factor(inputs.chance.value, v[i].index, 1.f);
1010 const float gain = voice_factor(inputs.velocity.value, v[i].index, 4.f);
1011 const int out = pitch + v[i].offset;
1012 if(draw >= chance || out < 0 || out > 127)
1013 continue;
1014 v[kept] = v[i];
1015 v[kept].offset = out;
1016 v[kept].velocity = int(std::lround(vel * double(gain)));
1017 kept++;
1018 }
1019 n = kept;
1020
1021 // Strum order
1022 bool down = inputs.strum_direction.value == Down;
1023 if(inputs.strum_direction.value == Alternate)
1024 {
1025 down = m_alternate_down;
1026 m_alternate_down = !m_alternate_down;
1027 }
1028 // Insertion sort: stable, and std::stable_sort may allocate.
1029 for(int i = 1; i < n; i++)
1030 {
1031 const voice x = v[i];
1032 int j = i;
1033 while(j > 0 && (down ? v[j - 1].offset < x.offset : v[j - 1].offset > x.offset))
1034 {
1035 v[j] = v[j - 1];
1036 j--;
1037 }
1038 v[j] = x;
1039 }
1040
1041 const double strum = std::max(0., double(inputs.strum.value)) * m_rate;
1042 const double tension = std::clamp(double(inputs.tension.value), -1., 1.);
1043 const double crescendo = std::clamp(double(inputs.crescendo.value), -1., 1.);
1044 const double exponent = std::exp2(2. * tension);
1045 for(int k = 0; k < n; k++)
1046 {
1047 const double x = n > 1 ? double(k) / double(n - 1) : 0.;
1048 const double total = strum * double(n - 1);
1049 v[k].delay = int64_t(std::llround(total * std::pow(x, exponent)));
1050 v[k].velocity
1051 = std::clamp(int(std::lround(v[k].velocity * (1. + crescendo * x))), 1, 127);
1052 }
1053
1054 auto* h = m_tracker.find(ch, pitch);
1055 if(!h)
1056 return;
1057 for(int k = 0; k < n; k++)
1058 {
1059 if(v[k].delay <= 0)
1060 {
1061 start_voice(*h, ch, v[k].offset, v[k].velocity, ts);
1062 }
1063 else
1064 {
1065 m_queue.schedule(
1066 m_now + ts + v[k].delay,
1067 libremidi::channel_events::note_on(ch, v[k].offset, v[k].velocity),
1068 tag_of(ch, pitch));
1069 }
1070 }
1071 }
1072
1075 void note_off(int ch, int pitch, int vel, int64_t ts) noexcept
1076 {
1077 if(inputs.learn.value)
1078 learn_note_off(pitch);
1079 end_input(ch, pitch, vel, ts);
1080 }
1081
1082 bool end_input(int ch, int pitch, int vel, int64_t ts) noexcept
1083 {
1084 cancel_pending(ch, pitch);
1085 return m_tracker.end(
1086 ch, pitch, ts, vel, [this](libremidi::message&& m) { emit(std::move(m)); });
1087 }
1088
1089 void cancel_pending(int ch, int pitch) noexcept
1090 {
1091 const uint32_t tag = tag_of(ch, pitch);
1092 m_queue.remove_if([tag](const auto& e) { return e.tag == tag; });
1093 }
1094
1095 void start_voice(tracker_type::held& h, int in_ch, int pitch, int vel, int64_t ts) noexcept
1096 {
1097 if(h.count >= max_voices || pitch < 0 || pitch > 127)
1098 return;
1099 auto send = [this](libremidi::message&& m) { emit(std::move(m)); };
1100 if(!inputs.mpe.value)
1101 {
1102 m_tracker.play(h, in_ch, pitch, vel, ts, send);
1103 return;
1104 }
1105
1106 const auto ch = m_alloc.allocate(m_now + ts, [this] { return random01(); });
1107 if(!ch)
1108 return;
1109 // The voice's channel takes on the expression of the input channel
1110 // before its note starts
1111 const int16_t bend = m_in_bend[in_ch - 1];
1112 if(m_out_bend[*ch - 1] != bend)
1113 {
1114 auto b = libremidi::channel_events::pitch_bend(*ch, int(bend));
1115 b.timestamp = ts;
1116 outputs.midi.push_back(std::move(b));
1117 m_out_bend[*ch - 1] = bend;
1118 }
1119 if(const int16_t p = m_in_pressure[in_ch - 1]; m_out_pressure[*ch - 1] != p)
1120 {
1121 auto b = libremidi::channel_events::aftertouch(*ch, uint8_t(p));
1122 b.timestamp = ts;
1123 outputs.midi.push_back(std::move(b));
1124 m_out_pressure[*ch - 1] = p;
1125 }
1126 if(const int16_t t = m_in_timbre[in_ch - 1]; m_out_timbre[*ch - 1] != t)
1127 {
1128 auto b = libremidi::channel_events::control_change(*ch, 74, uint8_t(t));
1129 b.timestamp = ts;
1130 outputs.midi.push_back(std::move(b));
1131 m_out_timbre[*ch - 1] = t;
1132 }
1133 m_mpe_voice[*ch - 1][pitch] = true;
1134 m_tracker.play(h, *ch, pitch, vel, ts, send);
1135 }
1136
1137 void emit(libremidi::message&& m) noexcept
1138 {
1139 if(mtk::is_note_off(m))
1140 {
1141 const int ch = mtk::channel_of(m);
1142 auto& mpe = m_mpe_voice[ch - 1][m.bytes[1] & 127];
1143 if(mpe)
1144 {
1145 mpe = false;
1146 m_alloc.release(ch, m_now + m.timestamp);
1147 }
1148 }
1149 outputs.midi.push_back(std::move(m));
1150 }
1151
1155 bool forward_expression(const libremidi::message& m, int ch, int pitch) noexcept
1156 {
1157 if(!inputs.mpe.value)
1158 return false;
1159 uint16_t sent = 0;
1160 bool any = false;
1161 for(int i = 0; i < m_tracker.size(); i++)
1162 {
1163 const auto& h = m_tracker[i];
1164 if(h.channel != ch || (pitch >= 0 && h.pitch != pitch))
1165 continue;
1166 for(int k = 0; k < h.count; k++)
1167 {
1168 const auto& o = h.outputs[k];
1169 any = true;
1170 if(pitch >= 0)
1171 {
1172 auto c = m;
1173 mtk::set_channel(c, o.channel);
1174 c.bytes[1] = o.pitch;
1175 outputs.midi.push_back(std::move(c));
1176 continue;
1177 }
1178 if(sent & (1u << (o.channel - 1)))
1179 continue;
1180 sent |= uint16_t(1u << (o.channel - 1));
1181 auto c = m;
1182 mtk::set_channel(c, o.channel);
1183 switch(mtk::classify(c))
1184 {
1185 case mtk::midi_kind::pitch_bend:
1186 m_out_bend[o.channel - 1] = m_in_bend[ch - 1];
1187 break;
1188 case mtk::midi_kind::channel_pressure:
1189 m_out_pressure[o.channel - 1] = m_in_pressure[ch - 1];
1190 break;
1191 case mtk::midi_kind::control_change:
1192 m_out_timbre[o.channel - 1] = m_in_timbre[ch - 1];
1193 break;
1194 default:
1195 break;
1196 }
1197 outputs.midi.push_back(std::move(c));
1198 }
1199 }
1200 return any;
1201 }
1202
1205 void release_channel(int ch, int64_t ts) noexcept
1206 {
1207 if(inputs.mpe.value)
1208 {
1209 m_queue.clear();
1210 m_tracker.end_all(ts, [this](libremidi::message&& m) { emit(std::move(m)); });
1211 return;
1212 }
1213 std::array<uint8_t, 128> pitches{};
1214 int n = 0;
1215 for(int i = 0; i < m_tracker.size() && n < 128; i++)
1216 if(m_tracker[i].channel == ch)
1217 pitches[n++] = m_tracker[i].pitch;
1218 for(int i = 0; i < n; i++)
1219 end_input(ch, pitches[i], 0, ts);
1220 }
1221
1222 void release_queue(int upto) noexcept
1223 {
1224 m_queue.release(m_now, upto, [this](libremidi::message&& m, uint32_t tag) {
1225 if(tag == 0)
1226 return;
1227 const int ch = int((tag - 1) >> 7) + 1;
1228 const int pitch = int((tag - 1) & 127);
1229 if(auto* h = m_tracker.find(ch, pitch))
1230 start_voice(*h, ch, m.bytes[1], m.bytes[2], m.timestamp);
1231 });
1232 }
1233
1234 void panic(int64_t ts) noexcept
1235 {
1236 m_queue.clear();
1237 m_tracker.end_all(ts, [this](libremidi::message&& m) { emit(std::move(m)); });
1238 m_alloc.reset();
1239 m_mpe_voice = {};
1240 m_alternate_down = false;
1241 reset_learning();
1242 reseed();
1243 }
1244
1245 tracker_type m_tracker;
1246 mtk::event_queue<512> m_queue;
1247 mtk::channel_allocator m_alloc;
1248 int_list<max_intervals> m_intervals;
1249
1250 memory_table m_memory{};
1251 std::array<char, max_memory_text> m_memory_text{};
1252 std::size_t m_memory_size{};
1253 bool m_memory_parsed{};
1254
1255 std::array<bool, 128> m_learn_down{};
1256 int m_learn_down_count{};
1257 std::array<int, max_voices> m_group{};
1258 int m_group_count{};
1259 std::array<int, max_voices> m_captured{};
1260 int m_captured_count{};
1261 bool m_trigger_down{};
1262
1263 std::array<std::array<bool, 128>, 16> m_mpe_voice{};
1264 std::array<int16_t, 16> m_in_bend{8192, 8192, 8192, 8192, 8192, 8192, 8192, 8192,
1265 8192, 8192, 8192, 8192, 8192, 8192, 8192, 8192};
1266 std::array<int16_t, 16> m_out_bend{m_in_bend};
1269 std::array<int16_t, 16> m_in_pressure{};
1270 std::array<int16_t, 16> m_out_pressure{};
1271 std::array<int16_t, 16> m_in_timbre{64, 64, 64, 64, 64, 64, 64, 64,
1272 64, 64, 64, 64, 64, 64, 64, 64};
1273 std::array<int16_t, 16> m_out_timbre{m_in_timbre};
1274
1275 double m_rate{48000.};
1276 int64_t m_now{};
1277 uint64_t m_rng{};
1278 uint64_t m_free_seed{};
1279 int m_seed{};
1280 bool m_alternate_down{};
1281 bool m_stop_requested{};
1282};
1283}
1284}
STL namespace.
Definition Chord.hpp:187
Definition Chord.hpp:178
Definition Chord.hpp:373
Definition Chord.hpp:122
static constexpr int memory_row_id
Identity of the row of a learned key, in the Memory list.
Definition Chord.hpp:175
static void parse_memory(std::string_view s, memory_table &table) noexcept
Definition Chord.hpp:629
static halp::string_list_value memory_rows_from_text(std::string_view s)
Definition Chord.hpp:681
static constexpr int max_memory_text
Definition Chord.hpp:173
static constexpr int max_voices
Definition Chord.hpp:169
static halp_meta(local_midi_tick_batch_reserve, 4096) enum Chord constexpr int legacy_chord_count
Definition Chord.hpp:149
static std::size_t print_memory_entry(int key, const learned_chord &c, char *buf) noexcept
Definition Chord.hpp:645
int voices(int pitch, std::array< int, max_voices > &out) noexcept
Definition Chord.hpp:512
static halp::string_list_value memory_rows(const memory_table &table)
Definition Chord.hpp:665
std::array< learned_chord, 128 > memory_table
The learned chord of each key.
Definition Chord.hpp:183
Keeps an int_list in sync with a text control, parsing only on change.
Definition Chord.hpp:99
A list of integers written as text: "4 7 -12" or "100, 50".
Definition Chord.hpp:43
int at_or_last(int i, int fallback) const noexcept
Element i, the last one repeating past the end; fallback when empty.
Definition Chord.hpp:88
void assign(const auto &values, int min, int max) noexcept
The elements of a list control, clamped.
Definition Chord.hpp:78
Definition MIDISync.hpp:149