@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/bass/index.ts
CHANGED
|
@@ -18,29 +18,57 @@ import { createRng } from '../random/index.js';
|
|
|
18
18
|
import { nearestScaleTone } from '../scale/index.js';
|
|
19
19
|
import type { KeyScale, NoteEvent } from '../types.js';
|
|
20
20
|
|
|
21
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* A chord sounding over a half-open beat span `[startBeat, endBeat)`.
|
|
23
|
+
*
|
|
24
|
+
* @category Composition
|
|
25
|
+
*/
|
|
22
26
|
export type BassSegment = {
|
|
23
27
|
startBeat: number;
|
|
24
28
|
endBeat: number;
|
|
25
29
|
chord: Chord;
|
|
26
30
|
};
|
|
27
31
|
|
|
28
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* The bass-line idiom to generate.
|
|
34
|
+
*
|
|
35
|
+
* @category Composition
|
|
36
|
+
*/
|
|
29
37
|
export type BassStyle = 'root' | 'rootFifth' | 'pop' | 'walking' | 'arpeggio';
|
|
30
38
|
|
|
31
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Options controlling {@link generateBassLine}.
|
|
41
|
+
*
|
|
42
|
+
* @category Composition
|
|
43
|
+
*/
|
|
32
44
|
export type BassLineOptions = {
|
|
33
45
|
/** Chord placement to follow; need not be pre-sorted. */
|
|
34
46
|
segments: BassSegment[];
|
|
35
47
|
/** Key/scale context, used for diatonic approach tones in `walking`. */
|
|
36
48
|
key: KeyScale;
|
|
37
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Time signature; used for metric accents.
|
|
51
|
+
*
|
|
52
|
+
* @defaultValue `{ numerator: 4, denominator: 4 }`
|
|
53
|
+
*/
|
|
38
54
|
ts?: TimeSignature;
|
|
39
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* Bass-line idiom.
|
|
57
|
+
*
|
|
58
|
+
* @defaultValue `'root'`
|
|
59
|
+
*/
|
|
40
60
|
style?: BassStyle;
|
|
41
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* Target register as a base MIDI octave; roots land around `octave*12+12`.
|
|
63
|
+
*
|
|
64
|
+
* @defaultValue 2
|
|
65
|
+
*/
|
|
42
66
|
octave?: number;
|
|
43
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* Seed for the deterministic PRNG.
|
|
69
|
+
*
|
|
70
|
+
* @defaultValue 0
|
|
71
|
+
*/
|
|
44
72
|
seed?: number;
|
|
45
73
|
};
|
|
46
74
|
|
|
@@ -98,10 +126,18 @@ function chordTonePcs(chord: Chord): number[] {
|
|
|
98
126
|
return pcs;
|
|
99
127
|
}
|
|
100
128
|
|
|
101
|
-
/**
|
|
129
|
+
/**
|
|
130
|
+
* The chord's fifth pitch class.
|
|
131
|
+
*
|
|
132
|
+
* Uses the perfect fifth when present, otherwise the chord's actual altered
|
|
133
|
+
* fifth (diminished = 6, augmented = 8 semitones above the root), so dim/aug/
|
|
134
|
+
* m7b5 chords sound their real fifth rather than a repeated root. Falls back to
|
|
135
|
+
* the root only when the chord has no fifth degree at all.
|
|
136
|
+
*/
|
|
102
137
|
function fifthPcOf(chord: Chord): number {
|
|
103
|
-
const
|
|
104
|
-
|
|
138
|
+
const fifths = new Set(chord.intervals.map((i) => pitchClass(i)));
|
|
139
|
+
const chosen = fifths.has(7) ? 7 : fifths.has(6) ? 6 : fifths.has(8) ? 8 : undefined;
|
|
140
|
+
return chosen === undefined ? pitchClass(chord.rootPc) : pitchClass(chord.rootPc + chosen);
|
|
105
141
|
}
|
|
106
142
|
|
|
107
143
|
/** The sounding bass pitch class of a chord: its slash bass, else its root. */
|
|
@@ -166,9 +202,10 @@ function buildPop(ctx: BuildContext, seg: BassSegment): void {
|
|
|
166
202
|
emitted = true;
|
|
167
203
|
} else if (ctx.rng.prob(PICKUP_PROB)) {
|
|
168
204
|
if (ctx.rng.prob(0.5)) {
|
|
169
|
-
// Octave pickup: the root
|
|
205
|
+
// Octave pickup: the root dropped toward the bottom of the register,
|
|
206
|
+
// clamped so it never falls below the declared band.
|
|
170
207
|
const base = placePc(rootPc, ctx.prevMidi, ctx.low);
|
|
171
|
-
emit(ctx, pos, rootPc, base - 12);
|
|
208
|
+
emit(ctx, pos, rootPc, Math.max(ctx.low, base - 12));
|
|
172
209
|
} else {
|
|
173
210
|
ctx.prevMidi = emit(ctx, pos, fifthPcOf(seg.chord));
|
|
174
211
|
}
|
|
@@ -231,6 +268,18 @@ function buildWalking(ctx: BuildContext, seg: BassSegment, next: BassSegment | u
|
|
|
231
268
|
*
|
|
232
269
|
* @param opts Segments, key, and generation options.
|
|
233
270
|
* @returns Bass notes sorted by onset, non-overlapping.
|
|
271
|
+
*
|
|
272
|
+
* @example
|
|
273
|
+
* ```ts
|
|
274
|
+
* import { generateBassLine, makeChord, majorKey } from '@libraz/libcantus';
|
|
275
|
+
* const segments = [
|
|
276
|
+
* { startBeat: 0, endBeat: 4, chord: makeChord(0, 'maj') },
|
|
277
|
+
* { startBeat: 4, endBeat: 8, chord: makeChord(7, 'maj') },
|
|
278
|
+
* ];
|
|
279
|
+
* const notes = generateBassLine({ segments, key: majorKey(0), style: 'walking' });
|
|
280
|
+
* ```
|
|
281
|
+
*
|
|
282
|
+
* @category Composition
|
|
234
283
|
*/
|
|
235
284
|
export function generateBassLine(opts: BassLineOptions): NoteEvent[] {
|
|
236
285
|
const segments = [...opts.segments].sort((a, b) => a.startBeat - b.startBeat);
|
package/src/chord/index.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { scaleTonesInDegreeOrder } from '../scale/index.js';
|
|
2
2
|
import type { KeyScale } from '../types.js';
|
|
3
3
|
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Chord quality identifiers understood by the chord builder.
|
|
6
|
+
*
|
|
7
|
+
* @category Chords
|
|
8
|
+
*/
|
|
5
9
|
export type ChordQuality =
|
|
6
10
|
| 'maj'
|
|
7
11
|
| 'min'
|
|
@@ -34,12 +38,36 @@ export type ChordQuality =
|
|
|
34
38
|
| '13'
|
|
35
39
|
| '5';
|
|
36
40
|
|
|
37
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* A bare spelled pitch used as an enharmonic hint: a diatonic letter
|
|
43
|
+
* (0..6 = C..B) plus a chromatic alteration (-2 double-flat .. +2 double-sharp).
|
|
44
|
+
* Mirrors the letter/alter half of the pitch module's `Note` without an octave.
|
|
45
|
+
*
|
|
46
|
+
* @category Chords
|
|
47
|
+
*/
|
|
48
|
+
export type PitchSpelling = {
|
|
49
|
+
letter: number;
|
|
50
|
+
alter: number;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A chord expressed as a root pitch class plus semitone offsets.
|
|
55
|
+
*
|
|
56
|
+
* `rootSpelling`/`bassSpelling` are optional enharmonic hints recorded by
|
|
57
|
+
* parsers (e.g. `parseChordSymbol('Bb7')`) so formatters can reproduce the
|
|
58
|
+
* original spelling instead of defaulting to sharps. Consumers may ignore
|
|
59
|
+
* them; a hint is only trusted when its pitch class still matches the
|
|
60
|
+
* corresponding `rootPc`/`bassPc`.
|
|
61
|
+
*
|
|
62
|
+
* @category Chords
|
|
63
|
+
*/
|
|
38
64
|
export type Chord = {
|
|
39
65
|
rootPc: number;
|
|
40
66
|
quality: ChordQuality;
|
|
41
67
|
intervals: number[];
|
|
42
68
|
bassPc?: number;
|
|
69
|
+
rootSpelling?: PitchSpelling;
|
|
70
|
+
bassSpelling?: PitchSpelling;
|
|
43
71
|
};
|
|
44
72
|
|
|
45
73
|
/** Semitone offsets from the root for each supported chord quality. */
|
|
@@ -81,7 +109,11 @@ function pitchClass(value: number): number {
|
|
|
81
109
|
return ((Math.trunc(value) % 12) + 12) % 12;
|
|
82
110
|
}
|
|
83
111
|
|
|
84
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* All chord qualities the builder understands, longest templates last.
|
|
114
|
+
*
|
|
115
|
+
* @category Chords
|
|
116
|
+
*/
|
|
85
117
|
export function chordQualities(): ChordQuality[] {
|
|
86
118
|
return Object.keys(QUALITY_INTERVALS) as ChordQuality[];
|
|
87
119
|
}
|
|
@@ -97,6 +129,8 @@ export function chordQualities(): ChordQuality[] {
|
|
|
97
129
|
* @param ext Chord quality to apply.
|
|
98
130
|
* @param key Key context supplying the diatonic root.
|
|
99
131
|
* @returns The constructed chord.
|
|
132
|
+
*
|
|
133
|
+
* @category Chords
|
|
100
134
|
*/
|
|
101
135
|
export function chordFromDegree(degree: number, ext: ChordQuality, key: KeyScale): Chord {
|
|
102
136
|
const tones = scaleTonesInDegreeOrder(key);
|
|
@@ -113,6 +147,15 @@ export function chordFromDegree(degree: number, ext: ChordQuality, key: KeyScale
|
|
|
113
147
|
* @param quality Chord quality supplying the interval template.
|
|
114
148
|
* @param bassPc Optional slash-chord bass pitch class.
|
|
115
149
|
* @returns The constructed chord.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```ts
|
|
153
|
+
* import { makeChord } from '@libraz/libcantus';
|
|
154
|
+
* const cmaj7 = makeChord(0, 'maj7');
|
|
155
|
+
* // { rootPc: 0, quality: 'maj7', intervals: [0, 4, 7, 11] }
|
|
156
|
+
* ```
|
|
157
|
+
*
|
|
158
|
+
* @category Chords
|
|
116
159
|
*/
|
|
117
160
|
export function makeChord(rootPc: number, quality: ChordQuality, bassPc?: number): Chord {
|
|
118
161
|
const chord: Chord = {
|
|
@@ -131,6 +174,14 @@ export function makeChord(rootPc: number, quality: ChordQuality, bassPc?: number
|
|
|
131
174
|
*
|
|
132
175
|
* @param chord The chord to enumerate.
|
|
133
176
|
* @returns The chord's pitch classes, sorted ascending in [0, 11].
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* ```ts
|
|
180
|
+
* import { makeChord, chordPitchClasses } from '@libraz/libcantus';
|
|
181
|
+
* chordPitchClasses(makeChord(0, 'maj7')); // [0, 4, 7, 11]
|
|
182
|
+
* ```
|
|
183
|
+
*
|
|
184
|
+
* @category Chords
|
|
134
185
|
*/
|
|
135
186
|
export function chordPitchClasses(chord: Chord): number[] {
|
|
136
187
|
const set = new Set<number>();
|
|
@@ -156,6 +207,8 @@ function chordToneOffsets(chord: Chord): Set<number> {
|
|
|
156
207
|
* @param pitch MIDI pitch or bare pitch class.
|
|
157
208
|
* @param chord The chord providing the root reference.
|
|
158
209
|
* @returns The chord-tone role, or null if the pitch has no basic role.
|
|
210
|
+
*
|
|
211
|
+
* @category Chords
|
|
159
212
|
*/
|
|
160
213
|
export function chordToneRole(
|
|
161
214
|
pitch: number,
|
|
@@ -275,6 +328,14 @@ function stackThirds(degree: number, key: KeyScale, size: 3 | 4): Chord {
|
|
|
275
328
|
* @param degree 0-based scale degree of the chord root.
|
|
276
329
|
* @param key Key/scale context.
|
|
277
330
|
* @returns The diatonic triad.
|
|
331
|
+
*
|
|
332
|
+
* @example
|
|
333
|
+
* ```ts
|
|
334
|
+
* import { majorKey, diatonicTriad } from '@libraz/libcantus';
|
|
335
|
+
* const tonic = diatonicTriad(0, majorKey(0)); // C major triad (degree 0 of C major)
|
|
336
|
+
* ```
|
|
337
|
+
*
|
|
338
|
+
* @category Chords
|
|
278
339
|
*/
|
|
279
340
|
export function diatonicTriad(degree: number, key: KeyScale): Chord {
|
|
280
341
|
return stackThirds(degree, key, 3);
|
|
@@ -286,6 +347,8 @@ export function diatonicTriad(degree: number, key: KeyScale): Chord {
|
|
|
286
347
|
* @param degree 0-based scale degree of the chord root.
|
|
287
348
|
* @param key Key/scale context.
|
|
288
349
|
* @returns The diatonic seventh chord.
|
|
350
|
+
*
|
|
351
|
+
* @category Chords
|
|
289
352
|
*/
|
|
290
353
|
export function diatonicSeventh(degree: number, key: KeyScale): Chord {
|
|
291
354
|
return stackThirds(degree, key, 4);
|
package/src/chordscale/index.ts
CHANGED
|
@@ -29,6 +29,8 @@ function maskHasPitchClass(mask: number, scaleRootPc: number, pc: number): boole
|
|
|
29
29
|
* @param scaleMask The scale's 12-bit mode mask.
|
|
30
30
|
* @param scaleRootPc The pitch class the mask is rooted on.
|
|
31
31
|
* @returns True if the scale is a superset of the chord.
|
|
32
|
+
*
|
|
33
|
+
* @category Scales
|
|
32
34
|
*/
|
|
33
35
|
export function scaleMatchesChord(
|
|
34
36
|
chordPcs: number[],
|
|
@@ -43,37 +45,67 @@ export function scaleMatchesChord(
|
|
|
43
45
|
return true;
|
|
44
46
|
}
|
|
45
47
|
|
|
46
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* A named scale rooted on a pitch class that fits over a chord.
|
|
50
|
+
*
|
|
51
|
+
* @category Scales
|
|
52
|
+
*/
|
|
47
53
|
export type ChordScaleMatch = {
|
|
48
54
|
name: string;
|
|
49
55
|
rootPc: number;
|
|
50
56
|
};
|
|
51
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Scale names whose mask duplicates another {@link NAMED_SCALES} entry
|
|
60
|
+
* (major/ionian, naturalMinor/aeolian). Chord-scale analysis conventionally
|
|
61
|
+
* speaks in mode names, so these aliases are skipped and only the modal name
|
|
62
|
+
* is reported.
|
|
63
|
+
*/
|
|
64
|
+
const ALIASED_SCALE_NAMES = new Set(['major', 'naturalMinor']);
|
|
65
|
+
|
|
66
|
+
/** Number of tones in a heptatonic (seven-note) scale. */
|
|
67
|
+
const HEPTATONIC_SIZE = 7;
|
|
68
|
+
|
|
52
69
|
/**
|
|
53
70
|
* List the scales that fit over a chord, best fit first.
|
|
54
71
|
*
|
|
55
72
|
* Only the chord root is considered as the scale root, matching the
|
|
56
73
|
* conventional chord-scale relationship. Every entry of {@link NAMED_SCALES}
|
|
57
|
-
* whose pitch-class set is a superset of the chord's is returned
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
74
|
+
* whose pitch-class set is a superset of the chord's is returned once per
|
|
75
|
+
* distinct pitch-class set (aliased masks such as major/ionian report only the
|
|
76
|
+
* modal name), ranked by fewest extra scale tones beyond the chord, then by
|
|
77
|
+
* scale size (heptatonic before larger scales), then by scale name. For bare
|
|
78
|
+
* triads and smaller chords, heptatonic scales rank before pentatonics and
|
|
79
|
+
* other sizes: a pentatonic adds no modal color over a triad, so the seven-note
|
|
80
|
+
* modes are the more useful answer. The chromatic scale is only returned as a
|
|
81
|
+
* fallback when no other scale contains the chord.
|
|
61
82
|
*
|
|
62
83
|
* @param chord The chord to fit scales over.
|
|
63
84
|
* @returns The matching scales rooted on the chord root, best fit first.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```ts
|
|
88
|
+
* import { makeChord, chordScales } from '@libraz/libcantus';
|
|
89
|
+
* const scales = chordScales(makeChord(0, 'maj7'));
|
|
90
|
+
* // scales for Cmaj7, best fit first; each is { name, rootPc: 0 }
|
|
91
|
+
* ```
|
|
92
|
+
*
|
|
93
|
+
* @category Scales
|
|
64
94
|
*/
|
|
65
95
|
export function chordScales(chord: Chord): ChordScaleMatch[] {
|
|
66
96
|
const chordPcs = chordPitchClasses(chord);
|
|
67
97
|
const rootPc = pitchClass(chord.rootPc);
|
|
68
98
|
const ranked: { name: string; extra: number; size: number }[] = [];
|
|
99
|
+
const seenMasks = new Set<number>();
|
|
69
100
|
for (const name of Object.keys(NAMED_SCALES)) {
|
|
70
|
-
if (name === 'chromatic') {
|
|
101
|
+
if (name === 'chromatic' || ALIASED_SCALE_NAMES.has(name)) {
|
|
71
102
|
continue;
|
|
72
103
|
}
|
|
73
104
|
const mask = NAMED_SCALES[name];
|
|
74
|
-
if (mask === undefined) {
|
|
105
|
+
if (mask === undefined || seenMasks.has(mask)) {
|
|
75
106
|
continue;
|
|
76
107
|
}
|
|
108
|
+
seenMasks.add(mask);
|
|
77
109
|
if (scaleMatchesChord(chordPcs, mask, rootPc)) {
|
|
78
110
|
const size = popcount12(mask);
|
|
79
111
|
ranked.push({ name, extra: size - chordPcs.length, size });
|
|
@@ -87,7 +119,11 @@ export function chordScales(chord: Chord): ChordScaleMatch[] {
|
|
|
87
119
|
}
|
|
88
120
|
return [];
|
|
89
121
|
}
|
|
122
|
+
const preferHeptatonic = chordPcs.length <= 3;
|
|
90
123
|
ranked.sort((a, b) => {
|
|
124
|
+
if (preferHeptatonic && (a.size === HEPTATONIC_SIZE) !== (b.size === HEPTATONIC_SIZE)) {
|
|
125
|
+
return a.size === HEPTATONIC_SIZE ? -1 : 1;
|
|
126
|
+
}
|
|
91
127
|
if (a.extra !== b.extra) {
|
|
92
128
|
return a.extra - b.extra;
|
|
93
129
|
}
|
|
@@ -109,6 +145,8 @@ export function chordScales(chord: Chord): ChordScaleMatch[] {
|
|
|
109
145
|
* @param chord The chord providing the chord tones.
|
|
110
146
|
* @param scaleName A key of {@link NAMED_SCALES}.
|
|
111
147
|
* @returns The avoid-note pitch classes, sorted ascending in [0, 11].
|
|
148
|
+
*
|
|
149
|
+
* @category Scales
|
|
112
150
|
*/
|
|
113
151
|
export function avoidNotes(chord: Chord, scaleName: string): number[] {
|
|
114
152
|
const mask = NAMED_SCALES[scaleName];
|
|
@@ -144,6 +182,14 @@ export function avoidNotes(chord: Chord, scaleName: string): number[] {
|
|
|
144
182
|
* @param chord The chord providing the chord tones.
|
|
145
183
|
* @param scaleName A key of {@link NAMED_SCALES}.
|
|
146
184
|
* @returns The available-tension pitch classes, sorted ascending in [0, 11].
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* ```ts
|
|
188
|
+
* import { makeChord, availableTensions } from '@libraz/libcantus';
|
|
189
|
+
* availableTensions(makeChord(0, 'maj7'), 'ionian'); // [2, 9] — color tones over Cmaj7
|
|
190
|
+
* ```
|
|
191
|
+
*
|
|
192
|
+
* @category Scales
|
|
147
193
|
*/
|
|
148
194
|
export function availableTensions(chord: Chord, scaleName: string): number[] {
|
|
149
195
|
const mask = NAMED_SCALES[scaleName];
|
|
@@ -167,7 +213,11 @@ export function availableTensions(chord: Chord, scaleName: string): number[] {
|
|
|
167
213
|
return tensions.sort((a, b) => a - b);
|
|
168
214
|
}
|
|
169
215
|
|
|
170
|
-
/**
|
|
216
|
+
/**
|
|
217
|
+
* A scale fit over a chord together with its avoid notes and tensions.
|
|
218
|
+
*
|
|
219
|
+
* @category Scales
|
|
220
|
+
*/
|
|
171
221
|
export type ChordScaleReportEntry = {
|
|
172
222
|
name: string;
|
|
173
223
|
rootPc: number;
|
|
@@ -184,6 +234,8 @@ export type ChordScaleReportEntry = {
|
|
|
184
234
|
* @param chord The chord to analyze.
|
|
185
235
|
* @param limit Optional maximum number of scales to report; all by default.
|
|
186
236
|
* @returns One entry per reported scale, best fit first.
|
|
237
|
+
*
|
|
238
|
+
* @category Scales
|
|
187
239
|
*/
|
|
188
240
|
export function chordScaleReport(chord: Chord, limit?: number): ChordScaleReportEntry[] {
|
|
189
241
|
const matches = chordScales(chord);
|
|
@@ -196,7 +248,11 @@ export function chordScaleReport(chord: Chord, limit?: number): ChordScaleReport
|
|
|
196
248
|
}));
|
|
197
249
|
}
|
|
198
250
|
|
|
199
|
-
/**
|
|
251
|
+
/**
|
|
252
|
+
* A chord paired with the scale chosen for it by {@link scalesForChanges}.
|
|
253
|
+
*
|
|
254
|
+
* @category Scales
|
|
255
|
+
*/
|
|
200
256
|
export type ScaleChoice = {
|
|
201
257
|
chord: Chord;
|
|
202
258
|
scale: ChordScaleMatch;
|
|
@@ -251,6 +307,8 @@ function symmetricDifferenceSize(a: Set<number>, b: Set<number>): number {
|
|
|
251
307
|
*
|
|
252
308
|
* @param chords The chord sequence to choose scales for.
|
|
253
309
|
* @returns One scale choice per chord, in input order.
|
|
310
|
+
*
|
|
311
|
+
* @category Scales
|
|
254
312
|
*/
|
|
255
313
|
export function scalesForChanges(chords: Chord[]): ScaleChoice[] {
|
|
256
314
|
if (chords.length === 0) {
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import type { Chord } from '../chord/index.js';
|
|
2
2
|
import { chordToneRole } from '../chord/index.js';
|
|
3
|
-
import {
|
|
4
|
-
createsParallelOctave,
|
|
5
|
-
createsParallelPerfect,
|
|
6
|
-
isForbiddenMelodicLeap,
|
|
7
|
-
} from '../counterpoint/index.js';
|
|
3
|
+
import { createsParallelPerfect, isForbiddenMelodicLeap } from '../counterpoint/index.js';
|
|
8
4
|
import type { TimeSignature } from '../meter/index.js';
|
|
9
5
|
import { isStrongBeat } from '../meter/index.js';
|
|
10
6
|
import type { Rng } from '../random/index.js';
|
|
@@ -14,7 +10,11 @@ import { enumerateSafePitches } from '../safety/index.js';
|
|
|
14
10
|
import { nearestScaleTone } from '../scale/index.js';
|
|
15
11
|
import type { KeyScale, NoteEvent } from '../types.js';
|
|
16
12
|
|
|
17
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Options controlling {@link generateCounterMelody}.
|
|
15
|
+
*
|
|
16
|
+
* @category Voicing & Counterpoint
|
|
17
|
+
*/
|
|
18
18
|
export type CounterMelodyOptions = {
|
|
19
19
|
/** The lead line to write against, in ascending onset order. */
|
|
20
20
|
melody: NoteEvent[];
|
|
@@ -22,22 +22,40 @@ export type CounterMelodyOptions = {
|
|
|
22
22
|
chordAt: (beat: number) => Chord | null;
|
|
23
23
|
/** Key/scale context for scale-tone decisions. */
|
|
24
24
|
key: KeyScale;
|
|
25
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Meter used for strong-beat decisions.
|
|
27
|
+
*
|
|
28
|
+
* @defaultValue 4/4
|
|
29
|
+
*/
|
|
26
30
|
ts?: TimeSignature;
|
|
27
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* Which side of the melody the counter line occupies.
|
|
33
|
+
*
|
|
34
|
+
* @defaultValue 'below'
|
|
35
|
+
*/
|
|
28
36
|
register?: 'above' | 'below';
|
|
29
37
|
/**
|
|
30
|
-
* Onset strategy: `'complement'`
|
|
31
|
-
*
|
|
38
|
+
* Onset strategy: `'complement'` moves where the melody holds or rests and
|
|
39
|
+
* reinforces some strong beats; `'follow'` mirrors melody onsets.
|
|
40
|
+
*
|
|
41
|
+
* @defaultValue 'complement'
|
|
32
42
|
*/
|
|
33
43
|
rhythm?: 'complement' | 'follow';
|
|
34
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Safety profile applied to candidate pitches.
|
|
46
|
+
*
|
|
47
|
+
* @defaultValue 'pop'
|
|
48
|
+
*/
|
|
35
49
|
profile?: 'strict' | 'pop';
|
|
36
50
|
/** Lowest MIDI pitch the counter line may use (default derived from `register`). */
|
|
37
51
|
pitchLow?: number;
|
|
38
52
|
/** Highest MIDI pitch the counter line may use (default derived from `register`). */
|
|
39
53
|
pitchHigh?: number;
|
|
40
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* PRNG seed; the same seed always yields the same line.
|
|
56
|
+
*
|
|
57
|
+
* @defaultValue 0
|
|
58
|
+
*/
|
|
41
59
|
seed?: number;
|
|
42
60
|
};
|
|
43
61
|
|
|
@@ -244,6 +262,17 @@ function fallbackPitch(
|
|
|
244
262
|
*
|
|
245
263
|
* @param opts The lead line, chord context callback, key, and generation knobs.
|
|
246
264
|
* @returns The counter line as note events sorted by onset; `[]` for an empty melody.
|
|
265
|
+
* @example
|
|
266
|
+
* ```ts
|
|
267
|
+
* import { generateCounterMelody, majorKey, parseChordSymbol } from '@libraz/libcantus';
|
|
268
|
+
* const melody = [{ pitch: 72, startBeat: 0, durationBeat: 2 }];
|
|
269
|
+
* const counter = generateCounterMelody({
|
|
270
|
+
* melody,
|
|
271
|
+
* chordAt: () => parseChordSymbol('C'),
|
|
272
|
+
* key: majorKey(0),
|
|
273
|
+
* }); // note events below the melody, in onset order
|
|
274
|
+
* ```
|
|
275
|
+
* @category Voicing & Counterpoint
|
|
247
276
|
*/
|
|
248
277
|
export function generateCounterMelody(opts: CounterMelodyOptions): NoteEvent[] {
|
|
249
278
|
const melody = [...opts.melody].sort((a, b) => a.startBeat - b.startBeat);
|
|
@@ -309,8 +338,9 @@ export function generateCounterMelody(opts: CounterMelodyOptions): NoteEvent[] {
|
|
|
309
338
|
if (
|
|
310
339
|
prevPitch !== undefined &&
|
|
311
340
|
melPrev !== undefined &&
|
|
312
|
-
|
|
313
|
-
|
|
341
|
+
// Parallel octaves are the perfect-class-zero case of this predicate,
|
|
342
|
+
// so a single check covers both parallel fifths and octaves.
|
|
343
|
+
createsParallelPerfect(prevPitch, candidate, melPrev, melPitch)
|
|
314
344
|
) {
|
|
315
345
|
continue;
|
|
316
346
|
}
|
|
@@ -12,6 +12,7 @@ function pitchClass(pitch: number): number {
|
|
|
12
12
|
* @param upperPitch Pitch of the nominally higher voice.
|
|
13
13
|
* @param lowerPitch Pitch of the nominally lower voice.
|
|
14
14
|
* @returns True when the upper voice sits below the lower voice.
|
|
15
|
+
* @category Voicing & Counterpoint
|
|
15
16
|
*/
|
|
16
17
|
export function createsVoiceCrossing(upperPitch: number, lowerPitch: number): boolean {
|
|
17
18
|
return upperPitch < lowerPitch;
|
|
@@ -24,6 +25,7 @@ export function createsVoiceCrossing(upperPitch: number, lowerPitch: number): bo
|
|
|
24
25
|
* @param b Second pitch.
|
|
25
26
|
* @param twoVoice When true, the perfect fourth counts as dissonant.
|
|
26
27
|
* @returns True when the vertical interval is dissonant.
|
|
28
|
+
* @category Voicing & Counterpoint
|
|
27
29
|
*/
|
|
28
30
|
export function createsVerticalDissonance(a: number, b: number, twoVoice: boolean): boolean {
|
|
29
31
|
return !isConsonantInterval(a - b, twoVoice);
|
|
@@ -39,6 +41,7 @@ export function createsVerticalDissonance(a: number, b: number, twoVoice: boolea
|
|
|
39
41
|
* @param prev Starting pitch.
|
|
40
42
|
* @param cur Ending pitch.
|
|
41
43
|
* @returns True when the leap is forbidden in strict counterpoint.
|
|
44
|
+
* @category Voicing & Counterpoint
|
|
42
45
|
*/
|
|
43
46
|
export function isForbiddenMelodicLeap(prev: number, cur: number): boolean {
|
|
44
47
|
const semis = Math.abs(cur - prev);
|
|
@@ -78,6 +81,8 @@ function bothVoicesMove(aMove: number, bMove: number): boolean {
|
|
|
78
81
|
* A fifth expanding to a twelfth counts (same perfect class); a fifth moving to
|
|
79
82
|
* an octave does not (different perfect kinds — the direct/hidden case owned by
|
|
80
83
|
* {@link createsHiddenParallelPerfect}).
|
|
84
|
+
*
|
|
85
|
+
* @category Voicing & Counterpoint
|
|
81
86
|
*/
|
|
82
87
|
export function createsParallelPerfect(
|
|
83
88
|
aPrev: number,
|
|
@@ -96,6 +101,15 @@ export function createsParallelPerfect(
|
|
|
96
101
|
/**
|
|
97
102
|
* Whether two voices move in consecutive parallel octaves (or unisons) by
|
|
98
103
|
* similar motion.
|
|
104
|
+
*
|
|
105
|
+
* This is a strict subset of {@link createsParallelPerfect}: a similar-motion
|
|
106
|
+
* octave-to-octave is the perfect-class-zero case that predicate already flags
|
|
107
|
+
* (and it additionally catches the contrary-motion anti-parallel). Callers that
|
|
108
|
+
* tally parallel violations should therefore use {@link createsParallelPerfect}
|
|
109
|
+
* alone to avoid double counting; this predicate remains for callers wanting a
|
|
110
|
+
* dedicated similar-motion octave test.
|
|
111
|
+
*
|
|
112
|
+
* @category Voicing & Counterpoint
|
|
99
113
|
*/
|
|
100
114
|
export function createsParallelOctave(
|
|
101
115
|
aPrev: number,
|
|
@@ -112,6 +126,8 @@ export function createsParallelOctave(
|
|
|
112
126
|
/**
|
|
113
127
|
* Whether two voices move in consecutive parallel unisons — both landing on the
|
|
114
128
|
* same pitch, having shared a pitch on the previous move.
|
|
129
|
+
*
|
|
130
|
+
* @category Voicing & Counterpoint
|
|
115
131
|
*/
|
|
116
132
|
export function createsParallelUnison(
|
|
117
133
|
aPrev: number,
|
|
@@ -132,6 +148,8 @@ export function createsParallelUnison(
|
|
|
132
148
|
* The traditional step exception is applied: the approach is allowed when the
|
|
133
149
|
* upper of the two voices moves by step, so only leaps into the perfect
|
|
134
150
|
* interval are flagged.
|
|
151
|
+
*
|
|
152
|
+
* @category Voicing & Counterpoint
|
|
135
153
|
*/
|
|
136
154
|
export function createsHiddenParallelPerfect(
|
|
137
155
|
aPrev: number,
|
|
@@ -168,6 +186,7 @@ export function createsHiddenParallelPerfect(
|
|
|
168
186
|
* @param upperCur Current pitch of the upper voice.
|
|
169
187
|
* @param lowerPrev Previous pitch of the lower voice.
|
|
170
188
|
* @param lowerCur Current pitch of the lower voice.
|
|
189
|
+
* @category Voicing & Counterpoint
|
|
171
190
|
*/
|
|
172
191
|
export function createsVoiceOverlap(
|
|
173
192
|
upperPrev: number,
|
|
@@ -186,6 +205,7 @@ export function createsVoiceOverlap(
|
|
|
186
205
|
* @param upperPitch Pitch of the higher voice.
|
|
187
206
|
* @param lowerPitch Pitch of the lower voice.
|
|
188
207
|
* @param maxSemitones Maximum allowed spacing in semitones (default an octave).
|
|
208
|
+
* @category Voicing & Counterpoint
|
|
189
209
|
*/
|
|
190
210
|
export function exceedsSpacing(upperPitch: number, lowerPitch: number, maxSemitones = 12): boolean {
|
|
191
211
|
return Math.abs(upperPitch - lowerPitch) > maxSemitones;
|
|
@@ -198,6 +218,7 @@ export function exceedsSpacing(upperPitch: number, lowerPitch: number, maxSemito
|
|
|
198
218
|
* @param cur The following pitch.
|
|
199
219
|
* @param key Key context supplying the tonic.
|
|
200
220
|
* @returns True when `prev` is the leading tone and `cur` is the tonic a step above.
|
|
221
|
+
* @category Voicing & Counterpoint
|
|
201
222
|
*/
|
|
202
223
|
export function isLeadingToneResolution(prev: number, cur: number, key: KeyScale): boolean {
|
|
203
224
|
const tonic = pitchClass(key.rootPc);
|