@leafo/lml 0.1.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/README.md +248 -0
- package/dist/auto-chords.d.ts +46 -0
- package/dist/auto-chords.d.ts.map +1 -0
- package/dist/auto-chords.js +240 -0
- package/dist/auto-chords.js.map +1 -0
- package/dist/grammar.d.ts +12 -0
- package/dist/grammar.d.ts.map +1 -0
- package/dist/grammar.js +1431 -0
- package/dist/grammar.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/music.d.ts +107 -0
- package/dist/music.d.ts.map +1 -0
- package/dist/music.js +704 -0
- package/dist/music.js.map +1 -0
- package/dist/note-list.d.ts +10 -0
- package/dist/note-list.d.ts.map +1 -0
- package/dist/note-list.js +88 -0
- package/dist/note-list.js.map +1 -0
- package/dist/parser.d.ts +27 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +181 -0
- package/dist/parser.js.map +1 -0
- package/dist/song.d.ts +50 -0
- package/dist/song.d.ts.map +1 -0
- package/dist/song.js +261 -0
- package/dist/song.js.map +1 -0
- package/package.json +47 -0
package/dist/music.js
ADDED
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
export const MIDDLE_C_PITCH = 60;
|
|
2
|
+
export const OCTAVE_SIZE = 12;
|
|
3
|
+
export const OFFSETS = {
|
|
4
|
+
0: "C",
|
|
5
|
+
2: "D",
|
|
6
|
+
4: "E",
|
|
7
|
+
5: "F",
|
|
8
|
+
7: "G",
|
|
9
|
+
9: "A",
|
|
10
|
+
11: "B",
|
|
11
|
+
"C": 0,
|
|
12
|
+
"D": 2,
|
|
13
|
+
"E": 4,
|
|
14
|
+
"F": 5,
|
|
15
|
+
"G": 7,
|
|
16
|
+
"A": 9,
|
|
17
|
+
"B": 11
|
|
18
|
+
};
|
|
19
|
+
export const LETTER_OFFSETS = {
|
|
20
|
+
0: 0,
|
|
21
|
+
2: 1,
|
|
22
|
+
4: 2,
|
|
23
|
+
5: 3,
|
|
24
|
+
7: 4,
|
|
25
|
+
9: 5,
|
|
26
|
+
11: 6
|
|
27
|
+
};
|
|
28
|
+
export const NOTE_NAME_OFFSETS = {
|
|
29
|
+
"C": 0,
|
|
30
|
+
"D": 1,
|
|
31
|
+
"E": 2,
|
|
32
|
+
"F": 3,
|
|
33
|
+
"G": 4,
|
|
34
|
+
"A": 5,
|
|
35
|
+
"B": 6,
|
|
36
|
+
};
|
|
37
|
+
export function noteName(pitch, sharpen = true) {
|
|
38
|
+
const octave = Math.floor(pitch / OCTAVE_SIZE);
|
|
39
|
+
const offset = pitch - octave * OCTAVE_SIZE;
|
|
40
|
+
let name = OFFSETS[offset];
|
|
41
|
+
if (!name) {
|
|
42
|
+
if (sharpen) {
|
|
43
|
+
name = OFFSETS[offset - 1] + "#";
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
name = OFFSETS[offset + 1] + "b";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return `${name}${octave}`;
|
|
50
|
+
}
|
|
51
|
+
function parseNoteAccidentals(note) {
|
|
52
|
+
const match = note.match(/^([A-G])(#|b)?/);
|
|
53
|
+
if (!match) {
|
|
54
|
+
throw new Error(`Invalid note format: ${note}`);
|
|
55
|
+
}
|
|
56
|
+
const [, , accidental] = match;
|
|
57
|
+
let n = 0;
|
|
58
|
+
if (accidental == "#") {
|
|
59
|
+
n += 1;
|
|
60
|
+
}
|
|
61
|
+
if (accidental == "b") {
|
|
62
|
+
n -= 1;
|
|
63
|
+
}
|
|
64
|
+
return n;
|
|
65
|
+
}
|
|
66
|
+
// get the octave independent offset in halfsteps (from C), used for comparison
|
|
67
|
+
function parseNoteOffset(note) {
|
|
68
|
+
const match = note.match(/^([A-G])(#|b)?/);
|
|
69
|
+
if (!match) {
|
|
70
|
+
throw new Error(`Invalid note format: ${note}`);
|
|
71
|
+
}
|
|
72
|
+
const [, letter, accidental] = match;
|
|
73
|
+
if (OFFSETS[letter] === undefined) {
|
|
74
|
+
throw new Error(`Invalid note letter: ${letter}`);
|
|
75
|
+
}
|
|
76
|
+
let n = OFFSETS[letter];
|
|
77
|
+
if (accidental == "#") {
|
|
78
|
+
n += 1;
|
|
79
|
+
}
|
|
80
|
+
if (accidental == "b") {
|
|
81
|
+
n -= 1;
|
|
82
|
+
}
|
|
83
|
+
return (n + 12) % 12; // wrap around for Cb and B#
|
|
84
|
+
}
|
|
85
|
+
export function parseNote(note) {
|
|
86
|
+
const parsed = note.match(/^([A-G])(#|b)?(\d+)$/);
|
|
87
|
+
if (!parsed) {
|
|
88
|
+
throw new Error(`parseNote: invalid note format '${note}'`);
|
|
89
|
+
}
|
|
90
|
+
const [, letter, accidental, octave] = parsed;
|
|
91
|
+
if (OFFSETS[letter] === undefined) {
|
|
92
|
+
throw new Error(`Invalid note letter: ${letter}`);
|
|
93
|
+
}
|
|
94
|
+
let n = OFFSETS[letter] + parseInt(octave, 10) * OCTAVE_SIZE;
|
|
95
|
+
if (accidental == "#") {
|
|
96
|
+
n += 1;
|
|
97
|
+
}
|
|
98
|
+
if (accidental == "b") {
|
|
99
|
+
n -= 1;
|
|
100
|
+
}
|
|
101
|
+
return n;
|
|
102
|
+
}
|
|
103
|
+
export function noteStaffOffset(note) {
|
|
104
|
+
const match = note.match(/(\w)[#b]?(\d+)/);
|
|
105
|
+
if (!match) {
|
|
106
|
+
throw new Error("Invalid note");
|
|
107
|
+
}
|
|
108
|
+
const [, name, octave] = match;
|
|
109
|
+
return +octave * 7 + NOTE_NAME_OFFSETS[name];
|
|
110
|
+
}
|
|
111
|
+
// octaveless note comparison
|
|
112
|
+
export function notesSame(a, b) {
|
|
113
|
+
return parseNoteOffset(a) == parseNoteOffset(b);
|
|
114
|
+
}
|
|
115
|
+
export function addInterval(note, halfSteps) {
|
|
116
|
+
return noteName(parseNote(note) + halfSteps);
|
|
117
|
+
}
|
|
118
|
+
// returns 0 if notes are same
|
|
119
|
+
// returns < 0 if a < b
|
|
120
|
+
// returns > 0 if a > b
|
|
121
|
+
export function compareNotes(a, b) {
|
|
122
|
+
return parseNote(a) - parseNote(b);
|
|
123
|
+
}
|
|
124
|
+
export function notesLessThan(a, b) {
|
|
125
|
+
return compareNotes(a, b) < 0;
|
|
126
|
+
}
|
|
127
|
+
export function notesGreaterThan(a, b) {
|
|
128
|
+
return compareNotes(a, b) > 0;
|
|
129
|
+
}
|
|
130
|
+
export class KeySignature {
|
|
131
|
+
// excludes the chromatic option
|
|
132
|
+
static allKeySignatures() {
|
|
133
|
+
return [
|
|
134
|
+
0, 1, 2, 3, 4, 5, -1, -2, -3, -4, -5, -6
|
|
135
|
+
].map(key => new KeySignature(key));
|
|
136
|
+
}
|
|
137
|
+
static forCount(count) {
|
|
138
|
+
if (!this.cache) {
|
|
139
|
+
this.cache = this.allKeySignatures();
|
|
140
|
+
}
|
|
141
|
+
for (const key of this.cache) {
|
|
142
|
+
if (key.count == count) {
|
|
143
|
+
return key;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// count: the number of accidentals in the key
|
|
148
|
+
constructor(count) {
|
|
149
|
+
this.count = count;
|
|
150
|
+
}
|
|
151
|
+
getCount() {
|
|
152
|
+
return this.count;
|
|
153
|
+
}
|
|
154
|
+
isChromatic() {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
isSharp() {
|
|
158
|
+
return this.count > 0;
|
|
159
|
+
}
|
|
160
|
+
isFlat() {
|
|
161
|
+
return this.count < 0;
|
|
162
|
+
}
|
|
163
|
+
name() {
|
|
164
|
+
let offset = this.count + 1;
|
|
165
|
+
if (offset < 0) {
|
|
166
|
+
offset += KeySignature.FIFTHS.length;
|
|
167
|
+
}
|
|
168
|
+
return KeySignature.FIFTHS[offset];
|
|
169
|
+
}
|
|
170
|
+
toString() {
|
|
171
|
+
return this.name();
|
|
172
|
+
}
|
|
173
|
+
// the default scale root for building scales from key signature
|
|
174
|
+
scaleRoot() {
|
|
175
|
+
return this.name();
|
|
176
|
+
}
|
|
177
|
+
// the scale used on the random note generator
|
|
178
|
+
defaultScale() {
|
|
179
|
+
return new MajorScale(this);
|
|
180
|
+
}
|
|
181
|
+
// convert note to enharmonic equivalent that fits into this key signature
|
|
182
|
+
enharmonic(note) {
|
|
183
|
+
if (this.isFlat()) {
|
|
184
|
+
if (note.indexOf("#") != -1) {
|
|
185
|
+
return noteName(parseNote(note), false);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (this.isSharp()) {
|
|
189
|
+
if (note.indexOf("b") != -1) {
|
|
190
|
+
return noteName(parseNote(note), true);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return note;
|
|
194
|
+
}
|
|
195
|
+
// which notes have accidentals in this key
|
|
196
|
+
accidentalNotes() {
|
|
197
|
+
const fifths = KeySignature.FIFTHS_TRUNCATED;
|
|
198
|
+
if (this.count > 0) {
|
|
199
|
+
return fifths.slice(0, this.count);
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
return fifths.slice(fifths.length + this.count).reverse();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// key note -> raw note
|
|
206
|
+
unconvertNote(note) {
|
|
207
|
+
if (this.count == 0) {
|
|
208
|
+
return typeof note === "number" ? noteName(note) : note;
|
|
209
|
+
}
|
|
210
|
+
if (typeof note == "number") {
|
|
211
|
+
note = noteName(note);
|
|
212
|
+
}
|
|
213
|
+
const match = note.match(/^([A-G])(\d+)?/);
|
|
214
|
+
if (!match) {
|
|
215
|
+
throw new Error("can't unconvert note with accidental");
|
|
216
|
+
}
|
|
217
|
+
const [, name, octave] = match;
|
|
218
|
+
for (const modifiedNote of this.accidentalNotes()) {
|
|
219
|
+
if (modifiedNote == name) {
|
|
220
|
+
return `${name}${this.isSharp() ? "#" : "b"}${octave}`;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return note;
|
|
224
|
+
}
|
|
225
|
+
// how many accidentals should display on note for this key
|
|
226
|
+
// null: nothing
|
|
227
|
+
// 0: a natural
|
|
228
|
+
// 1: a sharp
|
|
229
|
+
// -1: a flat
|
|
230
|
+
// 2: double sharp, etc.
|
|
231
|
+
accidentalsForNote(note) {
|
|
232
|
+
if (typeof note == "number") {
|
|
233
|
+
note = noteName(note);
|
|
234
|
+
}
|
|
235
|
+
const match = note.match(/^([A-G])(#|b)?/);
|
|
236
|
+
if (!match) {
|
|
237
|
+
throw new Error(`Invalid note format: ${note}`);
|
|
238
|
+
}
|
|
239
|
+
const [, name, a] = match;
|
|
240
|
+
if (a == "#") {
|
|
241
|
+
return this.isSharp() && this.accidentalNotes().includes(name) ? null : 1;
|
|
242
|
+
}
|
|
243
|
+
if (a == "b") {
|
|
244
|
+
return this.isFlat() && this.accidentalNotes().includes(name) ? null : -1;
|
|
245
|
+
}
|
|
246
|
+
for (const modifiedNote of this.accidentalNotes()) {
|
|
247
|
+
if (modifiedNote == name) {
|
|
248
|
+
return 0; // natural needed
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
// the notes to give accidentals to within the range [min, max], the returned
|
|
254
|
+
// notes will not be sharp or flat
|
|
255
|
+
notesInRange(min, max) {
|
|
256
|
+
if (this.count == 0) {
|
|
257
|
+
return [];
|
|
258
|
+
}
|
|
259
|
+
if (typeof max == "string") {
|
|
260
|
+
max = parseNote(max);
|
|
261
|
+
}
|
|
262
|
+
if (typeof min == "string") {
|
|
263
|
+
min = parseNote(min);
|
|
264
|
+
}
|
|
265
|
+
const octave = 5; // TODO: pick something close to min/max
|
|
266
|
+
let notes = null;
|
|
267
|
+
if (this.count > 0) {
|
|
268
|
+
let count = this.count;
|
|
269
|
+
notes = [parseNote(`F${octave}`)];
|
|
270
|
+
while (count > 1) {
|
|
271
|
+
count -= 1;
|
|
272
|
+
notes.push(notes[notes.length - 1] + 7);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (this.count < 0) {
|
|
276
|
+
let count = -1 * this.count;
|
|
277
|
+
notes = [parseNote(`B${octave}`)];
|
|
278
|
+
while (count > 1) {
|
|
279
|
+
count -= 1;
|
|
280
|
+
notes.push(notes[notes.length - 1] - 7);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (!notes)
|
|
284
|
+
return [];
|
|
285
|
+
return notes.map(function (n) {
|
|
286
|
+
while (n <= min) {
|
|
287
|
+
n += 12;
|
|
288
|
+
}
|
|
289
|
+
while (n > max) {
|
|
290
|
+
n -= 12;
|
|
291
|
+
}
|
|
292
|
+
return noteName(n);
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
KeySignature.FIFTHS = [
|
|
297
|
+
"F", "C", "G", "D", "A", "E", "B", "Gb", "Db", "Ab", "Eb", "Bb"
|
|
298
|
+
];
|
|
299
|
+
KeySignature.FIFTHS_TRUNCATED = [
|
|
300
|
+
"F", "C", "G", "D", "A", "E", "B"
|
|
301
|
+
];
|
|
302
|
+
KeySignature.cache = null;
|
|
303
|
+
export class ChromaticKeySignature extends KeySignature {
|
|
304
|
+
constructor() {
|
|
305
|
+
super(0); // render as c major
|
|
306
|
+
}
|
|
307
|
+
isChromatic() {
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
name() {
|
|
311
|
+
return "Chromatic";
|
|
312
|
+
}
|
|
313
|
+
scaleRoot() {
|
|
314
|
+
return "C";
|
|
315
|
+
}
|
|
316
|
+
defaultScale() {
|
|
317
|
+
return new ChromaticScale(this);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
export class Scale {
|
|
321
|
+
constructor(root) {
|
|
322
|
+
this.steps = [];
|
|
323
|
+
this.minor = false;
|
|
324
|
+
this.chromatic = false;
|
|
325
|
+
if (root instanceof KeySignature) {
|
|
326
|
+
root = root.scaleRoot();
|
|
327
|
+
}
|
|
328
|
+
if (!root.match(/^[A-G][b#]?$/)) {
|
|
329
|
+
throw new Error("scale root not properly formed: " + root);
|
|
330
|
+
}
|
|
331
|
+
this.root = root;
|
|
332
|
+
}
|
|
333
|
+
getFullRange() {
|
|
334
|
+
return this.getRange(0, (this.steps.length + 1) * 8);
|
|
335
|
+
}
|
|
336
|
+
getLooseRange(min, max) {
|
|
337
|
+
const fullRange = this.getFullRange();
|
|
338
|
+
const minPitch = parseNote(min);
|
|
339
|
+
const maxPitch = parseNote(max);
|
|
340
|
+
return fullRange.filter(note => {
|
|
341
|
+
const pitch = parseNote(note);
|
|
342
|
+
return pitch >= minPitch && pitch <= maxPitch;
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
getRange(octave, count = this.steps.length + 1, offset = 0) {
|
|
346
|
+
let current = parseNote(`${this.root}${octave}`);
|
|
347
|
+
const isFlat = this.isFlat();
|
|
348
|
+
const range = [];
|
|
349
|
+
let k = 0;
|
|
350
|
+
while (offset < 0) {
|
|
351
|
+
k--;
|
|
352
|
+
if (k < 0) {
|
|
353
|
+
k += this.steps.length;
|
|
354
|
+
}
|
|
355
|
+
current -= this.steps[k % this.steps.length];
|
|
356
|
+
offset++;
|
|
357
|
+
}
|
|
358
|
+
for (let i = 0; i < count + offset; i++) {
|
|
359
|
+
if (i >= offset) {
|
|
360
|
+
range.push(noteName(current, this.chromatic || !isFlat));
|
|
361
|
+
}
|
|
362
|
+
current += this.steps[k++ % this.steps.length];
|
|
363
|
+
}
|
|
364
|
+
return range;
|
|
365
|
+
}
|
|
366
|
+
isFlat() {
|
|
367
|
+
let idx = KeySignature.FIFTHS.indexOf(this.root);
|
|
368
|
+
if (idx == -1) {
|
|
369
|
+
// the root is sharp
|
|
370
|
+
let letter = this.root.charCodeAt(0) + 1;
|
|
371
|
+
if (letter > 71) {
|
|
372
|
+
letter -= 5;
|
|
373
|
+
}
|
|
374
|
+
const realRoot = String.fromCharCode(letter) + "#";
|
|
375
|
+
idx = KeySignature.FIFTHS.indexOf(realRoot);
|
|
376
|
+
}
|
|
377
|
+
if (this.minor) {
|
|
378
|
+
idx -= 3;
|
|
379
|
+
if (idx < 0) {
|
|
380
|
+
idx += KeySignature.FIFTHS.length;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return idx < 1 || idx > 6;
|
|
384
|
+
}
|
|
385
|
+
containsNote(note) {
|
|
386
|
+
let pitch = parseNoteOffset(note);
|
|
387
|
+
const rootPitch = parseNoteOffset(this.root);
|
|
388
|
+
// move note within an octave of root
|
|
389
|
+
while (pitch < rootPitch) {
|
|
390
|
+
pitch += OCTAVE_SIZE;
|
|
391
|
+
}
|
|
392
|
+
while (pitch >= rootPitch + OCTAVE_SIZE) {
|
|
393
|
+
pitch -= OCTAVE_SIZE;
|
|
394
|
+
}
|
|
395
|
+
let currentPitch = rootPitch;
|
|
396
|
+
let i = 0;
|
|
397
|
+
// keep incrementing until we hit it, or pass it
|
|
398
|
+
while (currentPitch <= pitch) {
|
|
399
|
+
if (currentPitch == pitch) {
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
currentPitch += this.steps[i % this.steps.length];
|
|
403
|
+
i++;
|
|
404
|
+
}
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
// degrees are 1 indexed
|
|
408
|
+
degreeToName(degree) {
|
|
409
|
+
// truncate to reasonable range
|
|
410
|
+
degree = (degree - 1) % this.steps.length + 1;
|
|
411
|
+
const range = this.getRange(0, degree);
|
|
412
|
+
const note = range[range.length - 1];
|
|
413
|
+
const m = note.match(/^[^\d]+/);
|
|
414
|
+
return m ? m[0] : note;
|
|
415
|
+
}
|
|
416
|
+
// degrees are 1 indexed
|
|
417
|
+
getDegree(note) {
|
|
418
|
+
let pitch = parseNoteOffset(note);
|
|
419
|
+
const rootPitch = parseNoteOffset(this.root);
|
|
420
|
+
// move note within an octave of root
|
|
421
|
+
while (pitch < rootPitch) {
|
|
422
|
+
pitch += OCTAVE_SIZE;
|
|
423
|
+
}
|
|
424
|
+
while (pitch >= rootPitch + OCTAVE_SIZE) {
|
|
425
|
+
pitch -= OCTAVE_SIZE;
|
|
426
|
+
}
|
|
427
|
+
let degree = 1;
|
|
428
|
+
let currentPitch = rootPitch;
|
|
429
|
+
if (currentPitch == pitch) {
|
|
430
|
+
return degree;
|
|
431
|
+
}
|
|
432
|
+
for (const offset of this.steps) {
|
|
433
|
+
currentPitch += offset;
|
|
434
|
+
degree += 1;
|
|
435
|
+
if (currentPitch == pitch) {
|
|
436
|
+
return degree;
|
|
437
|
+
}
|
|
438
|
+
if (currentPitch > pitch) {
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
throw new Error(`${note} is not in scale ${this.root}`);
|
|
443
|
+
}
|
|
444
|
+
// degree is one indexed
|
|
445
|
+
// new MajorScale().buildChordSteps(1, 2) -> major chord
|
|
446
|
+
buildChordSteps(degree, count) {
|
|
447
|
+
let idx = degree - 1;
|
|
448
|
+
const out = [];
|
|
449
|
+
while (count > 0) {
|
|
450
|
+
let stride = 2;
|
|
451
|
+
let step = 0;
|
|
452
|
+
while (stride > 0) {
|
|
453
|
+
step += this.steps[idx % this.steps.length];
|
|
454
|
+
idx++;
|
|
455
|
+
stride--;
|
|
456
|
+
}
|
|
457
|
+
out.push(step);
|
|
458
|
+
count--;
|
|
459
|
+
}
|
|
460
|
+
return out;
|
|
461
|
+
}
|
|
462
|
+
// all chords with count notes
|
|
463
|
+
allChords(noteCount = 3) {
|
|
464
|
+
const out = [];
|
|
465
|
+
for (let i = 0; i < this.steps.length; i++) {
|
|
466
|
+
const degree = i + 1;
|
|
467
|
+
const root = this.degreeToName(degree);
|
|
468
|
+
const steps = this.buildChordSteps(degree, noteCount - 1);
|
|
469
|
+
out.push(new Chord(root, steps));
|
|
470
|
+
}
|
|
471
|
+
return out;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
export class MajorScale extends Scale {
|
|
475
|
+
constructor(root) {
|
|
476
|
+
super(root);
|
|
477
|
+
this.steps = [2, 2, 1, 2, 2, 2, 1];
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
// natural minor
|
|
481
|
+
export class MinorScale extends Scale {
|
|
482
|
+
constructor(root) {
|
|
483
|
+
super(root);
|
|
484
|
+
this.minor = true;
|
|
485
|
+
this.steps = [2, 1, 2, 2, 1, 2, 2];
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
export class HarmonicMinorScale extends Scale {
|
|
489
|
+
constructor(root) {
|
|
490
|
+
super(root);
|
|
491
|
+
this.minor = true;
|
|
492
|
+
this.steps = [2, 1, 2, 2, 1, 3, 1];
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
export class AscendingMelodicMinorScale extends Scale {
|
|
496
|
+
constructor(root) {
|
|
497
|
+
super(root);
|
|
498
|
+
this.minor = true;
|
|
499
|
+
this.steps = [2, 1, 2, 2, 2, 2, 1];
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
export class MajorBluesScale extends Scale {
|
|
503
|
+
constructor(root) {
|
|
504
|
+
super(root);
|
|
505
|
+
// C, D, D#/Eb, E, G, A
|
|
506
|
+
this.steps = [2, 1, 1, 3, 2, 3];
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
export class MinorBluesScale extends Scale {
|
|
510
|
+
constructor(root) {
|
|
511
|
+
super(root);
|
|
512
|
+
this.minor = true;
|
|
513
|
+
// C, D#/Eb, F, F#/Gb, G, Bb
|
|
514
|
+
this.steps = [3, 2, 1, 1, 3, 2];
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
export class ChromaticScale extends Scale {
|
|
518
|
+
constructor(root) {
|
|
519
|
+
super(root);
|
|
520
|
+
this.chromatic = true;
|
|
521
|
+
this.steps = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1];
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
export class Chord extends Scale {
|
|
525
|
+
// Chord.notes("C5", "M", 1) -> first inversion C major chord
|
|
526
|
+
static notes(note, chordName, inversion = 0, notesCount = 0) {
|
|
527
|
+
const match = note.match(/^([^\d]+)(\d+)$/);
|
|
528
|
+
if (!match) {
|
|
529
|
+
throw new Error(`Invalid note format: ${note}`);
|
|
530
|
+
}
|
|
531
|
+
const [, root, octaveStr] = match;
|
|
532
|
+
const octave = +octaveStr;
|
|
533
|
+
const intervals = Chord.SHAPES[chordName];
|
|
534
|
+
if (notesCount == 0) {
|
|
535
|
+
notesCount = intervals.length + 1;
|
|
536
|
+
}
|
|
537
|
+
return new Chord(root, intervals).getRange(octave, notesCount, inversion);
|
|
538
|
+
}
|
|
539
|
+
constructor(root, intervals) {
|
|
540
|
+
super(root);
|
|
541
|
+
if (typeof intervals === "string") {
|
|
542
|
+
const shape = Chord.SHAPES[intervals];
|
|
543
|
+
if (!shape) {
|
|
544
|
+
throw new Error(`Unknown chord shape: ${intervals}`);
|
|
545
|
+
}
|
|
546
|
+
intervals = shape;
|
|
547
|
+
}
|
|
548
|
+
if (!intervals) {
|
|
549
|
+
throw new Error("Missing intervals for chord");
|
|
550
|
+
}
|
|
551
|
+
this.steps = [...intervals];
|
|
552
|
+
// add wrapping interval to get back to octave
|
|
553
|
+
let sum = 0;
|
|
554
|
+
for (const i of this.steps) {
|
|
555
|
+
sum += i;
|
|
556
|
+
}
|
|
557
|
+
let rest = -sum;
|
|
558
|
+
while (rest < 0) {
|
|
559
|
+
rest += OCTAVE_SIZE;
|
|
560
|
+
}
|
|
561
|
+
this.steps.push(rest);
|
|
562
|
+
}
|
|
563
|
+
// is major or dom7 chord
|
|
564
|
+
isDominant() {
|
|
565
|
+
const shapeName = this.chordShapeName();
|
|
566
|
+
if (shapeName == "M" || shapeName == "7") {
|
|
567
|
+
return true;
|
|
568
|
+
}
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
// can point to a chord that's a 4th below (third above)
|
|
572
|
+
// the target chord can either be major or minor (2,3,5,6) in new key
|
|
573
|
+
getSecondaryDominantTargets(noteCount = 3) {
|
|
574
|
+
if (!this.isDominant()) {
|
|
575
|
+
throw new Error(`chord is not dominant to begin with: ${this.chordShapeName()}`);
|
|
576
|
+
}
|
|
577
|
+
// new root is 5 halfsteps above the current (or 7 below)
|
|
578
|
+
const match = noteName(parseNote(`${this.root}5`) + 5).match(/^([^\d]+)(\d+)$/);
|
|
579
|
+
if (!match) {
|
|
580
|
+
throw new Error("Failed to compute secondary dominant target");
|
|
581
|
+
}
|
|
582
|
+
const [, newRoot] = match;
|
|
583
|
+
// triads
|
|
584
|
+
if (noteCount == 3) {
|
|
585
|
+
return ["M", "m"].map(quality => new Chord(newRoot, quality));
|
|
586
|
+
}
|
|
587
|
+
// sevenths
|
|
588
|
+
if (noteCount == 4) {
|
|
589
|
+
return ["M7", "m7"].map(quality => new Chord(newRoot, quality));
|
|
590
|
+
}
|
|
591
|
+
throw new Error(`don't know how to get secondary dominant for note count: ${noteCount}`);
|
|
592
|
+
}
|
|
593
|
+
chordShapeName() {
|
|
594
|
+
for (const shape in Chord.SHAPES) {
|
|
595
|
+
const intervals = Chord.SHAPES[shape];
|
|
596
|
+
if (this.steps.length - 1 != intervals.length) {
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
let match = true;
|
|
600
|
+
for (let k = 0; k < intervals.length; k++) {
|
|
601
|
+
if (intervals[k] != this.steps[k]) {
|
|
602
|
+
match = false;
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
if (match) {
|
|
607
|
+
return shape;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
// do all the notes fit this chord
|
|
612
|
+
containsNotes(notes) {
|
|
613
|
+
if (!notes.length) {
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
616
|
+
for (const note of notes) {
|
|
617
|
+
if (!this.containsNote(note)) {
|
|
618
|
+
return false;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return true;
|
|
622
|
+
}
|
|
623
|
+
// how many notes do the two chords share
|
|
624
|
+
countSharedNotes(otherChord) {
|
|
625
|
+
const myNotes = this.getRange(5, this.steps.length);
|
|
626
|
+
const theirNotes = otherChord.getRange(5, this.steps.length);
|
|
627
|
+
let count = 0;
|
|
628
|
+
const noteNames = {};
|
|
629
|
+
const normalizeNote = (note) => note.replace(/\d+$/, "");
|
|
630
|
+
for (const note of myNotes) {
|
|
631
|
+
noteNames[normalizeNote(note)] = true;
|
|
632
|
+
}
|
|
633
|
+
for (const note of theirNotes) {
|
|
634
|
+
const normalized = normalizeNote(note);
|
|
635
|
+
if (noteNames[normalized]) {
|
|
636
|
+
count += 1;
|
|
637
|
+
}
|
|
638
|
+
delete noteNames[normalized];
|
|
639
|
+
}
|
|
640
|
+
return count;
|
|
641
|
+
}
|
|
642
|
+
toString() {
|
|
643
|
+
let name = this.chordShapeName();
|
|
644
|
+
if (!name) {
|
|
645
|
+
console.warn("don't know name of chord", this.root, this.steps, this.getRange(5, 3));
|
|
646
|
+
name = "";
|
|
647
|
+
}
|
|
648
|
+
if (name == "M") {
|
|
649
|
+
name = "";
|
|
650
|
+
}
|
|
651
|
+
return `${this.root}${name}`;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
Chord.SHAPES = {
|
|
655
|
+
"M": [4, 3],
|
|
656
|
+
"m": [3, 4],
|
|
657
|
+
"dim": [3, 3], // diminished
|
|
658
|
+
"dimM7": [3, 3, 5],
|
|
659
|
+
"dim7": [3, 3, 3],
|
|
660
|
+
"aug": [4, 4],
|
|
661
|
+
"augM7": [4, 4, 3],
|
|
662
|
+
"M6": [4, 3, 2],
|
|
663
|
+
"m6": [3, 4, 2],
|
|
664
|
+
"M7": [4, 3, 4],
|
|
665
|
+
"7": [4, 3, 3],
|
|
666
|
+
"m7": [3, 4, 3],
|
|
667
|
+
"m7b5": [3, 3, 4],
|
|
668
|
+
"mM7": [3, 4, 4],
|
|
669
|
+
// exotic
|
|
670
|
+
"Q": [5, 5], // quartal
|
|
671
|
+
"Qb4": [4, 5],
|
|
672
|
+
};
|
|
673
|
+
export class Staff {
|
|
674
|
+
static forName(name) {
|
|
675
|
+
if (!this.cache) {
|
|
676
|
+
this.cache = Object.fromEntries(this.allStaves().map(s => [s.name, s]));
|
|
677
|
+
}
|
|
678
|
+
return this.cache[name];
|
|
679
|
+
}
|
|
680
|
+
static allStaves() {
|
|
681
|
+
return [
|
|
682
|
+
new Staff("treble", "E5", "F6", "G5"),
|
|
683
|
+
new Staff("bass", "G3", "A4", "F4")
|
|
684
|
+
// TODO: alto, middle C center
|
|
685
|
+
];
|
|
686
|
+
}
|
|
687
|
+
// upper and lower note are the notes for the lines on the top and bottom
|
|
688
|
+
constructor(name, lowerNote, upperNote, clefNote) {
|
|
689
|
+
this.name = name;
|
|
690
|
+
this.lowerNote = lowerNote;
|
|
691
|
+
this.upperNote = upperNote;
|
|
692
|
+
this.clefNote = clefNote;
|
|
693
|
+
}
|
|
694
|
+
// F, G, etc
|
|
695
|
+
clefName() {
|
|
696
|
+
const match = this.clefNote.match(/^([A-G])/);
|
|
697
|
+
if (!match) {
|
|
698
|
+
throw new Error(`Invalid clef note: ${this.clefNote}`);
|
|
699
|
+
}
|
|
700
|
+
return match[1];
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
Staff.cache = null;
|
|
704
|
+
//# sourceMappingURL=music.js.map
|