@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/progression/index.ts
CHANGED
|
@@ -3,13 +3,25 @@ import { diatonicTriad } from '../chord/index.js';
|
|
|
3
3
|
import { scaleTonesInDegreeOrder } from '../scale/index.js';
|
|
4
4
|
import type { KeyScale } from '../types.js';
|
|
5
5
|
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* Broad production style a progression preset suits.
|
|
8
|
+
*
|
|
9
|
+
* @category Composition
|
|
10
|
+
*/
|
|
7
11
|
export type ProgStyle = 'minimal' | 'dance' | 'idol' | 'rock';
|
|
8
12
|
|
|
9
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Harmonic-function role of a progression as a whole.
|
|
15
|
+
*
|
|
16
|
+
* @category Composition
|
|
17
|
+
*/
|
|
10
18
|
export type ProgFunction = 'loop' | 'tensionBuild' | 'cadenceStrong' | 'stable';
|
|
11
19
|
|
|
12
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* A named chord-progression preset expressed in scale degrees.
|
|
22
|
+
*
|
|
23
|
+
* @category Composition
|
|
24
|
+
*/
|
|
13
25
|
export type ProgressionPreset = {
|
|
14
26
|
id: string;
|
|
15
27
|
name: string;
|
|
@@ -18,7 +30,11 @@ export type ProgressionPreset = {
|
|
|
18
30
|
styles: ProgStyle[];
|
|
19
31
|
};
|
|
20
32
|
|
|
21
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* A chord placed on the timeline by {@link generateProgression}.
|
|
35
|
+
*
|
|
36
|
+
* @category Composition
|
|
37
|
+
*/
|
|
22
38
|
export type GeneratedChord = {
|
|
23
39
|
rootPc: number;
|
|
24
40
|
quality: ChordQuality;
|
|
@@ -30,7 +46,11 @@ export type GeneratedChord = {
|
|
|
30
46
|
secondaryDominant?: boolean;
|
|
31
47
|
};
|
|
32
48
|
|
|
33
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Options controlling {@link generateProgression}.
|
|
51
|
+
*
|
|
52
|
+
* @category Composition
|
|
53
|
+
*/
|
|
34
54
|
export type GenerateProgressionOptions = {
|
|
35
55
|
key: KeyScale;
|
|
36
56
|
style: ProgStyle;
|
|
@@ -38,6 +58,11 @@ export type GenerateProgressionOptions = {
|
|
|
38
58
|
presetId?: string;
|
|
39
59
|
ext?: ChordQuality | 'auto';
|
|
40
60
|
reharmonize?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Seed for the deterministic preset choice and reharmonization.
|
|
63
|
+
*
|
|
64
|
+
* @defaultValue 0
|
|
65
|
+
*/
|
|
41
66
|
seed?: number;
|
|
42
67
|
};
|
|
43
68
|
|
|
@@ -208,12 +233,20 @@ const BORROWED_OFFSET: Record<number, number> = {
|
|
|
208
233
|
14: 6, // #IV
|
|
209
234
|
};
|
|
210
235
|
|
|
211
|
-
/**
|
|
236
|
+
/**
|
|
237
|
+
* All built-in progression presets.
|
|
238
|
+
*
|
|
239
|
+
* @category Composition
|
|
240
|
+
*/
|
|
212
241
|
export function progressions(): ProgressionPreset[] {
|
|
213
242
|
return PRESETS.map((p) => ({ ...p, degrees: [...p.degrees], styles: [...p.styles] }));
|
|
214
243
|
}
|
|
215
244
|
|
|
216
|
-
/**
|
|
245
|
+
/**
|
|
246
|
+
* Presets whose style list includes the given style.
|
|
247
|
+
*
|
|
248
|
+
* @category Composition
|
|
249
|
+
*/
|
|
217
250
|
export function progressionsByStyle(style: ProgStyle): ProgressionPreset[] {
|
|
218
251
|
return progressions().filter((p) => p.styles.includes(style));
|
|
219
252
|
}
|
|
@@ -264,7 +297,9 @@ function autoQuality(degree: number, key: KeyScale): ChordQuality {
|
|
|
264
297
|
* Generate a chord progression laid out one chord per bar.
|
|
265
298
|
*
|
|
266
299
|
* A preset is chosen by `presetId` when given, otherwise deterministically from
|
|
267
|
-
* the presets matching `style`, seeded by `seed`.
|
|
300
|
+
* the presets matching `style`, seeded by `seed`. An unknown `presetId` is a
|
|
301
|
+
* caller error and throws rather than silently falling back to a random preset.
|
|
302
|
+
* The preset's degrees cycle to
|
|
268
303
|
* fill `bars`; each bar is four beats, so `startBeat` is `barIndex * 4`. Chord
|
|
269
304
|
* roots come from the key's diatonic scale-degree mapping. When `ext` is
|
|
270
305
|
* omitted or `'auto'`, each chord takes its diatonic triad quality; otherwise
|
|
@@ -274,12 +309,25 @@ function autoQuality(degree: number, key: KeyScale): ChordQuality {
|
|
|
274
309
|
*
|
|
275
310
|
* @param opts Generation options.
|
|
276
311
|
* @returns One chord per bar in timeline order.
|
|
312
|
+
* @throws If `presetId` is given but does not match any built-in preset.
|
|
313
|
+
*
|
|
314
|
+
* @example
|
|
315
|
+
* ```ts
|
|
316
|
+
* import { generateProgression, majorKey } from '@libraz/libcantus';
|
|
317
|
+
* const chords = generateProgression({ key: majorKey(0), style: 'dance', bars: 4 });
|
|
318
|
+
* // Deterministic for a given seed (defaults to 0); one GeneratedChord per bar.
|
|
319
|
+
* ```
|
|
320
|
+
*
|
|
321
|
+
* @category Composition
|
|
277
322
|
*/
|
|
278
323
|
export function generateProgression(opts: GenerateProgressionOptions): GeneratedChord[] {
|
|
279
324
|
const seed = opts.seed ?? 0;
|
|
280
325
|
let preset: ProgressionPreset | undefined;
|
|
281
326
|
if (opts.presetId !== undefined) {
|
|
282
327
|
preset = PRESETS.find((p) => p.id === opts.presetId);
|
|
328
|
+
if (preset === undefined) {
|
|
329
|
+
throw new Error(`Unknown progression preset: ${opts.presetId}`);
|
|
330
|
+
}
|
|
283
331
|
}
|
|
284
332
|
if (preset === undefined) {
|
|
285
333
|
const pool = PRESETS.filter((p) => p.styles.includes(opts.style));
|
package/src/random/index.ts
CHANGED
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
* seed the stream is fully reproducible.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* A deterministic PRNG with the sampling helpers the generators need.
|
|
10
|
+
*
|
|
11
|
+
* @category Utilities
|
|
12
|
+
*/
|
|
9
13
|
export type Rng = {
|
|
10
14
|
/** Next float in [0, 1). */
|
|
11
15
|
next: () => number;
|
|
@@ -22,6 +26,14 @@ export type Rng = {
|
|
|
22
26
|
*
|
|
23
27
|
* @param seed The 32-bit seed; the same seed always yields the same stream.
|
|
24
28
|
* @returns The seeded generator.
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* import { createRng } from '@libraz/libcantus';
|
|
32
|
+
* const rng = createRng(42);
|
|
33
|
+
* rng.next(); // deterministic float in [0, 1)
|
|
34
|
+
* rng.range(1, 6); // deterministic integer in [1, 6]
|
|
35
|
+
* ```
|
|
36
|
+
* @category Utilities
|
|
25
37
|
*/
|
|
26
38
|
export function createRng(seed: number): Rng {
|
|
27
39
|
let state = seed >>> 0;
|
package/src/reharmony/index.ts
CHANGED
|
@@ -32,7 +32,11 @@ function mod12(n: number): number {
|
|
|
32
32
|
return ((n % 12) + 12) % 12;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* The kind of substitution relationship a candidate realizes.
|
|
37
|
+
*
|
|
38
|
+
* @category Reharmonization
|
|
39
|
+
*/
|
|
36
40
|
export type SubstitutionType =
|
|
37
41
|
| 'tritone'
|
|
38
42
|
| 'relative'
|
|
@@ -40,7 +44,11 @@ export type SubstitutionType =
|
|
|
40
44
|
| 'chromatic-mediant'
|
|
41
45
|
| 'secondary-dominant';
|
|
42
46
|
|
|
43
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* A proposed chord substitution with its relationship, numeral, and function.
|
|
49
|
+
*
|
|
50
|
+
* @category Reharmonization
|
|
51
|
+
*/
|
|
44
52
|
export type Substitution = {
|
|
45
53
|
chord: Chord;
|
|
46
54
|
type: SubstitutionType;
|
|
@@ -48,7 +56,11 @@ export type Substitution = {
|
|
|
48
56
|
function: HarmonicFunction;
|
|
49
57
|
};
|
|
50
58
|
|
|
51
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* Options controlling which substitutions {@link substituteChord} returns.
|
|
61
|
+
*
|
|
62
|
+
* @category Reharmonization
|
|
63
|
+
*/
|
|
52
64
|
export type SubstituteOptions = {
|
|
53
65
|
/**
|
|
54
66
|
* Melody pitch classes that must remain chord tones. When given, only
|
|
@@ -132,6 +144,13 @@ const THIRD_OFFSETS = [3, 4, 8, 9] as const;
|
|
|
132
144
|
* @param key The prevailing key.
|
|
133
145
|
* @param opts Optional melody-preservation constraint.
|
|
134
146
|
* @returns The deduplicated substitution candidates.
|
|
147
|
+
* @example
|
|
148
|
+
* ```ts
|
|
149
|
+
* import { substituteChord, makeChord, majorKey } from '@libraz/libcantus';
|
|
150
|
+
* const subs = substituteChord(makeChord(7, 'dom7'), majorKey(0));
|
|
151
|
+
* // includes the tritone sub Db7 (rootPc 1, quality 'dom7')
|
|
152
|
+
* ```
|
|
153
|
+
* @category Reharmonization
|
|
135
154
|
*/
|
|
136
155
|
export function substituteChord(
|
|
137
156
|
chord: Chord,
|
|
@@ -214,7 +233,11 @@ export function substituteChord(
|
|
|
214
233
|
return results;
|
|
215
234
|
}
|
|
216
235
|
|
|
217
|
-
/**
|
|
236
|
+
/**
|
|
237
|
+
* A borrowed chord in the modal-interchange palette of a key.
|
|
238
|
+
*
|
|
239
|
+
* @category Reharmonization
|
|
240
|
+
*/
|
|
218
241
|
export type BorrowedChord = {
|
|
219
242
|
chord: Chord;
|
|
220
243
|
roman: string;
|
|
@@ -231,6 +254,7 @@ export type BorrowedChord = {
|
|
|
231
254
|
*
|
|
232
255
|
* @param key The prevailing key.
|
|
233
256
|
* @returns The borrowed-chord palette.
|
|
257
|
+
* @category Reharmonization
|
|
234
258
|
*/
|
|
235
259
|
export function modalInterchangePalette(key: KeyScale): BorrowedChord[] {
|
|
236
260
|
const palette: BorrowedChord[] = [];
|
|
@@ -275,6 +299,13 @@ export function modalInterchangePalette(key: KeyScale): BorrowedChord[] {
|
|
|
275
299
|
* @param chord The chord to reflect.
|
|
276
300
|
* @param key The prevailing key.
|
|
277
301
|
* @returns The negative-harmony counterpart of the chord.
|
|
302
|
+
* @example
|
|
303
|
+
* ```ts
|
|
304
|
+
* import { negativeHarmonyMirror, makeChord, majorKey } from '@libraz/libcantus';
|
|
305
|
+
* negativeHarmonyMirror(makeChord(7, 'maj'), majorKey(0));
|
|
306
|
+
* // G major reflected across C's axis => F minor (rootPc 5, quality 'min')
|
|
307
|
+
* ```
|
|
308
|
+
* @category Reharmonization
|
|
278
309
|
*/
|
|
279
310
|
export function negativeHarmonyMirror(chord: Chord, key: KeyScale): Chord {
|
|
280
311
|
const tonic = mod12(key.rootPc);
|
package/src/rhythm/index.ts
CHANGED
|
@@ -14,7 +14,11 @@
|
|
|
14
14
|
import { beatsPerBar, metricWeight, type TimeSignature } from '../meter/index.js';
|
|
15
15
|
import { createRng } from '../random/index.js';
|
|
16
16
|
|
|
17
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* A single rhythmic note: an onset position and how long it sounds.
|
|
19
|
+
*
|
|
20
|
+
* @category Rhythm & Meter
|
|
21
|
+
*/
|
|
18
22
|
export type RhythmEvent = {
|
|
19
23
|
/** Onset position in quarter-note beats, absolute from the start of the span. */
|
|
20
24
|
position: number;
|
|
@@ -22,21 +26,38 @@ export type RhythmEvent = {
|
|
|
22
26
|
duration: number;
|
|
23
27
|
};
|
|
24
28
|
|
|
25
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Options controlling {@link generateRhythm}.
|
|
31
|
+
*
|
|
32
|
+
* @category Rhythm & Meter
|
|
33
|
+
*/
|
|
26
34
|
export type RhythmOptions = {
|
|
27
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Seed for the deterministic PRNG.
|
|
37
|
+
*
|
|
38
|
+
* @defaultValue 0
|
|
39
|
+
*/
|
|
28
40
|
seed?: number;
|
|
29
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Number of bars to generate.
|
|
43
|
+
*
|
|
44
|
+
* @defaultValue 1
|
|
45
|
+
*/
|
|
30
46
|
bars?: number;
|
|
31
47
|
/**
|
|
32
48
|
* Grid resolution as the number of equal grid steps per quarter-note beat.
|
|
33
49
|
* The default of 2 yields an eighth-note grid; 4 yields a sixteenth-note
|
|
34
50
|
* grid, 3 an eighth-note triplet grid, and so on.
|
|
51
|
+
*
|
|
52
|
+
* @defaultValue 2
|
|
35
53
|
*/
|
|
36
54
|
subdivision?: number;
|
|
37
55
|
/**
|
|
38
56
|
* Overall onset density in [0, 1]. Scales the per-slot onset probability, so
|
|
39
|
-
* higher values fill more grid slots.
|
|
57
|
+
* higher values fill more grid slots. Values outside [0, 1] are clamped into
|
|
58
|
+
* range.
|
|
59
|
+
*
|
|
60
|
+
* @defaultValue 0.5
|
|
40
61
|
*/
|
|
41
62
|
density?: number;
|
|
42
63
|
};
|
|
@@ -53,6 +74,7 @@ const DEFAULT_BARS = 1;
|
|
|
53
74
|
*
|
|
54
75
|
* @param weight The metric weight, 0 (off-pulse) to 3 (downbeat).
|
|
55
76
|
* @returns A base onset probability in [0, 1].
|
|
77
|
+
* @category Rhythm & Meter
|
|
56
78
|
*/
|
|
57
79
|
export function onsetWeightCurve(weight: number): number {
|
|
58
80
|
switch (weight) {
|
|
@@ -71,29 +93,40 @@ export function onsetWeightCurve(weight: number): number {
|
|
|
71
93
|
* Generate a deterministic rhythmic pattern over `bars` bars of a time
|
|
72
94
|
* signature. A grid is built at the chosen subdivision; each slot becomes an
|
|
73
95
|
* onset with a probability derived from its metric weight scaled by `density`
|
|
74
|
-
* and sampled from the seeded PRNG.
|
|
75
|
-
*
|
|
76
|
-
* extends to the
|
|
96
|
+
* and sampled from the seeded PRNG. Every bar's downbeat is always an onset, so
|
|
97
|
+
* a pattern of any length has a reliable pulse independent of the seed. Each
|
|
98
|
+
* event's duration extends to the next onset, and the last event extends to the
|
|
99
|
+
* end of the span. `density` is clamped to [0, 1].
|
|
77
100
|
*
|
|
78
101
|
* @param ts The time signature.
|
|
79
102
|
* @param opts Generation options.
|
|
80
103
|
* @returns Onset events sorted by position, non-overlapping, covering the span.
|
|
104
|
+
* @example
|
|
105
|
+
* ```ts
|
|
106
|
+
* import { parseTimeSignature, generateRhythm } from '@libraz/libcantus';
|
|
107
|
+
* const ts = parseTimeSignature('4/4');
|
|
108
|
+
* generateRhythm(ts, { seed: 42, density: 0.6 }); // onset events over one bar
|
|
109
|
+
* ```
|
|
110
|
+
* @category Rhythm & Meter
|
|
81
111
|
*/
|
|
82
112
|
export function generateRhythm(ts: TimeSignature, opts: RhythmOptions = {}): RhythmEvent[] {
|
|
83
113
|
const seed = opts.seed ?? 0;
|
|
84
114
|
const bars = opts.bars ?? DEFAULT_BARS;
|
|
85
115
|
const subdivision = opts.subdivision ?? DEFAULT_SUBDIVISION;
|
|
86
|
-
const density = opts.density ?? DEFAULT_DENSITY;
|
|
116
|
+
const density = Math.min(1, Math.max(0, opts.density ?? DEFAULT_DENSITY));
|
|
87
117
|
|
|
88
|
-
const
|
|
118
|
+
const barBeats = beatsPerBar(ts);
|
|
119
|
+
const spanBeats = barBeats * bars;
|
|
89
120
|
const step = 1 / subdivision;
|
|
90
121
|
const slotCount = Math.round(spanBeats * subdivision);
|
|
122
|
+
const slotsPerBar = Math.round(barBeats * subdivision);
|
|
91
123
|
const rng = createRng(seed);
|
|
92
124
|
|
|
93
125
|
const positions: number[] = [];
|
|
94
126
|
for (let i = 0; i < slotCount; i += 1) {
|
|
95
127
|
const position = i * step;
|
|
96
|
-
|
|
128
|
+
// Force every bar's downbeat so the pulse is reliable regardless of seed.
|
|
129
|
+
if (i % slotsPerBar === 0) {
|
|
97
130
|
positions.push(position);
|
|
98
131
|
continue;
|
|
99
132
|
}
|
|
@@ -119,6 +152,7 @@ export function generateRhythm(ts: TimeSignature, opts: RhythmOptions = {}): Rhy
|
|
|
119
152
|
* @param events Events from {@link generateRhythm}.
|
|
120
153
|
* @param ts The time signature.
|
|
121
154
|
* @returns The onset count divided by the number of bars the events span.
|
|
155
|
+
* @category Rhythm & Meter
|
|
122
156
|
*/
|
|
123
157
|
export function rhythmDensity(events: RhythmEvent[], ts: TimeSignature): number {
|
|
124
158
|
if (events.length === 0) {
|
package/src/safety/index.ts
CHANGED
|
@@ -2,7 +2,6 @@ import type { Chord } from '../chord/index.js';
|
|
|
2
2
|
import { chordPitchClasses } from '../chord/index.js';
|
|
3
3
|
import {
|
|
4
4
|
createsHiddenParallelPerfect,
|
|
5
|
-
createsParallelOctave,
|
|
6
5
|
createsParallelPerfect,
|
|
7
6
|
createsVerticalDissonance,
|
|
8
7
|
isForbiddenMelodicLeap,
|
|
@@ -10,17 +9,29 @@ import {
|
|
|
10
9
|
import { isScaleTone } from '../scale/index.js';
|
|
11
10
|
import type { KeyScale } from '../types.js';
|
|
12
11
|
|
|
13
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Severity policy: `strict` counterpoint vs. lenient `pop` voice-leading.
|
|
14
|
+
*
|
|
15
|
+
* @category Arrangement & Analysis
|
|
16
|
+
*/
|
|
14
17
|
export type SafetyProfile = 'strict' | 'pop';
|
|
15
18
|
|
|
16
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Overall placeability verdict for a candidate pitch.
|
|
21
|
+
*
|
|
22
|
+
* @category Arrangement & Analysis
|
|
23
|
+
*/
|
|
17
24
|
export enum NoteSafety {
|
|
18
25
|
Safe = 0,
|
|
19
26
|
Warning = 1,
|
|
20
27
|
Dissonant = 2,
|
|
21
28
|
}
|
|
22
29
|
|
|
23
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* Bit flags describing why a pitch received its verdict.
|
|
32
|
+
*
|
|
33
|
+
* @category Arrangement & Analysis
|
|
34
|
+
*/
|
|
24
35
|
export enum ReasonFlag {
|
|
25
36
|
ChordTone = 1 << 0,
|
|
26
37
|
Tension = 1 << 1,
|
|
@@ -40,13 +51,21 @@ export enum ReasonFlag {
|
|
|
40
51
|
NeedsResolution = 1 << 15,
|
|
41
52
|
}
|
|
42
53
|
|
|
43
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* A neighbouring voice's current and previous pitch.
|
|
56
|
+
*
|
|
57
|
+
* @category Arrangement & Analysis
|
|
58
|
+
*/
|
|
44
59
|
export type VoiceSnapshot = {
|
|
45
60
|
pitch: number;
|
|
46
61
|
prevPitch?: number;
|
|
47
62
|
};
|
|
48
63
|
|
|
49
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* Everything needed to evaluate one candidate pitch.
|
|
66
|
+
*
|
|
67
|
+
* @category Arrangement & Analysis
|
|
68
|
+
*/
|
|
50
69
|
export type SafetyQuery = {
|
|
51
70
|
profile: SafetyProfile;
|
|
52
71
|
candidatePitch: number;
|
|
@@ -59,7 +78,11 @@ export type SafetyQuery = {
|
|
|
59
78
|
vocalHigh?: number;
|
|
60
79
|
};
|
|
61
80
|
|
|
62
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* The verdict, reason flags, and optional resolution guidance.
|
|
83
|
+
*
|
|
84
|
+
* @category Arrangement & Analysis
|
|
85
|
+
*/
|
|
63
86
|
export type SafetyResult = {
|
|
64
87
|
safety: NoteSafety;
|
|
65
88
|
reasons: number;
|
|
@@ -115,6 +138,20 @@ function stepResolution(pitch: number, chord: Chord): number | undefined {
|
|
|
115
138
|
*
|
|
116
139
|
* @param q The candidate and its harmonic/voice-leading context.
|
|
117
140
|
* @returns The verdict, reason bitmask, and optional resolution guidance.
|
|
141
|
+
* @example
|
|
142
|
+
* ```ts
|
|
143
|
+
* import { evaluateSafety, makeChord, majorKey, NoteSafety } from '@libraz/libcantus';
|
|
144
|
+
* const result = evaluateSafety({
|
|
145
|
+
* profile: 'pop',
|
|
146
|
+
* candidatePitch: 60, // C over a C major chord
|
|
147
|
+
* chord: makeChord(0, 'maj'),
|
|
148
|
+
* key: majorKey(0),
|
|
149
|
+
* otherVoices: [],
|
|
150
|
+
* strongBeat: true,
|
|
151
|
+
* });
|
|
152
|
+
* result.safety === NoteSafety.Safe; // true — C is a chord tone
|
|
153
|
+
* ```
|
|
154
|
+
* @category Arrangement & Analysis
|
|
118
155
|
*/
|
|
119
156
|
export function evaluateSafety(q: SafetyQuery): SafetyResult {
|
|
120
157
|
return evaluateInternal(q, true);
|
|
@@ -242,10 +279,9 @@ function evaluateInternal(q: SafetyQuery, collectSuggestions: boolean): SafetyRe
|
|
|
242
279
|
if (ov.prevPitch === undefined) {
|
|
243
280
|
continue;
|
|
244
281
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
) {
|
|
282
|
+
// createsParallelPerfect covers every perfect class (unison/octave and
|
|
283
|
+
// fifth), so no separate octave check is needed.
|
|
284
|
+
if (createsParallelPerfect(prev, pitch, ov.prevPitch, ov.pitch)) {
|
|
249
285
|
reasons |= ReasonFlag.ParallelPerfect;
|
|
250
286
|
raise(parallelSeverity);
|
|
251
287
|
}
|
|
@@ -346,6 +382,17 @@ function describe(reasons: number, q: SafetyQuery): string {
|
|
|
346
382
|
* @param pitchLow Lowest MIDI pitch to consider (inclusive).
|
|
347
383
|
* @param pitchHigh Highest MIDI pitch to consider (inclusive).
|
|
348
384
|
* @returns Placeable pitches (non-dissonant), chord tones before others, each group descending.
|
|
385
|
+
* @example
|
|
386
|
+
* ```ts
|
|
387
|
+
* import { enumerateSafePitches, makeChord, majorKey } from '@libraz/libcantus';
|
|
388
|
+
* const pitches = enumerateSafePitches(
|
|
389
|
+
* { profile: 'pop', chord: makeChord(0, 'maj'), key: majorKey(0), otherVoices: [], strongBeat: true },
|
|
390
|
+
* 60,
|
|
391
|
+
* 72,
|
|
392
|
+
* );
|
|
393
|
+
* pitches; // placeable pitches in [60, 72], chord tones first, each group descending
|
|
394
|
+
* ```
|
|
395
|
+
* @category Arrangement & Analysis
|
|
349
396
|
*/
|
|
350
397
|
export function enumerateSafePitches(
|
|
351
398
|
q: Omit<SafetyQuery, 'candidatePitch'>,
|