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
29/*
30This header-only library has no dependencies and can be shared into any other library merely
31by including it.
32*/
33
36namespace music_theory {
37
38constexpr int STANDARD_DIATONIC_STEPS = 7;
39constexpr int STANDARD_12EDO_STEPS = 12;
40
45inline int calcDisplacement(int pitchClass, int octave)
46{
47 pitchClass %= STANDARD_DIATONIC_STEPS;
48 const int relativeOctave = octave - 4;
49
50 return pitchClass + (STANDARD_DIATONIC_STEPS * relativeOctave);
51}
52
56template <typename T>
57constexpr inline T sign(T n)
58{
59 static_assert(std::is_arithmetic_v<T>, "sign requires a numeric type");
60 return n < T(0) ? T(-1) : T(1);
61}
62
68template <typename T>
69constexpr T signedModulus(T n, T d) {
70 static_assert(std::is_integral_v<T>, "signed_modulus requires an integer type");
71 return sign(n) * (std::abs(n) % d);
72}
73
82{
83private:
84 static constexpr std::array<int, STANDARD_DIATONIC_STEPS> MAJOR_KEYMAP = { 0, 2, 4, 5, 7, 9, 11 };
85 static constexpr std::array<int, STANDARD_DIATONIC_STEPS> MINOR_KEYMAP = { 0, 2, 3, 5, 7, 8, 10 };
86 int m_displacement;
87 int m_alteration; // alteration from key signature
88 int m_numberOfEdoDivisions; // number of divisions in the EDO (default 12)
89 std::vector<int> m_keyMap; // step map for the EDO
90
91 static constexpr std::array<std::array<int, 2>, 7> DIATONIC_INTERVAL_ADJUSTMENTS = { {
92 { 0, 0}, // unison
93 { 2, -1}, // second
94 { 4, -2}, // third
95 {-1, 1}, // fourth
96 { 1, 0}, // fifth
97 { 3, -1}, // sixth
98 { 5, -2} // seventh
99 }};
100
101public:
111 bool isMinor = false, int numberOfEdoDivisions = STANDARD_12EDO_STEPS,
112 const std::optional <std::vector<int>>& keyMap = std::nullopt)
113 : m_displacement(displacement), m_alteration(alteration), m_numberOfEdoDivisions(numberOfEdoDivisions)
114 {
115 if (keyMap) {
116 if (keyMap.value().size() != STANDARD_DIATONIC_STEPS) {
117 throw std::invalid_argument("The Transposer class only supports key map arrays of " + std::to_string(STANDARD_DIATONIC_STEPS) + " elements");
118 }
119 m_keyMap = keyMap.value();
120 } else if (isMinor) {
121 m_keyMap.assign(MINOR_KEYMAP.begin(), MINOR_KEYMAP.end());
122 } else {
123 m_keyMap.assign(MAJOR_KEYMAP.begin(), MAJOR_KEYMAP.end());
124 }
125 }
126
128 int displacement() const { return m_displacement; }
129
131 int alteration() const { return m_alteration; }
132
135 void diatonicTranspose(int interval)
136 {
137 m_displacement += interval;
138 }
139
142 void enharmonicTranspose(int direction)
143 {
144 const int keyStepEnharmonic = calcStepsBetweenScaleDegrees(m_displacement, m_displacement + sign(direction));
145 diatonicTranspose(sign(direction));
146 m_alteration -= sign(direction) * keyStepEnharmonic;
147 }
148
170 void chromaticTranspose(int interval, int chromaticAlteration)
171 {
172 const int intervalNormalized = signedModulus(interval, STANDARD_DIATONIC_STEPS);
173 const int stepsInAlteration = calcStepsInAlteration(interval, chromaticAlteration);
174 const int stepsInInterval = calcStepsInNormalizedInterval(intervalNormalized);
175 const int stepsInDiatonicInterval = calcStepsBetweenScaleDegrees(m_displacement, m_displacement + intervalNormalized);
176
177 const int effectiveAlteration = stepsInAlteration + stepsInInterval - sign(interval) * stepsInDiatonicInterval;
178
179 diatonicTranspose(interval);
180 m_alteration += effectiveAlteration;
181 }
182
190 {
191 while (std::abs(m_alteration) > 0) {
192 const int currSign = sign(m_alteration);
193 const int currAbsDisp = std::abs(m_alteration);
194 enharmonicTranspose(currSign);
195 if (std::abs(m_alteration) >= currAbsDisp) {
196 enharmonicTranspose(-currSign);
197 return;
198 }
199 if (currSign != sign(m_alteration)) {
200 break;
201 }
202 }
203 }
204
214 void stepwiseTranspose(int numberOfEdoDivisions)
215 {
216 m_alteration += numberOfEdoDivisions;
218 }
219
231 return calcAbsoluteDivision(displacement, alteration) == calcAbsoluteDivision(m_displacement, m_alteration);
232 }
233
234private:
235 int calcFifthSteps() const
236 {
237 // std::log(3.0 / 2.0) / std::log(2.0) is 0.5849625007211562.
238 static constexpr double kFifthsMultiplier = 0.5849625007211562;
239 return static_cast<int>(std::floor(m_numberOfEdoDivisions * kFifthsMultiplier) + 0.5);
240 }
241
242 int calcScaleDegree(int interval) const
243 {
244 int intervalNormalized = signedModulus(interval, int(m_keyMap.size()));
245 if (intervalNormalized < 0) {
246 intervalNormalized += int(m_keyMap.size());
247 }
248 return intervalNormalized;
249 }
250
251 int calcStepsBetweenScaleDegrees(int firstDisplacement, int secondDisplacement) const
252 {
253 const int firstScaleDegree = calcScaleDegree(firstDisplacement);
254 const int secondScaleDegree = calcScaleDegree(secondDisplacement);
255 int result = sign(secondDisplacement - firstDisplacement) * (m_keyMap[secondScaleDegree] - m_keyMap[firstScaleDegree]);
256 if (result < 0) {
257 result += m_numberOfEdoDivisions;
258 }
259 return result;
260 }
261
262 int calcStepsInAlteration(int interval, int alteration) const
263 {
264 const int fifthSteps = calcFifthSteps();
265 const int plusFifths = sign(interval) * alteration * 7; // number of fifths to add for a chromatic halfstep alteration (in any EDO)
266 const int minusOctaves = sign(interval) * alteration * -4; // number of octaves to subtract for a chromatic halfstep alteration (in any EDO)
267 const int result = sign(interval) * ((plusFifths * fifthSteps) + (minusOctaves * m_numberOfEdoDivisions));
268 return result;
269 }
270
271 int calcStepsInNormalizedInterval(int intervalNormalized) const
272 {
273 const int fifthSteps = calcFifthSteps();
274 const int index = std::abs(intervalNormalized);
275 const int plusFifths = DIATONIC_INTERVAL_ADJUSTMENTS[index][0]; // number of fifths
276 const int minusOctaves = DIATONIC_INTERVAL_ADJUSTMENTS[index][1]; // number of octaves
277
278 return sign(intervalNormalized) * ((plusFifths * fifthSteps) + (minusOctaves * m_numberOfEdoDivisions));
279 }
280
281 int calcAbsoluteDivision(int displacement, int alteration) const {
282 const int scaleDegree = calcScaleDegree(displacement); // 0..6
283 const int baseStep = m_keyMap[scaleDegree];
284
285 const int octaveCount = (displacement < 0 && displacement % STANDARD_DIATONIC_STEPS != 0)
288 const int octaveSteps = octaveCount * m_numberOfEdoDivisions;
289 const int chromaticSteps = calcStepsInAlteration(/*interval=*/+1, alteration);
290
291 return baseStep + chromaticSteps + octaveSteps;
292 }
293};
294
295} // namespace music_theory
Provides dependency-free transposition utilities that work with any scale that has 7 diatonic steps a...
Definition music_theory.hpp:82
int displacement() const
Return the current displacement value.
Definition music_theory.hpp:128
void chromaticTranspose(int interval, int chromaticAlteration)
Chromatically transposes by a specified chromatic interval.
Definition music_theory.hpp:170
void stepwiseTranspose(int numberOfEdoDivisions)
Transposes by the given number of EDO divisions and simplifies the spelling.
Definition music_theory.hpp:214
int alteration() const
Return the current chromatic alteration value.
Definition music_theory.hpp:131
void simplifySpelling()
Simplifies the spelling by reducing its alteration while preserving pitch.
Definition music_theory.hpp:189
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:110
void diatonicTranspose(int interval)
Transposes the displacement by the specified interval.
Definition music_theory.hpp:135
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:230
void enharmonicTranspose(int direction)
Transposes enharmonically relative to the current values.
Definition music_theory.hpp:142
A dependency-free, header-only collection of useful functions for music theory.
constexpr T signedModulus(T n, T d)
Calculates the modulus of positive and negative numbers in a predictable manner.
Definition music_theory.hpp:69
constexpr T sign(T n)
Calculates the sign of an integer.
Definition music_theory.hpp:57
int calcDisplacement(int pitchClass, int octave)
Calculates the displacement value for a given absolute pitch class and octave.
Definition music_theory.hpp:45
constexpr int STANDARD_12EDO_STEPS
this can be overriden when constructing a Transposer instance.
Definition music_theory.hpp:39
constexpr int STANDARD_DIATONIC_STEPS
currently this is the only supported number of diatonic steps.
Definition music_theory.hpp:38