@musodojo/music-theory-data 14.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +122 -0
- package/README.md +15 -0
- package/package.json +16 -0
- package/src/data/chords/mod.ts +86 -0
- package/src/data/colors/mod.ts +109 -0
- package/src/data/labels/mod.ts +2 -0
- package/src/data/labels/note-label-collections.ts +124 -0
- package/src/data/labels/note-labels.ts +451 -0
- package/src/data/mod.ts +4 -0
- package/src/data/note-collections/diatonic-modes.ts +267 -0
- package/src/data/note-collections/dominant-variants.ts +110 -0
- package/src/data/note-collections/harmonic-minor-modes.ts +224 -0
- package/src/data/note-collections/major-variants.ts +129 -0
- package/src/data/note-collections/melodic-minor-modes.ts +198 -0
- package/src/data/note-collections/mod.ts +74 -0
- package/src/data/note-collections/other-collections.ts +76 -0
- package/src/data/note-collections/todo.txt +265 -0
- package/src/mod.ts +24 -0
- package/src/types/chords.d.ts +48 -0
- package/src/types/midi.d.ts +133 -0
- package/src/types/mod.d.ts +3 -0
- package/src/types/note-collections.d.ts +13 -0
- package/src/utils/get-chords.ts +166 -0
- package/src/utils/get-midi-note-sequences.ts +168 -0
- package/src/utils/intervals.ts +74 -0
- package/src/utils/midi.ts +50 -0
- package/src/utils/mod.ts +5 -0
- package/src/utils/note-collections.ts +170 -0
- package/src/utils/note-names.ts +188 -0
- package/src/utils/rotate-array.ts +15 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import {
|
|
2
|
+
enharmonicNoteNameGroups,
|
|
3
|
+
type Interval,
|
|
4
|
+
intervalToIntegerMap,
|
|
5
|
+
type NoteInteger,
|
|
6
|
+
type NoteLetter,
|
|
7
|
+
noteLetters,
|
|
8
|
+
type NoteName,
|
|
9
|
+
noteNamesSet,
|
|
10
|
+
noteNameToIntegerMap,
|
|
11
|
+
type RootNote,
|
|
12
|
+
rootNotesSet,
|
|
13
|
+
} from "../data/labels/note-labels.ts";
|
|
14
|
+
import {
|
|
15
|
+
type NoteCollectionKey,
|
|
16
|
+
noteCollections,
|
|
17
|
+
} from "../data/note-collections/mod.ts";
|
|
18
|
+
import {
|
|
19
|
+
transformIntervals,
|
|
20
|
+
type TransformIntervalsOptions,
|
|
21
|
+
} from "./intervals.ts";
|
|
22
|
+
import { isValidNoteCollectionKey } from "./note-collections.ts";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Parses a string and returns a canonical `NoteName` if the string is a valid note name.
|
|
26
|
+
* This is useful for handling user input that may use ASCII characters like 'b' and '#',
|
|
27
|
+
* as well as double accidentals like 'x', '##', and 'bb'.
|
|
28
|
+
* @param name The string to parse.
|
|
29
|
+
* @returns A canonical `NoteName` (e.g., "B♭", "C𝄪") or `undefined` if the input is not a valid note.
|
|
30
|
+
*/
|
|
31
|
+
export function normalizeNoteNameString(name: string): NoteName | undefined {
|
|
32
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check if the name is already a canonical note name (e.g., "C", "B♭")
|
|
37
|
+
// This check is case-sensitive.
|
|
38
|
+
if (noteNamesSet.has(name as NoteName)) {
|
|
39
|
+
return name as NoteName;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const noteLetterRegex = /^[A-Ga-g]/;
|
|
43
|
+
const accidentalRegex = /([#♯xX𝄪]+)|([b♭𝄫]+)/gu;
|
|
44
|
+
|
|
45
|
+
const noteLetterMatch = name.match(noteLetterRegex);
|
|
46
|
+
if (!noteLetterMatch) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const noteLetter = noteLetterMatch[0].toUpperCase() as NoteLetter;
|
|
51
|
+
const accidentalString = name.substring(noteLetterMatch[0].length);
|
|
52
|
+
let validAccidentalLength = 0;
|
|
53
|
+
let noteAlterInteger = 0;
|
|
54
|
+
|
|
55
|
+
for (const match of accidentalString.matchAll(accidentalRegex)) {
|
|
56
|
+
const sharps = match[1];
|
|
57
|
+
if (sharps) {
|
|
58
|
+
for (const char of sharps) {
|
|
59
|
+
noteAlterInteger += (char.toLowerCase() === "x" || char === "𝄪")
|
|
60
|
+
? 2
|
|
61
|
+
: 1;
|
|
62
|
+
}
|
|
63
|
+
validAccidentalLength += sharps.length;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const flats = match[2];
|
|
67
|
+
if (flats) {
|
|
68
|
+
for (const char of flats) {
|
|
69
|
+
noteAlterInteger -= (char === "𝄫") ? 2 : 1;
|
|
70
|
+
}
|
|
71
|
+
validAccidentalLength += flats.length;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check if the entire accidental string had valid accidentals
|
|
76
|
+
if (accidentalString.length > validAccidentalLength) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let accidentalSymbols = "";
|
|
81
|
+
let currentAlteration = noteAlterInteger;
|
|
82
|
+
|
|
83
|
+
while (currentAlteration > 0) {
|
|
84
|
+
if (currentAlteration >= 2) {
|
|
85
|
+
accidentalSymbols += "𝄪";
|
|
86
|
+
currentAlteration -= 2;
|
|
87
|
+
} else {
|
|
88
|
+
accidentalSymbols += "♯";
|
|
89
|
+
currentAlteration -= 1;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
while (currentAlteration < 0) {
|
|
94
|
+
if (currentAlteration <= -2) {
|
|
95
|
+
accidentalSymbols += "𝄫";
|
|
96
|
+
currentAlteration += 2;
|
|
97
|
+
} else {
|
|
98
|
+
accidentalSymbols += "♭";
|
|
99
|
+
currentAlteration += 1;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const result = (noteLetter + accidentalSymbols) as NoteName;
|
|
104
|
+
|
|
105
|
+
return noteNamesSet.has(result) ? result : undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Parses a string and returns a canonical `RootNote` if the string is a valid root note.
|
|
110
|
+
* This is useful for handling user input that may use ASCII characters for accidentals.
|
|
111
|
+
* It is stricter than `normalizeNoteNameString` as it only allows notes present in the `rootNotes` array.
|
|
112
|
+
* @param name The string to parse.
|
|
113
|
+
* @returns A canonical `RootNote` or `undefined` if the input is not a valid root note.
|
|
114
|
+
*/
|
|
115
|
+
export function normalizeRootNoteString(name: string): RootNote | undefined {
|
|
116
|
+
const normalizedNote = normalizeNoteNameString(name);
|
|
117
|
+
|
|
118
|
+
if (normalizedNote && rootNotesSet.has(normalizedNote as RootNote)) {
|
|
119
|
+
return normalizedNote as RootNote;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function noteNameToInteger(
|
|
126
|
+
noteName: NoteName,
|
|
127
|
+
): NoteInteger | undefined {
|
|
128
|
+
return noteNameToIntegerMap.get(noteName);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function noteNameStringToInteger(
|
|
132
|
+
noteName: string,
|
|
133
|
+
): NoteInteger | undefined {
|
|
134
|
+
const normalized = normalizeNoteNameString(noteName);
|
|
135
|
+
if (!normalized) return undefined;
|
|
136
|
+
return noteNameToInteger(normalized);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function getNoteNamesFromRootAndIntervals(
|
|
140
|
+
rootNote: RootNote,
|
|
141
|
+
intervals: readonly Interval[],
|
|
142
|
+
options: TransformIntervalsOptions = {},
|
|
143
|
+
): NoteName[] {
|
|
144
|
+
const rootNoteInteger = noteNameToInteger(rootNote);
|
|
145
|
+
if (rootNoteInteger === undefined) return [];
|
|
146
|
+
const rootNoteLetter = rootNote.charAt(0).toUpperCase();
|
|
147
|
+
const rootNoteLetterIndex = noteLetters.indexOf(rootNoteLetter as NoteLetter);
|
|
148
|
+
|
|
149
|
+
const intervalsToConvert = Object.keys(options).length > 0
|
|
150
|
+
? transformIntervals(intervals, options)
|
|
151
|
+
: intervals;
|
|
152
|
+
|
|
153
|
+
const noteNames: NoteName[] = intervalsToConvert.flatMap((interval) => {
|
|
154
|
+
const intervalInteger = intervalToIntegerMap.get(interval);
|
|
155
|
+
if (intervalInteger === undefined) return []; // skip over invalid intervals silently
|
|
156
|
+
|
|
157
|
+
const absoluteNoteInteger = (rootNoteInteger + intervalInteger) % 12;
|
|
158
|
+
|
|
159
|
+
const intervalNumberMatch = interval.match(/\d+/)!;
|
|
160
|
+
const intervalNumber = parseInt(intervalNumberMatch[0], 10);
|
|
161
|
+
|
|
162
|
+
const targetNoteLetter =
|
|
163
|
+
noteLetters[(rootNoteLetterIndex + intervalNumber - 1) % 7];
|
|
164
|
+
|
|
165
|
+
const enharmonicGroup = enharmonicNoteNameGroups[absoluteNoteInteger];
|
|
166
|
+
const selectedNote = enharmonicGroup.find((noteName) =>
|
|
167
|
+
noteName.startsWith(targetNoteLetter)
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
return [selectedNote ?? enharmonicGroup[0]];
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
return noteNames;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function getNoteNamesFromRootAndCollectionKey(
|
|
177
|
+
rootNote: RootNote,
|
|
178
|
+
noteCollectionKey: NoteCollectionKey,
|
|
179
|
+
options: TransformIntervalsOptions = {},
|
|
180
|
+
): NoteName[] {
|
|
181
|
+
if (!isValidNoteCollectionKey(noteCollectionKey)) return [];
|
|
182
|
+
|
|
183
|
+
return getNoteNamesFromRootAndIntervals(
|
|
184
|
+
rootNote,
|
|
185
|
+
noteCollections[noteCollectionKey].intervals,
|
|
186
|
+
options,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function rotateArray<T>(array: T[], rotation: number): T[] {
|
|
2
|
+
const normalizedRotation = ((rotation % array.length) + array.length) %
|
|
3
|
+
array.length;
|
|
4
|
+
return array.slice(normalizedRotation).concat(
|
|
5
|
+
array.slice(0, normalizedRotation),
|
|
6
|
+
);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function rotateArrayToStartWith<T>(array: T[], start: T): T[] {
|
|
10
|
+
const index = array.indexOf(start);
|
|
11
|
+
if (index === -1) {
|
|
12
|
+
throw new Error(`Element "${start}" not found in the array.`);
|
|
13
|
+
}
|
|
14
|
+
return rotateArray(array, index);
|
|
15
|
+
}
|