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#pragma once
23
24#include <array>
25#include <vector>
26#include <cmath>
27#include <algorithm>
28#include <optional>
29#include <stdexcept>
30#include <string>
31
32/*
33This header-only library has no dependencies and can be shared into any other library merely
34by including it.
35*/
36
39namespace music_theory {
40
41constexpr int STANDARD_DIATONIC_STEPS = 7;
42constexpr int STANDARD_12EDO_STEPS = 12;
43
48enum class DiatonicMode : int
49{
50 Ionian = 0,
51 Dorian = 1,
52 Phrygian = 2,
53 Lydian = 3,
54 Mixolydian = 4,
55 Aeolian = 5,
56 Locrian = 6
57};
58
63inline int calcDisplacement(int pitchClass, int octave)
64{
65 pitchClass %= STANDARD_DIATONIC_STEPS;
66 const int relativeOctave = octave - 4;
67
68 return pitchClass + (STANDARD_DIATONIC_STEPS * relativeOctave);
69}
70
74template <typename T>
75constexpr inline T sign(T n)
76{
77 static_assert(std::is_arithmetic_v<T>, "sign requires a numeric type");
78 return n < T(0) ? T(-1) : T(1);
79}
80
86template <typename T>
87constexpr T signedModulus(T n, T d) {
88 static_assert(std::is_integral_v<T>, "signed_modulus requires an integer type");
89 return sign(n) * (std::abs(n) % d);
90}
91
100{
101private:
102 static constexpr std::array<int, STANDARD_DIATONIC_STEPS> MAJOR_KEYMAP = { 0, 2, 4, 5, 7, 9, 11 };
103 static constexpr std::array<int, STANDARD_DIATONIC_STEPS> MINOR_KEYMAP = { 0, 2, 3, 5, 7, 8, 10 };
104 int m_displacement;
105 int m_alteration; // alteration from key signature
106 int m_numberOfEdoDivisions; // number of divisions in the EDO (default 12)
107 std::vector<int> m_keyMap; // step map for the EDO
108
109 static constexpr std::array<std::array<int, 2>, 7> DIATONIC_INTERVAL_ADJUSTMENTS = { {
110 { 0, 0}, // unison
111 { 2, -1}, // second
112 { 4, -2}, // third
113 {-1, 1}, // fourth
114 { 1, 0}, // fifth
115 { 3, -1}, // sixth
116 { 5, -2} // seventh
117 }};
118
119public:
129 bool isMinor = false, int numberOfEdoDivisions = STANDARD_12EDO_STEPS,
130 const std::optional <std::vector<int>>& keyMap = std::nullopt)
131 : m_displacement(displacement), m_alteration(alteration), m_numberOfEdoDivisions(numberOfEdoDivisions)
132 {
133 if (keyMap) {
134 if (keyMap.value().size() != STANDARD_DIATONIC_STEPS) {
135 throw std::invalid_argument("The Transposer class only supports key map arrays of " + std::to_string(STANDARD_DIATONIC_STEPS) + " elements");
136 }
137 m_keyMap = keyMap.value();
138 } else if (isMinor) {
139 m_keyMap.assign(MINOR_KEYMAP.begin(), MINOR_KEYMAP.end());
140 } else {
141 m_keyMap.assign(MAJOR_KEYMAP.begin(), MAJOR_KEYMAP.end());
142 }
143 }
144
146 int displacement() const { return m_displacement; }
147
149 int alteration() const { return m_alteration; }
150
153 void diatonicTranspose(int interval)
154 {
155 m_displacement += interval;
156 }
157
160 void enharmonicTranspose(int direction)
161 {
162 const int keyStepEnharmonic = calcStepsBetweenScaleDegrees(m_displacement, m_displacement + sign(direction));
163 diatonicTranspose(sign(direction));
164 m_alteration -= sign(direction) * keyStepEnharmonic;
165 }
166
188 void chromaticTranspose(int interval, int chromaticAlteration)
189 {
190 const int intervalNormalized = signedModulus(interval, STANDARD_DIATONIC_STEPS);
191 const int stepsInAlteration = calcStepsInAlteration(interval, chromaticAlteration);
192 const int stepsInInterval = calcStepsInNormalizedInterval(intervalNormalized);
193 const int stepsInDiatonicInterval = calcStepsBetweenScaleDegrees(m_displacement, m_displacement + intervalNormalized);
194
195 const int effectiveAlteration = stepsInAlteration + stepsInInterval - sign(interval) * stepsInDiatonicInterval;
196
197 diatonicTranspose(interval);
198 m_alteration += effectiveAlteration;
199 }
200
208 {
209 while (std::abs(m_alteration) > 0) {
210 const int currSign = sign(m_alteration);
211 const int currAbsDisp = std::abs(m_alteration);
212 enharmonicTranspose(currSign);
213 if (std::abs(m_alteration) >= currAbsDisp) {
214 enharmonicTranspose(-currSign);
215 return;
216 }
217 if (currSign != sign(m_alteration)) {
218 break;
219 }
220 }
221 }
222
232 void stepwiseTranspose(int numberOfEdoDivisions)
233 {
234 m_alteration += numberOfEdoDivisions;
236 }
237
249 return calcAbsoluteDivision(displacement, alteration) == calcAbsoluteDivision(m_displacement, m_alteration);
250 }
251
252private:
253 int calcFifthSteps() const
254 {
255 // std::log(3.0 / 2.0) / std::log(2.0) is 0.5849625007211562.
256 static constexpr double kFifthsMultiplier = 0.5849625007211562;
257 return static_cast<int>(std::floor(m_numberOfEdoDivisions * kFifthsMultiplier) + 0.5);
258 }
259
260 int calcScaleDegree(int interval) const
261 {
262 int intervalNormalized = signedModulus(interval, int(m_keyMap.size()));
263 if (intervalNormalized < 0) {
264 intervalNormalized += int(m_keyMap.size());
265 }
266 return intervalNormalized;
267 }
268
269 int calcStepsBetweenScaleDegrees(int firstDisplacement, int secondDisplacement) const
270 {
271 const int firstScaleDegree = calcScaleDegree(firstDisplacement);
272 const int secondScaleDegree = calcScaleDegree(secondDisplacement);
273 int result = sign(secondDisplacement - firstDisplacement) * (m_keyMap[secondScaleDegree] - m_keyMap[firstScaleDegree]);
274 if (result < 0) {
275 result += m_numberOfEdoDivisions;
276 }
277 return result;
278 }
279
280 int calcStepsInAlteration(int interval, int alteration) const
281 {
282 const int fifthSteps = calcFifthSteps();
283 const int plusFifths = sign(interval) * alteration * 7; // number of fifths to add for a chromatic halfstep alteration (in any EDO)
284 const int minusOctaves = sign(interval) * alteration * -4; // number of octaves to subtract for a chromatic halfstep alteration (in any EDO)
285 const int result = sign(interval) * ((plusFifths * fifthSteps) + (minusOctaves * m_numberOfEdoDivisions));
286 return result;
287 }
288
289 int calcStepsInNormalizedInterval(int intervalNormalized) const
290 {
291 const int fifthSteps = calcFifthSteps();
292 const int index = std::abs(intervalNormalized);
293 const int plusFifths = DIATONIC_INTERVAL_ADJUSTMENTS[index][0]; // number of fifths
294 const int minusOctaves = DIATONIC_INTERVAL_ADJUSTMENTS[index][1]; // number of octaves
295
296 return sign(intervalNormalized) * ((plusFifths * fifthSteps) + (minusOctaves * m_numberOfEdoDivisions));
297 }
298
299 int calcAbsoluteDivision(int displacement, int alteration) const {
300 const int scaleDegree = calcScaleDegree(displacement); // 0..6
301 const int baseStep = m_keyMap[scaleDegree];
302
303 const int octaveCount = (displacement < 0 && displacement % STANDARD_DIATONIC_STEPS != 0)
306 const int octaveSteps = octaveCount * m_numberOfEdoDivisions;
307 const int chromaticSteps = calcStepsInAlteration(/*interval=*/+1, alteration);
308
309 return baseStep + chromaticSteps + octaveSteps;
310 }
311};
312
313} // namespace music_theory
Provides dependency-free transposition utilities that work with any scale that has 7 diatonic steps a...
Definition music_theory.hpp:100
int displacement() const
Return the current displacement value.
Definition music_theory.hpp:146
void chromaticTranspose(int interval, int chromaticAlteration)
Chromatically transposes by a specified chromatic interval.
Definition music_theory.hpp:188
void stepwiseTranspose(int numberOfEdoDivisions)
Transposes by the given number of EDO divisions and simplifies the spelling.
Definition music_theory.hpp:232
int alteration() const
Return the current chromatic alteration value.
Definition music_theory.hpp:149
void simplifySpelling()
Simplifies the spelling by reducing its alteration while preserving pitch.
Definition music_theory.hpp:207
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:128
void diatonicTranspose(int interval)
Transposes the displacement by the specified interval.
Definition music_theory.hpp:153
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:248
void enharmonicTranspose(int direction)
Transposes enharmonically relative to the current values.
Definition music_theory.hpp:160
A dependency-free, header-only collection of useful functions for music theory.
DiatonicMode
Represents the seven standard diatonic musical modes.
Definition music_theory.hpp:49
@ 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:87
constexpr T sign(T n)
Calculates the sign of an integer.
Definition music_theory.hpp:75
int calcDisplacement(int pitchClass, int octave)
Calculates the displacement value for a given absolute pitch class and octave.
Definition music_theory.hpp:63
constexpr int STANDARD_12EDO_STEPS
this can be overriden when constructing a Transposer instance.
Definition music_theory.hpp:42
constexpr int STANDARD_DIATONIC_STEPS
currently this is the only supported number of diatonic steps.
Definition music_theory.hpp:41