@libraz/libcantus 0.9.0 → 0.9.1
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 +10 -2
- package/dist/index.cjs +577 -225
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1533 -172
- package/dist/index.d.ts +1533 -172
- package/dist/index.js +577 -225
- package/dist/index.js.map +1 -1
- package/package.json +6 -1
- package/src/analysis/index.ts +73 -7
- package/src/arrange/index.ts +241 -79
- package/src/bass/index.ts +61 -12
- package/src/chord/index.ts +66 -3
- package/src/chordscale/index.ts +67 -9
- package/src/countermelody/index.ts +44 -14
- package/src/counterpoint/index.ts +21 -0
- package/src/detect/index.ts +79 -20
- package/src/drums/fills.ts +31 -20
- package/src/drums/index.ts +125 -7
- package/src/drums/internal.ts +7 -1
- package/src/drums/swing.ts +7 -2
- package/src/functional/index.ts +160 -51
- package/src/groove/index.ts +116 -20
- package/src/harmonize/index.ts +85 -12
- package/src/harmony/index.ts +19 -2
- package/src/index.ts +1 -0
- package/src/interval/index.ts +15 -1
- package/src/meter/index.ts +108 -5
- package/src/model/index.ts +161 -12
- package/src/motif/index.ts +87 -10
- package/src/pitch/index.ts +58 -3
- package/src/progression/index.ts +56 -8
- package/src/random/index.ts +13 -1
- package/src/reharmony/index.ts +35 -4
- package/src/rhythm/index.ts +45 -11
- package/src/safety/index.ts +58 -11
- package/src/scale/index.ts +135 -19
- package/src/spelling/index.ts +18 -0
- package/src/symbol/index.ts +84 -36
- package/src/timeline/index.ts +80 -13
- package/src/tuning/index.ts +28 -1
- package/src/voicing/index.ts +121 -21
package/src/functional/index.ts
CHANGED
|
@@ -19,10 +19,18 @@ import {
|
|
|
19
19
|
} from '../scale/index.js';
|
|
20
20
|
import type { KeyScale } from '../types.js';
|
|
21
21
|
|
|
22
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* The three broad harmonic functions of tonal music.
|
|
24
|
+
*
|
|
25
|
+
* @category Functional Harmony
|
|
26
|
+
*/
|
|
23
27
|
export type HarmonicFunction = 'tonic' | 'subdominant' | 'dominant';
|
|
24
28
|
|
|
25
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* A recognized cadence type, or null when a chord pair forms none.
|
|
31
|
+
*
|
|
32
|
+
* @category Functional Harmony
|
|
33
|
+
*/
|
|
26
34
|
export type Cadence = 'authentic' | 'plagal' | 'half' | 'deceptive' | null;
|
|
27
35
|
|
|
28
36
|
/** Roman numeral glyphs indexed by degree number - 1. */
|
|
@@ -75,7 +83,11 @@ function mod12(n: number): number {
|
|
|
75
83
|
return ((n % 12) + 12) % 12;
|
|
76
84
|
}
|
|
77
85
|
|
|
78
|
-
/**
|
|
86
|
+
/**
|
|
87
|
+
* Whether a key's scale has a minor third and no major third (a minor key).
|
|
88
|
+
*
|
|
89
|
+
* @category Functional Harmony
|
|
90
|
+
*/
|
|
79
91
|
export function isMinorKey(key: KeyScale): boolean {
|
|
80
92
|
const hasMinorThird = (key.modeMask12 >> 3) & 1;
|
|
81
93
|
const hasMajorThird = (key.modeMask12 >> 4) & 1;
|
|
@@ -108,13 +120,56 @@ function seventhQuality(base: 'maj' | 'min' | 'dim' | 'aug', halfDim: boolean):
|
|
|
108
120
|
/** Figure strings that denote a recognized figured-bass inversion. */
|
|
109
121
|
const INVERSION_FIGURES = new Set(['6', '64', '65', '43', '42', '2', '7']);
|
|
110
122
|
|
|
111
|
-
/**
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
123
|
+
/**
|
|
124
|
+
* Canonical Roman-numeral rendering (numeral case and quality suffix) for every
|
|
125
|
+
* supported chord quality. The table is the single source of truth for both
|
|
126
|
+
* directions: {@link chordToRoman} renders from it and {@link romanToChord}
|
|
127
|
+
* recognizes its exact case-sensitive suffixes, so the two stay mutual
|
|
128
|
+
* inverses by construction. Suffixes are chosen to never collide with
|
|
129
|
+
* figured-bass inversion digits (`6`, `64`, `65`, `43`, `42`, `2`) — the added
|
|
130
|
+
* sixth is `add6`, the six-nine chord is `69` — and each (case, suffix) pair is
|
|
131
|
+
* unique.
|
|
132
|
+
*/
|
|
133
|
+
const ROMAN_STYLE: Record<ChordQuality, { lower: boolean; suffix: string }> = {
|
|
134
|
+
maj: { lower: false, suffix: '' },
|
|
135
|
+
min: { lower: true, suffix: '' },
|
|
136
|
+
dim: { lower: true, suffix: 'o' },
|
|
137
|
+
aug: { lower: false, suffix: '+' },
|
|
138
|
+
maj7: { lower: false, suffix: 'maj7' },
|
|
139
|
+
min7: { lower: true, suffix: '7' },
|
|
140
|
+
dom7: { lower: false, suffix: '7' },
|
|
141
|
+
dim7: { lower: true, suffix: 'o7' },
|
|
142
|
+
m7b5: { lower: true, suffix: 'ø7' },
|
|
143
|
+
minMaj7: { lower: true, suffix: 'maj7' },
|
|
144
|
+
aug7: { lower: false, suffix: '+7' },
|
|
145
|
+
augMaj7: { lower: false, suffix: '+maj7' },
|
|
146
|
+
majb5: { lower: false, suffix: 'b5' },
|
|
147
|
+
'6': { lower: false, suffix: 'add6' },
|
|
148
|
+
min6: { lower: true, suffix: 'add6' },
|
|
149
|
+
'6/9': { lower: false, suffix: '69' },
|
|
150
|
+
sus2: { lower: false, suffix: 'sus2' },
|
|
151
|
+
sus4: { lower: false, suffix: 'sus4' },
|
|
152
|
+
add9: { lower: false, suffix: 'add9' },
|
|
153
|
+
add11: { lower: false, suffix: 'add11' },
|
|
154
|
+
maj9: { lower: false, suffix: 'maj9' },
|
|
155
|
+
min9: { lower: true, suffix: '9' },
|
|
156
|
+
dom9: { lower: false, suffix: '9' },
|
|
157
|
+
'7b9': { lower: false, suffix: '7b9' },
|
|
158
|
+
'7#9': { lower: false, suffix: '7#9' },
|
|
159
|
+
'7#11': { lower: false, suffix: '7#11' },
|
|
160
|
+
'7b13': { lower: false, suffix: '7b13' },
|
|
161
|
+
'11': { lower: false, suffix: '11' },
|
|
162
|
+
'13': { lower: false, suffix: '13' },
|
|
163
|
+
'5': { lower: false, suffix: '5' },
|
|
116
164
|
};
|
|
117
165
|
|
|
166
|
+
/** Reverse lookup of {@link ROMAN_STYLE}: `(case, exact suffix) -> quality`. */
|
|
167
|
+
const SUFFIX_QUALITY: ReadonlyMap<string, ChordQuality> = new Map(
|
|
168
|
+
(Object.entries(ROMAN_STYLE) as [ChordQuality, { lower: boolean; suffix: string }][]).map(
|
|
169
|
+
([quality, style]) => [`${style.lower ? 'l' : 'u'}:${style.suffix}`, quality],
|
|
170
|
+
),
|
|
171
|
+
);
|
|
172
|
+
|
|
118
173
|
/** Read figured-bass digits into a chord inversion and whether a seventh is implied. */
|
|
119
174
|
function parseInversion(figures: string): { inversion: number; seventh: boolean } {
|
|
120
175
|
if (figures === '65') {
|
|
@@ -157,20 +212,24 @@ function parseSimpleRoman(
|
|
|
157
212
|
const suffix = match[3] ?? '';
|
|
158
213
|
const rootPc = mod12(degreeRootPc(degreeNumber, key) + accidental);
|
|
159
214
|
|
|
215
|
+
// Canonical quality suffixes (exact match, case-sensitive on the numeral)
|
|
216
|
+
// come first so every chordToRoman rendering re-parses to the same quality:
|
|
217
|
+
// `V9` -> dom9 but `ii9` -> min9 and `Imaj9` -> maj9, and added-tone suffixes
|
|
218
|
+
// whose digits would otherwise be misread as figured bass (`Iadd6`, `I69`,
|
|
219
|
+
// `Isus2`, `Iadd11`) resolve to their qualities in root position.
|
|
220
|
+
const canonical = SUFFIX_QUALITY.get(`${isUpper ? 'u' : 'l'}:${suffix}`);
|
|
221
|
+
if (canonical !== undefined) {
|
|
222
|
+
return { rootPc, quality: canonical, inversion: 0 };
|
|
223
|
+
}
|
|
224
|
+
|
|
160
225
|
const isDim = /o|°|dim/.test(suffix);
|
|
161
226
|
const isAug = /\+|aug/.test(suffix);
|
|
162
227
|
const isHalfDim = /ø/.test(suffix);
|
|
163
228
|
const explicitMaj7 = /maj7|M7/.test(suffix);
|
|
164
229
|
const figures = suffix.replace(/maj7|M7/g, '').replace(/[^0-9]/g, '');
|
|
165
230
|
|
|
166
|
-
// Extension figures (9/11/13) denote a fixed root-position tension quality,
|
|
167
|
-
// not a figured-bass inversion, so they short-circuit the case/quality logic.
|
|
168
|
-
const extension = EXTENSION_FIGURES[figures];
|
|
169
|
-
if (extension !== undefined) {
|
|
170
|
-
return { rootPc, quality: extension, inversion: 0 };
|
|
171
|
-
}
|
|
172
231
|
// Reject figure strings that are neither a known figured-bass inversion nor a
|
|
173
|
-
//
|
|
232
|
+
// canonical quality suffix rather than silently degrading to a root triad.
|
|
174
233
|
if (figures !== '' && !INVERSION_FIGURES.has(figures)) {
|
|
175
234
|
throw new Error(`Unsupported figured-bass or extension "${figures}" in Roman numeral: ${text}`);
|
|
176
235
|
}
|
|
@@ -191,7 +250,7 @@ function parseSimpleRoman(
|
|
|
191
250
|
|
|
192
251
|
let quality: ChordQuality;
|
|
193
252
|
if (explicitMaj7) {
|
|
194
|
-
quality = base === 'min' ? 'minMaj7' : 'maj7';
|
|
253
|
+
quality = base === 'min' ? 'minMaj7' : base === 'aug' ? 'augMaj7' : 'maj7';
|
|
195
254
|
} else if (hasSeventh) {
|
|
196
255
|
quality = seventhQuality(base, isHalfDim);
|
|
197
256
|
} else if (base === 'dim') {
|
|
@@ -228,14 +287,24 @@ function chordFromParsed(parsed: {
|
|
|
228
287
|
* carry a `bassPc`.
|
|
229
288
|
*
|
|
230
289
|
* The `ø` glyph always denotes the half-diminished seventh (`iiø` == `iiø7` ==
|
|
231
|
-
* `m7b5`), since no half-diminished triad exists.
|
|
232
|
-
*
|
|
290
|
+
* `m7b5`), since no half-diminished triad exists. Extension and added-tone
|
|
291
|
+
* suffixes are the canonical case-sensitive forms emitted by
|
|
292
|
+
* {@link chordToRoman} and honor the numeral case: `V9` -> `dom9`, `ii9` ->
|
|
293
|
+
* `min9`, `Imaj9` -> `maj9`, `V11` -> `11`, `V13` -> `13`, `Iadd6`/`iadd6` ->
|
|
294
|
+
* `6`/`min6`, `I69` -> `6/9`, `Isus2`/`Isus4`, `Iadd9`/`Iadd11`, `Ib5` ->
|
|
295
|
+
* `majb5`, `I5` -> `5`, and the altered dominants `V7b9`/`V7#9`/`V7#11`/`V7b13`.
|
|
233
296
|
* A figure string that is neither a recognized inversion nor a supported
|
|
234
|
-
*
|
|
297
|
+
* quality suffix throws rather than silently degrading to a triad.
|
|
235
298
|
*
|
|
236
299
|
* @param text The Roman numeral.
|
|
237
300
|
* @param key The prevailing key.
|
|
238
301
|
* @returns The chord.
|
|
302
|
+
* @example
|
|
303
|
+
* ```ts
|
|
304
|
+
* import { romanToChord, majorKey } from '@libraz/libcantus';
|
|
305
|
+
* romanToChord('V7', majorKey(0)); // G7 in C major: { rootPc: 7, quality: 'dom7', ... }
|
|
306
|
+
* ```
|
|
307
|
+
* @category Functional Harmony
|
|
239
308
|
*/
|
|
240
309
|
export function romanToChord(text: string, key: KeyScale): Chord {
|
|
241
310
|
const trimmed = text.trim();
|
|
@@ -252,30 +321,7 @@ export function romanToChord(text: string, key: KeyScale): Chord {
|
|
|
252
321
|
|
|
253
322
|
/** Case and suffix for rendering a chord quality as a Roman numeral. */
|
|
254
323
|
function romanStyle(quality: ChordQuality): { lower: boolean; suffix: string } {
|
|
255
|
-
|
|
256
|
-
case 'min':
|
|
257
|
-
return { lower: true, suffix: '' };
|
|
258
|
-
case 'dim':
|
|
259
|
-
return { lower: true, suffix: 'o' };
|
|
260
|
-
case 'aug':
|
|
261
|
-
return { lower: false, suffix: '+' };
|
|
262
|
-
case 'maj7':
|
|
263
|
-
return { lower: false, suffix: 'maj7' };
|
|
264
|
-
case 'dom7':
|
|
265
|
-
return { lower: false, suffix: '7' };
|
|
266
|
-
case 'min7':
|
|
267
|
-
return { lower: true, suffix: '7' };
|
|
268
|
-
case 'm7b5':
|
|
269
|
-
return { lower: true, suffix: 'ø7' };
|
|
270
|
-
case 'dim7':
|
|
271
|
-
return { lower: true, suffix: 'o7' };
|
|
272
|
-
case 'minMaj7':
|
|
273
|
-
return { lower: true, suffix: 'maj7' };
|
|
274
|
-
case 'maj':
|
|
275
|
-
return { lower: false, suffix: '' };
|
|
276
|
-
default:
|
|
277
|
-
return { lower: false, suffix: quality };
|
|
278
|
-
}
|
|
324
|
+
return ROMAN_STYLE[quality];
|
|
279
325
|
}
|
|
280
326
|
|
|
281
327
|
/** Choose the degree number and accidental to spell a root as a Roman numeral. */
|
|
@@ -323,6 +369,9 @@ function baseMarker(quality: ChordQuality): string {
|
|
|
323
369
|
if (quality === 'aug' || quality === 'aug7') {
|
|
324
370
|
return '+';
|
|
325
371
|
}
|
|
372
|
+
if (quality === 'augMaj7') {
|
|
373
|
+
return '+maj7';
|
|
374
|
+
}
|
|
326
375
|
if (quality === 'maj7' || quality === 'minMaj7') {
|
|
327
376
|
return 'maj7';
|
|
328
377
|
}
|
|
@@ -332,6 +381,26 @@ function baseMarker(quality: ChordQuality): string {
|
|
|
332
381
|
const TRIAD_FIGURES: Record<number, string> = { 1: '6', 2: '64' };
|
|
333
382
|
const SEVENTH_FIGURES: Record<number, string> = { 1: '65', 2: '43', 3: '42' };
|
|
334
383
|
|
|
384
|
+
/** Qualities whose inversions render as lossless triad figures (6, 64). */
|
|
385
|
+
const TRIAD_FIGURE_QUALITIES: ReadonlySet<ChordQuality> = new Set(['maj', 'min', 'dim', 'aug']);
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Qualities carrying a chordal seventh, whose inversions render as seventh
|
|
389
|
+
* figures (65, 43, 42). Membership is decided by the quality itself — a 7th
|
|
390
|
+
* above the root (10 or 11 semitones, or the diminished 7th of `dim7`) — never
|
|
391
|
+
* by interval count, which would turn added-tone chords into false sevenths.
|
|
392
|
+
*/
|
|
393
|
+
const SEVENTH_FIGURE_QUALITIES: ReadonlySet<ChordQuality> = new Set([
|
|
394
|
+
'maj7',
|
|
395
|
+
'min7',
|
|
396
|
+
'dom7',
|
|
397
|
+
'dim7',
|
|
398
|
+
'm7b5',
|
|
399
|
+
'minMaj7',
|
|
400
|
+
'aug7',
|
|
401
|
+
'augMaj7',
|
|
402
|
+
]);
|
|
403
|
+
|
|
335
404
|
/**
|
|
336
405
|
* Render a chord as a Roman numeral relative to a key.
|
|
337
406
|
*
|
|
@@ -339,11 +408,20 @@ const SEVENTH_FIGURES: Record<number, string> = { 1: '65', 2: '43', 3: '42' };
|
|
|
339
408
|
* correct in both major and minor keys (and any custom scale). The quality
|
|
340
409
|
* selects the case and suffix; chromatic roots receive a flat/sharp spelling by
|
|
341
410
|
* convention. When the chord carries a `bassPc` on a chord tone, a figured-bass
|
|
342
|
-
* inversion (`6`, `64`, `65`, `43`, `42`) is emitted
|
|
411
|
+
* inversion (`6`, `64`, `65`, `43`, `42`) is emitted for plain triads and true
|
|
412
|
+
* seventh chords; added-tone and extended qualities have no lossless figure and
|
|
413
|
+
* render in root position instead (the bass is dropped, pitch classes are
|
|
414
|
+
* preserved).
|
|
343
415
|
*
|
|
344
416
|
* @param chord The chord to name.
|
|
345
417
|
* @param key The prevailing key.
|
|
346
418
|
* @returns The Roman numeral string.
|
|
419
|
+
* @example
|
|
420
|
+
* ```ts
|
|
421
|
+
* import { chordToRoman, makeChord, majorKey } from '@libraz/libcantus';
|
|
422
|
+
* chordToRoman(makeChord(7, 'dom7'), majorKey(0)); // => 'V7' (G7 in C major)
|
|
423
|
+
* ```
|
|
424
|
+
* @category Functional Harmony
|
|
347
425
|
*/
|
|
348
426
|
export function chordToRoman(chord: Chord, key: KeyScale): string {
|
|
349
427
|
const { degreeNumber, accidental } = romanSpelling(chord.rootPc, key);
|
|
@@ -359,14 +437,18 @@ export function chordToRoman(chord: Chord, key: KeyScale): string {
|
|
|
359
437
|
}
|
|
360
438
|
}
|
|
361
439
|
if (inversion > 0) {
|
|
362
|
-
const
|
|
363
|
-
|
|
440
|
+
const figure = SEVENTH_FIGURE_QUALITIES.has(chord.quality)
|
|
441
|
+
? SEVENTH_FIGURES[inversion]
|
|
442
|
+
: TRIAD_FIGURE_QUALITIES.has(chord.quality)
|
|
443
|
+
? TRIAD_FIGURES[inversion]
|
|
444
|
+
: undefined;
|
|
364
445
|
if (figure !== undefined) {
|
|
365
446
|
return `${accidental}${cased}${baseMarker(chord.quality)}${figure}`;
|
|
366
447
|
}
|
|
367
|
-
//
|
|
368
|
-
//
|
|
369
|
-
// rendering with the quality suffix rather than
|
|
448
|
+
// No lossless figured-bass symbol exists — the quality is an added-tone or
|
|
449
|
+
// extended chord, or the bass falls on a tension beyond the seventh — so
|
|
450
|
+
// fall back to root-position rendering with the quality suffix rather than
|
|
451
|
+
// emitting a figure that would re-parse as a different chord.
|
|
370
452
|
}
|
|
371
453
|
return `${accidental}${cased}${suffix}`;
|
|
372
454
|
}
|
|
@@ -379,15 +461,24 @@ export function chordToRoman(chord: Chord, key: KeyScale): string {
|
|
|
379
461
|
* @param chord The chord.
|
|
380
462
|
* @param key The prevailing key.
|
|
381
463
|
* @returns The harmonic function.
|
|
464
|
+
* @category Functional Harmony
|
|
382
465
|
*/
|
|
383
466
|
export function functionOf(chord: Chord, key: KeyScale): HarmonicFunction {
|
|
384
467
|
return FUNCTION_BY_OFFSET[mod12(chord.rootPc - key.rootPc)] ?? 'tonic';
|
|
385
468
|
}
|
|
386
469
|
|
|
387
|
-
/**
|
|
470
|
+
/**
|
|
471
|
+
* The origin of a recognized non-diatonic chord, or null when none applies.
|
|
472
|
+
*
|
|
473
|
+
* @category Functional Harmony
|
|
474
|
+
*/
|
|
388
475
|
export type BorrowedSource = 'parallel-minor' | 'parallel-major' | 'neapolitan' | null;
|
|
389
476
|
|
|
390
|
-
/**
|
|
477
|
+
/**
|
|
478
|
+
* The result of {@link analyzeChord}: function, borrowing, and Roman numeral.
|
|
479
|
+
*
|
|
480
|
+
* @category Functional Harmony
|
|
481
|
+
*/
|
|
391
482
|
export type ChordAnalysis = {
|
|
392
483
|
function: HarmonicFunction;
|
|
393
484
|
borrowed: boolean;
|
|
@@ -406,6 +497,7 @@ export type ChordAnalysis = {
|
|
|
406
497
|
* @param chord The chord to test.
|
|
407
498
|
* @param key The prevailing key.
|
|
408
499
|
* @returns True if all chord pitch classes are scale tones.
|
|
500
|
+
* @category Functional Harmony
|
|
409
501
|
*/
|
|
410
502
|
export function isDiatonic(chord: Chord, key: KeyScale): boolean {
|
|
411
503
|
return chordPitchClasses(chord).every((pc) => isScaleTone(pc, key));
|
|
@@ -419,6 +511,7 @@ export function isDiatonic(chord: Chord, key: KeyScale): boolean {
|
|
|
419
511
|
*
|
|
420
512
|
* @param key The key to mirror.
|
|
421
513
|
* @returns The parallel major or natural-minor key on the same tonic.
|
|
514
|
+
* @category Functional Harmony
|
|
422
515
|
*/
|
|
423
516
|
export function parallelKey(key: KeyScale): KeyScale {
|
|
424
517
|
return {
|
|
@@ -458,6 +551,7 @@ function isNeapolitan(chord: Chord, key: KeyScale): boolean {
|
|
|
458
551
|
* @param chord The chord to test.
|
|
459
552
|
* @param key The prevailing key.
|
|
460
553
|
* @returns True if the chord is borrowed from the parallel mode.
|
|
554
|
+
* @category Functional Harmony
|
|
461
555
|
*/
|
|
462
556
|
export function isBorrowedChord(chord: Chord, key: KeyScale): boolean {
|
|
463
557
|
if (isDiatonic(chord, key)) {
|
|
@@ -480,6 +574,7 @@ export function isBorrowedChord(chord: Chord, key: KeyScale): boolean {
|
|
|
480
574
|
* @param chord The chord to classify.
|
|
481
575
|
* @param key The prevailing key.
|
|
482
576
|
* @returns The borrowing source, or null.
|
|
577
|
+
* @category Functional Harmony
|
|
483
578
|
*/
|
|
484
579
|
export function borrowedSource(chord: Chord, key: KeyScale): BorrowedSource {
|
|
485
580
|
if (isDiatonic(chord, key)) {
|
|
@@ -537,6 +632,13 @@ function qualityAwareFunction(chord: Chord, key: KeyScale): HarmonicFunction {
|
|
|
537
632
|
* @param chord The chord to analyze.
|
|
538
633
|
* @param key The prevailing key.
|
|
539
634
|
* @returns The chord analysis.
|
|
635
|
+
* @example
|
|
636
|
+
* ```ts
|
|
637
|
+
* import { analyzeChord, makeChord, majorKey } from '@libraz/libcantus';
|
|
638
|
+
* analyzeChord(makeChord(7, 'dom7'), majorKey(0));
|
|
639
|
+
* // { function: 'dominant', borrowed: false, source: null, roman: 'V7' }
|
|
640
|
+
* ```
|
|
641
|
+
* @category Functional Harmony
|
|
540
642
|
*/
|
|
541
643
|
export function analyzeChord(chord: Chord, key: KeyScale): ChordAnalysis {
|
|
542
644
|
const source = borrowedSource(chord, key);
|
|
@@ -563,6 +665,12 @@ export function analyzeChord(chord: Chord, key: KeyScale): ChordAnalysis {
|
|
|
563
665
|
* @param to The final chord.
|
|
564
666
|
* @param key The prevailing key.
|
|
565
667
|
* @returns The cadence type, or null.
|
|
668
|
+
* @example
|
|
669
|
+
* ```ts
|
|
670
|
+
* import { detectCadence, makeChord, majorKey } from '@libraz/libcantus';
|
|
671
|
+
* detectCadence(makeChord(7, 'maj'), makeChord(0, 'maj'), majorKey(0)); // => 'authentic'
|
|
672
|
+
* ```
|
|
673
|
+
* @category Functional Harmony
|
|
566
674
|
*/
|
|
567
675
|
export function detectCadence(from: Chord, to: Chord, key: KeyScale): Cadence {
|
|
568
676
|
const tonic = mod12(key.rootPc);
|
|
@@ -595,6 +703,7 @@ export function detectCadence(from: Chord, to: Chord, key: KeyScale): Cadence {
|
|
|
595
703
|
* @param targetDegree 0-based scale degree to tonicize.
|
|
596
704
|
* @param key The prevailing key.
|
|
597
705
|
* @returns A dominant-seventh chord a fifth above the target's root.
|
|
706
|
+
* @category Functional Harmony
|
|
598
707
|
*/
|
|
599
708
|
export function secondaryDominant(targetDegree: number, key: KeyScale): Chord {
|
|
600
709
|
const targetRoot = degreeRootPc(targetDegree + 1, key);
|
package/src/groove/index.ts
CHANGED
|
@@ -8,23 +8,56 @@
|
|
|
8
8
|
* seeded PRNG from the `random` module so results are fully reproducible.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
beatsPerBar,
|
|
13
|
+
formatTimeSignature,
|
|
14
|
+
metricWeight,
|
|
15
|
+
type TimeSignature,
|
|
16
|
+
} from '../meter/index.js';
|
|
12
17
|
import { createRng } from '../random/index.js';
|
|
13
18
|
import type { NoteEvent } from '../types.js';
|
|
14
19
|
|
|
15
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Options controlling {@link humanize}.
|
|
22
|
+
*
|
|
23
|
+
* @category Rhythm & Meter
|
|
24
|
+
*/
|
|
16
25
|
export type HumanizeOptions = {
|
|
17
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Time signature used to derive metric accents.
|
|
28
|
+
*
|
|
29
|
+
* @defaultValue 4/4
|
|
30
|
+
*/
|
|
18
31
|
ts?: TimeSignature;
|
|
19
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Maximum timing jitter in quarter-note beats, applied as ±this value.
|
|
34
|
+
*
|
|
35
|
+
* @defaultValue 0.02
|
|
36
|
+
*/
|
|
20
37
|
timing?: number;
|
|
21
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Maximum velocity jitter (MIDI units), applied as ±this value.
|
|
40
|
+
*
|
|
41
|
+
* @defaultValue 8
|
|
42
|
+
*/
|
|
22
43
|
velocity?: number;
|
|
23
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* How much louder strong beats get relative to weak ones, in MIDI velocity units.
|
|
46
|
+
*
|
|
47
|
+
* @defaultValue 12
|
|
48
|
+
*/
|
|
24
49
|
accent?: number;
|
|
25
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Velocity assumed for events with no `velocity` of their own.
|
|
52
|
+
*
|
|
53
|
+
* @defaultValue 80
|
|
54
|
+
*/
|
|
26
55
|
baseVelocity?: number;
|
|
27
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Seed for the deterministic PRNG.
|
|
58
|
+
*
|
|
59
|
+
* @defaultValue 0
|
|
60
|
+
*/
|
|
28
61
|
seed?: number;
|
|
29
62
|
};
|
|
30
63
|
|
|
@@ -56,6 +89,13 @@ const MAX_VELOCITY = 127;
|
|
|
56
89
|
* @param events The events to humanize.
|
|
57
90
|
* @param opts Humanization options.
|
|
58
91
|
* @returns Humanized copies of `events`, in input order.
|
|
92
|
+
* @example
|
|
93
|
+
* ```ts
|
|
94
|
+
* import { humanize } from '@libraz/libcantus';
|
|
95
|
+
* const events = [{ pitch: 60, startBeat: 0, durationBeat: 1, velocity: 80 }];
|
|
96
|
+
* humanize(events, { seed: 1, timing: 0.03 }); // copies with jittered timing and accented velocity
|
|
97
|
+
* ```
|
|
98
|
+
* @category Rhythm & Meter
|
|
59
99
|
*/
|
|
60
100
|
export function humanize(events: NoteEvent[], opts: HumanizeOptions = {}): NoteEvent[] {
|
|
61
101
|
const ts = opts.ts ?? DEFAULT_TS;
|
|
@@ -84,15 +124,27 @@ export function humanize(events: NoteEvent[], opts: HumanizeOptions = {}): NoteE
|
|
|
84
124
|
});
|
|
85
125
|
}
|
|
86
126
|
|
|
87
|
-
/**
|
|
127
|
+
/**
|
|
128
|
+
* The recorded feel at a single grid slot: an average timing deviation and velocity.
|
|
129
|
+
*
|
|
130
|
+
* @category Rhythm & Meter
|
|
131
|
+
*/
|
|
88
132
|
export type GrooveSlot = {
|
|
89
133
|
/** Average `actualStartBeat - quantizedBeat` (in quarter-note beats) for events landing on this slot. */
|
|
90
134
|
timingOffset: number;
|
|
91
|
-
/**
|
|
92
|
-
|
|
135
|
+
/**
|
|
136
|
+
* Average velocity of events landing on this slot, or `null` if no event
|
|
137
|
+
* with a velocity did. `null` (not 0) is the "unrecorded" sentinel so that a
|
|
138
|
+
* genuine velocity of 0 is preserved and applied like any other value.
|
|
139
|
+
*/
|
|
140
|
+
velocity: number | null;
|
|
93
141
|
};
|
|
94
142
|
|
|
95
|
-
/**
|
|
143
|
+
/**
|
|
144
|
+
* A per-bar grid of timing and velocity deviations captured from a performance.
|
|
145
|
+
*
|
|
146
|
+
* @category Rhythm & Meter
|
|
147
|
+
*/
|
|
96
148
|
export type GrooveTemplate = {
|
|
97
149
|
/** Grid resolution: equal grid steps per quarter-note beat. */
|
|
98
150
|
subdivision: number;
|
|
@@ -100,6 +152,13 @@ export type GrooveTemplate = {
|
|
|
100
152
|
slotsPerBar: number;
|
|
101
153
|
/** One entry per grid slot, in slot-index order (slot 0 is the downbeat). */
|
|
102
154
|
slots: GrooveSlot[];
|
|
155
|
+
/**
|
|
156
|
+
* The time signature the template was extracted under. {@link applyGrooveTemplate}
|
|
157
|
+
* requires the apply-time meter to match this so the per-bar grid lines up
|
|
158
|
+
* instead of silently drifting. Optional for backward compatibility with
|
|
159
|
+
* templates constructed without it (in which case no check is performed).
|
|
160
|
+
*/
|
|
161
|
+
ts?: TimeSignature;
|
|
103
162
|
};
|
|
104
163
|
|
|
105
164
|
/**
|
|
@@ -137,13 +196,16 @@ function quantizeToGrid(
|
|
|
137
196
|
/**
|
|
138
197
|
* Extract a groove template from a set of note events: for each grid slot,
|
|
139
198
|
* the average timing deviation from the quantized grid and the average
|
|
140
|
-
* velocity of events landing on it. Slots with no
|
|
141
|
-
* `
|
|
199
|
+
* velocity of events landing on it. Slots with no velocity-carrying event
|
|
200
|
+
* record `velocity: null` (the "unrecorded" sentinel); empty slots default to
|
|
201
|
+
* `{ timingOffset: 0, velocity: null }`. The time signature is stored on the
|
|
202
|
+
* template so {@link applyGrooveTemplate} can verify the meter matches.
|
|
142
203
|
*
|
|
143
204
|
* @param events The (typically "groovy", human-played) events to analyze.
|
|
144
205
|
* @param ts The time signature, used to compute the bar length.
|
|
145
206
|
* @param subdivision Grid resolution: equal grid steps per quarter-note beat.
|
|
146
207
|
* @returns The extracted groove template.
|
|
208
|
+
* @category Rhythm & Meter
|
|
147
209
|
*/
|
|
148
210
|
export function extractGrooveTemplate(
|
|
149
211
|
events: NoteEvent[],
|
|
@@ -178,36 +240,67 @@ export function extractGrooveTemplate(
|
|
|
178
240
|
for (let i = 0; i < slotsPerBar; i += 1) {
|
|
179
241
|
const count = counts[i] ?? 0;
|
|
180
242
|
if (count === 0) {
|
|
181
|
-
slots.push({ timingOffset: 0, velocity:
|
|
243
|
+
slots.push({ timingOffset: 0, velocity: null });
|
|
182
244
|
continue;
|
|
183
245
|
}
|
|
184
246
|
const velocityCount = velocityCounts[i] ?? 0;
|
|
185
247
|
slots.push({
|
|
186
248
|
timingOffset: (offsetSums[i] ?? 0) / count,
|
|
187
|
-
velocity: velocityCount > 0 ? (velocitySums[i] ?? 0) / velocityCount :
|
|
249
|
+
velocity: velocityCount > 0 ? (velocitySums[i] ?? 0) / velocityCount : null,
|
|
188
250
|
});
|
|
189
251
|
}
|
|
190
252
|
|
|
191
|
-
return { subdivision, slotsPerBar, slots };
|
|
253
|
+
return { subdivision, slotsPerBar, slots, ts };
|
|
192
254
|
}
|
|
193
255
|
|
|
194
256
|
/**
|
|
195
257
|
* Apply a groove template to a set of (typically quantized) note events: each
|
|
196
258
|
* event is snapped to its grid slot and then offset by that slot's recorded
|
|
197
259
|
* timing deviation, pushing stiff timing toward the template's feel.
|
|
198
|
-
* Velocity is set to the slot's recorded average when it recorded
|
|
199
|
-
*
|
|
260
|
+
* Velocity is set to the slot's recorded average when it recorded one (a
|
|
261
|
+
* non-`null` velocity, including a genuine 0); otherwise the event's own
|
|
262
|
+
* velocity is left untouched.
|
|
263
|
+
*
|
|
264
|
+
* If the template carries a time signature (see {@link GrooveTemplate.ts}) it
|
|
265
|
+
* must match `ts`, since a differing bar length would misalign the per-bar
|
|
266
|
+
* grid and drift the feel; a mismatch throws rather than corrupting timing
|
|
267
|
+
* silently.
|
|
200
268
|
*
|
|
201
269
|
* @param events The events to reshape.
|
|
202
270
|
* @param template The groove template, from {@link extractGrooveTemplate}.
|
|
203
271
|
* @param ts The time signature, used to compute the bar length.
|
|
204
272
|
* @returns Reshaped copies of `events`, in input order.
|
|
273
|
+
* @throws If `template.ts` is set and does not match `ts`.
|
|
274
|
+
* @example
|
|
275
|
+
* ```ts
|
|
276
|
+
* import { extractGrooveTemplate, applyGrooveTemplate, parseTimeSignature } from '@libraz/libcantus';
|
|
277
|
+
* const ts = parseTimeSignature('4/4');
|
|
278
|
+
* // Learn the feel from a loosely played performance...
|
|
279
|
+
* const groovy = [
|
|
280
|
+
* { pitch: 36, startBeat: 0.02, durationBeat: 1, velocity: 100 },
|
|
281
|
+
* { pitch: 38, startBeat: 1.06, durationBeat: 1, velocity: 70 },
|
|
282
|
+
* ];
|
|
283
|
+
* const template = extractGrooveTemplate(groovy, ts, 4);
|
|
284
|
+
* // ...then impose it on stiff, quantized events.
|
|
285
|
+
* const quantized = [{ pitch: 36, startBeat: 0, durationBeat: 1, velocity: 80 }];
|
|
286
|
+
* applyGrooveTemplate(quantized, template, ts); // events nudged toward the captured feel
|
|
287
|
+
* ```
|
|
288
|
+
* @category Rhythm & Meter
|
|
205
289
|
*/
|
|
206
290
|
export function applyGrooveTemplate(
|
|
207
291
|
events: NoteEvent[],
|
|
208
292
|
template: GrooveTemplate,
|
|
209
293
|
ts: TimeSignature,
|
|
210
294
|
): NoteEvent[] {
|
|
295
|
+
if (
|
|
296
|
+
template.ts &&
|
|
297
|
+
(template.ts.numerator !== ts.numerator || template.ts.denominator !== ts.denominator)
|
|
298
|
+
) {
|
|
299
|
+
throw new Error(
|
|
300
|
+
`Groove template meter ${formatTimeSignature(template.ts)} does not match ` +
|
|
301
|
+
`apply-time meter ${formatTimeSignature(ts)}`,
|
|
302
|
+
);
|
|
303
|
+
}
|
|
211
304
|
const barBeats = beatsPerBar(ts);
|
|
212
305
|
return events.map((event) => {
|
|
213
306
|
const { quantizedBeat, slotIndex } = quantizeToGrid(
|
|
@@ -218,7 +311,10 @@ export function applyGrooveTemplate(
|
|
|
218
311
|
);
|
|
219
312
|
const slot = template.slots[slotIndex];
|
|
220
313
|
const startBeat = quantizedBeat + (slot?.timingOffset ?? 0);
|
|
221
|
-
const velocity =
|
|
314
|
+
const velocity =
|
|
315
|
+
slot && slot.velocity !== null && slot.velocity !== undefined
|
|
316
|
+
? Math.round(slot.velocity)
|
|
317
|
+
: event.velocity;
|
|
222
318
|
|
|
223
319
|
return {
|
|
224
320
|
pitch: event.pitch,
|