@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/tuning/index.ts
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
/**
|
|
12
12
|
* An equal-tempered tuning: a reference pitch and a number of equal divisions of
|
|
13
13
|
* the octave. With `divisions === 12` a step index is an ordinary MIDI number.
|
|
14
|
+
*
|
|
15
|
+
* @category Pitch & Intervals
|
|
14
16
|
*/
|
|
15
17
|
export type Tuning = {
|
|
16
18
|
/** Step index (MIDI number when `divisions` is 12) whose frequency is `refFreq`. */
|
|
@@ -21,7 +23,11 @@ export type Tuning = {
|
|
|
21
23
|
divisions: number;
|
|
22
24
|
};
|
|
23
25
|
|
|
24
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Standard twelve-tone equal temperament, A4 (MIDI 69) = 440 Hz.
|
|
28
|
+
*
|
|
29
|
+
* @category Pitch & Intervals
|
|
30
|
+
*/
|
|
25
31
|
export const TWELVE_TET: Tuning = { refStep: 69, refFreq: 440, divisions: 12 };
|
|
26
32
|
|
|
27
33
|
/**
|
|
@@ -31,6 +37,13 @@ export const TWELVE_TET: Tuning = { refStep: 69, refFreq: 440, divisions: 12 };
|
|
|
31
37
|
* @param refFreq Reference frequency in Hz (default 440).
|
|
32
38
|
* @param refStep Step index of the reference (default 69).
|
|
33
39
|
* @returns The tuning.
|
|
40
|
+
* @example
|
|
41
|
+
* ```ts
|
|
42
|
+
* import { edo, frequencyOf } from '@libraz/libcantus';
|
|
43
|
+
* const et19 = edo(19); // { refStep: 69, refFreq: 440, divisions: 19 }
|
|
44
|
+
* frequencyOf(70, et19); // one 19-EDO step above A4
|
|
45
|
+
* ```
|
|
46
|
+
* @category Pitch & Intervals
|
|
34
47
|
*/
|
|
35
48
|
export function edo(n: number, refFreq = 440, refStep = 69): Tuning {
|
|
36
49
|
return { refStep, refFreq, divisions: n };
|
|
@@ -42,6 +55,13 @@ export function edo(n: number, refFreq = 440, refStep = 69): Tuning {
|
|
|
42
55
|
* @param step Step index (a MIDI number under 12-EDO).
|
|
43
56
|
* @param tuning The tuning (default 12-TET).
|
|
44
57
|
* @returns The frequency in Hz.
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* import { frequencyOf } from '@libraz/libcantus';
|
|
61
|
+
* frequencyOf(69); // 440 (A4 in 12-TET)
|
|
62
|
+
* frequencyOf(60); // middle C in Hz
|
|
63
|
+
* ```
|
|
64
|
+
* @category Pitch & Intervals
|
|
45
65
|
*/
|
|
46
66
|
export function frequencyOf(step: number, tuning: Tuning = TWELVE_TET): number {
|
|
47
67
|
return tuning.refFreq * 2 ** ((step - tuning.refStep) / tuning.divisions);
|
|
@@ -54,6 +74,7 @@ export function frequencyOf(step: number, tuning: Tuning = TWELVE_TET): number {
|
|
|
54
74
|
* @param freq Frequency in Hz.
|
|
55
75
|
* @param tuning The tuning (default 12-TET).
|
|
56
76
|
* @returns The nearest step index.
|
|
77
|
+
* @category Pitch & Intervals
|
|
57
78
|
*/
|
|
58
79
|
export function nearestStep(freq: number, tuning: Tuning = TWELVE_TET): number {
|
|
59
80
|
return Math.round(tuning.refStep + tuning.divisions * Math.log2(freq / tuning.refFreq));
|
|
@@ -65,6 +86,7 @@ export function nearestStep(freq: number, tuning: Tuning = TWELVE_TET): number {
|
|
|
65
86
|
* @param a Lower/first frequency in Hz.
|
|
66
87
|
* @param b Upper/second frequency in Hz.
|
|
67
88
|
* @returns Cents from `a` to `b` (negative if `b` is lower).
|
|
89
|
+
* @category Pitch & Intervals
|
|
68
90
|
*/
|
|
69
91
|
export function centsBetweenFreq(a: number, b: number): number {
|
|
70
92
|
return 1200 * Math.log2(b / a);
|
|
@@ -76,6 +98,7 @@ export function centsBetweenFreq(a: number, b: number): number {
|
|
|
76
98
|
* @param steps Number of steps.
|
|
77
99
|
* @param tuning The tuning (default 12-TET).
|
|
78
100
|
* @returns The cents.
|
|
101
|
+
* @category Pitch & Intervals
|
|
79
102
|
*/
|
|
80
103
|
export function centsOfSteps(steps: number, tuning: Tuning = TWELVE_TET): number {
|
|
81
104
|
return (steps * 1200) / tuning.divisions;
|
|
@@ -88,6 +111,7 @@ export function centsOfSteps(steps: number, tuning: Tuning = TWELVE_TET): number
|
|
|
88
111
|
* @param numerator Ratio numerator.
|
|
89
112
|
* @param denominator Ratio denominator.
|
|
90
113
|
* @returns The interval in cents.
|
|
114
|
+
* @category Pitch & Intervals
|
|
91
115
|
*/
|
|
92
116
|
export function ratioToCents(numerator: number, denominator: number): number {
|
|
93
117
|
return 1200 * Math.log2(numerator / denominator);
|
|
@@ -96,6 +120,8 @@ export function ratioToCents(numerator: number, denominator: number): number {
|
|
|
96
120
|
/**
|
|
97
121
|
* Five-limit just-intonation ratios for the twelve interval classes above a
|
|
98
122
|
* unison, indexed by semitone class (0..12).
|
|
123
|
+
*
|
|
124
|
+
* @category Pitch & Intervals
|
|
99
125
|
*/
|
|
100
126
|
export const JUST_RATIOS: Record<number, [number, number]> = {
|
|
101
127
|
0: [1, 1],
|
|
@@ -119,6 +145,7 @@ export const JUST_RATIOS: Record<number, [number, number]> = {
|
|
|
119
145
|
*
|
|
120
146
|
* @param semitoneClass Semitone class in [0, 12].
|
|
121
147
|
* @returns The deviation in cents, or NaN if the class has no listed ratio.
|
|
148
|
+
* @category Pitch & Intervals
|
|
122
149
|
*/
|
|
123
150
|
export function justDeviationCents(semitoneClass: number): number {
|
|
124
151
|
const ratio = JUST_RATIOS[semitoneClass];
|
package/src/voicing/index.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import type { Chord } from '../chord/index.js';
|
|
2
2
|
import { chordPitchClasses, chordToneRole } from '../chord/index.js';
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
createsHiddenParallelPerfect,
|
|
5
5
|
createsParallelPerfect,
|
|
6
6
|
createsVoiceCrossing,
|
|
7
7
|
createsVoiceOverlap,
|
|
8
8
|
exceedsSpacing,
|
|
9
9
|
} from '../counterpoint/index.js';
|
|
10
10
|
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* An inclusive MIDI pitch range for a single voice.
|
|
13
|
+
*
|
|
14
|
+
* @category Voicing & Counterpoint
|
|
15
|
+
*/
|
|
12
16
|
export type VoiceRange = {
|
|
13
17
|
min: number;
|
|
14
18
|
max: number;
|
|
@@ -18,6 +22,8 @@ export type VoiceRange = {
|
|
|
18
22
|
* Default four-voice SATB ranges, ascending (index 0 = lowest):
|
|
19
23
|
* bass E2–C4 (40–60), tenor C3–G4 (48–67), alto G3–D5 (55–74),
|
|
20
24
|
* soprano C4–G5 (60–79).
|
|
25
|
+
*
|
|
26
|
+
* @category Voicing & Counterpoint
|
|
21
27
|
*/
|
|
22
28
|
export const SATB_RANGES: readonly VoiceRange[] = [
|
|
23
29
|
{ min: 40, max: 60 },
|
|
@@ -26,13 +32,25 @@ export const SATB_RANGES: readonly VoiceRange[] = [
|
|
|
26
32
|
{ min: 60, max: 79 },
|
|
27
33
|
];
|
|
28
34
|
|
|
29
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Options controlling {@link voiceChord} and {@link voiceProgression}.
|
|
37
|
+
*
|
|
38
|
+
* @category Voicing & Counterpoint
|
|
39
|
+
*/
|
|
30
40
|
export type VoicingOptions = {
|
|
31
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Number of voices to realize. Ignored when `ranges` is given.
|
|
43
|
+
*
|
|
44
|
+
* @defaultValue 4
|
|
45
|
+
*/
|
|
32
46
|
voices?: number;
|
|
33
47
|
/** Explicit per-voice ranges, ascending (index 0 = lowest). Takes precedence over `voices`. */
|
|
34
48
|
ranges?: VoiceRange[];
|
|
35
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Maximum spacing in semitones between adjacent upper voices.
|
|
51
|
+
*
|
|
52
|
+
* @defaultValue 12
|
|
53
|
+
*/
|
|
36
54
|
maxSpacing?: number;
|
|
37
55
|
};
|
|
38
56
|
|
|
@@ -51,6 +69,13 @@ const VIOLATION_PENALTY = 1000;
|
|
|
51
69
|
const MISSING_TONE_PENALTY = 500;
|
|
52
70
|
/** Score penalty per doubled tone that is neither the root nor the fifth. */
|
|
53
71
|
const POOR_DOUBLING_PENALTY = 4;
|
|
72
|
+
/**
|
|
73
|
+
* Moderate penalty for a hidden/direct perfect fifth or octave reached on the
|
|
74
|
+
* outer-voice (bass–soprano) pair. Unlike a true parallel perfect it is
|
|
75
|
+
* discouraged rather than forbidden, so the weight sits alongside voice-leading
|
|
76
|
+
* motion rather than the hard {@link VIOLATION_PENALTY}.
|
|
77
|
+
*/
|
|
78
|
+
const HIDDEN_PERFECT_PENALTY = 6;
|
|
54
79
|
/** Hard cap on candidate voicings evaluated per chord, keeping the search bounded. */
|
|
55
80
|
const MAX_CANDIDATES = 4000;
|
|
56
81
|
|
|
@@ -91,7 +116,13 @@ function resolveRanges(opts?: VoicingOptions): VoiceRange[] {
|
|
|
91
116
|
return ranges;
|
|
92
117
|
}
|
|
93
118
|
|
|
94
|
-
/**
|
|
119
|
+
/**
|
|
120
|
+
* All MIDI pitches of a pitch class inside an inclusive range, ordered from the
|
|
121
|
+
* centre of the range outward (ties break toward the lower pitch). Enumerating
|
|
122
|
+
* centre-outward keeps the candidate set balanced around the register when it
|
|
123
|
+
* is truncated at {@link MAX_CANDIDATES}, instead of skewing to the low octaves
|
|
124
|
+
* that a plain ascending scan would visit first.
|
|
125
|
+
*/
|
|
95
126
|
function pitchesForPc(pc: number, range: VoiceRange): number[] {
|
|
96
127
|
const result: number[] = [];
|
|
97
128
|
for (let midi = Math.ceil(range.min); midi <= range.max; midi += 1) {
|
|
@@ -99,7 +130,12 @@ function pitchesForPc(pc: number, range: VoiceRange): number[] {
|
|
|
99
130
|
result.push(midi);
|
|
100
131
|
}
|
|
101
132
|
}
|
|
102
|
-
|
|
133
|
+
const center = (range.min + range.max) / 2;
|
|
134
|
+
return result.sort((a, b) => {
|
|
135
|
+
const da = Math.abs(a - center);
|
|
136
|
+
const db = Math.abs(b - center);
|
|
137
|
+
return da === db ? a - b : da - db;
|
|
138
|
+
});
|
|
103
139
|
}
|
|
104
140
|
|
|
105
141
|
/**
|
|
@@ -178,9 +214,10 @@ function structuralPenalty(pitches: number[], chord: Chord): number {
|
|
|
178
214
|
|
|
179
215
|
/**
|
|
180
216
|
* Count counterpoint violations between two consecutive voicings of equal
|
|
181
|
-
* length: parallel perfects and
|
|
182
|
-
*
|
|
183
|
-
*
|
|
217
|
+
* length: parallel perfects (fifths and octaves alike, since octaves are the
|
|
218
|
+
* perfect-class-zero case of {@link createsParallelPerfect}) on every voice
|
|
219
|
+
* pair, voice crossings in the new voicing, and — on adjacent pairs — voice
|
|
220
|
+
* overlaps and over-wide spacing between upper voices.
|
|
184
221
|
*/
|
|
185
222
|
function violationCount(prev: number[], cur: number[], maxSpacing: number): number {
|
|
186
223
|
let count = 0;
|
|
@@ -201,9 +238,6 @@ function violationCount(prev: number[], cur: number[], maxSpacing: number): numb
|
|
|
201
238
|
if (createsParallelPerfect(prevUpper, curUpper, prevLower, curLower)) {
|
|
202
239
|
count += 1;
|
|
203
240
|
}
|
|
204
|
-
if (createsParallelOctave(prevUpper, curUpper, prevLower, curLower)) {
|
|
205
|
-
count += 1;
|
|
206
|
-
}
|
|
207
241
|
if (createsVoiceCrossing(curUpper, curLower)) {
|
|
208
242
|
count += 1;
|
|
209
243
|
}
|
|
@@ -222,12 +256,16 @@ function violationCount(prev: number[], cur: number[], maxSpacing: number): numb
|
|
|
222
256
|
|
|
223
257
|
/**
|
|
224
258
|
* Total voice-leading cost between two voicings: the sum of absolute semitone
|
|
225
|
-
* motion across voices
|
|
226
|
-
*
|
|
259
|
+
* motion across voices, plus a moderate {@link HIDDEN_PERFECT_PENALTY} when the
|
|
260
|
+
* outer-voice (bass–soprano) pair reaches a hidden/direct perfect fifth or
|
|
261
|
+
* octave by similar motion. The arrays must be the same length; when they
|
|
262
|
+
* differ the voicings are not comparable and the cost is `Infinity`.
|
|
227
263
|
*
|
|
228
264
|
* @param from The previous voicing, one MIDI pitch per voice.
|
|
229
265
|
* @param to The next voicing, one MIDI pitch per voice.
|
|
230
|
-
* @returns The summed absolute motion
|
|
266
|
+
* @returns The summed absolute motion (plus any hidden-perfect penalty), or
|
|
267
|
+
* `Infinity` when lengths differ.
|
|
268
|
+
* @category Voicing & Counterpoint
|
|
231
269
|
*/
|
|
232
270
|
export function voiceLeadingCost(from: number[], to: number[]): number {
|
|
233
271
|
if (from.length !== to.length) {
|
|
@@ -242,6 +280,23 @@ export function voiceLeadingCost(from: number[], to: number[]): number {
|
|
|
242
280
|
}
|
|
243
281
|
total += Math.abs(b - a);
|
|
244
282
|
}
|
|
283
|
+
// Discourage hidden/direct perfects between the outermost voices, where they
|
|
284
|
+
// are most audible. True parallels are handled (and forbidden) elsewhere.
|
|
285
|
+
if (from.length >= 2) {
|
|
286
|
+
const bassPrev = from[0];
|
|
287
|
+
const bassCur = to[0];
|
|
288
|
+
const sopPrev = from[from.length - 1];
|
|
289
|
+
const sopCur = to[to.length - 1];
|
|
290
|
+
if (
|
|
291
|
+
bassPrev !== undefined &&
|
|
292
|
+
bassCur !== undefined &&
|
|
293
|
+
sopPrev !== undefined &&
|
|
294
|
+
sopCur !== undefined &&
|
|
295
|
+
createsHiddenParallelPerfect(bassPrev, bassCur, sopPrev, sopCur)
|
|
296
|
+
) {
|
|
297
|
+
total += HIDDEN_PERFECT_PENALTY;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
245
300
|
return total;
|
|
246
301
|
}
|
|
247
302
|
|
|
@@ -257,6 +312,13 @@ export function voiceLeadingCost(from: number[], to: number[]): number {
|
|
|
257
312
|
* @param opts Voicing options; defaults to four voices in {@link SATB_RANGES}.
|
|
258
313
|
* @returns MIDI pitches, ascending, one per voice.
|
|
259
314
|
* @throws If no voicing fits the given ranges.
|
|
315
|
+
* @example
|
|
316
|
+
* ```ts
|
|
317
|
+
* import { parseChordSymbol, voiceChord } from '@libraz/libcantus';
|
|
318
|
+
* const chord = parseChordSymbol('Cmaj7');
|
|
319
|
+
* voiceChord(chord); // four ascending MIDI pitches within the SATB ranges
|
|
320
|
+
* ```
|
|
321
|
+
* @category Voicing & Counterpoint
|
|
260
322
|
*/
|
|
261
323
|
export function voiceChord(chord: Chord, opts?: VoicingOptions): number[] {
|
|
262
324
|
const ranges = resolveRanges(opts);
|
|
@@ -299,6 +361,13 @@ export function voiceChord(chord: Chord, opts?: VoicingOptions): number[] {
|
|
|
299
361
|
* @param opts Voicing options; defaults to four voices in {@link SATB_RANGES}.
|
|
300
362
|
* @returns One voicing per chord, each ascending with one MIDI pitch per voice.
|
|
301
363
|
* @throws If any chord admits no voicing within the given ranges.
|
|
364
|
+
* @example
|
|
365
|
+
* ```ts
|
|
366
|
+
* import { parseChordSymbol, voiceProgression } from '@libraz/libcantus';
|
|
367
|
+
* const chords = ['C', 'Am', 'F', 'G'].map((s) => parseChordSymbol(s));
|
|
368
|
+
* voiceProgression(chords); // one four-voice voicing per chord, smoothly led
|
|
369
|
+
* ```
|
|
370
|
+
* @category Voicing & Counterpoint
|
|
302
371
|
*/
|
|
303
372
|
export function voiceProgression(chords: Chord[], opts?: VoicingOptions): number[][] {
|
|
304
373
|
const ranges = resolveRanges(opts);
|
|
@@ -343,18 +412,36 @@ export function voiceProgression(chords: Chord[], opts?: VoicingOptions): number
|
|
|
343
412
|
* root/third/fifth for triads; the fifth and tensions are omitted.
|
|
344
413
|
* - `rootless`: the root omitted, keeping third/fifth/seventh and tensions
|
|
345
414
|
* (a typical left-hand jazz voicing).
|
|
415
|
+
*
|
|
416
|
+
* @category Voicing & Counterpoint
|
|
346
417
|
*/
|
|
347
418
|
export type VoicingStyle = 'close' | 'drop2' | 'drop3' | 'shell' | 'rootless';
|
|
348
419
|
|
|
349
|
-
/**
|
|
420
|
+
/**
|
|
421
|
+
* Options controlling {@link voiceChordStyled}.
|
|
422
|
+
*
|
|
423
|
+
* @category Voicing & Counterpoint
|
|
424
|
+
*/
|
|
350
425
|
export type StyledVoicingOptions = {
|
|
351
|
-
/**
|
|
426
|
+
/**
|
|
427
|
+
* Voicing style to build.
|
|
428
|
+
*
|
|
429
|
+
* @defaultValue 'close'
|
|
430
|
+
*/
|
|
352
431
|
style?: VoicingStyle;
|
|
353
432
|
/** Constrain the highest voice to this pitch class (0..11) when given. */
|
|
354
433
|
topNote?: number;
|
|
355
|
-
/**
|
|
434
|
+
/**
|
|
435
|
+
* Base octave for the close stack; the stack begins near `12 * octave`.
|
|
436
|
+
*
|
|
437
|
+
* @defaultValue 4
|
|
438
|
+
*/
|
|
356
439
|
octave?: number;
|
|
357
|
-
/**
|
|
440
|
+
/**
|
|
441
|
+
* Omit the root regardless of style (relevant for jazz voicings).
|
|
442
|
+
*
|
|
443
|
+
* @defaultValue false
|
|
444
|
+
*/
|
|
358
445
|
rootless?: boolean;
|
|
359
446
|
};
|
|
360
447
|
|
|
@@ -408,6 +495,13 @@ function nearestPc(target: number, pcs: number[]): number {
|
|
|
408
495
|
* @param chord The chord to voice.
|
|
409
496
|
* @param opts Styled voicing options; defaults to a close voicing at octave 4.
|
|
410
497
|
* @returns MIDI pitches, ascending, one per retained voice.
|
|
498
|
+
* @example
|
|
499
|
+
* ```ts
|
|
500
|
+
* import { parseChordSymbol, voiceChordStyled } from '@libraz/libcantus';
|
|
501
|
+
* const chord = parseChordSymbol('Dm7');
|
|
502
|
+
* voiceChordStyled(chord, { style: 'drop2' }); // ascending MIDI pitches, drop-2 voicing
|
|
503
|
+
* ```
|
|
504
|
+
* @category Voicing & Counterpoint
|
|
411
505
|
*/
|
|
412
506
|
export function voiceChordStyled(chord: Chord, opts?: StyledVoicingOptions): number[] {
|
|
413
507
|
const style = opts?.style ?? 'close';
|
|
@@ -496,12 +590,18 @@ export function voiceChordStyled(chord: Chord, opts?: StyledVoicingOptions): num
|
|
|
496
590
|
* @param opts Voicing options; when omitted, ranges follow `current`'s span.
|
|
497
591
|
* @returns The chosen voicing, ascending with one MIDI pitch per voice.
|
|
498
592
|
* @throws If no voicing fits the derived ranges.
|
|
593
|
+
* @category Voicing & Counterpoint
|
|
499
594
|
*/
|
|
500
595
|
export function nextVoicing(current: number[], chord: Chord, opts?: VoicingOptions): number[] {
|
|
501
596
|
const ranges =
|
|
502
597
|
opts?.ranges !== undefined || opts?.voices !== undefined
|
|
503
598
|
? resolveRanges(opts)
|
|
504
|
-
: current.map((pitch) =>
|
|
599
|
+
: current.map((pitch) => {
|
|
600
|
+
// Window one octave around each current pitch, clamped so extreme-low
|
|
601
|
+
// or extreme-high input can never yield MIDI outside [0, 127].
|
|
602
|
+
const centre = Math.min(127, Math.max(0, pitch));
|
|
603
|
+
return { min: Math.max(0, centre - 12), max: Math.min(127, centre + 12) };
|
|
604
|
+
});
|
|
505
605
|
const maxSpacing = opts?.maxSpacing ?? DEFAULT_MAX_SPACING;
|
|
506
606
|
const candidates = enumerateVoicings(chord, ranges, maxSpacing);
|
|
507
607
|
let best: number[] | undefined;
|