DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
HarmonyConstants.h
Go to the documentation of this file.
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 <cstdint>
28#include <algorithm>
29#include <optional>
30
31namespace dspark {
32namespace harmony
33{
34 //==============================================================================
35 // 0. CORE TYPEDEFS
36 //==============================================================================
37
45 using NoteSet = std::uint16_t;
46
51 using Degree = int;
52
53 //==============================================================================
54 // 1. CHORDTAG - A FILTER FOR CHORD TYPES
55 //==============================================================================
56
70 enum class ChordTag : std::uint16_t
71 {
72 MajorTriad = 1u << 0,
73 MinorTriad = 1u << 1,
74 DiminishedTriad = 1u << 2,
75 AugmentedTriad = 1u << 3,
76 Major7 = 1u << 4,
77 Dominant7 = 1u << 5,
78 Minor7 = 1u << 6,
79 HalfDim7 = 1u << 7,
80 Dim7 = 1u << 8,
81 Sus2Triad = 1u << 9,
82 Sus4Triad = 1u << 10,
83 Major9 = 1u << 11,
84 Dominant9 = 1u << 12,
85 Minor9 = 1u << 13,
86 Major11 = 1u << 14,
87 Dominant11 = 1u << 15,
88 All = 0xFFFFu
89 };
90
95 [[nodiscard]] constexpr ChordTag operator|(ChordTag lhs, ChordTag rhs) noexcept
96 {
97 using U = std::underlying_type_t<ChordTag>;
98 return static_cast<ChordTag>(static_cast<U>(lhs) | static_cast<U>(rhs));
99 }
100
105 [[nodiscard]] constexpr ChordTag operator&(ChordTag lhs, ChordTag rhs) noexcept
106 {
107 using U = std::underlying_type_t<ChordTag>;
108 return static_cast<ChordTag>(static_cast<U>(lhs) & static_cast<U>(rhs));
109 }
110
114 [[nodiscard]] constexpr bool hasTags(ChordTag tags, ChordTag wanted) noexcept
115 {
116 using U = std::underlying_type_t<ChordTag>;
117 return (static_cast<U>(tags) & static_cast<U>(wanted)) == static_cast<U>(wanted);
118 }
119
120 //==============================================================================
121 // 2. SCALE DESCRIPTOR
122 //==============================================================================
123
128 struct Scale
129 {
130 std::string_view name;
133 };
134
135 //==============================================================================
136 // 3. INTERNAL HELPERS & MASK GENERATION
137 //==============================================================================
138
142 [[nodiscard]] consteval NoteSet makeMask(
143 bool b0, bool b1, bool b2, bool b3,
144 bool b4, bool b5, bool b6, bool b7,
145 bool b8, bool b9, bool b10, bool b11) noexcept
146 {
147 return (static_cast<NoteSet>(b0) << 0) | (static_cast<NoteSet>(b1) << 1) |
148 (static_cast<NoteSet>(b2) << 2) | (static_cast<NoteSet>(b3) << 3) |
149 (static_cast<NoteSet>(b4) << 4) | (static_cast<NoteSet>(b5) << 5) |
150 (static_cast<NoteSet>(b6) << 6) | (static_cast<NoteSet>(b7) << 7) |
151 (static_cast<NoteSet>(b8) << 8) | (static_cast<NoteSet>(b9) << 9) |
152 (static_cast<NoteSet>(b10) << 10) | (static_cast<NoteSet>(b11) << 11);
153 }
154
159 [[nodiscard]] consteval NoteSet makeMask(std::initializer_list<int> degrees) noexcept
160 {
161 NoteSet m = 0;
162 for (int d : degrees) {
163 if (d >= 0) { m |= static_cast<NoteSet>(1u << (d % 12)); }
164 }
165 return m & 0x0FFFu;
166 }
167
168 namespace detail
169 {
173 constexpr void copy(char* dst, std::string_view src, std::size_t dstCapacity) noexcept
174 {
175 std::size_t toCopy = src.size();
176 if (toCopy + 1u > dstCapacity) { toCopy = (dstCapacity > 0) ? dstCapacity - 1u : 0u; }
177 for (std::size_t i = 0; i < toCopy; ++i) { dst[i] = src[i]; }
178 if (dstCapacity > 0) { dst[toCopy] = '\0'; }
179 }
180
184 [[nodiscard]] constexpr std::array<int, 7> activeDegrees(NoteSet mask) noexcept
185 {
186 std::array<int, 12> temp{};
187 int tcount = 0;
188 for (int i = 0; i < 12; ++i) {
189 if (mask & (1u << i)) temp[tcount++] = i;
190 }
191
192 std::array<int, 7> out{};
193 if (tcount == 0) {
194 for (int i = 0; i < 7; ++i) out[i] = i;
195 return out;
196 }
197
198 for (int i = 0; i < 7; ++i) {
199 int idx = i % tcount;
200 int octave = i / tcount;
201 out[i] = temp[idx] + octave * 12;
202 }
203 return out;
204 }
205
209 [[nodiscard]] constexpr int interval(const std::array<int, 7>& deg, int degIdx, int skip) noexcept
210 {
211 int a = deg[degIdx];
212 int b = deg[(degIdx + skip) % 7];
213 while (b <= a) { b += 12; }
214 return b - a;
215 }
216 } // namespace detail
217
218 //==============================================================================
219 // 4. DATABASE OF SCALES & CONSTANTS
220 //==============================================================================
221
222 inline constexpr std::array<Scale, 61> allScales = [](){
223 std::array<Scale, 61> scales{};
224
225 // Major modes
226 scales[0] = {"Ionian", makeMask({0,2,4,5,7,9,11}), ChordTag::MajorTriad | ChordTag::Major7 | ChordTag::Dominant7};
227 scales[1] = {"Dorian", makeMask({0,2,3,5,7,9,10}), ChordTag::MinorTriad | ChordTag::Minor7};
228 scales[2] = {"Phrygian", makeMask({0,1,3,5,7,8,10}), ChordTag::MinorTriad | ChordTag::DiminishedTriad | ChordTag::HalfDim7};
229 scales[3] = {"Lydian", makeMask({0,2,4,6,7,9,11}), ChordTag::MajorTriad | ChordTag::Major7};
230 scales[4] = {"Mixolydian", makeMask({0,2,4,5,7,9,10}), ChordTag::MajorTriad | ChordTag::Dominant7};
231 scales[5] = {"Aeolian", makeMask({0,2,3,5,7,8,10}), ChordTag::MinorTriad | ChordTag::Minor7};
232 scales[6] = {"Locrian", makeMask({0,1,3,5,6,8,10}), ChordTag::MinorTriad | ChordTag::DiminishedTriad | ChordTag::HalfDim7};
233
234 // Melodic-minor modes
235 scales[7] = {"MelodicMinor", makeMask({0,2,3,5,7,9,11}), ChordTag::MinorTriad | ChordTag::Minor7};
236 scales[8] = {"Dorianb2", makeMask({0,1,3,5,7,9,10}), ChordTag::MinorTriad};
237 scales[9] = {"LydianAugmented", makeMask({0,2,4,6,8,9,11}), ChordTag::MajorTriad | ChordTag::AugmentedTriad};
238 scales[10] = {"LydianDominant", makeMask({0,2,4,6,7,9,10}), ChordTag::Dominant7};
239 scales[11] = {"Mixolydianb6", makeMask({0,2,4,5,7,8,10}), ChordTag::MajorTriad | ChordTag::Dominant7};
240 scales[12] = {"HalfDiminished", makeMask({0,2,3,5,6,8,10}), ChordTag::MinorTriad | ChordTag::DiminishedTriad | ChordTag::HalfDim7};
241 scales[13] = {"AlteredDominant", makeMask({0,1,3,4,6,8,10}), ChordTag::Dominant7};
242
243 // Harmonic-minor modes
244 scales[14] = {"HarmonicMinor", makeMask({0,2,3,5,7,8,11}), ChordTag::MinorTriad | ChordTag::Minor7};
245 scales[15] = {"Locrian6", makeMask({0,1,3,5,6,9,10}), ChordTag::MinorTriad | ChordTag::DiminishedTriad};
246 scales[16] = {"IonianAugmented", makeMask({0,2,4,5,8,9,11}), ChordTag::MajorTriad | ChordTag::AugmentedTriad};
247 scales[17] = {"DorianSharp4", makeMask({0,2,3,6,7,9,10}), ChordTag::MinorTriad};
248 scales[18] = {"PhrygianDominant",makeMask({0,1,4,5,7,8,10}), ChordTag::MajorTriad | ChordTag::Dominant7};
249 scales[19] = {"LydianSharp2", makeMask({0,3,4,6,7,9,11}), ChordTag::MajorTriad | ChordTag::Major7};
250 scales[20] = {"UltraLocrian", makeMask({0,1,3,4,6,8,9}), ChordTag::DiminishedTriad | ChordTag::Dim7};
251
252 // Harmonic-major modes (each mask is the exact rotation of scales[21];
253 // the mode test derives them by rotation and pins every one)
254 scales[21] = {"HarmonicMajor", makeMask({0,2,4,5,7,8,11}), ChordTag::MajorTriad | ChordTag::AugmentedTriad};
255 scales[22] = {"Dorianb5", makeMask({0,2,3,5,6,9,10}), ChordTag::DiminishedTriad | ChordTag::HalfDim7};
256 scales[23] = {"Phrygianb4", makeMask({0,1,3,4,7,8,10}), ChordTag::MinorTriad | ChordTag::Minor7};
257 scales[24] = {"LydianDiminished", makeMask({0,2,3,6,7,9,11}), ChordTag::MinorTriad};
258 scales[25] = {"Mixolydianb9", makeMask({0,1,4,5,7,9,10}), ChordTag::MajorTriad | ChordTag::Dominant7};
259 scales[26] = {"LydianAugmented2", makeMask({0,3,4,6,8,9,11}), ChordTag::AugmentedTriad | ChordTag::Major7};
260 scales[27] = {"LocrianDiminished", makeMask({0,1,3,5,6,8,9}), ChordTag::DiminishedTriad | ChordTag::Dim7};
261
262 // Double-harmonic family
263 scales[28] = {"DoubleHarmonic", makeMask({0,1,4,5,7,8,11}), ChordTag::MajorTriad};
264 scales[29] = {"HungarianMinor", makeMask({0,2,3,6,7,8,11}), ChordTag::MinorTriad};
265 scales[30] = {"Byzantine", makeMask({0,1,4,5,7,8,11}), ChordTag::MajorTriad};
266 scales[31] = {"Ionian b5", makeMask({0,2,4,5,6,9,11}), ChordTag::MajorTriad | ChordTag::DiminishedTriad};
267 scales[32] = {"Lydian #6", makeMask({0,2,4,6,7,10,11}),ChordTag::MajorTriad | ChordTag::Major7};
268
269 // Pentatonics
270 scales[33] = {"MajorPentatonic", makeMask({0,2,4,7,9}), ChordTag::MajorTriad};
271 scales[34] = {"MinorPentatonic", makeMask({0,3,5,7,10}), ChordTag::MinorTriad};
272 scales[35] = {"EgyptianPentatonic",makeMask({0,2,5,7,10}), ChordTag::MajorTriad};
273 scales[36] = {"Hirajoshi", makeMask({0,2,3,7,8}), ChordTag::MinorTriad};
274 scales[37] = {"InSen", makeMask({0,1,5,7,10}), ChordTag::MinorTriad};
275 scales[38] = {"Yo", makeMask({0,2,5,7,9}), ChordTag::MajorTriad};
276
277 // Symmetrical / exotic
278 scales[39] = {"WholeTone", makeMask({0,2,4,6,8,10}), ChordTag::AugmentedTriad};
279 scales[40] = {"Chromatic", makeMask({0,1,2,3,4,5,6,7,8,9,10,11}), ChordTag::All};
280 scales[41] = {"Diminished", makeMask({0,2,3,5,6,8,9,11}), ChordTag::MinorTriad | ChordTag::DiminishedTriad | ChordTag::HalfDim7 | ChordTag::Dim7};
281 scales[42] = {"Diminished2", makeMask({0,1,3,4,6,7,9,10}), ChordTag::MinorTriad | ChordTag::DiminishedTriad | ChordTag::HalfDim7 | ChordTag::Dim7};
282 scales[43] = {"Augmented", makeMask({0,3,4,7,8,11}), ChordTag::AugmentedTriad};
283
284 // Additional Scales
285 scales[44] = {"Algerian", makeMask({0,2,3,6,7,8,11}), ChordTag::MinorTriad};
286 scales[45] = {"Arabian", makeMask({0,2,4,5,6,8,10}), ChordTag::MajorTriad};
287 scales[46] = {"Balinese", makeMask({0,1,3,7,8}), ChordTag::MinorTriad};
288 scales[47] = {"Chinese", makeMask({0,4,6,7,11}), ChordTag::MajorTriad};
289 scales[48] = {"Gypsy", makeMask({0,1,4,5,7,8,10}), ChordTag::MajorTriad | ChordTag::Dominant7};
290 scales[49] = {"Hindu", makeMask({0,2,4,5,7,8,10}), ChordTag::MajorTriad | ChordTag::Dominant7};
291 scales[50] = {"Hungarian", makeMask({0,2,3,6,7,8,11}), ChordTag::MinorTriad};
292 scales[51] = {"Japanese", makeMask({0,1,5,7,8}), ChordTag::MinorTriad};
293 scales[52] = {"Javanese", makeMask({0,1,3,5,7,9,10}), ChordTag::MinorTriad | ChordTag::Minor7};
294 scales[53] = {"Mongolian", makeMask({0,2,4,7,9}), ChordTag::MajorTriad};
295 scales[54] = {"Neapolitan", makeMask({0,1,3,5,7,8,11}), ChordTag::MinorTriad};
296 scales[55] = {"Oriental", makeMask({0,1,4,5,6,9,10}), ChordTag::MajorTriad};
297 scales[56] = {"Persian", makeMask({0,1,4,5,6,8,11}), ChordTag::MajorTriad};
298 scales[57] = {"Prometheus", makeMask({0,2,4,6,9,10}), ChordTag::MajorTriad};
299 scales[58] = {"Spanish", makeMask({0,1,3,4,5,7,8,10}), ChordTag::DiminishedTriad};
300 scales[59] = {"Tritone", makeMask({0,1,4,6,7,10}), ChordTag::Dominant7};
301 scales[60] = {"Ukrainian", makeMask({0,2,3,6,7,9,10}), ChordTag::MinorTriad};
302
303 return scales;
304 }();
305
306 inline constexpr std::array<std::string_view, 12> sharpNames{
307 "C","C#","D","D#","E","F","F#","G","G#","A","A#","B"
308 };
309 inline constexpr std::array<std::string_view, 12> flatNames{
310 "C","Db","D","Eb","E","F","Gb","G","Ab","A","Bb","B"
311 };
312 // Standard key-signature spelling per chromatic root: sharps for C, D, E,
313 // F#, G, A, B (and C# > Db is resolved toward flats, Db being the common
314 // key); flats for Db, Eb, F, Ab, Bb.
315 inline constexpr std::array<bool,12> useSharpsForRoot{
316 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1
317 };
318
319 //==============================================================================
320 // 5. CONTEXT-AWARE NOTE NAMES & PARSING
321 //==============================================================================
322
329 [[nodiscard]] constexpr std::string_view noteName(int midi, int root = 0) noexcept
330 {
331 int idx = (midi % 12 + 12) % 12;
332 int r = (root % 12 + 12) % 12;
333 return useSharpsForRoot[r] ? sharpNames[idx] : flatNames[idx];
334 }
335
341 [[nodiscard]] constexpr std::optional<int> parseNote(std::string_view s) noexcept
342 {
343 // Exactly 21 entries: a larger array would value-initialize the rest
344 // to {"", 0}, and the empty name would make parseNote("") "succeed"
345 // as pitch-class 0 instead of returning nullopt.
346 constexpr std::array<std::pair<std::string_view,int>, 21> lut{{
347 {"C",0}, {"B#",0},
348 {"C#",1}, {"Db",1},
349 {"D",2}, {"D#",3}, {"Eb",3},
350 {"E",4}, {"E#",5}, {"Fb",4},
351 {"F",5}, {"F#",6}, {"Gb",6},
352 {"G",7}, {"G#",8}, {"Ab",8},
353 {"A",9}, {"A#",10},{"Bb",10},
354 {"B",11}, {"Cb",11}
355 }};
356
357 for (const auto& [name,val] : lut) {
358 if (name.size() == s.size() &&
359 std::equal(name.begin(), name.end(), s.begin(),
360 [](char a, char b){ return (a|32) == (b|32); })) {
361 return val;
362 }
363 }
364 return std::nullopt;
365 }
366
367 //==============================================================================
368 // 6. TRANSPOSE A SCALE
369 //==============================================================================
370
377 [[nodiscard]] constexpr NoteSet scaleAtRoot(NoteSet base, int root) noexcept
378 {
379 root = (root % 12 + 12) % 12;
380 std::uint32_t b = static_cast<std::uint32_t>(base) & 0x0FFFu;
381 std::uint32_t res = ((b << root) | (b >> (12 - root))) & 0x0FFFu;
382 return static_cast<NoteSet>(res);
383 }
384
385 //==============================================================================
386 // 7. CHORD DESCRIPTOR
387 //==============================================================================
388
393 struct Chord
394 {
395 std::string_view name;
396 std::array<int, 7> intervals;
397 };
398
399 inline constexpr std::array<Chord, 15> allChords{{
400 {"Major", {0, 4, 7, -1, -1, -1, -1}},
401 {"Minor", {0, 3, 7, -1, -1, -1, -1}},
402 {"Diminished", {0, 3, 6, -1, -1, -1, -1}},
403 {"Augmented", {0, 4, 8, -1, -1, -1, -1}},
404 {"Major7", {0, 4, 7, 11, -1, -1, -1}},
405 {"Dominant7", {0, 4, 7, 10, -1, -1, -1}},
406 {"Minor7", {0, 3, 7, 10, -1, -1, -1}},
407 {"HalfDim7", {0, 3, 6, 10, -1, -1, -1}},
408 {"Dim7", {0, 3, 6, 9, -1, -1, -1}},
409 {"Sus2", {0, 2, 7, -1, -1, -1, -1}},
410 {"Sus4", {0, 5, 7, -1, -1, -1, -1}},
411 {"Major9", {0, 4, 7, 11, 14, -1, -1}},
412 {"Dominant9", {0, 4, 7, 10, 14, -1, -1}},
413 {"Minor9", {0, 3, 7, 10, 14, -1, -1}},
414 {"Major13", {0, 4, 7, 11, 14, 17, 21}}
415 }};
416
417 //==============================================================================
418 // 8. CHORD GENERATION & REVERSE LOOKUP
419 //==============================================================================
420
428 [[nodiscard]] constexpr std::array<int, 7>
429 chordAtRootMidi(const Chord& c, int rootMidi, int inversion = 0) noexcept
430 {
431 std::array<int, 7> notes{};
432 int count = 0;
433
434 for (int i = 0; i < 7; ++i) {
435 const int deg = c.intervals[i];
436 if (deg < 0) break;
437 notes[count++] = rootMidi + deg;
438 }
439
440 if (count == 0) return { -1,-1,-1,-1,-1,-1,-1 };
441
442 if (count > 0) {
443 inversion = (inversion >= 0) ? (inversion % count) : 0;
444 for (int i = 0; i < inversion; ++i) { notes[i] += 12; }
445 std::sort(notes.begin(), notes.begin() + count);
446 }
447
448 for (int i = count; i < 7; ++i) { notes[i] = -1; }
449 return notes;
450 }
451
457 [[nodiscard]] constexpr std::array<const Scale*, 16>
458 scalesForChordMask(NoteSet chordMask) noexcept
459 {
460 std::array<const Scale*, 16> out{};
461 std::size_t idx = 0;
462 for (const auto& s : allScales) {
463 if (((s.mask & chordMask) == chordMask) && idx < out.size()) {
464 out[idx++] = &s;
465 }
466 }
467 return out;
468 }
469
475 [[nodiscard]] constexpr std::array<const Scale*, 16>
476 scalesForChord(const Chord& chord) noexcept
477 {
478 NoteSet chordMask = 0;
479 for (int d : chord.intervals) {
480 if (d >= 0) { chordMask |= static_cast<NoteSet>(1u << (d % 12)); }
481 }
482 return scalesForChordMask(chordMask);
483 }
484
485 //==============================================================================
486 // 9. DIATONIC CHORD GENERATION
487 //==============================================================================
488
493 enum class ChordLevel : std::uint8_t
494 {
495 TriadsOnly = 0,
496 Triads7 = 1,
497 Triads79 = 2,
498 Triads7911 = 3,
499 Triads791113 = 4
500 };
501
507 {
508 std::array<char, 24> name;
510 std::array<int, 7> intervals;
511
516 [[nodiscard]] constexpr std::string_view view() const noexcept
517 {
518 std::size_t len = 0;
519 while (len < name.size() && name[len] != '\0') { ++len; }
520 return std::string_view(name.data(), len);
521 }
522 };
523
531 [[nodiscard]] constexpr DiatonicChord
532 diatonicChord(const Scale& sc, Degree degree, ChordLevel level) noexcept
533 {
534 const auto deg = detail::activeDegrees(sc.mask);
535 if (degree < 0 || degree >= 7) return {}; // Invalid input protection
536
537 const int third = detail::interval(deg, degree, 2);
538 const int fifth = detail::interval(deg, degree, 4);
539 const int seventh = detail::interval(deg, degree, 6);
540 const int ninth = detail::interval(deg, degree, 1);
541 const int eleventh = detail::interval(deg, degree, 3);
542 const int thirteenth = detail::interval(deg, degree, 5);
543
544 DiatonicChord c{};
545 c.intervals = {0, third, fifth,
546 (level >= ChordLevel::Triads7) ? seventh : -1,
547 (level >= ChordLevel::Triads79) ? ninth : -1,
548 (level >= ChordLevel::Triads7911) ? eleventh : -1,
549 (level >= ChordLevel::Triads791113) ? thirteenth : -1};
550
551 auto buildName = [&](std::string_view baseName, ChordLevel lvl)
552 {
553 char buf[24]{}; // Match DiatonicChord::name capacity (fits longest symbol)
554 std::size_t pos = 0;
555 detail::copy(buf + pos, baseName, sizeof(buf) - pos);
556 pos += baseName.size();
557
558 if (lvl >= ChordLevel::Triads79) { detail::copy(buf + pos, "(9)", sizeof(buf) - pos); pos += 3; }
559 if (lvl >= ChordLevel::Triads7911) { detail::copy(buf + pos, "(11)", sizeof(buf) - pos); pos += 4; }
560 if (lvl >= ChordLevel::Triads791113) { detail::copy(buf + pos, "(13)", sizeof(buf) - pos); pos += 4; }
561
562 detail::copy(c.name.data(), std::string_view(buf, pos), c.name.size());
563 };
564
565 std::string_view base;
566 if (third == 4 && fifth == 7) base = "M";
567 else if (third == 3 && fifth == 7) base = "m";
568 else if (third == 3 && fifth == 6) base = "dim";
569 else if (third == 4 && fifth == 8) base = "aug";
570 else base = "?";
571
572 std::string_view name7;
573 if (base == "dim" && seventh == 10) name7 = "m7b5";
574 else if (base == "dim" && seventh == 9) name7 = "dim7";
575 else if (base == "M" && seventh == 10) name7 = "7";
576 else if (base == "M" && seventh == 11) name7 = "maj7";
577 else if (base == "m" && seventh == 10) name7 = "m7";
578 else if (base == "m" && seventh == 11) name7 = "m(maj7)";
579 else if (base == "aug" && seventh == 10) name7 = "aug7";
580 else name7 = base;
581
582 switch (level)
583 {
584 case ChordLevel::TriadsOnly: buildName(base, ChordLevel::TriadsOnly); break;
585 case ChordLevel::Triads7: buildName(name7, ChordLevel::Triads7); break;
586 case ChordLevel::Triads79: buildName(name7, ChordLevel::Triads79); break;
587 case ChordLevel::Triads7911: buildName(name7, ChordLevel::Triads7911); break;
588 case ChordLevel::Triads791113: buildName(name7, ChordLevel::Triads791113); break;
589 }
590
591 return c;
592 }
593
600 [[nodiscard]] constexpr std::array<int, 7>
601 diatonicChordToMidi(const DiatonicChord& c, int rootMidi) noexcept
602 {
603 std::array<int, 7> notes{};
604 for (std::size_t i = 0; i < 7; ++i) {
605 notes[i] = (c.intervals[i] >= 0) ? rootMidi + c.intervals[i] : -1;
606 }
607 return notes;
608 }
609
610 //==============================================================================
611 // 10. OCTAVE & NOTE HELPERS
612 //==============================================================================
613
619 [[nodiscard]] constexpr std::optional<int> parseNoteWithOctave(std::string_view note) noexcept
620 {
621 std::size_t len = note.size();
622 while (len > 0 && note[len - 1] >= '0' && note[len - 1] <= '9') { --len; }
623 if (len > 0 && note[len - 1] == '-') { --len; }
624
625 std::string_view baseNote = note.substr(0, len);
626 return parseNote(baseNote);
627 }
628
634 [[nodiscard]] constexpr int getOctaveFromNote(std::string_view note) noexcept
635 {
636 if (note.empty()) return 4;
637 std::size_t len = note.size();
638
639 while (len > 0 && note[len - 1] >= '0' && note[len - 1] <= '9') { --len; }
640
641 bool isNegative = false;
642 if (len > 0 && note[len - 1] == '-') {
643 isNegative = true;
644 --len;
645 }
646
647 if (len < note.size() && !(isNegative && len + 1 == note.size())) {
648 int parsed = 0;
649 std::size_t start = isNegative ? len + 1 : len;
650 for (std::size_t i = start; i < note.size(); ++i) {
651 parsed = parsed * 10 + (note[i] - '0');
652 }
653 return isNegative ? -parsed : parsed;
654 }
655
656 return 4;
657 }
658
665 [[nodiscard]] constexpr int transposeByOctaves(int midi, int octaveDelta) noexcept
666 {
667 return midi + octaveDelta * 12;
668 }
669
670} // namespace harmony
671} // 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.