DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
HarmonyConstants.h
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
25#include <array>
26#include <string_view>
27#include <cstddef> // std::size_t (detail::copy, scalesForChordMask...)
28#include <cstdint>
29#include <algorithm>
30#include <optional>
31#include <type_traits> // std::underlying_type_t (ChordTag operators)
32#include <utility> // std::pair (parseNote lookup table)
33#include <initializer_list> // std::initializer_list (makeMask overload)
34
35namespace dspark {
36namespace harmony
37{
38 //==============================================================================
39 // 0. CORE TYPEDEFS
40 //==============================================================================
41
49 using NoteSet = std::uint16_t;
50
55 using Degree = int;
56
57 //==============================================================================
58 // 1. CHORDTAG - A FILTER FOR CHORD TYPES
59 //==============================================================================
60
74 enum class ChordTag : std::uint16_t
75 {
76 MajorTriad = 1u << 0,
77 MinorTriad = 1u << 1,
78 DiminishedTriad = 1u << 2,
79 AugmentedTriad = 1u << 3,
80 Major7 = 1u << 4,
81 Dominant7 = 1u << 5,
82 Minor7 = 1u << 6,
83 HalfDim7 = 1u << 7,
84 Dim7 = 1u << 8,
85 Sus2Triad = 1u << 9,
86 Sus4Triad = 1u << 10,
87 Major9 = 1u << 11,
88 Dominant9 = 1u << 12,
89 Minor9 = 1u << 13,
90 Major11 = 1u << 14,
91 Dominant11 = 1u << 15,
92 All = 0xFFFFu
93 };
94
99 [[nodiscard]] constexpr ChordTag operator|(ChordTag lhs, ChordTag rhs) noexcept
100 {
101 using U = std::underlying_type_t<ChordTag>;
102 return static_cast<ChordTag>(static_cast<U>(lhs) | static_cast<U>(rhs));
103 }
104
109 [[nodiscard]] constexpr ChordTag operator&(ChordTag lhs, ChordTag rhs) noexcept
110 {
111 using U = std::underlying_type_t<ChordTag>;
112 return static_cast<ChordTag>(static_cast<U>(lhs) & static_cast<U>(rhs));
113 }
114
118 [[nodiscard]] constexpr bool hasTags(ChordTag tags, ChordTag wanted) noexcept
119 {
120 using U = std::underlying_type_t<ChordTag>;
121 return (static_cast<U>(tags) & static_cast<U>(wanted)) == static_cast<U>(wanted);
122 }
123
124 //==============================================================================
125 // 2. SCALE DESCRIPTOR
126 //==============================================================================
127
132 struct Scale
133 {
134 std::string_view name;
137 };
138
139 //==============================================================================
140 // 3. INTERNAL HELPERS & MASK GENERATION
141 //==============================================================================
142
146 [[nodiscard]] consteval NoteSet makeMask(
147 bool b0, bool b1, bool b2, bool b3,
148 bool b4, bool b5, bool b6, bool b7,
149 bool b8, bool b9, bool b10, bool b11) noexcept
150 {
151 return (static_cast<NoteSet>(b0) << 0) | (static_cast<NoteSet>(b1) << 1) |
152 (static_cast<NoteSet>(b2) << 2) | (static_cast<NoteSet>(b3) << 3) |
153 (static_cast<NoteSet>(b4) << 4) | (static_cast<NoteSet>(b5) << 5) |
154 (static_cast<NoteSet>(b6) << 6) | (static_cast<NoteSet>(b7) << 7) |
155 (static_cast<NoteSet>(b8) << 8) | (static_cast<NoteSet>(b9) << 9) |
156 (static_cast<NoteSet>(b10) << 10) | (static_cast<NoteSet>(b11) << 11);
157 }
158
163 [[nodiscard]] consteval NoteSet makeMask(std::initializer_list<int> degrees) noexcept
164 {
165 NoteSet m = 0;
166 for (int d : degrees) {
167 if (d >= 0) { m |= static_cast<NoteSet>(1u << (d % 12)); }
168 }
169 return m & 0x0FFFu;
170 }
171
172 namespace detail
173 {
177 constexpr void copy(char* dst, std::string_view src, std::size_t dstCapacity) noexcept
178 {
179 std::size_t toCopy = src.size();
180 if (toCopy + 1u > dstCapacity) { toCopy = (dstCapacity > 0) ? dstCapacity - 1u : 0u; }
181 for (std::size_t i = 0; i < toCopy; ++i) { dst[i] = src[i]; }
182 if (dstCapacity > 0) { dst[toCopy] = '\0'; }
183 }
184
198 [[nodiscard]] constexpr std::array<int, 7> activeDegrees(NoteSet mask) noexcept
199 {
200 std::array<int, 12> temp{};
201 int tcount = 0;
202 for (int i = 0; i < 12; ++i) {
203 if (mask & (1u << i)) temp[tcount++] = i;
204 }
205
206 std::array<int, 7> out{};
207 if (tcount == 0) {
208 for (int i = 0; i < 7; ++i) out[i] = i;
209 return out;
210 }
211
212 for (int i = 0; i < 7; ++i) {
213 int idx = i % tcount;
214 int octave = i / tcount;
215 out[i] = temp[idx] + octave * 12;
216 }
217 return out;
218 }
219
223 [[nodiscard]] constexpr int interval(const std::array<int, 7>& deg, int degIdx, int skip) noexcept
224 {
225 int a = deg[degIdx];
226 int b = deg[(degIdx + skip) % 7];
227 while (b <= a) { b += 12; }
228 return b - a;
229 }
230 } // namespace detail
231
232 //==============================================================================
233 // 4. DATABASE OF SCALES & CONSTANTS
234 //==============================================================================
235
236 inline constexpr std::array<Scale, 61> allScales = [](){
237 std::array<Scale, 61> scales{};
238
239 // Major modes
240 scales[0] = {"Ionian", makeMask({0,2,4,5,7,9,11}), ChordTag::MajorTriad | ChordTag::Major7 | ChordTag::Dominant7};
241 scales[1] = {"Dorian", makeMask({0,2,3,5,7,9,10}), ChordTag::MinorTriad | ChordTag::Minor7};
242 scales[2] = {"Phrygian", makeMask({0,1,3,5,7,8,10}), ChordTag::MinorTriad | ChordTag::DiminishedTriad | ChordTag::HalfDim7};
243 scales[3] = {"Lydian", makeMask({0,2,4,6,7,9,11}), ChordTag::MajorTriad | ChordTag::Major7};
244 scales[4] = {"Mixolydian", makeMask({0,2,4,5,7,9,10}), ChordTag::MajorTriad | ChordTag::Dominant7};
245 scales[5] = {"Aeolian", makeMask({0,2,3,5,7,8,10}), ChordTag::MinorTriad | ChordTag::Minor7};
246 scales[6] = {"Locrian", makeMask({0,1,3,5,6,8,10}), ChordTag::MinorTriad | ChordTag::DiminishedTriad | ChordTag::HalfDim7};
247
248 // Melodic-minor modes
249 scales[7] = {"MelodicMinor", makeMask({0,2,3,5,7,9,11}), ChordTag::MinorTriad | ChordTag::Minor7};
250 scales[8] = {"Dorianb2", makeMask({0,1,3,5,7,9,10}), ChordTag::MinorTriad};
251 scales[9] = {"LydianAugmented", makeMask({0,2,4,6,8,9,11}), ChordTag::MajorTriad | ChordTag::AugmentedTriad};
252 scales[10] = {"LydianDominant", makeMask({0,2,4,6,7,9,10}), ChordTag::Dominant7};
253 scales[11] = {"Mixolydianb6", makeMask({0,2,4,5,7,8,10}), ChordTag::MajorTriad | ChordTag::Dominant7};
254 scales[12] = {"HalfDiminished", makeMask({0,2,3,5,6,8,10}), ChordTag::MinorTriad | ChordTag::DiminishedTriad | ChordTag::HalfDim7};
255 scales[13] = {"AlteredDominant", makeMask({0,1,3,4,6,8,10}), ChordTag::Dominant7};
256
257 // Harmonic-minor modes
258 scales[14] = {"HarmonicMinor", makeMask({0,2,3,5,7,8,11}), ChordTag::MinorTriad | ChordTag::Minor7};
259 scales[15] = {"Locrian6", makeMask({0,1,3,5,6,9,10}), ChordTag::MinorTriad | ChordTag::DiminishedTriad};
260 scales[16] = {"IonianAugmented", makeMask({0,2,4,5,8,9,11}), ChordTag::MajorTriad | ChordTag::AugmentedTriad};
261 scales[17] = {"DorianSharp4", makeMask({0,2,3,6,7,9,10}), ChordTag::MinorTriad};
262 scales[18] = {"PhrygianDominant",makeMask({0,1,4,5,7,8,10}), ChordTag::MajorTriad | ChordTag::Dominant7};
263 scales[19] = {"LydianSharp2", makeMask({0,3,4,6,7,9,11}), ChordTag::MajorTriad | ChordTag::Major7};
264 scales[20] = {"UltraLocrian", makeMask({0,1,3,4,6,8,9}), ChordTag::DiminishedTriad | ChordTag::Dim7};
265
266 // Harmonic-major modes (each mask is the exact rotation of scales[21];
267 // the mode test derives them by rotation and pins every one)
268 scales[21] = {"HarmonicMajor", makeMask({0,2,4,5,7,8,11}), ChordTag::MajorTriad | ChordTag::AugmentedTriad};
269 scales[22] = {"Dorianb5", makeMask({0,2,3,5,6,9,10}), ChordTag::DiminishedTriad | ChordTag::HalfDim7};
270 scales[23] = {"Phrygianb4", makeMask({0,1,3,4,7,8,10}), ChordTag::MinorTriad | ChordTag::Minor7};
271 scales[24] = {"LydianDiminished", makeMask({0,2,3,6,7,9,11}), ChordTag::MinorTriad};
272 scales[25] = {"Mixolydianb9", makeMask({0,1,4,5,7,9,10}), ChordTag::MajorTriad | ChordTag::Dominant7};
273 scales[26] = {"LydianAugmented2", makeMask({0,3,4,6,8,9,11}), ChordTag::AugmentedTriad | ChordTag::Major7};
274 scales[27] = {"LocrianDiminished", makeMask({0,1,3,5,6,8,9}), ChordTag::DiminishedTriad | ChordTag::Dim7};
275
276 // Double-harmonic family
277 scales[28] = {"DoubleHarmonic", makeMask({0,1,4,5,7,8,11}), ChordTag::MajorTriad};
278 scales[29] = {"HungarianMinor", makeMask({0,2,3,6,7,8,11}), ChordTag::MinorTriad};
279 scales[30] = {"Byzantine", makeMask({0,1,4,5,7,8,11}), ChordTag::MajorTriad};
280 scales[31] = {"Ionian b5", makeMask({0,2,4,5,6,9,11}), ChordTag::MajorTriad | ChordTag::DiminishedTriad};
281 scales[32] = {"Lydian #6", makeMask({0,2,4,6,7,10,11}),ChordTag::MajorTriad | ChordTag::Major7};
282
283 // Pentatonics
284 scales[33] = {"MajorPentatonic", makeMask({0,2,4,7,9}), ChordTag::MajorTriad};
285 scales[34] = {"MinorPentatonic", makeMask({0,3,5,7,10}), ChordTag::MinorTriad};
286 scales[35] = {"EgyptianPentatonic",makeMask({0,2,5,7,10}), ChordTag::MajorTriad};
287 scales[36] = {"Hirajoshi", makeMask({0,2,3,7,8}), ChordTag::MinorTriad};
288 scales[37] = {"InSen", makeMask({0,1,5,7,10}), ChordTag::MinorTriad};
289 scales[38] = {"Yo", makeMask({0,2,5,7,9}), ChordTag::MajorTriad};
290
291 // Symmetrical / exotic
292 scales[39] = {"WholeTone", makeMask({0,2,4,6,8,10}), ChordTag::AugmentedTriad};
293 scales[40] = {"Chromatic", makeMask({0,1,2,3,4,5,6,7,8,9,10,11}), ChordTag::All};
294 scales[41] = {"Diminished", makeMask({0,2,3,5,6,8,9,11}), ChordTag::MinorTriad | ChordTag::DiminishedTriad | ChordTag::HalfDim7 | ChordTag::Dim7};
295 scales[42] = {"Diminished2", makeMask({0,1,3,4,6,7,9,10}), ChordTag::MinorTriad | ChordTag::DiminishedTriad | ChordTag::HalfDim7 | ChordTag::Dim7};
296 scales[43] = {"Augmented", makeMask({0,3,4,7,8,11}), ChordTag::AugmentedTriad};
297
298 // Additional Scales
299 scales[44] = {"Algerian", makeMask({0,2,3,6,7,8,11}), ChordTag::MinorTriad};
300 scales[45] = {"Arabian", makeMask({0,2,4,5,6,8,10}), ChordTag::MajorTriad};
301 scales[46] = {"Balinese", makeMask({0,1,3,7,8}), ChordTag::MinorTriad};
302 scales[47] = {"Chinese", makeMask({0,4,6,7,11}), ChordTag::MajorTriad};
303 scales[48] = {"Gypsy", makeMask({0,1,4,5,7,8,10}), ChordTag::MajorTriad | ChordTag::Dominant7};
304 scales[49] = {"Hindu", makeMask({0,2,4,5,7,8,10}), ChordTag::MajorTriad | ChordTag::Dominant7};
305 scales[50] = {"Hungarian", makeMask({0,2,3,6,7,8,11}), ChordTag::MinorTriad};
306 scales[51] = {"Japanese", makeMask({0,1,5,7,8}), ChordTag::MinorTriad};
307 scales[52] = {"Javanese", makeMask({0,1,3,5,7,9,10}), ChordTag::MinorTriad | ChordTag::Minor7};
308 scales[53] = {"Mongolian", makeMask({0,2,4,7,9}), ChordTag::MajorTriad};
309 scales[54] = {"Neapolitan", makeMask({0,1,3,5,7,8,11}), ChordTag::MinorTriad};
310 scales[55] = {"Oriental", makeMask({0,1,4,5,6,9,10}), ChordTag::MajorTriad};
311 scales[56] = {"Persian", makeMask({0,1,4,5,6,8,11}), ChordTag::MajorTriad};
312 scales[57] = {"Prometheus", makeMask({0,2,4,6,9,10}), ChordTag::MajorTriad};
313 scales[58] = {"Spanish", makeMask({0,1,3,4,5,7,8,10}), ChordTag::DiminishedTriad};
314 scales[59] = {"Tritone", makeMask({0,1,4,6,7,10}), ChordTag::Dominant7};
315 scales[60] = {"Ukrainian", makeMask({0,2,3,6,7,9,10}), ChordTag::MinorTriad};
316
317 return scales;
318 }();
319
320 inline constexpr std::array<std::string_view, 12> sharpNames{
321 "C","C#","D","D#","E","F","F#","G","G#","A","A#","B"
322 };
323 inline constexpr std::array<std::string_view, 12> flatNames{
324 "C","Db","D","Eb","E","F","Gb","G","Ab","A","Bb","B"
325 };
326 // Standard key-signature spelling per chromatic root: sharps for C, D, E,
327 // F#, G, A, B (and C# > Db is resolved toward flats, Db being the common
328 // key); flats for Db, Eb, F, Ab, Bb.
329 inline constexpr std::array<bool,12> useSharpsForRoot{
330 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1
331 };
332
333 //==============================================================================
334 // 5. CONTEXT-AWARE NOTE NAMES & PARSING
335 //==============================================================================
336
343 [[nodiscard]] constexpr std::string_view noteName(int midi, int root = 0) noexcept
344 {
345 int idx = (midi % 12 + 12) % 12;
346 int r = (root % 12 + 12) % 12;
347 return useSharpsForRoot[r] ? sharpNames[idx] : flatNames[idx];
348 }
349
355 [[nodiscard]] constexpr std::optional<int> parseNote(std::string_view s) noexcept
356 {
357 // Exactly 21 entries: a larger array would value-initialize the rest
358 // to {"", 0}, and the empty name would make parseNote("") "succeed"
359 // as pitch-class 0 instead of returning nullopt.
360 constexpr std::array<std::pair<std::string_view,int>, 21> lut{{
361 {"C",0}, {"B#",0},
362 {"C#",1}, {"Db",1},
363 {"D",2}, {"D#",3}, {"Eb",3},
364 {"E",4}, {"E#",5}, {"Fb",4},
365 {"F",5}, {"F#",6}, {"Gb",6},
366 {"G",7}, {"G#",8}, {"Ab",8},
367 {"A",9}, {"A#",10},{"Bb",10},
368 {"B",11}, {"Cb",11}
369 }};
370
371 for (const auto& [name,val] : lut) {
372 if (name.size() == s.size() &&
373 std::equal(name.begin(), name.end(), s.begin(),
374 [](char a, char b){ return (a|32) == (b|32); })) {
375 return val;
376 }
377 }
378 return std::nullopt;
379 }
380
381 //==============================================================================
382 // 6. TRANSPOSE A SCALE
383 //==============================================================================
384
391 [[nodiscard]] constexpr NoteSet scaleAtRoot(NoteSet base, int root) noexcept
392 {
393 root = (root % 12 + 12) % 12;
394 std::uint32_t b = static_cast<std::uint32_t>(base) & 0x0FFFu;
395 std::uint32_t res = ((b << root) | (b >> (12 - root))) & 0x0FFFu;
396 return static_cast<NoteSet>(res);
397 }
398
399 //==============================================================================
400 // 7. CHORD DESCRIPTOR
401 //==============================================================================
402
407 struct Chord
408 {
409 std::string_view name;
410 std::array<int, 7> intervals;
411 };
412
413 inline constexpr std::array<Chord, 15> allChords{{
414 {"Major", {0, 4, 7, -1, -1, -1, -1}},
415 {"Minor", {0, 3, 7, -1, -1, -1, -1}},
416 {"Diminished", {0, 3, 6, -1, -1, -1, -1}},
417 {"Augmented", {0, 4, 8, -1, -1, -1, -1}},
418 {"Major7", {0, 4, 7, 11, -1, -1, -1}},
419 {"Dominant7", {0, 4, 7, 10, -1, -1, -1}},
420 {"Minor7", {0, 3, 7, 10, -1, -1, -1}},
421 {"HalfDim7", {0, 3, 6, 10, -1, -1, -1}},
422 {"Dim7", {0, 3, 6, 9, -1, -1, -1}},
423 {"Sus2", {0, 2, 7, -1, -1, -1, -1}},
424 {"Sus4", {0, 5, 7, -1, -1, -1, -1}},
425 {"Major9", {0, 4, 7, 11, 14, -1, -1}},
426 {"Dominant9", {0, 4, 7, 10, 14, -1, -1}},
427 {"Minor9", {0, 3, 7, 10, 14, -1, -1}},
428 {"Major13", {0, 4, 7, 11, 14, 17, 21}}
429 }};
430
431 //==============================================================================
432 // 8. CHORD GENERATION & REVERSE LOOKUP
433 //==============================================================================
434
442 [[nodiscard]] constexpr std::array<int, 7>
443 chordAtRootMidi(const Chord& c, int rootMidi, int inversion = 0) noexcept
444 {
445 std::array<int, 7> notes{};
446 int count = 0;
447
448 for (int i = 0; i < 7; ++i) {
449 const int deg = c.intervals[i];
450 if (deg < 0) break;
451 notes[count++] = rootMidi + deg;
452 }
453
454 if (count == 0) return { -1,-1,-1,-1,-1,-1,-1 };
455
456 if (count > 0) {
457 inversion = (inversion >= 0) ? (inversion % count) : 0;
458 for (int i = 0; i < inversion; ++i) { notes[i] += 12; }
459 std::sort(notes.begin(), notes.begin() + count);
460 }
461
462 for (int i = count; i < 7; ++i) { notes[i] = -1; }
463 return notes;
464 }
465
471 [[nodiscard]] constexpr std::array<const Scale*, 16>
472 scalesForChordMask(NoteSet chordMask) noexcept
473 {
474 std::array<const Scale*, 16> out{};
475 std::size_t idx = 0;
476 for (const auto& s : allScales) {
477 if (((s.mask & chordMask) == chordMask) && idx < out.size()) {
478 out[idx++] = &s;
479 }
480 }
481 return out;
482 }
483
489 [[nodiscard]] constexpr std::array<const Scale*, 16>
490 scalesForChord(const Chord& chord) noexcept
491 {
492 NoteSet chordMask = 0;
493 for (int d : chord.intervals) {
494 if (d >= 0) { chordMask |= static_cast<NoteSet>(1u << (d % 12)); }
495 }
496 return scalesForChordMask(chordMask);
497 }
498
499 //==============================================================================
500 // 9. DIATONIC CHORD GENERATION
501 //==============================================================================
502
507 enum class ChordLevel : std::uint8_t
508 {
509 TriadsOnly = 0,
510 Triads7 = 1,
511 Triads79 = 2,
512 Triads7911 = 3,
513 Triads791113 = 4
514 };
515
521 {
522 std::array<char, 24> name;
524 std::array<int, 7> intervals;
525
530 [[nodiscard]] constexpr std::string_view view() const noexcept
531 {
532 std::size_t len = 0;
533 while (len < name.size() && name[len] != '\0') { ++len; }
534 return std::string_view(name.data(), len);
535 }
536 };
537
559 [[nodiscard]] constexpr DiatonicChord
560 diatonicChord(const Scale& sc, Degree degree, ChordLevel level) noexcept
561 {
562 const auto deg = detail::activeDegrees(sc.mask);
563 if (degree < 0 || degree >= 7) return {}; // Invalid input protection
564
565 const int third = detail::interval(deg, degree, 2);
566 const int fifth = detail::interval(deg, degree, 4);
567 const int seventh = detail::interval(deg, degree, 6);
568 const int ninth = detail::interval(deg, degree, 1);
569 const int eleventh = detail::interval(deg, degree, 3);
570 const int thirteenth = detail::interval(deg, degree, 5);
571
572 DiatonicChord c{};
573 c.intervals = {0, third, fifth,
574 (level >= ChordLevel::Triads7) ? seventh : -1,
575 (level >= ChordLevel::Triads79) ? ninth : -1,
576 (level >= ChordLevel::Triads7911) ? eleventh : -1,
577 (level >= ChordLevel::Triads791113) ? thirteenth : -1};
578
579 auto buildName = [&](std::string_view baseName, ChordLevel lvl)
580 {
581 char buf[24]{}; // Match DiatonicChord::name capacity (fits longest symbol)
582 std::size_t pos = 0;
583 detail::copy(buf + pos, baseName, sizeof(buf) - pos);
584 pos += baseName.size();
585
586 if (lvl >= ChordLevel::Triads79) { detail::copy(buf + pos, "(9)", sizeof(buf) - pos); pos += 3; }
587 if (lvl >= ChordLevel::Triads7911) { detail::copy(buf + pos, "(11)", sizeof(buf) - pos); pos += 4; }
588 if (lvl >= ChordLevel::Triads791113) { detail::copy(buf + pos, "(13)", sizeof(buf) - pos); pos += 4; }
589
590 detail::copy(c.name.data(), std::string_view(buf, pos), c.name.size());
591 };
592
593 std::string_view base;
594 if (third == 4 && fifth == 7) base = "M";
595 else if (third == 3 && fifth == 7) base = "m";
596 else if (third == 3 && fifth == 6) base = "dim";
597 else if (third == 4 && fifth == 8) base = "aug";
598 else base = "?";
599
600 std::string_view name7;
601 if (base == "dim" && seventh == 10) name7 = "m7b5";
602 else if (base == "dim" && seventh == 9) name7 = "dim7";
603 else if (base == "M" && seventh == 10) name7 = "7";
604 else if (base == "M" && seventh == 11) name7 = "maj7";
605 else if (base == "m" && seventh == 10) name7 = "m7";
606 else if (base == "m" && seventh == 11) name7 = "m(maj7)";
607 else if (base == "aug" && seventh == 10) name7 = "aug7";
608 else name7 = base;
609
610 switch (level)
611 {
612 case ChordLevel::TriadsOnly: buildName(base, ChordLevel::TriadsOnly); break;
613 case ChordLevel::Triads7: buildName(name7, ChordLevel::Triads7); break;
614 case ChordLevel::Triads79: buildName(name7, ChordLevel::Triads79); break;
615 case ChordLevel::Triads7911: buildName(name7, ChordLevel::Triads7911); break;
616 case ChordLevel::Triads791113: buildName(name7, ChordLevel::Triads791113); break;
617 }
618
619 return c;
620 }
621
628 [[nodiscard]] constexpr std::array<int, 7>
629 diatonicChordToMidi(const DiatonicChord& c, int rootMidi) noexcept
630 {
631 std::array<int, 7> notes{};
632 for (std::size_t i = 0; i < 7; ++i) {
633 notes[i] = (c.intervals[i] >= 0) ? rootMidi + c.intervals[i] : -1;
634 }
635 return notes;
636 }
637
638 //==============================================================================
639 // 10. OCTAVE & NOTE HELPERS
640 //==============================================================================
641
647 [[nodiscard]] constexpr std::optional<int> parseNoteWithOctave(std::string_view note) noexcept
648 {
649 std::size_t len = note.size();
650 while (len > 0 && note[len - 1] >= '0' && note[len - 1] <= '9') { --len; }
651 if (len > 0 && note[len - 1] == '-') { --len; }
652
653 std::string_view baseNote = note.substr(0, len);
654 return parseNote(baseNote);
655 }
656
662 [[nodiscard]] constexpr int getOctaveFromNote(std::string_view note) noexcept
663 {
664 if (note.empty()) return 4;
665 std::size_t len = note.size();
666
667 while (len > 0 && note[len - 1] >= '0' && note[len - 1] <= '9') { --len; }
668
669 bool isNegative = false;
670 if (len > 0 && note[len - 1] == '-') {
671 isNegative = true;
672 --len;
673 }
674
675 if (len < note.size() && !(isNegative && len + 1 == note.size())) {
676 int parsed = 0;
677 std::size_t start = isNegative ? len + 1 : len;
678 for (std::size_t i = start; i < note.size(); ++i) {
679 parsed = parsed * 10 + (note[i] - '0');
680 }
681 return isNegative ? -parsed : parsed;
682 }
683
684 return 4;
685 }
686
693 [[nodiscard]] constexpr int transposeByOctaves(int midi, int octaveDelta) noexcept
694 {
695 return midi + octaveDelta * 12;
696 }
697
698} // namespace harmony
699} // namespace dspark
constexpr void copy(char *dst, std::string_view src, std::size_t dstCapacity) noexcept
Safe small string copy used only for compile-time-friendly name building.
constexpr int interval(const std::array< int, 7 > &deg, int degIdx, int skip) noexcept
Calculate semitone interval skipping 'skip' degrees in the scale.
constexpr std::array< int, 7 > activeDegrees(NoteSet mask) noexcept
Extract active scale degrees and expand them sequentially across the octave.
constexpr int getOctaveFromNote(std::string_view note) noexcept
Extract octave number from a note string. Supports negative octaves.
constexpr std::array< const Scale *, 16 > scalesForChordMask(NoteSet chordMask) noexcept
Reverse lookup: find up to 16 scales that fully contain chordMask.
constexpr ChordTag operator&(ChordTag lhs, ChordTag rhs) noexcept
Bitwise AND operator for ChordTag flags (intersection).
constexpr std::array< Scale, 61 > allScales
constexpr std::array< bool, 12 > useSharpsForRoot
std::uint16_t NoteSet
A 12-bit bitmask representing the 12 pitch-classes of the chromatic scale.
constexpr std::array< int, 7 > chordAtRootMidi(const Chord &c, int rootMidi, int inversion=0) noexcept
Build MIDI note numbers for a chord recipe located at a specific root.
constexpr std::optional< int > parseNoteWithOctave(std::string_view note) noexcept
Parse a note string containing an optional octave (e.g. "C#4", "C-1").
int Degree
Integer index used to select a degree inside standard diatonic representations (0-based).
constexpr int transposeByOctaves(int midi, int octaveDelta) noexcept
Transpose a MIDI note by a discrete number of full octaves.
constexpr std::string_view noteName(int midi, int root=0) noexcept
Returns a human-readable note name for the given MIDI note (0..127).
constexpr std::array< Chord, 15 > allChords
constexpr std::array< int, 7 > diatonicChordToMidi(const DiatonicChord &c, int rootMidi) noexcept
Convert a DiatonicChord's interval recipe into absolute MIDI notes.
constexpr std::array< std::string_view, 12 > flatNames
ChordLevel
Complexity level describing which extensions to generate for diatonic chords.
@ Triads7
Generate up to 7th chords.
@ TriadsOnly
Generate base triads only (R-3-5).
consteval NoteSet makeMask(bool b0, bool b1, bool b2, bool b3, bool b4, bool b5, bool b6, bool b7, bool b8, bool b9, bool b10, bool b11) noexcept
Build a NoteSet from 12 boolean flags (b0 = C, b1 = C#/Db, ... b11 = B).
constexpr std::optional< int > parseNote(std::string_view s) noexcept
Parse a simple note name (no octave) into a pitch-class (0..11).
constexpr bool hasTags(ChordTag tags, ChordTag wanted) noexcept
Tests whether tags contains ALL flags in wanted.
constexpr NoteSet scaleAtRoot(NoteSet base, int root) noexcept
Circularly rotate a NoteSet so it becomes rooted at a specific key.
constexpr std::array< const Scale *, 16 > scalesForChord(const Chord &chord) noexcept
Reverse lookup wrapper: find scales that can contain a specific Chord recipe.
ChordTag
Bitmask flags describing chord "families" characteristic of a scale.
@ Sus4Triad
Contains a suspended-4 triad.
@ MinorTriad
Contains a minor triad (e.g., C-Eb-G).
@ Dim7
Contains a fully-diminished 7th chord.
@ Major7
Contains a major 7th chord.
@ Dominant9
Contains a dominant 9th chord.
@ Dominant11
Contains a dominant 11th chord.
@ MajorTriad
Contains a major triad (e.g., C-E-G).
@ Sus2Triad
Contains a suspended-2 triad.
@ All
Convenience: all flags set.
@ Dominant7
Contains a dominant 7th chord.
@ DiminishedTriad
Contains a diminished triad (e.g., C-Eb-Gb).
@ Minor7
Contains a minor 7th chord.
@ Major11
Contains a major 11th chord.
@ AugmentedTriad
Contains an augmented triad (e.g., C-E-G#).
@ Minor9
Contains a minor 9th chord.
@ Major9
Contains a major 9th chord.
@ HalfDim7
Contains a half-diminished 7th chord.
constexpr ChordTag operator|(ChordTag lhs, ChordTag rhs) noexcept
Bitwise OR operator for ChordTag flags.
constexpr DiatonicChord diatonicChord(const Scale &sc, Degree degree, ChordLevel level) noexcept
Generates the diatonic chord built upon a specific degree of a scale.
constexpr std::array< std::string_view, 12 > sharpNames
Main namespace for the DSPark framework.
A chord "recipe" defining the required intervals relative to its root.
std::array< int, 7 > intervals
Intervals in semitones from root. -1 = not present.
Contains the structure and generated symbol name of a diatonic chord.
constexpr std::string_view view() const noexcept
Safely obtain a string_view of the internal name buffer.
std::array< int, 7 > intervals
Intervals in semitones (R-3-5-7-9-11-13). -1 = absent.
Descriptor holding the name, pitch mask and chord tags for a musical scale.
NoteSet mask
12-bit mask with set bits for the scale degrees.
std::string_view name
Human-readable scale name (root = C in the database).
ChordTag tags
Flags describing chord families present in the scale.