@libraz/libcantus 0.9.3 → 0.9.4
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 +2 -2
- package/dist/analyze/index.cjs +39 -1947
- package/dist/analyze/index.cjs.map +1 -1
- package/dist/analyze/index.d.cts +197 -37
- package/dist/analyze/index.d.ts +197 -37
- package/dist/analyze/index.js +19 -8
- package/dist/chunk-36N73KZ3.cjs +246 -0
- package/dist/chunk-36N73KZ3.cjs.map +1 -0
- package/dist/chunk-4HLLZPXT.cjs +3077 -0
- package/dist/chunk-4HLLZPXT.cjs.map +1 -0
- package/dist/chunk-7MGZ7D62.cjs +498 -0
- package/dist/chunk-7MGZ7D62.cjs.map +1 -0
- package/dist/{chunk-4MHA432G.js → chunk-7NKDPF74.js} +403 -180
- package/dist/chunk-7NKDPF74.js.map +1 -0
- package/dist/chunk-CAN76NCU.cjs +679 -0
- package/dist/chunk-CAN76NCU.cjs.map +1 -0
- package/dist/{chunk-MIWO425M.js → chunk-D2FAJBYL.js} +101 -34
- package/dist/chunk-D2FAJBYL.js.map +1 -0
- package/dist/{chunk-SGU7SPLK.js → chunk-EHVEBWM5.js} +1056 -531
- package/dist/chunk-EHVEBWM5.js.map +1 -0
- package/dist/chunk-FCANXBAT.cjs +30 -0
- package/dist/chunk-FCANXBAT.cjs.map +1 -0
- package/dist/chunk-FNDWXRRI.cjs +479 -0
- package/dist/chunk-FNDWXRRI.cjs.map +1 -0
- package/dist/{chunk-IT2O4XFE.js → chunk-GQSGS2N4.js} +43 -24
- package/dist/chunk-GQSGS2N4.js.map +1 -0
- package/dist/{chunk-62QZKCJQ.js → chunk-HKGYCQ34.js} +10 -6
- package/dist/chunk-HKGYCQ34.js.map +1 -0
- package/dist/{chunk-AKLYPWIN.js → chunk-I24MV65U.js} +29 -6
- package/dist/chunk-I24MV65U.js.map +1 -0
- package/dist/{chunk-CHMXELDS.js → chunk-KMHOICIY.js} +311 -181
- package/dist/chunk-KMHOICIY.js.map +1 -0
- package/dist/chunk-LW53AHQU.cjs +1117 -0
- package/dist/chunk-LW53AHQU.cjs.map +1 -0
- package/dist/{chunk-2ZWMSJH6.js → chunk-NKIBQ6PT.js} +2 -2
- package/dist/chunk-NKIBQ6PT.js.map +1 -0
- package/dist/chunk-NP4NA2FB.cjs +106 -0
- package/dist/chunk-NP4NA2FB.cjs.map +1 -0
- package/dist/chunk-NVE34YGZ.cjs +1449 -0
- package/dist/chunk-NVE34YGZ.cjs.map +1 -0
- package/dist/{chunk-EH6EM37U.js → chunk-NYYWMV2C.js} +61 -49
- package/dist/chunk-NYYWMV2C.js.map +1 -0
- package/dist/{chunk-GOAW4VYL.js → chunk-Q2XUP227.js} +243 -117
- package/dist/chunk-Q2XUP227.js.map +1 -0
- package/dist/chunk-Q6OP4U43.cjs +327 -0
- package/dist/chunk-Q6OP4U43.cjs.map +1 -0
- package/dist/chunk-RC6GBMCD.cjs +845 -0
- package/dist/chunk-RC6GBMCD.cjs.map +1 -0
- package/dist/{chunk-C5XYKUVG.js → chunk-RK35RIN7.js} +370 -157
- package/dist/chunk-RK35RIN7.js.map +1 -0
- package/dist/chunk-RUMU3UA6.cjs +53 -0
- package/dist/chunk-RUMU3UA6.cjs.map +1 -0
- package/dist/chunk-T64C7ZD7.cjs +1 -0
- package/dist/chunk-T64C7ZD7.cjs.map +1 -0
- package/dist/chunk-XEBG3G53.js +498 -0
- package/dist/chunk-XEBG3G53.js.map +1 -0
- package/dist/{chunk-I5R7ZBJS.js → chunk-YLEMGEFH.js} +103 -59
- package/dist/chunk-YLEMGEFH.js.map +1 -0
- package/dist/chunk-ZEJR4TKL.cjs +173 -0
- package/dist/chunk-ZEJR4TKL.cjs.map +1 -0
- package/dist/core/index.cjs +130 -647
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +301 -39
- package/dist/core/index.d.ts +301 -39
- package/dist/core/index.js +54 -16
- package/dist/{function-DYt4iKsn.d.cts → function-BtiZb9i9.d.ts} +28 -7
- package/dist/{function-DVH4Eusr.d.ts → function-DNugAOrt.d.cts} +28 -7
- package/dist/generate/index.cjs +28 -4085
- package/dist/generate/index.cjs.map +1 -1
- package/dist/generate/index.d.cts +370 -62
- package/dist/generate/index.d.ts +370 -62
- package/dist/generate/index.js +23 -9
- package/dist/{index-CxqBQtH1.d.cts → index-6VR8-kit.d.ts} +19 -4
- package/dist/index-70p168VM.d.ts +192 -0
- package/dist/{index-CpOsP1y9.d.cts → index-Be74mcon.d.cts} +18 -17
- package/dist/index-BtXT6UQ0.d.cts +192 -0
- package/dist/{index-DxvARIP2.d.ts → index-C5UBbQ4G.d.ts} +18 -17
- package/dist/{index-Bt4tOUD9.d.ts → index-C83mZI1P.d.ts} +93 -8
- package/dist/{index-Bg5dYMmZ.d.ts → index-D6b6na69.d.cts} +14 -11
- package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
- package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
- package/dist/index-OG65ocOr.d.cts +272 -0
- package/dist/index-OG65ocOr.d.ts +272 -0
- package/dist/{index-Jc0Ryi79.d.ts → index-tJhkmQgE.d.cts} +19 -4
- package/dist/{index-Dikst44v.d.cts → index-tSQh-xwY.d.ts} +14 -11
- package/dist/{index-BNlGP86A.d.cts → index-y_ak9_Hi.d.cts} +93 -8
- package/dist/index.cjs +419 -7383
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -14
- package/dist/index.d.ts +14 -14
- package/dist/index.js +111 -27
- package/dist/model/index.cjs +8 -2562
- package/dist/model/index.cjs.map +1 -1
- package/dist/model/index.d.cts +433 -75
- package/dist/model/index.d.ts +433 -75
- package/dist/model/index.js +6 -7
- package/dist/styled-DIRkk9Uz.d.ts +506 -0
- package/dist/styled-DJbkJQfj.d.cts +506 -0
- package/dist/theory/index.cjs +172 -1822
- package/dist/theory/index.cjs.map +1 -1
- package/dist/theory/index.d.cts +151 -233
- package/dist/theory/index.d.ts +151 -233
- package/dist/theory/index.js +30 -7
- package/dist/{types-MGP29r3t.d.cts → types-BN6_Q9Kl.d.cts} +11 -2
- package/dist/{types-MGP29r3t.d.ts → types-BN6_Q9Kl.d.ts} +11 -2
- package/package.json +24 -6
- package/dist/chunk-2ZWMSJH6.js.map +0 -1
- package/dist/chunk-4MHA432G.js.map +0 -1
- package/dist/chunk-62QZKCJQ.js.map +0 -1
- package/dist/chunk-AKLYPWIN.js.map +0 -1
- package/dist/chunk-C5XYKUVG.js.map +0 -1
- package/dist/chunk-CHMXELDS.js.map +0 -1
- package/dist/chunk-DYWFNC4A.js +0 -152
- package/dist/chunk-DYWFNC4A.js.map +0 -1
- package/dist/chunk-EH6EM37U.js.map +0 -1
- package/dist/chunk-FBQK3ZQN.js +0 -131
- package/dist/chunk-FBQK3ZQN.js.map +0 -1
- package/dist/chunk-GOAW4VYL.js.map +0 -1
- package/dist/chunk-I5R7ZBJS.js.map +0 -1
- package/dist/chunk-IT2O4XFE.js.map +0 -1
- package/dist/chunk-MIWO425M.js.map +0 -1
- package/dist/chunk-SGU7SPLK.js.map +0 -1
- package/dist/index-CGMZNCx6.d.cts +0 -140
- package/dist/index-CGMZNCx6.d.ts +0 -140
- package/dist/index-CnZCo29u.d.cts +0 -108
- package/dist/index-JnF4v9bx.d.ts +0 -108
- package/dist/styled-DZ7GjShb.d.ts +0 -282
- package/dist/styled-ZTbdaVFM.d.cts +0 -282
- package/src/analyze/arrange/index.ts +0 -21
- package/src/analyze/arrange/internal.ts +0 -127
- package/src/analyze/arrange/tension.ts +0 -199
- package/src/analyze/arrange/tracks.ts +0 -312
- package/src/analyze/detect/index.ts +0 -272
- package/src/analyze/functional/borrowed.ts +0 -84
- package/src/analyze/functional/cadence.ts +0 -66
- package/src/analyze/functional/function.ts +0 -184
- package/src/analyze/functional/index.ts +0 -23
- package/src/analyze/functional/internal.ts +0 -29
- package/src/analyze/functional/roman.ts +0 -402
- package/src/analyze/index.ts +0 -49
- package/src/analyze/timeline/index.ts +0 -451
- package/src/analyze/voice/index.ts +0 -339
- package/src/core/event-index/index.ts +0 -104
- package/src/core/index.ts +0 -61
- package/src/core/interval/index.ts +0 -81
- package/src/core/meter/index.ts +0 -302
- package/src/core/pitch/index.ts +0 -311
- package/src/core/random/index.ts +0 -76
- package/src/core/tuning/index.ts +0 -186
- package/src/core/types.ts +0 -29
- package/src/core/validation/index.ts +0 -119
- package/src/generate/bass/index.ts +0 -373
- package/src/generate/countermelody/index.ts +0 -470
- package/src/generate/drums/beat.ts +0 -266
- package/src/generate/drums/euclid.ts +0 -65
- package/src/generate/drums/fills.ts +0 -356
- package/src/generate/drums/ghost.ts +0 -119
- package/src/generate/drums/hihat.ts +0 -285
- package/src/generate/drums/hit.ts +0 -31
- package/src/generate/drums/index.ts +0 -361
- package/src/generate/drums/internal.ts +0 -257
- package/src/generate/drums/kick.ts +0 -160
- package/src/generate/drums/percussion.ts +0 -121
- package/src/generate/drums/rng.ts +0 -7
- package/src/generate/drums/swing.ts +0 -71
- package/src/generate/groove/index.ts +0 -351
- package/src/generate/harmonize/index.ts +0 -451
- package/src/generate/index.ts +0 -58
- package/src/generate/motif/index.ts +0 -422
- package/src/generate/progression/index.ts +0 -362
- package/src/generate/reharmony/index.ts +0 -323
- package/src/generate/rhythm/index.ts +0 -175
- package/src/index.ts +0 -13
- package/src/model/chord.ts +0 -442
- package/src/model/index.ts +0 -15
- package/src/model/interval.ts +0 -82
- package/src/model/key.ts +0 -266
- package/src/model/note.ts +0 -176
- package/src/model/progression.ts +0 -161
- package/src/model/shared.ts +0 -12
- package/src/theory/chord/index.ts +0 -375
- package/src/theory/chordscale/index.ts +0 -374
- package/src/theory/counterpoint/index.ts +0 -230
- package/src/theory/harmony/index.ts +0 -77
- package/src/theory/index.ts +0 -112
- package/src/theory/safety/index.ts +0 -488
- package/src/theory/scale/degrees.ts +0 -123
- package/src/theory/scale/index.ts +0 -28
- package/src/theory/scale/key.ts +0 -56
- package/src/theory/scale/masks.ts +0 -153
- package/src/theory/spelling/index.ts +0 -249
- package/src/theory/symbol/index.ts +0 -268
- package/src/theory/voicing/index.ts +0 -5
- package/src/theory/voicing/internal.ts +0 -168
- package/src/theory/voicing/leading.ts +0 -112
- package/src/theory/voicing/satb.ts +0 -216
- package/src/theory/voicing/styled.ts +0 -199
- package/tsconfig.json +0 -23
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Deterministic rhythm generation over a metric grid.
|
|
3
|
-
*
|
|
4
|
-
* Builds a fixed-resolution grid across one or more bars of a time signature
|
|
5
|
-
* and decides, per grid slot, whether it becomes a note onset. Onset
|
|
6
|
-
* probability follows the metric-accent hierarchy from the `meter` module, so
|
|
7
|
-
* strong positions (downbeats, secondary pulses) are favored over weak
|
|
8
|
-
* off-pulse subdivisions. Given a seed the output is fully reproducible.
|
|
9
|
-
*
|
|
10
|
-
* Positions and durations are measured in quarter-note beats, matching the
|
|
11
|
-
* library-wide convention.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { beatsPerBar, metricWeight, type TimeSignature } from '../../core/meter/index.js';
|
|
15
|
-
import { createRng } from '../../core/random/index.js';
|
|
16
|
-
import {
|
|
17
|
-
assertFiniteNumber,
|
|
18
|
-
assertGenerationBudget,
|
|
19
|
-
assertPositiveInt,
|
|
20
|
-
} from '../../core/validation/index.js';
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* A single rhythmic note: an onset position and how long it sounds.
|
|
24
|
-
*
|
|
25
|
-
* @category Rhythm & Meter
|
|
26
|
-
*/
|
|
27
|
-
export type RhythmEvent = {
|
|
28
|
-
/** Onset position in quarter-note beats, absolute from the start of the span. */
|
|
29
|
-
position: number;
|
|
30
|
-
/** Duration in quarter-note beats, extending to the next onset. */
|
|
31
|
-
duration: number;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Options controlling {@link generateRhythm}.
|
|
36
|
-
*
|
|
37
|
-
* @category Rhythm & Meter
|
|
38
|
-
*/
|
|
39
|
-
export type RhythmOptions = {
|
|
40
|
-
/**
|
|
41
|
-
* Seed for the deterministic PRNG.
|
|
42
|
-
*
|
|
43
|
-
* @defaultValue 0
|
|
44
|
-
*/
|
|
45
|
-
seed?: number;
|
|
46
|
-
/**
|
|
47
|
-
* Number of bars to generate.
|
|
48
|
-
*
|
|
49
|
-
* @defaultValue 1
|
|
50
|
-
*/
|
|
51
|
-
bars?: number;
|
|
52
|
-
/**
|
|
53
|
-
* Grid resolution as the number of equal grid steps per quarter-note beat.
|
|
54
|
-
* The default of 2 yields an eighth-note grid; 4 yields a sixteenth-note
|
|
55
|
-
* grid, 3 an eighth-note triplet grid, and so on.
|
|
56
|
-
*
|
|
57
|
-
* @defaultValue 2
|
|
58
|
-
*/
|
|
59
|
-
subdivision?: number;
|
|
60
|
-
/**
|
|
61
|
-
* Overall onset density in [0, 1]. Scales the per-slot onset probability, so
|
|
62
|
-
* higher values fill more grid slots. Values outside [0, 1] are clamped into
|
|
63
|
-
* range.
|
|
64
|
-
*
|
|
65
|
-
* @defaultValue 0.5
|
|
66
|
-
*/
|
|
67
|
-
density?: number;
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
const DEFAULT_SUBDIVISION = 2;
|
|
71
|
-
const DEFAULT_DENSITY = 0.5;
|
|
72
|
-
const DEFAULT_BARS = 1;
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Map a metric weight (0–3 from {@link metricWeight}) to a base onset
|
|
76
|
-
* probability before density scaling. Strong positions are much more likely to
|
|
77
|
-
* carry an onset than weak ones; the curve is intentionally small and explicit
|
|
78
|
-
* so its behavior is easy to inspect and test.
|
|
79
|
-
*
|
|
80
|
-
* @param weight The metric weight, 0 (off-pulse) to 3 (downbeat).
|
|
81
|
-
* @returns A base onset probability in [0, 1].
|
|
82
|
-
* @category Rhythm & Meter
|
|
83
|
-
*/
|
|
84
|
-
export function onsetWeightCurve(weight: number): number {
|
|
85
|
-
switch (weight) {
|
|
86
|
-
case 3:
|
|
87
|
-
return 1;
|
|
88
|
-
case 2:
|
|
89
|
-
return 0.8;
|
|
90
|
-
case 1:
|
|
91
|
-
return 0.55;
|
|
92
|
-
default:
|
|
93
|
-
return 0.25;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Generate a deterministic rhythmic pattern over `bars` bars of a time
|
|
99
|
-
* signature. A grid is built at the chosen subdivision; each slot becomes an
|
|
100
|
-
* onset with a probability derived from its metric weight scaled by `density`
|
|
101
|
-
* and sampled from the seeded PRNG. Every bar's downbeat is always an onset, so
|
|
102
|
-
* a pattern of any length has a reliable pulse independent of the seed. Each
|
|
103
|
-
* event's duration extends to the next onset, and the last event extends to the
|
|
104
|
-
* end of the span. `density` is clamped to [0, 1].
|
|
105
|
-
*
|
|
106
|
-
* @param ts The time signature.
|
|
107
|
-
* @param opts Generation options.
|
|
108
|
-
* @returns Onset events sorted by position, non-overlapping, covering the span.
|
|
109
|
-
* @example
|
|
110
|
-
* ```ts
|
|
111
|
-
* import { parseTimeSignature, generateRhythm } from '@libraz/libcantus';
|
|
112
|
-
* const ts = parseTimeSignature('4/4');
|
|
113
|
-
* generateRhythm(ts, { seed: 42, density: 0.6 }); // onset events over one bar
|
|
114
|
-
* ```
|
|
115
|
-
* @category Rhythm & Meter
|
|
116
|
-
*/
|
|
117
|
-
export function generateRhythm(ts: TimeSignature, opts: RhythmOptions = {}): RhythmEvent[] {
|
|
118
|
-
const seed = opts.seed ?? 0;
|
|
119
|
-
const bars = opts.bars ?? DEFAULT_BARS;
|
|
120
|
-
const subdivision = opts.subdivision ?? DEFAULT_SUBDIVISION;
|
|
121
|
-
const requestedDensity = assertFiniteNumber(opts.density ?? DEFAULT_DENSITY, 'rhythm density');
|
|
122
|
-
const density = Math.min(1, Math.max(0, requestedDensity));
|
|
123
|
-
|
|
124
|
-
assertPositiveInt(bars, 'rhythm bars');
|
|
125
|
-
assertPositiveInt(subdivision, 'rhythm subdivision');
|
|
126
|
-
|
|
127
|
-
const barBeats = beatsPerBar(ts);
|
|
128
|
-
const spanBeats = barBeats * bars;
|
|
129
|
-
const step = 1 / subdivision;
|
|
130
|
-
const slotCount = Math.round(spanBeats * subdivision);
|
|
131
|
-
assertGenerationBudget(slotCount, 'rhythm grid slots');
|
|
132
|
-
const slotsPerBar = Math.round(barBeats * subdivision);
|
|
133
|
-
const rng = createRng(seed);
|
|
134
|
-
|
|
135
|
-
const positions: number[] = [];
|
|
136
|
-
for (let i = 0; i < slotCount; i += 1) {
|
|
137
|
-
const position = i * step;
|
|
138
|
-
// Force every bar's downbeat so the pulse is reliable regardless of seed.
|
|
139
|
-
if (i % slotsPerBar === 0) {
|
|
140
|
-
positions.push(position);
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
const weight = metricWeight(position, ts);
|
|
144
|
-
const probability = onsetWeightCurve(weight) * density;
|
|
145
|
-
if (rng.prob(probability)) {
|
|
146
|
-
positions.push(position);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const events: RhythmEvent[] = [];
|
|
151
|
-
for (let i = 0; i < positions.length; i += 1) {
|
|
152
|
-
const position = positions[i] ?? 0;
|
|
153
|
-
const nextPosition = positions[i + 1] ?? spanBeats;
|
|
154
|
-
events.push({ position, duration: nextPosition - position });
|
|
155
|
-
}
|
|
156
|
-
return events;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Onset density of a generated pattern: the mean number of onsets per bar.
|
|
161
|
-
*
|
|
162
|
-
* @param events Events from {@link generateRhythm}.
|
|
163
|
-
* @param ts The time signature.
|
|
164
|
-
* @returns The onset count divided by the number of bars the events span.
|
|
165
|
-
* @category Rhythm & Meter
|
|
166
|
-
*/
|
|
167
|
-
export function rhythmDensity(events: RhythmEvent[], ts: TimeSignature): number {
|
|
168
|
-
if (events.length === 0) {
|
|
169
|
-
return 0;
|
|
170
|
-
}
|
|
171
|
-
const last = events[events.length - 1];
|
|
172
|
-
const spanBeats = (last?.position ?? 0) + (last?.duration ?? 0);
|
|
173
|
-
const bars = spanBeats / beatsPerBar(ts);
|
|
174
|
-
return bars > 0 ? events.length / bars : 0;
|
|
175
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Complete public API.
|
|
3
|
-
*
|
|
4
|
-
* Each layer barrel is the canonical source of that layer's runtime and type
|
|
5
|
-
* exports. The package root composes those barrels so a symbol added to a
|
|
6
|
-
* public layer cannot silently disappear from the root entry point.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
export * from './analyze/index.js';
|
|
10
|
-
export * from './core/index.js';
|
|
11
|
-
export * from './generate/index.js';
|
|
12
|
-
export * from './model/index.js';
|
|
13
|
-
export * from './theory/index.js';
|
package/src/model/chord.ts
DELETED
|
@@ -1,442 +0,0 @@
|
|
|
1
|
-
import { type DetectChordOptions, detectChord, detectChordBest } from '../analyze/detect/index.js';
|
|
2
|
-
import {
|
|
3
|
-
analyzeChord,
|
|
4
|
-
type BorrowedSource,
|
|
5
|
-
borrowedSource,
|
|
6
|
-
type ChordAnalysis,
|
|
7
|
-
chordToRoman,
|
|
8
|
-
functionOf,
|
|
9
|
-
type HarmonicFunction,
|
|
10
|
-
isBorrowedChord,
|
|
11
|
-
} from '../analyze/functional/index.js';
|
|
12
|
-
import { negativeHarmonyMirror } from '../generate/reharmony/index.js';
|
|
13
|
-
import {
|
|
14
|
-
type Chord as ChordData,
|
|
15
|
-
type ChordQuality,
|
|
16
|
-
chordPitchClasses,
|
|
17
|
-
makeChord,
|
|
18
|
-
} from '../theory/chord/index.js';
|
|
19
|
-
import {
|
|
20
|
-
availableTensions,
|
|
21
|
-
avoidNotes,
|
|
22
|
-
type ChordScaleMatch,
|
|
23
|
-
chordScales,
|
|
24
|
-
} from '../theory/chordscale/index.js';
|
|
25
|
-
import { spellChord, spellChordFromRoot, spellPitchClass } from '../theory/spelling/index.js';
|
|
26
|
-
import { formatChordSymbol, parseChordSymbol } from '../theory/symbol/index.js';
|
|
27
|
-
import {
|
|
28
|
-
type StyledVoicingOptions,
|
|
29
|
-
type VoicingOptions,
|
|
30
|
-
voiceChord,
|
|
31
|
-
voiceChordStyled,
|
|
32
|
-
} from '../theory/voicing/index.js';
|
|
33
|
-
import type { Key } from './key.js';
|
|
34
|
-
import { Note } from './note.js';
|
|
35
|
-
import { Progression } from './progression.js';
|
|
36
|
-
import { mod12, spellPitchClassBare } from './shared.js';
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Defensive copy of a plain chord.
|
|
40
|
-
*
|
|
41
|
-
* Any enharmonic spelling hints (`rootSpelling`/`bassSpelling`, populated by
|
|
42
|
-
* `parseChordSymbol`) are carried through so a flat-named chord round-trips
|
|
43
|
-
* through the class API; a hint that no longer matches its pitch class is
|
|
44
|
-
* simply ignored by the formatter.
|
|
45
|
-
*/
|
|
46
|
-
function copyChord(data: ChordData): ChordData {
|
|
47
|
-
const copy: ChordData = {
|
|
48
|
-
rootPc: data.rootPc,
|
|
49
|
-
quality: data.quality,
|
|
50
|
-
intervals: [...data.intervals],
|
|
51
|
-
};
|
|
52
|
-
if (data.bassPc !== undefined) {
|
|
53
|
-
copy.bassPc = data.bassPc;
|
|
54
|
-
}
|
|
55
|
-
if (data.rootSpelling !== undefined) {
|
|
56
|
-
copy.rootSpelling = { letter: data.rootSpelling.letter, alter: data.rootSpelling.alter };
|
|
57
|
-
}
|
|
58
|
-
if (data.bassSpelling !== undefined) {
|
|
59
|
-
copy.bassSpelling = { letter: data.bassSpelling.letter, alter: data.bassSpelling.alter };
|
|
60
|
-
}
|
|
61
|
-
return copy;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* An immutable chord: a root pitch class, quality, interval template, and
|
|
66
|
-
* optional slash bass, optionally carrying a {@link Key} context. Analysis
|
|
67
|
-
* methods (`roman`, `function`, `analyze`, ...) use an explicitly passed key
|
|
68
|
-
* first and fall back to the carried context.
|
|
69
|
-
*
|
|
70
|
-
* @category Class API
|
|
71
|
-
* @example
|
|
72
|
-
* ```ts
|
|
73
|
-
* import { Chord } from '@libraz/libcantus';
|
|
74
|
-
* Chord.parse('Cmaj7').invert(1).symbol(); // 'Cmaj7/E' (third in the bass)
|
|
75
|
-
* ```
|
|
76
|
-
*/
|
|
77
|
-
export class Chord {
|
|
78
|
-
readonly #data: ChordData;
|
|
79
|
-
readonly #key: Key | undefined;
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Wrap a plain chord object.
|
|
83
|
-
*
|
|
84
|
-
* @param data The chord; it is copied, never retained or mutated.
|
|
85
|
-
* @param key Optional key context for analysis methods.
|
|
86
|
-
*/
|
|
87
|
-
constructor(data: ChordData, key?: Key) {
|
|
88
|
-
const copy = copyChord(data);
|
|
89
|
-
if (copy.rootSpelling === undefined && key !== undefined) {
|
|
90
|
-
copy.rootSpelling = spellPitchClass(copy.rootPc, key.tonic.data, key.scale);
|
|
91
|
-
}
|
|
92
|
-
this.#data = copy;
|
|
93
|
-
this.#key = key;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Build a chord from a root and quality.
|
|
98
|
-
*
|
|
99
|
-
* @param root Root as a note name (e.g. `'Eb'`) or a pitch class.
|
|
100
|
-
* @param quality The chord quality.
|
|
101
|
-
* @param bass Optional slash-chord bass pitch class.
|
|
102
|
-
* @returns The chord (without key context).
|
|
103
|
-
*/
|
|
104
|
-
static of(root: string | number, quality: ChordQuality, bass?: number): Chord {
|
|
105
|
-
if (typeof root === 'string') {
|
|
106
|
-
const rootNote = Note.of(root);
|
|
107
|
-
const data = makeChord(rootNote.pitchClass, quality, bass);
|
|
108
|
-
data.rootSpelling = { letter: rootNote.letter, alter: rootNote.alter };
|
|
109
|
-
return new Chord(data);
|
|
110
|
-
}
|
|
111
|
-
return new Chord(makeChord(root, quality, bass));
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Wrap an existing plain chord object.
|
|
116
|
-
*
|
|
117
|
-
* @param data The plain chord.
|
|
118
|
-
* @returns The wrapped chord (without key context).
|
|
119
|
-
*/
|
|
120
|
-
static from(data: ChordData): Chord {
|
|
121
|
-
return new Chord(data);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Parse a lead-sheet chord symbol (e.g. `'Cmaj7'`, `'F#m7b5'`, `'C/G'`).
|
|
126
|
-
*
|
|
127
|
-
* @param symbol The chord symbol.
|
|
128
|
-
* @returns The chord (without key context).
|
|
129
|
-
* @throws If the root or quality is not recognized.
|
|
130
|
-
*/
|
|
131
|
-
static parse(symbol: string): Chord {
|
|
132
|
-
return new Chord(parseChordSymbol(symbol));
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Identify the chords matching a set of pitches, best interpretation first.
|
|
137
|
-
*
|
|
138
|
-
* @param pitches MIDI pitches or bare pitch classes.
|
|
139
|
-
* @returns Ranked chord interpretations (may be empty).
|
|
140
|
-
*/
|
|
141
|
-
static detect(pitches: number[], opts?: DetectChordOptions): Chord[] {
|
|
142
|
-
return detectChord(pitches, opts).map(
|
|
143
|
-
(match) => new Chord(makeChord(match.rootPc, match.quality, match.bassPc)),
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* The single best chord interpretation of a pitch set.
|
|
149
|
-
*
|
|
150
|
-
* @param pitches MIDI pitches or bare pitch classes.
|
|
151
|
-
* @returns The top-ranked chord, or null when nothing matches.
|
|
152
|
-
*/
|
|
153
|
-
static detectBest(pitches: number[], opts?: DetectChordOptions): Chord | null {
|
|
154
|
-
const best = detectChordBest(pitches, opts);
|
|
155
|
-
return best === null ? null : new Chord(best);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/** The root pitch class (0..11). */
|
|
159
|
-
get rootPc(): number {
|
|
160
|
-
return this.#data.rootPc;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/** The chord quality. */
|
|
164
|
-
get quality(): ChordQuality {
|
|
165
|
-
return this.#data.quality;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/** A copy of the semitone offsets above the root. */
|
|
169
|
-
get intervals(): number[] {
|
|
170
|
-
return [...this.#data.intervals];
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/** The slash-chord bass pitch class, or undefined in root position. */
|
|
174
|
-
get bassPc(): number | undefined {
|
|
175
|
-
return this.#data.bassPc;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/** A copy of the underlying plain chord object. */
|
|
179
|
-
get data(): ChordData {
|
|
180
|
-
return copyChord(this.#data);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/** The carried key context, if any. */
|
|
184
|
-
get key(): Key | undefined {
|
|
185
|
-
return this.#key;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* A copy of this chord carrying the given key context.
|
|
190
|
-
*
|
|
191
|
-
* @param key The key context to attach.
|
|
192
|
-
* @returns The new chord.
|
|
193
|
-
*/
|
|
194
|
-
withKey(key: Key): Chord {
|
|
195
|
-
return new Chord(this.#data, key);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* The chord's sorted, deduplicated pitch classes.
|
|
200
|
-
*
|
|
201
|
-
* @returns Pitch classes ascending in [0, 11].
|
|
202
|
-
*/
|
|
203
|
-
pitchClasses(): number[] {
|
|
204
|
-
return chordPitchClasses(this.#data);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* The chord's Roman numeral in a key.
|
|
209
|
-
*
|
|
210
|
-
* @param key Key to analyze in; falls back to the carried context.
|
|
211
|
-
* @returns The Roman numeral string.
|
|
212
|
-
* @throws If no key is given and none is carried.
|
|
213
|
-
*/
|
|
214
|
-
roman(key?: Key): string {
|
|
215
|
-
return chordToRoman(this.#data, this.#resolveKey(key).scale);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* The chord's harmonic function (tonic / subdominant / dominant) in a key.
|
|
220
|
-
*
|
|
221
|
-
* @param key Key to analyze in; falls back to the carried context.
|
|
222
|
-
* @returns The harmonic function.
|
|
223
|
-
* @throws If no key is given and none is carried.
|
|
224
|
-
*/
|
|
225
|
-
function(key?: Key): HarmonicFunction {
|
|
226
|
-
return functionOf(this.#data, this.#resolveKey(key).scale);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Full functional analysis: function, borrowing, and Roman numeral.
|
|
231
|
-
*
|
|
232
|
-
* @param key Key to analyze in; falls back to the carried context.
|
|
233
|
-
* @returns The chord analysis.
|
|
234
|
-
* @throws If no key is given and none is carried.
|
|
235
|
-
*/
|
|
236
|
-
analyze(key?: Key): ChordAnalysis {
|
|
237
|
-
return analyzeChord(this.#data, this.#resolveKey(key).scale);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Whether the chord is borrowed from the parallel mode (modal interchange).
|
|
242
|
-
*
|
|
243
|
-
* @param key Key to analyze in; falls back to the carried context.
|
|
244
|
-
* @returns True if the chord is borrowed.
|
|
245
|
-
* @throws If no key is given and none is carried.
|
|
246
|
-
*/
|
|
247
|
-
isBorrowed(key?: Key): boolean {
|
|
248
|
-
return isBorrowedChord(this.#data, this.#resolveKey(key).scale);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* The origin of a non-diatonic chord (parallel mode or Neapolitan), or null.
|
|
253
|
-
*
|
|
254
|
-
* @param key Key to analyze in; falls back to the carried context.
|
|
255
|
-
* @returns The borrowing source, or null.
|
|
256
|
-
* @throws If no key is given and none is carried.
|
|
257
|
-
*/
|
|
258
|
-
borrowedSource(key?: Key): BorrowedSource {
|
|
259
|
-
return borrowedSource(this.#data, this.#resolveKey(key).scale);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* The chord rendered as a lead-sheet symbol (e.g. `'Cmaj7'`, `'F#m7'`, `'C/G'`).
|
|
264
|
-
*
|
|
265
|
-
* @param opts Set `flats: true` to spell the root/bass with flats.
|
|
266
|
-
* @returns The chord symbol.
|
|
267
|
-
*/
|
|
268
|
-
symbol(opts?: { flats?: boolean }): string {
|
|
269
|
-
return formatChordSymbol(this.#data, opts);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Realize the chord as one MIDI pitch per voice, ascending.
|
|
274
|
-
*
|
|
275
|
-
* @param opts Voicing options; defaults to four SATB voices.
|
|
276
|
-
* @returns MIDI pitches, ascending, one per voice.
|
|
277
|
-
* @throws If no voicing fits the given ranges.
|
|
278
|
-
*/
|
|
279
|
-
voice(opts?: VoicingOptions): number[] {
|
|
280
|
-
return voiceChord(this.#data, opts);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* Realize the chord as a single styled voicing (`close`, `drop2`, `drop3`,
|
|
285
|
-
* `shell`, or `rootless`), optionally constraining the top voice.
|
|
286
|
-
*
|
|
287
|
-
* @param opts Styled-voicing options; defaults to a close-position voicing.
|
|
288
|
-
* @returns MIDI pitches, ascending.
|
|
289
|
-
*/
|
|
290
|
-
styledVoicing(opts?: StyledVoicingOptions): number[] {
|
|
291
|
-
return voiceChordStyled(this.#data, opts);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* The negative-harmony mirror of the chord about the key's tonic–dominant
|
|
296
|
-
* axis (major becomes minor and vice versa).
|
|
297
|
-
*
|
|
298
|
-
* @param key Key providing the reflection axis; falls back to the carried
|
|
299
|
-
* context.
|
|
300
|
-
* @returns The mirrored chord, keeping any key context.
|
|
301
|
-
* @throws If no key is given and none is carried.
|
|
302
|
-
*/
|
|
303
|
-
negativeHarmony(key?: Key): Chord {
|
|
304
|
-
const resolved = this.#resolveKey(key);
|
|
305
|
-
// Retain the key that anchored the reflection (explicit first, then carried)
|
|
306
|
-
// so a later no-arg analysis method still has a key context.
|
|
307
|
-
return new Chord(negativeHarmonyMirror(this.#data, resolved.scale), key ?? this.#key);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* The n-th inversion: a copy whose bass is the chord tone `n` steps above the
|
|
312
|
-
* root in the interval template (`invert(1)` puts the third in the bass).
|
|
313
|
-
* `n` wraps around the template length; negative values count backwards.
|
|
314
|
-
*
|
|
315
|
-
* `invert(0)` (and any `n` that wraps to it) is root position, so it carries
|
|
316
|
-
* no slash bass and equals the original chord.
|
|
317
|
-
*
|
|
318
|
-
* @param n The inversion number.
|
|
319
|
-
* @returns The inverted chord, keeping any key context.
|
|
320
|
-
* @throws If the chord has no intervals.
|
|
321
|
-
*/
|
|
322
|
-
invert(n: number): Chord {
|
|
323
|
-
const intervals = this.#data.intervals;
|
|
324
|
-
const length = intervals.length;
|
|
325
|
-
if (length === 0) {
|
|
326
|
-
throw new Error('cannot invert a chord with no intervals');
|
|
327
|
-
}
|
|
328
|
-
const index = ((n % length) + length) % length;
|
|
329
|
-
const data = copyChord(this.#data);
|
|
330
|
-
if (index === 0) {
|
|
331
|
-
delete data.bassPc;
|
|
332
|
-
delete data.bassSpelling;
|
|
333
|
-
} else {
|
|
334
|
-
data.bassPc = mod12(this.#data.rootPc + (intervals[index] ?? 0));
|
|
335
|
-
const rootSpelling =
|
|
336
|
-
data.rootSpelling ??
|
|
337
|
-
(this.#key
|
|
338
|
-
? spellPitchClass(data.rootPc, this.#key.tonic.data, this.#key.scale)
|
|
339
|
-
: spellPitchClassBare(data.rootPc, 'sharp'));
|
|
340
|
-
const bassSpelling = spellChordFromRoot(data, rootSpelling)[index];
|
|
341
|
-
if (bassSpelling !== undefined) {
|
|
342
|
-
data.bassSpelling = bassSpelling;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
return new Chord(data, this.#key);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
/**
|
|
349
|
-
* The named scales that fit over this chord, best fit first, rooted on the
|
|
350
|
-
* chord root.
|
|
351
|
-
*
|
|
352
|
-
* @returns The matching scales.
|
|
353
|
-
*/
|
|
354
|
-
scales(): ChordScaleMatch[] {
|
|
355
|
-
return chordScales(this.#data);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
/**
|
|
359
|
-
* The available tensions (usable non-chord, non-avoid scale tones) of a
|
|
360
|
-
* scale over this chord.
|
|
361
|
-
*
|
|
362
|
-
* @param scaleName A named scale, rooted on the chord root.
|
|
363
|
-
* @returns Tension pitch classes, ascending in [0, 11].
|
|
364
|
-
*/
|
|
365
|
-
tensions(scaleName: string): number[] {
|
|
366
|
-
return availableTensions(this.#data, scaleName);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* The avoid notes (scale tones a semitone above a chord tone) of a scale
|
|
371
|
-
* over this chord.
|
|
372
|
-
*
|
|
373
|
-
* @param scaleName A named scale, rooted on the chord root.
|
|
374
|
-
* @returns Avoid-note pitch classes, ascending in [0, 11].
|
|
375
|
-
*/
|
|
376
|
-
avoidNotes(scaleName: string): number[] {
|
|
377
|
-
return avoidNotes(this.#data, scaleName);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/**
|
|
381
|
-
* Spell the chord tones with letter names, root first, in the key's spelling.
|
|
382
|
-
*
|
|
383
|
-
* @param key Key providing the spelled tonic; falls back to the carried
|
|
384
|
-
* context.
|
|
385
|
-
* @returns Spelled octave-less notes in the chord's own (tertian) order.
|
|
386
|
-
* @throws If no key is given and none is carried.
|
|
387
|
-
*/
|
|
388
|
-
spell(key?: Key): Note[] {
|
|
389
|
-
const resolved = this.#resolveKey(key);
|
|
390
|
-
return spellChord(this.#data, resolved.tonic.data, resolved.scale).map(
|
|
391
|
-
(note) => new Note(note),
|
|
392
|
-
);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
/**
|
|
396
|
-
* Start a progression with this chord followed by others, carrying this
|
|
397
|
-
* chord's key context (if any).
|
|
398
|
-
*
|
|
399
|
-
* @param others The chords following this one.
|
|
400
|
-
* @returns The progression.
|
|
401
|
-
*/
|
|
402
|
-
progressionTo(...others: Chord[]): Progression {
|
|
403
|
-
return new Progression([this, ...others], this.#key);
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
/**
|
|
407
|
-
* Whether another chord has the same root, quality, intervals, and bass.
|
|
408
|
-
* Key context is not compared.
|
|
409
|
-
*
|
|
410
|
-
* @param other The chord to compare.
|
|
411
|
-
* @returns True if the chord data is identical.
|
|
412
|
-
*/
|
|
413
|
-
equals(other: Chord): boolean {
|
|
414
|
-
const a = this.#data;
|
|
415
|
-
const b = other.#data;
|
|
416
|
-
return (
|
|
417
|
-
a.rootPc === b.rootPc &&
|
|
418
|
-
a.quality === b.quality &&
|
|
419
|
-
a.bassPc === b.bassPc &&
|
|
420
|
-
a.intervals.length === b.intervals.length &&
|
|
421
|
-
a.intervals.every((interval, i) => interval === b.intervals[i])
|
|
422
|
-
);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
/**
|
|
426
|
-
* The plain chord data, for JSON serialization.
|
|
427
|
-
*
|
|
428
|
-
* @returns A copy of the underlying plain chord object.
|
|
429
|
-
*/
|
|
430
|
-
toJSON(): ChordData {
|
|
431
|
-
return this.data;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
/** Resolve the key for an analysis method: explicit first, then carried. */
|
|
435
|
-
#resolveKey(key?: Key): Key {
|
|
436
|
-
const resolved = key ?? this.#key;
|
|
437
|
-
if (resolved === undefined) {
|
|
438
|
-
throw new Error('chord has no key context; pass a Key or attach one with withKey()');
|
|
439
|
-
}
|
|
440
|
-
return resolved;
|
|
441
|
-
}
|
|
442
|
-
}
|
package/src/model/index.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Fluent, immutable object model over the functional core.
|
|
3
|
-
*
|
|
4
|
-
* Each class wraps one of the library's plain data types (spelled notes,
|
|
5
|
-
* chords, key/scales) and delegates every operation to the existing pure
|
|
6
|
-
* functions. All instances are immutable: fields are read-only, transforming
|
|
7
|
-
* methods return new instances, and getters hand out defensive copies of any
|
|
8
|
-
* mutable data.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
export { Chord } from './chord.js';
|
|
12
|
-
export { Interval } from './interval.js';
|
|
13
|
-
export { Key } from './key.js';
|
|
14
|
-
export { Note } from './note.js';
|
|
15
|
-
export { Progression } from './progression.js';
|