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 <vector>
29#include <cmath>
30#include <algorithm>
31#include <optional>
32#include <stdexcept>
33#include <string>
34
35/*
36This header-only library has no dependencies and can be shared into any other library merely
37by including it.
38*/
39
42namespace music_theory {
43
45constexpr int STANDARD_DIATONIC_STEPS = 7;
46constexpr int STANDARD_12EDO_STEPS = 12;
47
48constexpr std::array<int, STANDARD_DIATONIC_STEPS> MAJOR_KEYMAP = { 0, 2, 4, 5, 7, 9, 11 };
49constexpr std::array<int, STANDARD_DIATONIC_STEPS> MINOR_KEYMAP = { 0, 2, 3, 5, 7, 8, 10 };
50
54constexpr std::array<std::array<int, 2>, STANDARD_DIATONIC_STEPS> DIATONIC_INTERVAL_ADJUSTMENTS = { {
55 { 0, 0 }, // unison
56 { 2, -1 }, // second
57 { 4, -2 }, // third
58 {-1, 1 }, // fourth
59 { 1, 0 }, // fifth
60 { 3, -1 }, // sixth
61 { 5, -2 } // seventh
62}};
63
65enum class NoteName : int
66{
67 C = 0,
68 D = 1,
69 E = 2,
70 F = 3,
71 G = 4,
72 A = 5,
73 B = 6
74};
75
76static constexpr std::array<music_theory::NoteName, music_theory::STANDARD_DIATONIC_STEPS> noteNames = {
77 NoteName::C, NoteName::D, NoteName::E, NoteName::F, NoteName::G, NoteName::A, NoteName::B
78};
79
84enum class DiatonicMode : int
85{
86 Ionian = 0,
87 Dorian = 1,
88 Phrygian = 2,
89 Lydian = 3,
90 Mixolydian = 4,
91 Aeolian = 5,
92 Locrian = 6
93};
94
97enum class ClefType
98{
99 Unknown,
100 G,
101 C,
102 F,
105 Tab,
106 TabSerif
107};
108
113constexpr int calcDisplacement(int pitchClass, int octave)
114{
115 pitchClass %= STANDARD_DIATONIC_STEPS;
116 const int relativeOctave = octave - 4;
117
118 return pitchClass + (STANDARD_DIATONIC_STEPS * relativeOctave);
119}
120
124template <typename T>
125constexpr T sign(T n)
126{
127 static_assert(std::is_arithmetic_v<T>, "sign requires a numeric type");
128 return n < T(0) ? T(-1) : T(1);
129}
130
136template <typename T>
137constexpr T signedModulus(T n, T d)
138{
139 static_assert(std::is_integral_v<T>, "signedModulus requires an integer type");
140 return sign(n) * (std::abs(n) % d);
141}
142
149template <typename T>
150constexpr T positiveModulus(T n, T d, T* q = nullptr)
151{
152 static_assert(std::is_integral_v<T>, "positiveModulus requires an integer type");
153 if (q) *q = n / d;
154 T result = signedModulus(n, d);
155 if (result < 0) {
156 result += d;
157 if (q) --(*q);
158 }
159 return result;
160}
161
166constexpr int calc12EdoHalfstepsInInterval(int interval, int chromaticAlteration)
167{
168 int octaves{};
169 int diatonic = positiveModulus(interval, STANDARD_DIATONIC_STEPS, &octaves);
170 return MAJOR_KEYMAP[diatonic] + (octaves * STANDARD_12EDO_STEPS) + chromaticAlteration;
171}
172
177constexpr int calcAlterationFrom12EdoHalfsteps(int interval, int halfsteps)
178{
179 int octaves{};
180 int diatonic = positiveModulus(interval, STANDARD_DIATONIC_STEPS, &octaves);
181 int expectedHalfsteps = MAJOR_KEYMAP[diatonic] + (octaves * STANDARD_12EDO_STEPS);
182 return halfsteps - expectedHalfsteps;
183}
184
189constexpr int calcAlterationFromKeySigChange(int interval, int keySigChange)
190{
191 int diatonic = positiveModulus(interval, STANDARD_DIATONIC_STEPS);
192 int expectedKeyChange = DIATONIC_INTERVAL_ADJUSTMENTS[diatonic][0];
193 if (interval < 0) {
194 if (std::abs(expectedKeyChange) > 1) { // imperfect intervals
195 expectedKeyChange -= STANDARD_DIATONIC_STEPS;
196 }
197 }
198 int alteration = (keySigChange - expectedKeyChange) / STANDARD_DIATONIC_STEPS;
199 return alteration;
200}
201
208constexpr int calcKeySigChangeFromInterval(int interval, int chromaticAlteration)
209{
210 const int diatonic = positiveModulus(interval, STANDARD_DIATONIC_STEPS);
211 int expectedKeyChange = DIATONIC_INTERVAL_ADJUSTMENTS[diatonic][0];
212 if (interval < 0) {
213 if (std::abs(expectedKeyChange) > 1) { // imperfect intervals
214 expectedKeyChange -= STANDARD_DIATONIC_STEPS;
215 }
216 }
217 return expectedKeyChange + (chromaticAlteration * STANDARD_DIATONIC_STEPS);
218}
219
223constexpr bool calcTranspositionIsOctave(int displacement, int alteration)
224{
225 return (displacement % STANDARD_DIATONIC_STEPS) == 0 && alteration == 0;
226}
227
236{
237private:
238 int m_displacement;
239 int m_alteration; // alteration from key signature
240 int m_numberOfEdoDivisions; // number of divisions in the EDO (default 12)
241 std::vector<int> m_keyMap; // step map for the EDO
242
243public:
253 bool isMinor = false, int numberOfEdoDivisions = STANDARD_12EDO_STEPS,
254 const std::optional <std::vector<int>>& keyMap = std::nullopt)
255 : m_displacement(displacement), m_alteration(alteration), m_numberOfEdoDivisions(numberOfEdoDivisions)
256 {
257 if (keyMap) {
258 if (keyMap.value().size() != STANDARD_DIATONIC_STEPS) {
259 throw std::invalid_argument("The Transposer class only supports key map arrays of " + std::to_string(STANDARD_DIATONIC_STEPS) + " elements");
260 }
261 m_keyMap = keyMap.value();
262 } else if (isMinor) {
263 m_keyMap.assign(MINOR_KEYMAP.begin(), MINOR_KEYMAP.end());
264 } else {
265 m_keyMap.assign(MAJOR_KEYMAP.begin(), MAJOR_KEYMAP.end());
266 }
267 }
268
270 int displacement() const { return m_displacement; }
271
273 int alteration() const { return m_alteration; }
274
277 void diatonicTranspose(int interval)
278 {
279 m_displacement += interval;
280 }
281
284 void enharmonicTranspose(int direction)
285 {
286 const int keyStepEnharmonic = calcStepsBetweenScaleDegrees(m_displacement, m_displacement + sign(direction));
287 diatonicTranspose(sign(direction));
288 m_alteration -= sign(direction) * keyStepEnharmonic;
289 }
290
312 void chromaticTranspose(int interval, int chromaticAlteration)
313 {
314 const int intervalNormalized = signedModulus(interval, STANDARD_DIATONIC_STEPS);
315 const int stepsInAlteration = calcStepsInAlteration(interval, chromaticAlteration);
316 const int stepsInInterval = calcStepsInNormalizedInterval(intervalNormalized);
317 const int stepsInDiatonicInterval = calcStepsBetweenScaleDegrees(m_displacement, m_displacement + intervalNormalized);
318
319 const int effectiveAlteration = stepsInAlteration + stepsInInterval - sign(interval) * stepsInDiatonicInterval;
320
321 diatonicTranspose(interval);
322 m_alteration += effectiveAlteration;
323 }
324
332 {
333 while (std::abs(m_alteration) > 0) {
334 const int currSign = sign(m_alteration);
335 const int currAbsDisp = std::abs(m_alteration);
336 enharmonicTranspose(currSign);
337 if (std::abs(m_alteration) >= currAbsDisp) {
338 enharmonicTranspose(-currSign);
339 return;
340 }
341 if (currSign != sign(m_alteration)) {
342 break;
343 }
344 }
345 }
346
356 void stepwiseTranspose(int numberOfEdoDivisions)
357 {
358 m_alteration += numberOfEdoDivisions;
360 }
361
373 return calcAbsoluteDivision(displacement, alteration) == calcAbsoluteDivision(m_displacement, m_alteration);
374 }
375
376private:
377 int calcFifthSteps() const
378 {
379 // std::log(3.0 / 2.0) / std::log(2.0) is 0.5849625007211562.
380 static constexpr double kFifthsMultiplier = 0.5849625007211562;
381 return static_cast<int>(std::floor(m_numberOfEdoDivisions * kFifthsMultiplier) + 0.5);
382 }
383
384 int calcScaleDegree(int interval) const
385 { return positiveModulus(interval, int(m_keyMap.size())); }
386
387 int calcStepsBetweenScaleDegrees(int firstDisplacement, int secondDisplacement) const
388 {
389 const int firstScaleDegree = calcScaleDegree(firstDisplacement);
390 const int secondScaleDegree = calcScaleDegree(secondDisplacement);
391 int result = sign(secondDisplacement - firstDisplacement) * (m_keyMap[secondScaleDegree] - m_keyMap[firstScaleDegree]);
392 if (result < 0) {
393 result += m_numberOfEdoDivisions;
394 }
395 return result;
396 }
397
398 int calcStepsInAlteration(int interval, int alteration) const
399 {
400 const int fifthSteps = calcFifthSteps();
401 const int plusFifths = sign(interval) * alteration * 7; // number of fifths to add for a chromatic halfstep alteration (in any EDO)
402 const int minusOctaves = sign(interval) * alteration * -4; // number of octaves to subtract for a chromatic halfstep alteration (in any EDO)
403 const int result = sign(interval) * ((plusFifths * fifthSteps) + (minusOctaves * m_numberOfEdoDivisions));
404 return result;
405 }
406
407 int calcStepsInNormalizedInterval(int intervalNormalized) const
408 {
409 const int fifthSteps = calcFifthSteps();
410 const int index = std::abs(intervalNormalized);
411 const int plusFifths = DIATONIC_INTERVAL_ADJUSTMENTS[index][0]; // number of fifths
412 const int minusOctaves = DIATONIC_INTERVAL_ADJUSTMENTS[index][1]; // number of octaves
413
414 return sign(intervalNormalized) * ((plusFifths * fifthSteps) + (minusOctaves * m_numberOfEdoDivisions));
415 }
416
417 int calcAbsoluteDivision(int displacement, int alteration) const {
418 const int scaleDegree = calcScaleDegree(displacement); // 0..6
419 const int baseStep = m_keyMap[scaleDegree];
420
421 const int octaveCount = (displacement < 0 && displacement % STANDARD_DIATONIC_STEPS != 0)
424 const int octaveSteps = octaveCount * m_numberOfEdoDivisions;
425 const int chromaticSteps = calcStepsInAlteration(/*interval=*/+1, alteration);
426
427 return baseStep + chromaticSteps + octaveSteps;
428 }
429};
430
431} // namespace music_theory
432
433#endif // MUSIC_THEORY_HPP
Provides dependency-free transposition utilities that work with any scale that has 7 diatonic steps a...
Definition music_theory.hpp:236
int displacement() const
Return the current displacement value.
Definition music_theory.hpp:270
void chromaticTranspose(int interval, int chromaticAlteration)
Chromatically transposes by a specified chromatic interval.
Definition music_theory.hpp:312
void stepwiseTranspose(int numberOfEdoDivisions)
Transposes by the given number of EDO divisions and simplifies the spelling.
Definition music_theory.hpp:356
int alteration() const
Return the current chromatic alteration value.
Definition music_theory.hpp:273
void simplifySpelling()
Simplifies the spelling by reducing its alteration while preserving pitch.
Definition music_theory.hpp:331
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:252
void diatonicTranspose(int interval)
Transposes the displacement by the specified interval.
Definition music_theory.hpp:277
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:372
void enharmonicTranspose(int direction)
Transposes enharmonically relative to the current values.
Definition music_theory.hpp:284
A dependency-free, header-only collection of useful functions for music theory.
DiatonicMode
Represents the seven standard diatonic musical modes.
Definition music_theory.hpp:85
@ 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:137
constexpr int STANDARD_NUMBER_OF_STAFFLINES
The standard number of lines on a staff.
Definition music_theory.hpp:44
constexpr int calcAlterationFrom12EdoHalfsteps(int interval, int halfsteps)
Calculates the alteration in chromatic halfsteps for the specified interval/halfsteps combination.
Definition music_theory.hpp:177
constexpr std::array< int, STANDARD_DIATONIC_STEPS > MAJOR_KEYMAP
keymap for 12-EDO major keys
Definition music_theory.hpp:48
constexpr T sign(T n)
Calculates the sign of an integer.
Definition music_theory.hpp:125
ClefType
Represents the possible types of clef, irrespective of octave transposition.
Definition music_theory.hpp:98
@ 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 calcDisplacement(int pitchClass, int octave)
Calculates the displacement value for a given absolute pitch class and octave.
Definition music_theory.hpp:113
constexpr int STANDARD_12EDO_STEPS
this can be overriden when constructing a Transposer instance.
Definition music_theory.hpp:46
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:208
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:189
constexpr bool calcTranspositionIsOctave(int displacement, int alteration)
Determines if the transposition values result in trasposing by one or more octaves.
Definition music_theory.hpp:223
constexpr int STANDARD_DIATONIC_STEPS
currently this is the only supported number of diatonic steps.
Definition music_theory.hpp:45
NoteName
The available note names in array order.
Definition music_theory.hpp:66
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:54
constexpr int calc12EdoHalfstepsInInterval(int interval, int chromaticAlteration)
Calculates the number of 12-EDO chromatic halfsteps in the specified interval.
Definition music_theory.hpp:166
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:150
constexpr std::array< int, STANDARD_DIATONIC_STEPS > MINOR_KEYMAP
keymap for 12-EDO minor keys
Definition music_theory.hpp:49