MUSX Document Model
Loading...
Searching...
No Matches
music_theory.hpp
1/*
2 * Copyright (C) 2025, Robert Patterson
3 *
4 * Permission is hereby granted, free of charge, to any person obtaining a copy
5 * of this software and associated documentation files (the "Software"), to deal
6 * in the Software without restriction, including without limitation the rights
7 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 * copies of the Software, and to permit persons to whom the Software is
9 * furnished to do so, subject to the following conditions:
10 *
11 * The above copyright notice and this permission notice shall be included in
12 * all copies or substantial portions of the Software.
13 *
14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 * THE SOFTWARE.
21 */
22
23 // Do not use `#pragma once` here, because the file may be included in multiple projects
24#ifndef MUSIC_THEORY_HPP
25#define MUSIC_THEORY_HPP
26
27#include <array>
28#include <cassert>
29#include <vector>
30#include <cmath>
31#include <algorithm>
32#include <optional>
33#include <stdexcept>
34#include <string>
35
36/*
37This header-only library has no dependencies and can be shared into any other library merely
38by including it.
39*/
40
43namespace music_theory {
44
46constexpr int STANDARD_DIATONIC_STEPS = 7;
47constexpr int STANDARD_12EDO_STEPS = 12;
48
49constexpr std::array<int, STANDARD_DIATONIC_STEPS> MAJOR_KEYMAP = { 0, 2, 4, 5, 7, 9, 11 };
50constexpr std::array<int, STANDARD_DIATONIC_STEPS> MINOR_KEYMAP = { 0, 2, 3, 5, 7, 8, 10 };
51
55constexpr std::array<std::array<int, 2>, STANDARD_DIATONIC_STEPS> DIATONIC_INTERVAL_ADJUSTMENTS = { {
56 { 0, 0 }, // unison
57 { 2, -1 }, // second
58 { 4, -2 }, // third
59 {-1, 1 }, // fourth
60 { 1, 0 }, // fifth
61 { 3, -1 }, // sixth
62 { 5, -2 } // seventh
63}};
64
66enum class NoteName : int
67{
68 C = 0,
69 D = 1,
70 E = 2,
71 F = 3,
72 G = 4,
73 A = 5,
74 B = 6
75};
76
83struct Pitch
84{
86 constexpr Pitch() = default;
87
92 constexpr Pitch(NoteName pitchName, int pitchOctave, int pitchAlteration = 0)
93 : noteName(pitchName), octave(pitchOctave), alteration(pitchAlteration)
94 {
95 }
96
98 int octave{};
99 int alteration{};
100};
101
102static constexpr std::array<music_theory::NoteName, music_theory::STANDARD_DIATONIC_STEPS> noteNames = {
103 NoteName::C, NoteName::D, NoteName::E, NoteName::F, NoteName::G, NoteName::A, NoteName::B
104};
105
112constexpr char calcNoteNameLetter(NoteName noteName)
113{
114 switch (noteName) {
115 case NoteName::C: return 'C';
116 case NoteName::D: return 'D';
117 case NoteName::E: return 'E';
118 case NoteName::F: return 'F';
119 case NoteName::G: return 'G';
120 case NoteName::A: return 'A';
121 case NoteName::B: return 'B';
122 }
123 assert(false);
124 throw std::invalid_argument("invalid NoteName value " + std::to_string(int(noteName)));
125}
126
131enum class DiatonicMode : int
132{
133 Ionian = 0,
134 Dorian = 1,
135 Phrygian = 2,
136 Lydian = 3,
137 Mixolydian = 4,
138 Aeolian = 5,
139 Locrian = 6
140};
141
144enum class ClefType
145{
146 Unknown,
147 G,
148 C,
149 F,
152 Tab,
153 TabSerif
154};
155
159constexpr int calcDisplacement(const Pitch& pitch)
160{
161 int pitchClassVal = int(pitch.noteName) % STANDARD_DIATONIC_STEPS;
162 const int relativeOctave = pitch.octave - 4;
163
164 return pitchClassVal + (STANDARD_DIATONIC_STEPS * relativeOctave);
165}
166
170template <typename T>
171constexpr T sign(T n)
172{
173 static_assert(std::is_arithmetic_v<T>, "sign requires a numeric type");
174 return n < T(0) ? T(-1) : T(1);
175}
176
186template <typename T>
187constexpr T signedModulus(T n, T d)
188{
189 static_assert(std::is_integral_v<T>, "signedModulus requires an integer type");
190 return n % d;
191}
192
199template <typename T>
200constexpr T positiveModulus(T n, T d, T* q = nullptr)
201{
202 static_assert(std::is_integral_v<T>, "positiveModulus requires an integer type");
203 if (q) *q = n / d;
204 T result = signedModulus(n, d);
205 if (result < 0) {
206 result += d;
207 if (q) --(*q);
208 }
209 return result;
210}
211
227constexpr int calcPitchClass(NoteName noteName, int alteration = 0,
228 int numberOfEdoDivisions = STANDARD_12EDO_STEPS,
229 const std::array<int, STANDARD_DIATONIC_STEPS>& keyMap = MAJOR_KEYMAP)
230{
231 const int natural = keyMap[positiveModulus(int(noteName), STANDARD_DIATONIC_STEPS)];
232 return positiveModulus(natural + alteration, numberOfEdoDivisions);
233}
234
240constexpr int calcPitchClass(const Pitch& pitch,
241 int numberOfEdoDivisions = STANDARD_12EDO_STEPS,
242 const std::array<int, STANDARD_DIATONIC_STEPS>& keyMap = MAJOR_KEYMAP)
243{
244 return calcPitchClass(pitch.noteName, pitch.alteration, numberOfEdoDivisions, keyMap);
245}
246
251constexpr int calc12EdoHalfstepsInInterval(int interval, int chromaticAlteration)
252{
253 int octaves{};
254 int diatonic = positiveModulus(interval, STANDARD_DIATONIC_STEPS, &octaves);
255 return MAJOR_KEYMAP[diatonic] + (octaves * STANDARD_12EDO_STEPS) + chromaticAlteration;
256}
257
262constexpr int calcAlterationFrom12EdoHalfsteps(int interval, int halfsteps)
263{
264 int octaves{};
265 int diatonic = positiveModulus(interval, STANDARD_DIATONIC_STEPS, &octaves);
266 int expectedHalfsteps = MAJOR_KEYMAP[diatonic] + (octaves * STANDARD_12EDO_STEPS);
267 return halfsteps - expectedHalfsteps;
268}
269
274constexpr int calcAlterationFromKeySigChange(int interval, int keySigChange)
275{
276 int diatonic = positiveModulus(interval, STANDARD_DIATONIC_STEPS);
277 int expectedKeyChange = DIATONIC_INTERVAL_ADJUSTMENTS[diatonic][0];
278 if (interval < 0) {
279 if (std::abs(expectedKeyChange) > 1) { // imperfect intervals
280 expectedKeyChange -= STANDARD_DIATONIC_STEPS;
281 }
282 }
283 int alteration = (keySigChange - expectedKeyChange) / STANDARD_DIATONIC_STEPS;
284 return alteration;
285}
286
293constexpr int calcKeySigChangeFromInterval(int interval, int chromaticAlteration)
294{
295 const int diatonic = positiveModulus(interval, STANDARD_DIATONIC_STEPS);
296 int expectedKeyChange = DIATONIC_INTERVAL_ADJUSTMENTS[diatonic][0];
297 if (interval < 0) {
298 if (std::abs(expectedKeyChange) > 1) { // imperfect intervals
299 expectedKeyChange -= STANDARD_DIATONIC_STEPS;
300 }
301 }
302 return expectedKeyChange + (chromaticAlteration * STANDARD_DIATONIC_STEPS);
303}
304
308constexpr bool calcTranspositionIsOctave(int displacement, int alteration)
309{
310 return (displacement % STANDARD_DIATONIC_STEPS) == 0 && alteration == 0;
311}
312
321{
322private:
323 int m_displacement;
324 int m_alteration; // alteration from key signature
325 int m_numberOfEdoDivisions; // number of divisions in the EDO (default 12)
326 std::vector<int> m_keyMap; // step map for the EDO
327
328public:
331 explicit Transposer(const Pitch& pitch)
332 : Transposer(calcDisplacement(pitch), pitch.alteration)
333 {
334 }
335
345 bool isMinor = false, int numberOfEdoDivisions = STANDARD_12EDO_STEPS,
346 const std::optional <std::vector<int>>& keyMap = std::nullopt)
347 : m_displacement(displacement), m_alteration(alteration), m_numberOfEdoDivisions(numberOfEdoDivisions)
348 {
349 if (keyMap) {
350 if (keyMap.value().size() != STANDARD_DIATONIC_STEPS) {
351 throw std::invalid_argument("The Transposer class only supports key map arrays of " + std::to_string(STANDARD_DIATONIC_STEPS) + " elements");
352 }
353 m_keyMap = keyMap.value();
354 } else if (isMinor) {
355 m_keyMap.assign(MINOR_KEYMAP.begin(), MINOR_KEYMAP.end());
356 } else {
357 m_keyMap.assign(MAJOR_KEYMAP.begin(), MAJOR_KEYMAP.end());
358 }
359 }
360
362 int displacement() const { return m_displacement; }
363
365 int alteration() const { return m_alteration; }
366
369 void diatonicTranspose(int interval)
370 {
371 m_displacement += interval;
372 }
373
376 void enharmonicTranspose(int diatonicSteps)
377 {
378 const int stepSign = sign(diatonicSteps);
379 for (int i = 0; i < std::abs(diatonicSteps); ++i) {
380 const int keyStepEnharmonic = calcStepsBetweenScaleDegrees(m_displacement, m_displacement + stepSign);
381 diatonicTranspose(stepSign);
382 m_alteration -= stepSign * keyStepEnharmonic;
383 }
384 }
385
407 void chromaticTranspose(int interval, int chromaticAlteration)
408 {
409 const int intervalNormalized = signedModulus(interval, STANDARD_DIATONIC_STEPS);
410 const int stepsInAlteration = calcStepsInAlteration(interval, chromaticAlteration);
411 const int stepsInInterval = calcStepsInNormalizedInterval(intervalNormalized);
412 const int stepsInDiatonicInterval = calcStepsBetweenScaleDegrees(m_displacement, m_displacement + intervalNormalized);
413
414 const int effectiveAlteration = stepsInAlteration + stepsInInterval - sign(interval) * stepsInDiatonicInterval;
415
416 diatonicTranspose(interval);
417 m_alteration += effectiveAlteration;
418 }
419
427 {
428 while (std::abs(m_alteration) > 0) {
429 const int currSign = sign(m_alteration);
430 const int currAbsDisp = std::abs(m_alteration);
431 enharmonicTranspose(currSign);
432 if (std::abs(m_alteration) >= currAbsDisp) {
433 enharmonicTranspose(-currSign);
434 return;
435 }
436 if (currSign != sign(m_alteration)) {
437 break;
438 }
439 }
440 }
441
451 void stepwiseTranspose(int numberOfEdoDivisions)
452 {
453 m_alteration += numberOfEdoDivisions;
455 }
456
468 return calcAbsoluteDivision(displacement, alteration) == calcAbsoluteDivision(m_displacement, m_alteration);
469 }
470
471private:
472 int calcFifthSteps() const
473 {
474 // std::log(3.0 / 2.0) / std::log(2.0) is 0.5849625007211562.
475 static constexpr double kFifthsMultiplier = 0.5849625007211562;
476 return static_cast<int>(std::floor(m_numberOfEdoDivisions * kFifthsMultiplier) + 0.5);
477 }
478
479 int calcScaleDegree(int interval) const
480 { return positiveModulus(interval, int(m_keyMap.size())); }
481
482 int calcStepsBetweenScaleDegrees(int firstDisplacement, int secondDisplacement) const
483 {
484 const int firstScaleDegree = calcScaleDegree(firstDisplacement);
485 const int secondScaleDegree = calcScaleDegree(secondDisplacement);
486 int result = sign(secondDisplacement - firstDisplacement) * (m_keyMap[secondScaleDegree] - m_keyMap[firstScaleDegree]);
487 if (result < 0) {
488 result += m_numberOfEdoDivisions;
489 }
490 return result;
491 }
492
493 int calcStepsInAlteration(int interval, int alteration) const
494 {
495 const int fifthSteps = calcFifthSteps();
496 const int plusFifths = sign(interval) * alteration * 7; // number of fifths to add for a chromatic halfstep alteration (in any EDO)
497 const int minusOctaves = sign(interval) * alteration * -4; // number of octaves to subtract for a chromatic halfstep alteration (in any EDO)
498 const int result = sign(interval) * ((plusFifths * fifthSteps) + (minusOctaves * m_numberOfEdoDivisions));
499 return result;
500 }
501
502 int calcStepsInNormalizedInterval(int intervalNormalized) const
503 {
504 const int fifthSteps = calcFifthSteps();
505 const int index = std::abs(intervalNormalized);
506 const int plusFifths = DIATONIC_INTERVAL_ADJUSTMENTS[index][0]; // number of fifths
507 const int minusOctaves = DIATONIC_INTERVAL_ADJUSTMENTS[index][1]; // number of octaves
508
509 return sign(intervalNormalized) * ((plusFifths * fifthSteps) + (minusOctaves * m_numberOfEdoDivisions));
510 }
511
512 int calcAbsoluteDivision(int displacement, int alteration) const {
513 const int scaleDegree = calcScaleDegree(displacement); // 0..6
514 const int baseStep = m_keyMap[scaleDegree];
515
516 const int octaveCount = (displacement < 0 && displacement % STANDARD_DIATONIC_STEPS != 0)
519 const int octaveSteps = octaveCount * m_numberOfEdoDivisions;
520 const int chromaticSteps = calcStepsInAlteration(/*interval=*/+1, alteration);
521
522 return baseStep + chromaticSteps + octaveSteps;
523 }
524};
525
526} // namespace music_theory
527
528#endif // MUSIC_THEORY_HPP
Provides dependency-free transposition utilities that work with any scale that has 7 diatonic steps a...
Definition music_theory.hpp:321
int displacement() const
Return the current displacement value.
Definition music_theory.hpp:362
void chromaticTranspose(int interval, int chromaticAlteration)
Chromatically transposes by a specified chromatic interval.
Definition music_theory.hpp:407
void stepwiseTranspose(int numberOfEdoDivisions)
Transposes by the given number of EDO divisions and simplifies the spelling.
Definition music_theory.hpp:451
int alteration() const
Return the current chromatic alteration value.
Definition music_theory.hpp:365
Transposer(const Pitch &pitch)
Constructs a 12-EDO major-scale transposer for a spelled pitch.
Definition music_theory.hpp:331
void simplifySpelling()
Simplifies the spelling by reducing its alteration while preserving pitch.
Definition music_theory.hpp:426
Transposer(int displacement, int alteration, bool isMinor=false, int numberOfEdoDivisions=STANDARD_12EDO_STEPS, const std::optional< std::vector< int > > &keyMap=std::nullopt)
Constructor function.
Definition music_theory.hpp:344
void diatonicTranspose(int interval)
Transposes the displacement by the specified interval.
Definition music_theory.hpp:369
bool isEnharmonicEquivalent(int displacement, int alteration) const
Determines if the given displacement and alteration refer to the same pitch as the current state.
Definition music_theory.hpp:467
void enharmonicTranspose(int diatonicSteps)
Transposes enharmonically relative to the current values.
Definition music_theory.hpp:376
A dependency-free, header-only collection of useful functions for music theory.
constexpr char calcNoteNameLetter(NoteName noteName)
Returns the uppercase letter that names a diatonic note, such as 'C' for NoteName::C.
Definition music_theory.hpp:112
DiatonicMode
Represents the seven standard diatonic musical modes.
Definition music_theory.hpp:132
@ Phrygian
minor with flat 2
@ Locrian
diminished with flat 2 and 5
@ Lydian
major with raised 4
@ Dorian
minor with raised 6
@ Mixolydian
major with flat 7
constexpr T signedModulus(T n, T d)
Calculates the modulus of positive and negative numbers in a predictable manner.
Definition music_theory.hpp:187
constexpr int STANDARD_NUMBER_OF_STAFFLINES
The standard number of lines on a staff.
Definition music_theory.hpp:45
constexpr int calcDisplacement(const Pitch &pitch)
Calculates the displacement value for a spelled pitch.
Definition music_theory.hpp:159
constexpr int calcPitchClass(NoteName noteName, int alteration=0, int numberOfEdoDivisions=STANDARD_12EDO_STEPS, const std::array< int, STANDARD_DIATONIC_STEPS > &keyMap=MAJOR_KEYMAP)
Calculates the pitch class of a spelled note name.
Definition music_theory.hpp:227
constexpr int calcAlterationFrom12EdoHalfsteps(int interval, int halfsteps)
Calculates the alteration in chromatic halfsteps for the specified interval/halfsteps combination.
Definition music_theory.hpp:262
constexpr std::array< int, STANDARD_DIATONIC_STEPS > MAJOR_KEYMAP
keymap for 12-EDO major keys
Definition music_theory.hpp:49
constexpr T sign(T n)
Calculates the sign of an integer.
Definition music_theory.hpp:171
ClefType
Represents the possible types of clef, irrespective of octave transposition.
Definition music_theory.hpp:145
@ TabSerif
Tablature clef (TAB) with serif font.
@ Percussion2
Narrow rectangle centered on middle staff line (corresponds to SMuFL glyph unpitchedPercussionClef2)
@ Tab
Tablature clef (TAB) with non-serif font.
@ Unknown
Unknown clef type (default value with {} initializer)
@ Percussion1
2 thick vertical lines centered on middle staff line (corresponds to SMuFL glyph unpitchedPercussionC...
constexpr int STANDARD_12EDO_STEPS
this can be overriden when constructing a Transposer instance.
Definition music_theory.hpp:47
constexpr int calcKeySigChangeFromInterval(int interval, int chromaticAlteration)
Calculates the resulting key signature change (sharps/flats) produced by a diatonic interval and chro...
Definition music_theory.hpp:293
constexpr int calcAlterationFromKeySigChange(int interval, int keySigChange)
Determines the chromatic alteration needed for a diatonic interval to produce a desired key signature...
Definition music_theory.hpp:274
constexpr bool calcTranspositionIsOctave(int displacement, int alteration)
Determines if the transposition values result in trasposing by one or more octaves.
Definition music_theory.hpp:308
constexpr int STANDARD_DIATONIC_STEPS
currently this is the only supported number of diatonic steps.
Definition music_theory.hpp:46
NoteName
The available note names in array order.
Definition music_theory.hpp:67
constexpr std::array< std::array< int, 2 >, STANDARD_DIATONIC_STEPS > DIATONIC_INTERVAL_ADJUSTMENTS
Array of chromatic intervals. Each member array contains.
Definition music_theory.hpp:55
constexpr int calc12EdoHalfstepsInInterval(int interval, int chromaticAlteration)
Calculates the number of 12-EDO chromatic halfsteps in the specified interval.
Definition music_theory.hpp:251
constexpr T positiveModulus(T n, T d, T *q=nullptr)
Calculates a positive modulus in the range [0, d-1], even for negative dividends.
Definition music_theory.hpp:200
constexpr std::array< int, STANDARD_DIATONIC_STEPS > MINOR_KEYMAP
keymap for 12-EDO minor keys
Definition music_theory.hpp:50
A spelled pitch, expressed relative to C4.
Definition music_theory.hpp:84
NoteName noteName
The diatonic note name.
Definition music_theory.hpp:97
constexpr Pitch()=default
Creates an unspecified pitch.
int octave
The octave number, where C4 is middle C.
Definition music_theory.hpp:98
constexpr Pitch(NoteName pitchName, int pitchOctave, int pitchAlteration=0)
Creates a spelled pitch.
Definition music_theory.hpp:92
int alteration
The alteration relative to the natural note name, in EDO divisions.
Definition music_theory.hpp:99