@libraz/libcantus 0.9.4 → 1.0.0
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 +61 -261
- package/README_ja.md +115 -0
- package/dist/analyze/index.cjs +54 -7
- package/dist/analyze/index.cjs.map +1 -1
- package/dist/analyze/index.d.cts +213 -456
- package/dist/analyze/index.d.ts +213 -456
- package/dist/analyze/index.js +56 -9
- package/dist/chunk-3JXCW6DX.cjs +3220 -0
- package/dist/chunk-3JXCW6DX.cjs.map +1 -0
- package/dist/chunk-4A2RMUCC.js +7647 -0
- package/dist/chunk-4A2RMUCC.js.map +1 -0
- package/dist/chunk-6XPTBE7Y.cjs +49 -0
- package/dist/chunk-6XPTBE7Y.cjs.map +1 -0
- package/dist/chunk-AFM2ULJH.cjs +5121 -0
- package/dist/chunk-AFM2ULJH.cjs.map +1 -0
- package/dist/chunk-E3F4AHAH.cjs +690 -0
- package/dist/chunk-E3F4AHAH.cjs.map +1 -0
- package/dist/chunk-ELCTZUJJ.js +379 -0
- package/dist/chunk-ELCTZUJJ.js.map +1 -0
- package/dist/chunk-FVSI36UV.js +690 -0
- package/dist/chunk-FVSI36UV.js.map +1 -0
- package/dist/chunk-GEDTJHAC.cjs +1 -0
- package/dist/chunk-GEDTJHAC.cjs.map +1 -0
- package/dist/chunk-GOZG57ER.js +3220 -0
- package/dist/chunk-GOZG57ER.js.map +1 -0
- package/dist/chunk-H3VORETI.js +2243 -0
- package/dist/chunk-H3VORETI.js.map +1 -0
- package/dist/chunk-H42UHPYW.js +1577 -0
- package/dist/chunk-H42UHPYW.js.map +1 -0
- package/dist/chunk-HFPS7JFD.js +1 -0
- package/dist/chunk-INHVXP6Z.js +3053 -0
- package/dist/chunk-INHVXP6Z.js.map +1 -0
- package/dist/chunk-K4ZJUGF4.cjs +379 -0
- package/dist/chunk-K4ZJUGF4.cjs.map +1 -0
- package/dist/chunk-KNJEKLKV.js +49 -0
- package/dist/chunk-KNJEKLKV.js.map +1 -0
- package/dist/chunk-M4CWTJWJ.cjs +7647 -0
- package/dist/chunk-M4CWTJWJ.cjs.map +1 -0
- package/dist/chunk-MXO72PLY.cjs +211 -0
- package/dist/chunk-MXO72PLY.cjs.map +1 -0
- package/dist/chunk-NLEU4QSE.cjs +360 -0
- package/dist/chunk-NLEU4QSE.cjs.map +1 -0
- package/dist/chunk-OE6WK2Y2.cjs +1577 -0
- package/dist/chunk-OE6WK2Y2.cjs.map +1 -0
- package/dist/chunk-Q2WGKJNN.cjs +1064 -0
- package/dist/chunk-Q2WGKJNN.cjs.map +1 -0
- package/dist/chunk-QYQQ5J6E.js +1 -0
- package/dist/chunk-QYQQ5J6E.js.map +1 -0
- package/dist/chunk-RHIJ3S4I.cjs +1 -0
- package/dist/chunk-RHIJ3S4I.cjs.map +1 -0
- package/dist/chunk-TTCLJX6D.js +211 -0
- package/dist/chunk-TTCLJX6D.js.map +1 -0
- package/dist/chunk-UADYAYAT.cjs +3053 -0
- package/dist/chunk-UADYAYAT.cjs.map +1 -0
- package/dist/chunk-VOHZB45S.js +5121 -0
- package/dist/chunk-VOHZB45S.js.map +1 -0
- package/dist/chunk-VTI6FOLB.js +1064 -0
- package/dist/chunk-VTI6FOLB.js.map +1 -0
- package/dist/chunk-WWURE4XT.cjs +2243 -0
- package/dist/chunk-WWURE4XT.cjs.map +1 -0
- package/dist/chunk-YH3BIKHH.js +360 -0
- package/dist/chunk-YH3BIKHH.js.map +1 -0
- package/dist/core/index.cjs +103 -6
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +96 -345
- package/dist/core/index.d.ts +96 -345
- package/dist/core/index.js +104 -7
- package/dist/generate/index.cjs +89 -10
- package/dist/generate/index.cjs.map +1 -1
- package/dist/generate/index.d.cts +437 -1095
- package/dist/generate/index.d.ts +437 -1095
- package/dist/generate/index.js +94 -15
- package/dist/index-9wcMhG73.d.ts +1095 -0
- package/dist/{index-D6b6na69.d.cts → index-B4o55NDr.d.cts} +13 -9
- package/dist/index-B9MNZboQ.d.cts +770 -0
- package/dist/index-B9MNZboQ.d.ts +770 -0
- package/dist/index-BGFW-8P2.d.cts +2307 -0
- package/dist/index-BO6xE9YU.d.cts +1804 -0
- package/dist/index-BkGmlyI3.d.cts +470 -0
- package/dist/index-CCNJRG-P.d.cts +1095 -0
- package/dist/index-CGpRv5sl.d.cts +643 -0
- package/dist/index-C_8o--VE.d.cts +1242 -0
- package/dist/index-CgHqxdps.d.ts +2307 -0
- package/dist/index-Chbst3Jx.d.ts +1343 -0
- package/dist/index-Cj49F892.d.ts +470 -0
- package/dist/{index-tSQh-xwY.d.ts → index-CunYKLiu.d.ts} +13 -9
- package/dist/index-D57QRbVn.d.ts +1804 -0
- package/dist/index-DInQqT8Q.d.cts +66 -0
- package/dist/index-DInQqT8Q.d.ts +66 -0
- package/dist/index-Dzr13q3d.d.cts +1343 -0
- package/dist/index-G1uwGGWQ.d.ts +643 -0
- package/dist/index-szBv3496.d.ts +1242 -0
- package/dist/index.cjs +358 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -15
- package/dist/index.d.ts +17 -15
- package/dist/index.js +407 -64
- package/dist/masks-DYbE485J.d.cts +445 -0
- package/dist/masks-DYbE485J.d.ts +445 -0
- package/dist/model/index.cjs +39 -7
- package/dist/model/index.cjs.map +1 -1
- package/dist/model/index.d.cts +4685 -243
- package/dist/model/index.d.ts +4685 -243
- package/dist/model/index.js +40 -8
- package/dist/positional-Cptlj-xs.d.ts +287 -0
- package/dist/positional-DvH_sd8u.d.cts +287 -0
- package/dist/theory/index.cjs +101 -7
- package/dist/theory/index.cjs.map +1 -1
- package/dist/theory/index.d.cts +513 -173
- package/dist/theory/index.d.ts +513 -173
- package/dist/theory/index.js +118 -24
- package/package.json +16 -11
- package/dist/chunk-36N73KZ3.cjs +0 -246
- package/dist/chunk-36N73KZ3.cjs.map +0 -1
- package/dist/chunk-4HLLZPXT.cjs +0 -3077
- package/dist/chunk-4HLLZPXT.cjs.map +0 -1
- package/dist/chunk-7MGZ7D62.cjs +0 -498
- package/dist/chunk-7MGZ7D62.cjs.map +0 -1
- package/dist/chunk-7NKDPF74.js +0 -3077
- package/dist/chunk-7NKDPF74.js.map +0 -1
- package/dist/chunk-CAN76NCU.cjs +0 -679
- package/dist/chunk-CAN76NCU.cjs.map +0 -1
- package/dist/chunk-D2FAJBYL.js +0 -479
- package/dist/chunk-D2FAJBYL.js.map +0 -1
- package/dist/chunk-EHVEBWM5.js +0 -1449
- package/dist/chunk-EHVEBWM5.js.map +0 -1
- package/dist/chunk-FCANXBAT.cjs +0 -30
- package/dist/chunk-FCANXBAT.cjs.map +0 -1
- package/dist/chunk-FNDWXRRI.cjs +0 -479
- package/dist/chunk-FNDWXRRI.cjs.map +0 -1
- package/dist/chunk-GQSGS2N4.js +0 -173
- package/dist/chunk-GQSGS2N4.js.map +0 -1
- package/dist/chunk-HKGYCQ34.js +0 -53
- package/dist/chunk-HKGYCQ34.js.map +0 -1
- package/dist/chunk-I24MV65U.js +0 -106
- package/dist/chunk-I24MV65U.js.map +0 -1
- package/dist/chunk-KMHOICIY.js +0 -679
- package/dist/chunk-KMHOICIY.js.map +0 -1
- package/dist/chunk-LW53AHQU.cjs +0 -1117
- package/dist/chunk-LW53AHQU.cjs.map +0 -1
- package/dist/chunk-N6IHQUOX.js +0 -1
- package/dist/chunk-NKIBQ6PT.js +0 -30
- package/dist/chunk-NKIBQ6PT.js.map +0 -1
- package/dist/chunk-NP4NA2FB.cjs +0 -106
- package/dist/chunk-NP4NA2FB.cjs.map +0 -1
- package/dist/chunk-NVE34YGZ.cjs +0 -1449
- package/dist/chunk-NVE34YGZ.cjs.map +0 -1
- package/dist/chunk-NYYWMV2C.js +0 -327
- package/dist/chunk-NYYWMV2C.js.map +0 -1
- package/dist/chunk-Q2XUP227.js +0 -845
- package/dist/chunk-Q2XUP227.js.map +0 -1
- package/dist/chunk-Q6OP4U43.cjs +0 -327
- package/dist/chunk-Q6OP4U43.cjs.map +0 -1
- package/dist/chunk-RC6GBMCD.cjs +0 -845
- package/dist/chunk-RC6GBMCD.cjs.map +0 -1
- package/dist/chunk-RK35RIN7.js +0 -1117
- package/dist/chunk-RK35RIN7.js.map +0 -1
- package/dist/chunk-RUMU3UA6.cjs +0 -53
- package/dist/chunk-RUMU3UA6.cjs.map +0 -1
- package/dist/chunk-T64C7ZD7.cjs +0 -1
- package/dist/chunk-T64C7ZD7.cjs.map +0 -1
- package/dist/chunk-XEBG3G53.js +0 -498
- package/dist/chunk-XEBG3G53.js.map +0 -1
- package/dist/chunk-YLEMGEFH.js +0 -246
- package/dist/chunk-YLEMGEFH.js.map +0 -1
- package/dist/chunk-ZEJR4TKL.cjs +0 -173
- package/dist/chunk-ZEJR4TKL.cjs.map +0 -1
- package/dist/function-BtiZb9i9.d.ts +0 -208
- package/dist/function-DNugAOrt.d.cts +0 -208
- package/dist/index-6VR8-kit.d.ts +0 -143
- package/dist/index-70p168VM.d.ts +0 -192
- package/dist/index-Be74mcon.d.cts +0 -152
- package/dist/index-BtXT6UQ0.d.cts +0 -192
- package/dist/index-C5UBbQ4G.d.ts +0 -152
- package/dist/index-C83mZI1P.d.ts +0 -242
- package/dist/index-DXd9JikN.d.cts +0 -246
- package/dist/index-DXd9JikN.d.ts +0 -246
- package/dist/index-OG65ocOr.d.cts +0 -272
- package/dist/index-OG65ocOr.d.ts +0 -272
- package/dist/index-tJhkmQgE.d.cts +0 -143
- package/dist/index-y_ak9_Hi.d.cts +0 -242
- package/dist/styled-DIRkk9Uz.d.ts +0 -506
- package/dist/styled-DJbkJQfj.d.cts +0 -506
- package/dist/types-BN6_Q9Kl.d.cts +0 -39
- package/dist/types-BN6_Q9Kl.d.ts +0 -39
- /package/dist/{chunk-N6IHQUOX.js.map → chunk-HFPS7JFD.js.map} +0 -0
|
@@ -0,0 +1,1804 @@
|
|
|
1
|
+
import { d as KeyScale, g as NoteEvent, S as SpelledInterval } from './index-B9MNZboQ.js';
|
|
2
|
+
import { d as ChordQuality, b as Chord, N as NoteSafety, S as SafetyProfile } from './index-szBv3496.js';
|
|
3
|
+
import { r as ScaleName } from './masks-DYbE485J.js';
|
|
4
|
+
import { i as KeyProfileName, j as KeyProfilePair, R as RejectedCandidate, k as KeyRegion, e as ChordTimeline, C as CadenceHit, a as AnalyzedNote, T as TheoryLabel, K as KeyContext, b as CadenceResult } from './index-Chbst3Jx.js';
|
|
5
|
+
import { T as TimeSignature, c as MeterMap, b as MeterLike } from './index-Cj49F892.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Church-mode candidates for {@link detectKey}.
|
|
9
|
+
*
|
|
10
|
+
* A mode is not a key with a different scale bolted on: it is the same tonal
|
|
11
|
+
* hierarchy — tonic strongest, dominant next, the remaining diatonic degrees
|
|
12
|
+
* behind them — heard over a scale in which one degree has been replaced. Its
|
|
13
|
+
* profile is therefore derived from the profile of the major or minor key it
|
|
14
|
+
* sits closest to, rather than being a table of its own.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A church mode {@link detectKey} can rank alongside the major and minor keys.
|
|
19
|
+
*
|
|
20
|
+
* Ionian and Aeolian are absent because they are the major and the natural minor
|
|
21
|
+
* scale: a piece in either is already reported by the plain major or minor
|
|
22
|
+
* candidate, and adding a second candidate over the same pitch classes and the
|
|
23
|
+
* same profile would only produce a tie with itself.
|
|
24
|
+
*
|
|
25
|
+
* @category Recognition
|
|
26
|
+
*/
|
|
27
|
+
type ModalScaleName = 'dorian' | 'phrygian' | 'lydian' | 'mixolydian' | 'locrian';
|
|
28
|
+
/**
|
|
29
|
+
* How a modal candidate is built out of its parallel major or minor key.
|
|
30
|
+
*
|
|
31
|
+
* @category Recognition
|
|
32
|
+
*/
|
|
33
|
+
type ModalCandidate = {
|
|
34
|
+
/** The name reported as {@link KeyMatch.scaleName} when this candidate wins. */
|
|
35
|
+
scaleName: ModalScaleName;
|
|
36
|
+
/** The major or minor key this mode is closest to, reported as `mode`. */
|
|
37
|
+
mode: 'major' | 'minor';
|
|
38
|
+
/** The 12-bit scale mask of the mode. */
|
|
39
|
+
mask: number;
|
|
40
|
+
/**
|
|
41
|
+
* Degree pairs the mode substitutes, as `[replaced, replacing]` semitone
|
|
42
|
+
* offsets above the tonic. Applying them to the parallel scale yields `mask`,
|
|
43
|
+
* and applying them to the parallel profile yields this candidate's profile.
|
|
44
|
+
*/
|
|
45
|
+
substitutions: readonly (readonly [number, number])[];
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Every mode {@link DetectKeyOptions.modes} accepts, in candidate order.
|
|
49
|
+
*
|
|
50
|
+
* @category Recognition
|
|
51
|
+
*/
|
|
52
|
+
declare const MODAL_SCALE_NAMES: readonly ModalScaleName[];
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Recognition: infer a chord name from a set of pitches, or a key from a set of
|
|
56
|
+
* pitch classes. This is the inverse direction of the chord/scale builders,
|
|
57
|
+
* which only go name -> notes.
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A candidate chord interpretation of a pitch set.
|
|
62
|
+
*
|
|
63
|
+
* @category Recognition
|
|
64
|
+
*/
|
|
65
|
+
type ChordMatch = {
|
|
66
|
+
rootPc: number;
|
|
67
|
+
quality: ChordQuality;
|
|
68
|
+
/** Chord tones absent from the input (an incomplete voicing). */
|
|
69
|
+
missingPcs: number[];
|
|
70
|
+
/** Input pitch classes not belonging to the chord. */
|
|
71
|
+
extraPcs: number[];
|
|
72
|
+
/** True when the input pitch-class set equals the chord exactly. */
|
|
73
|
+
exact: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Inversion implied by a known bass: 0 for root position, 1..n for an
|
|
76
|
+
* inversion. Null when no inversion can be named — either the input is an
|
|
77
|
+
* unordered pitch-class set with no bass, or the bass is not a chord tone (a
|
|
78
|
+
* pedal or passing bass), in which case `bassPc` still reports it.
|
|
79
|
+
*/
|
|
80
|
+
inversion: number | null;
|
|
81
|
+
/** Bass pitch class when the lowest note is not the root. */
|
|
82
|
+
bassPc?: number;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Input interpretation for {@link detectChord} and {@link detectChordBest}.
|
|
86
|
+
*
|
|
87
|
+
* @category Recognition
|
|
88
|
+
*/
|
|
89
|
+
type DetectChordOptions = {
|
|
90
|
+
/**
|
|
91
|
+
* `midi` uses the numerically lowest pitch as bass; `pitchClass` treats the
|
|
92
|
+
* input as unordered. `auto` (default) selects pitch-class mode only when all
|
|
93
|
+
* values lie in 0..11.
|
|
94
|
+
*/
|
|
95
|
+
input?: 'auto' | 'midi' | 'pitchClass';
|
|
96
|
+
/** Explicit bass pitch class, including for an unordered pitch-class set. */
|
|
97
|
+
bassPc?: number;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* A candidate key interpretation of a pitch-class set.
|
|
101
|
+
*
|
|
102
|
+
* @category Recognition
|
|
103
|
+
*/
|
|
104
|
+
type KeyMatch = {
|
|
105
|
+
/** The scale that scored best for this tonic and mode, `variant` included. */
|
|
106
|
+
key: KeyScale;
|
|
107
|
+
/**
|
|
108
|
+
* The major or minor key this match is closest to. A modal match reports the
|
|
109
|
+
* mode it leans on — dorian, phrygian and locrian are minor, lydian and
|
|
110
|
+
* mixolydian major — so a caller that reads nothing but `mode` still gets a
|
|
111
|
+
* musically sane answer; `scaleName` is where the mode itself is named.
|
|
112
|
+
*/
|
|
113
|
+
mode: 'major' | 'minor';
|
|
114
|
+
/**
|
|
115
|
+
* Which scale form `key` uses. Minor keys report whichever of the natural,
|
|
116
|
+
* harmonic and melodic masks covers the most input weight; the variant does
|
|
117
|
+
* not take part in ranking. A church mode reports `'modal'`, since its scale
|
|
118
|
+
* is neither of the major nor one of the three minor forms.
|
|
119
|
+
*/
|
|
120
|
+
variant: KeyVariant;
|
|
121
|
+
/**
|
|
122
|
+
* The entry of `NAMED_SCALES` whose mask is `key.modeMask12`, naming the scale
|
|
123
|
+
* exactly where `mode` and `variant` only place it: `'major'`,
|
|
124
|
+
* `'naturalMinor'`, `'harmonicMinor'`, `'melodicMinor'`, or one of the church
|
|
125
|
+
* modes when {@link DetectKeyOptions.modes} put them in the running. Ionian
|
|
126
|
+
* and Aeolian are reported as `'major'` and `'naturalMinor'`, the names of the
|
|
127
|
+
* candidates they are the same scale as.
|
|
128
|
+
*/
|
|
129
|
+
scaleName: ScaleName;
|
|
130
|
+
/**
|
|
131
|
+
* Fraction of the distinct input pitch classes that belong to `key`, in
|
|
132
|
+
* [0, 1]. Measured against the returned scale, so it always agrees with
|
|
133
|
+
* `isScaleTone(pc, match.key)`. This is a coverage figure, not the ranking:
|
|
134
|
+
* see `score`.
|
|
135
|
+
*/
|
|
136
|
+
fit: number;
|
|
137
|
+
/**
|
|
138
|
+
* The value the results are ranked by, in [-1, 1]: the Pearson correlation
|
|
139
|
+
* between the weighted pitch-class distribution of the input and this
|
|
140
|
+
* candidate's key profile rotated onto its tonic. 1 is a distribution shaped
|
|
141
|
+
* exactly like the profile, 0 no relationship, negative an anti-correlation.
|
|
142
|
+
* Unlike `fit` it is a distribution measure, not a membership count, so where
|
|
143
|
+
* the weight falls among the scale degrees is what separates a key from its
|
|
144
|
+
* relative. Always a finite number; see {@link DetectKeyOptions.profile} for
|
|
145
|
+
* the degenerate case.
|
|
146
|
+
*/
|
|
147
|
+
score: number;
|
|
148
|
+
/**
|
|
149
|
+
* Why this candidate scored what it did, in the phrasing
|
|
150
|
+
* {@link analyzeVoice} uses for a note.
|
|
151
|
+
*
|
|
152
|
+
* Present only when {@link DetectKeyOptions.explain} asked for it: a ranking
|
|
153
|
+
* is 24 candidates before the modes are counted, and most callers read the
|
|
154
|
+
* first one or two.
|
|
155
|
+
*/
|
|
156
|
+
rationale?: string;
|
|
157
|
+
/**
|
|
158
|
+
* The candidates that were considered and rejected, and why each ranked
|
|
159
|
+
* below this one.
|
|
160
|
+
*
|
|
161
|
+
* Carried by the top-ranked match alone — the rivals of a runner-up are the
|
|
162
|
+
* rest of the list, which the caller already holds — and only when
|
|
163
|
+
* {@link DetectKeyOptions.explain} asked for it.
|
|
164
|
+
*/
|
|
165
|
+
alternatives?: RejectedCandidate[];
|
|
166
|
+
};
|
|
167
|
+
/**
|
|
168
|
+
* Which form of a scale a {@link KeyMatch} settled on.
|
|
169
|
+
*
|
|
170
|
+
* @category Recognition
|
|
171
|
+
*/
|
|
172
|
+
type KeyVariant = 'major' | 'natural' | 'harmonic' | 'melodic' | 'modal';
|
|
173
|
+
/**
|
|
174
|
+
* Input weighting for {@link detectKey}.
|
|
175
|
+
*
|
|
176
|
+
* @category Recognition
|
|
177
|
+
*/
|
|
178
|
+
type DetectKeyOptions = {
|
|
179
|
+
/**
|
|
180
|
+
* How much each pitch counts toward the histogram, one entry per pitch.
|
|
181
|
+
* Defaults to one per pitch, which weighs a thirty-second-note ornament as
|
|
182
|
+
* heavily as the whole note under it. {@link detectKeyFromNotes} supplies
|
|
183
|
+
* duration times velocity, matching how chord inference weighs its own
|
|
184
|
+
* histogram.
|
|
185
|
+
*/
|
|
186
|
+
weights?: readonly number[];
|
|
187
|
+
/**
|
|
188
|
+
* Which key profile ranks the candidates: the 12 degree weights a key is
|
|
189
|
+
* expected to distribute its music over, correlated against the distribution
|
|
190
|
+
* actually observed.
|
|
191
|
+
*
|
|
192
|
+
* `'krumhansl'` (the default) uses the Krumhansl–Kessler probe-tone ratings,
|
|
193
|
+
* `'temperley'` the Kostka–Payne corpus proportions, and `'flat'` a flat
|
|
194
|
+
* profile over the scale, which reduces the ranking to plain scale membership
|
|
195
|
+
* — the behaviour this detector had before profile correlation, kept as a way
|
|
196
|
+
* back for a caller that depends on it. A custom pair supplies two 12-entry
|
|
197
|
+
* vectors indexed from the tonic (index 0 = tonic, 1 = flat second, and so on
|
|
198
|
+
* to 11 = major seventh).
|
|
199
|
+
*
|
|
200
|
+
* When either the distribution or the rotated profile has zero variance the
|
|
201
|
+
* correlation is undefined; such a candidate is scored by the normalized dot
|
|
202
|
+
* product (cosine similarity) of the same two vectors instead, so a result
|
|
203
|
+
* never carries NaN.
|
|
204
|
+
*
|
|
205
|
+
* @defaultValue `'krumhansl'`
|
|
206
|
+
*/
|
|
207
|
+
profile?: KeyProfileName | KeyProfilePair;
|
|
208
|
+
/**
|
|
209
|
+
* Whether the church modes join the 24 major and minor keys as candidates.
|
|
210
|
+
*
|
|
211
|
+
* Off by default, because a modal candidate can only change a ranking by
|
|
212
|
+
* outranking something: a caller working in common-practice repertoire, where
|
|
213
|
+
* a raised sixth over a minor tonic is a melodic-minor inflection rather than
|
|
214
|
+
* a dorian tonality, should keep reading the answer it reads today. Turn it on
|
|
215
|
+
* (`true` for all five, or a list to name the modes a repertoire actually
|
|
216
|
+
* uses) and each enabled mode adds one candidate per tonic, ranked on a
|
|
217
|
+
* profile derived from its parallel major or minor key.
|
|
218
|
+
*
|
|
219
|
+
* Ionian and Aeolian are not on the list: they are the major and natural minor
|
|
220
|
+
* candidates, which already carry `scaleName` `'major'` and `'naturalMinor'`.
|
|
221
|
+
*
|
|
222
|
+
* @defaultValue `false`
|
|
223
|
+
*/
|
|
224
|
+
modes?: boolean | readonly ModalScaleName[];
|
|
225
|
+
/**
|
|
226
|
+
* Attach a {@link KeyMatch.rationale} to every candidate, and
|
|
227
|
+
* {@link KeyMatch.alternatives} to the top-ranked one.
|
|
228
|
+
*
|
|
229
|
+
* Off by default, and the one explanation in the library that is: the other
|
|
230
|
+
* entry points explain a single reading, while a ranking explains 24 of them
|
|
231
|
+
* (36 or more with the modes on), each needing a phrase built for a candidate
|
|
232
|
+
* nobody asked about. That cost outweighs the detection itself, so it is paid
|
|
233
|
+
* only on request.
|
|
234
|
+
*
|
|
235
|
+
* @defaultValue false
|
|
236
|
+
*/
|
|
237
|
+
explain?: boolean;
|
|
238
|
+
/**
|
|
239
|
+
* Upper bound on the input pitches processed by this detection call.
|
|
240
|
+
*
|
|
241
|
+
* @defaultValue {@link DEFAULT_GENERATION_BUDGET}
|
|
242
|
+
*/
|
|
243
|
+
budget?: number;
|
|
244
|
+
};
|
|
245
|
+
/**
|
|
246
|
+
* Identify chords matching a set of pitches.
|
|
247
|
+
*
|
|
248
|
+
* Every input pitch class is tried as a root against every known chord quality.
|
|
249
|
+
* A match is reported when all of the chord's tones are present in the input, or
|
|
250
|
+
* when the only absent tone is the perfect fifth (a common omission in shell
|
|
251
|
+
* voicings); matches are ranked best-first by fewest extra notes, then fewest
|
|
252
|
+
* missing notes, then most specific (largest) chord. An exact match (no extras,
|
|
253
|
+
* no missing) is flagged and ranked first.
|
|
254
|
+
*
|
|
255
|
+
* @param pitches MIDI pitches or bare pitch classes (octave-agnostic).
|
|
256
|
+
* @returns Ranked chord interpretations (may be empty).
|
|
257
|
+
* @example
|
|
258
|
+
* ```ts
|
|
259
|
+
* import { detectChord } from '@libraz/libcantus';
|
|
260
|
+
* const matches = detectChord([60, 64, 67]); // C E G
|
|
261
|
+
* matches[0]; // { rootPc: 0, quality: 'maj', exact: true, ... }
|
|
262
|
+
* ```
|
|
263
|
+
* @category Recognition
|
|
264
|
+
*/
|
|
265
|
+
declare function detectChord(pitches: readonly number[], opts?: DetectChordOptions): ChordMatch[];
|
|
266
|
+
/**
|
|
267
|
+
* The single best chord interpretation of a pitch set, or null if none.
|
|
268
|
+
*
|
|
269
|
+
* @param pitches MIDI pitches or bare pitch classes.
|
|
270
|
+
* @returns The top-ranked chord, or null when nothing matches.
|
|
271
|
+
* @example
|
|
272
|
+
* ```ts
|
|
273
|
+
* import { detectChordBest } from '@libraz/libcantus';
|
|
274
|
+
* detectChordBest([60, 64, 67]); // C major triad: { rootPc: 0, quality: 'maj', ... }
|
|
275
|
+
* ```
|
|
276
|
+
* @category Recognition
|
|
277
|
+
*/
|
|
278
|
+
declare function detectChordBest(pitches: readonly number[], opts?: DetectChordOptions): Chord | null;
|
|
279
|
+
/**
|
|
280
|
+
* Rank major and minor keys by how well a set of pitch classes is distributed
|
|
281
|
+
* like each key.
|
|
282
|
+
*
|
|
283
|
+
* The input becomes a weighted pitch-class distribution, and every one of the
|
|
284
|
+
* 24 candidates is scored by the Pearson correlation between that distribution
|
|
285
|
+
* and its key profile rotated onto the candidate tonic (see
|
|
286
|
+
* {@link DetectKeyOptions.profile}). Correlating against a profile rather than
|
|
287
|
+
* counting scale members is what tells a key apart from its relative: C major
|
|
288
|
+
* and A minor contain the same seven pitch classes, so a membership count can
|
|
289
|
+
* only separate them by a tie-break, while their profiles expect the weight on
|
|
290
|
+
* different degrees.
|
|
291
|
+
*
|
|
292
|
+
* Choosing the minor variant is deliberately not part of the ranking. A minor
|
|
293
|
+
* candidate is ranked once, on the minor profile, which already expects some
|
|
294
|
+
* weight on the raised sixth and seventh; only then does it report whichever of
|
|
295
|
+
* the natural, harmonic and melodic masks covers the most input weight, and
|
|
296
|
+
* `fit` is measured against that mask. That split is why a harmonic-minor
|
|
297
|
+
* cadence ranks its own tonic first without the harmonic variant having to win
|
|
298
|
+
* a scoring contest against the natural one.
|
|
299
|
+
*
|
|
300
|
+
* {@link DetectKeyOptions.modes} adds the church modes to the same contest, one
|
|
301
|
+
* candidate per mode per tonic, each ranked on a profile derived from its
|
|
302
|
+
* parallel major or minor key. A modal winner reports the mode in `scaleName`
|
|
303
|
+
* and still reports the key it leans on in `mode`. The option is off by default,
|
|
304
|
+
* so a caller that does not ask for modes gets the ranking of the 24 keys it has
|
|
305
|
+
* always got.
|
|
306
|
+
*
|
|
307
|
+
* Ties are broken by `fit` descending, then tonic pitch class ascending, then by
|
|
308
|
+
* scale — major, the minor forms, then the modes — so the order is fully
|
|
309
|
+
* determined by the input rather than by the order candidates happen to be built
|
|
310
|
+
* in. Returns all 24 keys ranked best-first, plus 12 more per enabled mode, or
|
|
311
|
+
* an empty array for an empty input (mirroring {@link detectChord}).
|
|
312
|
+
*
|
|
313
|
+
* {@link DetectKeyOptions.explain} adds a `rationale` to every candidate and,
|
|
314
|
+
* to the winner, the runners-up it beat with the link of that tie-break chain
|
|
315
|
+
* that separated each. It is off by default; see the option for why.
|
|
316
|
+
*
|
|
317
|
+
* @param pitches MIDI pitches or bare pitch classes.
|
|
318
|
+
* @param opts How to weigh the input, which profile to rank with, and whether
|
|
319
|
+
* the modes take part; see {@link DetectKeyOptions}.
|
|
320
|
+
* @returns Ranked key interpretations (empty for an empty input), each carrying
|
|
321
|
+
* a finite `score` in [-1, 1].
|
|
322
|
+
* @throws {InvalidInputError} When `weights` does not have one entry per pitch,
|
|
323
|
+
* `profile` is not a known name or a valid pair of 12-entry vectors, or
|
|
324
|
+
* `modes` names something that is not a church mode.
|
|
325
|
+
* @throws {RangeError} When a pitch is outside the MIDI domain or the input
|
|
326
|
+
* exceeds the budget.
|
|
327
|
+
* @example
|
|
328
|
+
* ```ts
|
|
329
|
+
* import { detectKey } from '@libraz/libcantus';
|
|
330
|
+
* const keys = detectKey([60, 62, 64, 65, 67, 69, 71]); // C major scale
|
|
331
|
+
* keys[0].mode; // 'major', with keys[0].key.rootPc === 0
|
|
332
|
+
* keys[1].mode; // 'minor' on 9: A minor, the relative, ranked just below
|
|
333
|
+
* const riff = detectKey([62, 62, 65, 69, 71, 69, 65, 62], { modes: true });
|
|
334
|
+
* riff[0].scaleName; // 'dorian', on rootPc 2, still reported as mode 'minor'
|
|
335
|
+
* ```
|
|
336
|
+
* @category Recognition
|
|
337
|
+
*/
|
|
338
|
+
declare function detectKey(pitches: readonly number[], opts?: DetectKeyOptions): KeyMatch[];
|
|
339
|
+
/**
|
|
340
|
+
* Rank keys by how well they contain a set of note events, weighting each note
|
|
341
|
+
* by how much of the music it actually occupies.
|
|
342
|
+
*
|
|
343
|
+
* {@link detectKey} counts every pitch once, which lets a run of fast ornamental
|
|
344
|
+
* notes outvote the sustained harmony that establishes the key. This weighs
|
|
345
|
+
* each note by duration times velocity, the same measure chord inference uses,
|
|
346
|
+
* so the two agree on what the music emphasises. Notes that never sound (zero
|
|
347
|
+
* or negative duration) are ignored. The weighted distribution is then ranked
|
|
348
|
+
* exactly as {@link detectKey} ranks it, by correlation against a key profile,
|
|
349
|
+
* so `score` is a finite correlation in [-1, 1].
|
|
350
|
+
*
|
|
351
|
+
* @param notes The note events to weigh.
|
|
352
|
+
* @param opts Which profile to rank with, whether the modes take part, and the
|
|
353
|
+
* budget for processing imported note events; the weights are derived from the
|
|
354
|
+
* notes themselves.
|
|
355
|
+
* @returns Ranked key interpretations (empty when nothing sounds).
|
|
356
|
+
* @throws {InvalidInputError} When `profile` is not a known name or a valid
|
|
357
|
+
* pair of 12-entry vectors, or `modes` names something that is not a church
|
|
358
|
+
* mode.
|
|
359
|
+
* @throws {RangeError} When a note event is malformed or the input exceeds the
|
|
360
|
+
* budget.
|
|
361
|
+
* @example
|
|
362
|
+
* ```ts
|
|
363
|
+
* import { detectKeyFromNotes } from '@libraz/libcantus';
|
|
364
|
+
* detectKeyFromNotes([{ pitch: 60, startBeat: 0, durationBeat: 4 }])[0]?.key.rootPc; // 0
|
|
365
|
+
* ```
|
|
366
|
+
* @category Recognition
|
|
367
|
+
*/
|
|
368
|
+
declare function detectKeyFromNotes(notes: readonly NoteEvent[], opts?: Omit<DetectKeyOptions, 'weights'>): KeyMatch[];
|
|
369
|
+
/**
|
|
370
|
+
* The single best key interpretation of a pitch set.
|
|
371
|
+
*
|
|
372
|
+
* The counterpart of {@link detectChordBest}: the ranked list is the general
|
|
373
|
+
* answer, but a caller that just wants "what key is this" should not have to
|
|
374
|
+
* index into it and assert the result is there. The winner is the candidate
|
|
375
|
+
* whose key profile correlates best with the input distribution, with the same
|
|
376
|
+
* deterministic tie-break {@link detectKey} applies, so its `score` is a finite
|
|
377
|
+
* correlation in [-1, 1] rather than a membership share.
|
|
378
|
+
*
|
|
379
|
+
* This is where {@link DetectKeyOptions.explain} earns its keep: the winner is
|
|
380
|
+
* the only match a caller of this function sees, and with `explain` it carries
|
|
381
|
+
* both its own rationale and the candidates it beat.
|
|
382
|
+
*
|
|
383
|
+
* @param pitches MIDI pitches or bare pitch classes.
|
|
384
|
+
* @param opts How to weigh the input, which profile to rank with, and whether
|
|
385
|
+
* the modes take part; see {@link DetectKeyOptions}.
|
|
386
|
+
* @returns The top-ranked key, or null when nothing sounds.
|
|
387
|
+
* @throws {InvalidInputError} When `weights` does not have one entry per pitch,
|
|
388
|
+
* `profile` is not a known name or a valid pair of 12-entry vectors, or
|
|
389
|
+
* `modes` names something that is not a church mode.
|
|
390
|
+
* @throws {RangeError} When a pitch is outside the MIDI domain or the input
|
|
391
|
+
* exceeds the budget.
|
|
392
|
+
* @example
|
|
393
|
+
* ```ts
|
|
394
|
+
* import { detectKeyBest } from '@libraz/libcantus';
|
|
395
|
+
* detectKeyBest([0, 2, 4, 5, 7, 9, 11])?.mode; // 'major'
|
|
396
|
+
* ```
|
|
397
|
+
* @category Recognition
|
|
398
|
+
*/
|
|
399
|
+
declare function detectKeyBest(pitches: readonly number[], opts?: DetectKeyOptions): KeyMatch | null;
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Arrangement track-role analysis: a single harmony is inferred from every
|
|
403
|
+
* track's notes pooled together, each track is then analysed against that shared
|
|
404
|
+
* harmony, and notes that clash with the sounding chord are collected as
|
|
405
|
+
* conflicts.
|
|
406
|
+
*/
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* The musical role a track plays in the arrangement.
|
|
410
|
+
*
|
|
411
|
+
* Only `drums` changes the analysis: its pitches select instruments rather than
|
|
412
|
+
* naming harmony, so such a track is excluded from chord and key inference and
|
|
413
|
+
* from every voice-leading comparison, and is reported back with no
|
|
414
|
+
* annotations. `harmony` is meaningful through
|
|
415
|
+
* {@link ArrangementOptions.harmonyTracks}, which names the tracks the chords
|
|
416
|
+
* are inferred from. The rest are labels carried through to
|
|
417
|
+
* {@link TrackAnalysis.role} for the caller's own use; nothing is inferred, so
|
|
418
|
+
* a track left unlabelled reports `'other'` rather than a guess.
|
|
419
|
+
*
|
|
420
|
+
* @category Arrangement & Analysis
|
|
421
|
+
*/
|
|
422
|
+
type TrackRole = 'melody' | 'harmony' | 'bass' | 'drums' | 'other';
|
|
423
|
+
/**
|
|
424
|
+
* One input track: its notes plus optional name and role.
|
|
425
|
+
*
|
|
426
|
+
* @category Arrangement & Analysis
|
|
427
|
+
*/
|
|
428
|
+
type ArrangementTrack = {
|
|
429
|
+
name?: string;
|
|
430
|
+
role?: TrackRole;
|
|
431
|
+
notes: readonly NoteEvent[];
|
|
432
|
+
};
|
|
433
|
+
/**
|
|
434
|
+
* A track after analysis: its resolved name, role, and per-note annotations.
|
|
435
|
+
*
|
|
436
|
+
* @category Arrangement & Analysis
|
|
437
|
+
*/
|
|
438
|
+
type TrackAnalysis = {
|
|
439
|
+
name: string;
|
|
440
|
+
role: TrackRole;
|
|
441
|
+
/** One {@link AnalyzedNote} per note, labelled against the inferred chords. */
|
|
442
|
+
notes: AnalyzedNote[];
|
|
443
|
+
};
|
|
444
|
+
/**
|
|
445
|
+
* A note that clashes with the harmony sounding beneath it.
|
|
446
|
+
*
|
|
447
|
+
* @category Arrangement & Analysis
|
|
448
|
+
*/
|
|
449
|
+
type Conflict = {
|
|
450
|
+
beat: number;
|
|
451
|
+
trackName: string;
|
|
452
|
+
/** The clashing track's index in the caller's input array. */
|
|
453
|
+
trackIndex: number;
|
|
454
|
+
/** The note's index in that track's own note array. */
|
|
455
|
+
originalIndex?: number;
|
|
456
|
+
/** The id the note carries in {@link TrackAnalysis.notes}. */
|
|
457
|
+
noteId: number;
|
|
458
|
+
pitch: number;
|
|
459
|
+
/**
|
|
460
|
+
* The note's theory labels at its onset, so a caller can tell a genuine clash
|
|
461
|
+
* from a passing tone, a neighbour, or a prepared suspension — all of which
|
|
462
|
+
* are ordinary non-chord tones that still evaluate as unsafe.
|
|
463
|
+
*/
|
|
464
|
+
labels: TheoryLabel[];
|
|
465
|
+
/** The clash severity: {@link NoteSafety.Warning} or {@link NoteSafety.Dissonant}. */
|
|
466
|
+
safety: NoteSafety;
|
|
467
|
+
/** {@link SafetyResult.reasons} bitmask. */
|
|
468
|
+
reasons: number;
|
|
469
|
+
/** Preferred stepwise resolution, when the safety evaluator provides one. */
|
|
470
|
+
resolveTo?: number;
|
|
471
|
+
/** Nearby fully safe replacement pitches, nearest first. */
|
|
472
|
+
suggestions?: number[];
|
|
473
|
+
rationale?: string;
|
|
474
|
+
};
|
|
475
|
+
/**
|
|
476
|
+
* The full result of {@link analyzeArrangement}.
|
|
477
|
+
*
|
|
478
|
+
* @category Arrangement & Analysis
|
|
479
|
+
*/
|
|
480
|
+
type ArrangementAnalysis = {
|
|
481
|
+
/**
|
|
482
|
+
* The key regions the analysis ran against, in time order. Roman numerals,
|
|
483
|
+
* harmonic function and note safety are all judged against the region
|
|
484
|
+
* covering the beat, so a piece that modulates is not read against the key it
|
|
485
|
+
* started in for every bar after it leaves.
|
|
486
|
+
*/
|
|
487
|
+
keys: KeyRegion[];
|
|
488
|
+
/** The key held longest across {@link ArrangementAnalysis.keys}. */
|
|
489
|
+
prevailingKey: KeyScale;
|
|
490
|
+
timeline: ChordTimeline;
|
|
491
|
+
segmentConfidence: number[];
|
|
492
|
+
cadences: CadenceHit[];
|
|
493
|
+
tracks: TrackAnalysis[];
|
|
494
|
+
/** Notes clashing with the sounding harmony, worst severity first. */
|
|
495
|
+
conflicts: Conflict[];
|
|
496
|
+
};
|
|
497
|
+
/**
|
|
498
|
+
* Options controlling {@link analyzeArrangement} and {@link tensionCurve}.
|
|
499
|
+
*
|
|
500
|
+
* @category Arrangement & Analysis
|
|
501
|
+
*/
|
|
502
|
+
type ArrangementOptions = {
|
|
503
|
+
/**
|
|
504
|
+
* A single key held across the whole arrangement. Omit it, and `keys` with
|
|
505
|
+
* it, to have the key searched for over time so a modulating piece is read
|
|
506
|
+
* against the key actually in force.
|
|
507
|
+
*/
|
|
508
|
+
key?: KeyScale;
|
|
509
|
+
/**
|
|
510
|
+
* Key regions to judge against, when a previous pass already worked them out.
|
|
511
|
+
* Takes precedence over `key`; supplying both is answering the same question
|
|
512
|
+
* twice, and the more specific answer wins.
|
|
513
|
+
*/
|
|
514
|
+
keys?: KeyRegion[];
|
|
515
|
+
/**
|
|
516
|
+
* A chord timeline to analyse against, instead of inferring one from the
|
|
517
|
+
* notes. Supply it to analyse a hand-written progression, or to reuse the
|
|
518
|
+
* timeline of an earlier call rather than re-running chord inference.
|
|
519
|
+
*/
|
|
520
|
+
timeline?: ChordTimeline;
|
|
521
|
+
/**
|
|
522
|
+
* Indices of the tracks the harmony is inferred from. Defaults to every
|
|
523
|
+
* pitched track. Percussion tracks are excluded either way. An index naming
|
|
524
|
+
* no track is an input error rather than one that matches nothing.
|
|
525
|
+
*/
|
|
526
|
+
harmonyTracks?: number[];
|
|
527
|
+
/**
|
|
528
|
+
* Lowest severity reported in `conflicts`.
|
|
529
|
+
*
|
|
530
|
+
* @defaultValue {@link NoteSafety.Warning}
|
|
531
|
+
*/
|
|
532
|
+
minSeverity?: NoteSafety;
|
|
533
|
+
/**
|
|
534
|
+
* A single time signature held across the whole arrangement, as sugar for a
|
|
535
|
+
* one-element `meters`; defaults to 4/4. Giving both is an input error.
|
|
536
|
+
*
|
|
537
|
+
* @defaultValue `4/4`
|
|
538
|
+
*/
|
|
539
|
+
ts?: TimeSignature;
|
|
540
|
+
/**
|
|
541
|
+
* The meter as it changes over the arrangement. Bar lines, downbeats and
|
|
542
|
+
* metric weight all follow the signature in force at the beat in question.
|
|
543
|
+
*
|
|
544
|
+
* @defaultValue 4/4 throughout
|
|
545
|
+
*/
|
|
546
|
+
meters?: MeterMap;
|
|
547
|
+
/**
|
|
548
|
+
* Length of the pickup in beats, when the piece starts with one.
|
|
549
|
+
*
|
|
550
|
+
* The first downbeat is beat 0, so an upbeat is written at negative beats.
|
|
551
|
+
* Declaring its length rejects a note that starts before the pickup does;
|
|
552
|
+
* leave it unset to accept any finite onset.
|
|
553
|
+
*
|
|
554
|
+
* @defaultValue no declared pickup
|
|
555
|
+
*/
|
|
556
|
+
pickupBeats?: number;
|
|
557
|
+
/**
|
|
558
|
+
* Chord-slot length in beats; defaults to the length of the opening bar.
|
|
559
|
+
*
|
|
560
|
+
* @defaultValue the length of the opening bar
|
|
561
|
+
*/
|
|
562
|
+
harmonicRhythm?: number;
|
|
563
|
+
/**
|
|
564
|
+
* Safety profile used for conflict detection; defaults to `pop`.
|
|
565
|
+
*
|
|
566
|
+
* @defaultValue `'pop'`
|
|
567
|
+
*/
|
|
568
|
+
profile?: SafetyProfile;
|
|
569
|
+
/**
|
|
570
|
+
* Upper bound on the work this call may do — note counts, windows, and
|
|
571
|
+
* candidate counts are each checked against it before anything is allocated.
|
|
572
|
+
*
|
|
573
|
+
* Raise it to analyse a piece larger than the default allows; the default is
|
|
574
|
+
* {@link DEFAULT_GENERATION_BUDGET}, chosen so a runaway input fails fast
|
|
575
|
+
* rather than blocking the thread.
|
|
576
|
+
*
|
|
577
|
+
* @defaultValue {@link DEFAULT_GENERATION_BUDGET}
|
|
578
|
+
*/
|
|
579
|
+
budget?: number;
|
|
580
|
+
};
|
|
581
|
+
/**
|
|
582
|
+
* Analyse a whole arrangement against the harmony inferred from it.
|
|
583
|
+
*
|
|
584
|
+
* The chord timeline and the key regions it is read against are inferred from
|
|
585
|
+
* the pooled notes of every pitched track (see
|
|
586
|
+
* {@link chordTimelineFromNotes}); pooling all voices is
|
|
587
|
+
* robust even when roles are absent or a track doubles the harmony, so it is
|
|
588
|
+
* preferred over deriving the harmony from a subset. A track marked
|
|
589
|
+
* `role: 'drums'` is excluded — its pitches name instruments, not harmony, and
|
|
590
|
+
* pooling them would corrupt the key, the timeline and every reading built on
|
|
591
|
+
* them. Pass `harmonyTracks` to restrict inference further, or `timeline` to
|
|
592
|
+
* analyse against a progression you already have. Zero- and negative-length
|
|
593
|
+
* notes never sound, so they are dropped at ingest and appear in neither the
|
|
594
|
+
* annotations nor the conflicts.
|
|
595
|
+
*
|
|
596
|
+
* Voice analysis assumes one monophonic voice at a time, so each track is
|
|
597
|
+
* first partitioned into monophonic sub-voices (a polyphonic block-chord track
|
|
598
|
+
* fans out into one sub-voice per simultaneous note; a monophonic track is
|
|
599
|
+
* unaffected). Each sub-voice is labelled note-by-note against the harmony
|
|
600
|
+
* with {@link analyzeVoice}, seeing every other sub-voice — including siblings
|
|
601
|
+
* within the same track — as its accompaniment. Every note is then re-checked
|
|
602
|
+
* with {@link evaluateSafety} at its onset and again at each chord change it
|
|
603
|
+
* sustains across, so a held note that clashes with a later chord is caught:
|
|
604
|
+
* evaluations that are not {@link NoteSafety.Safe} become conflicts, one per
|
|
605
|
+
* clashing beat, sorted worst severity first and then by beat.
|
|
606
|
+
*
|
|
607
|
+
* Every ordinary non-chord tone — a passing note, a neighbour, a prepared
|
|
608
|
+
* suspension — is by definition unsafe against the chord under it, so the
|
|
609
|
+
* conflict list is a report, not a fault list. Each conflict carries the note's
|
|
610
|
+
* `labels` and its `trackIndex`/`originalIndex`, so a caller can tell those
|
|
611
|
+
* apart and map a conflict back to the note it passed in; `minSeverity` narrows
|
|
612
|
+
* the list to outright dissonance.
|
|
613
|
+
*
|
|
614
|
+
* @param tracks The tracks to analyse.
|
|
615
|
+
* @param opts Analysis options; see {@link ArrangementOptions}.
|
|
616
|
+
* @returns The inferred harmony, per-track annotations, cadences, and conflicts.
|
|
617
|
+
* @example
|
|
618
|
+
* ```ts
|
|
619
|
+
* import { analyzeArrangement } from '@libraz/libcantus';
|
|
620
|
+
* const melody = [
|
|
621
|
+
* { pitch: 60, startBeat: 0, durationBeat: 2 },
|
|
622
|
+
* { pitch: 67, startBeat: 2, durationBeat: 2 },
|
|
623
|
+
* ];
|
|
624
|
+
* const { prevailingKey, conflicts } = analyzeArrangement([{ role: 'melody', notes: melody }]);
|
|
625
|
+
* prevailingKey.rootPc; // the key held longest across the analysis
|
|
626
|
+
* conflicts; // notes clashing with the inferred harmony, worst severity first
|
|
627
|
+
* ```
|
|
628
|
+
* @category Arrangement & Analysis
|
|
629
|
+
*/
|
|
630
|
+
declare function analyzeArrangement(tracks: ArrangementTrack[], opts?: ArrangementOptions): ArrangementAnalysis;
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Incremental arrangement analysis: an analysis held open across edits, so a
|
|
634
|
+
* host that re-analyses on every keystroke pays for the beats that changed
|
|
635
|
+
* rather than for the whole piece.
|
|
636
|
+
*/
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* One track's notes as they stand after an edit.
|
|
640
|
+
*
|
|
641
|
+
* The whole note array is given rather than a description of what moved: a host
|
|
642
|
+
* always has the track's current contents, and working out what actually
|
|
643
|
+
* changed is this module's job, not the caller's.
|
|
644
|
+
*
|
|
645
|
+
* @category Arrangement & Analysis
|
|
646
|
+
*/
|
|
647
|
+
type TrackEdit = {
|
|
648
|
+
/** Index of the track in the session's track array. */
|
|
649
|
+
trackIndex: number;
|
|
650
|
+
/** The track's notes after the edit. */
|
|
651
|
+
notes: readonly NoteEvent[];
|
|
652
|
+
};
|
|
653
|
+
/**
|
|
654
|
+
* An arrangement analysis held open for editing.
|
|
655
|
+
*
|
|
656
|
+
* @category Arrangement & Analysis
|
|
657
|
+
*/
|
|
658
|
+
type ArrangementSession = {
|
|
659
|
+
/** The tracks the current analysis was made from. */
|
|
660
|
+
readonly tracks: readonly ArrangementTrack[];
|
|
661
|
+
/** The analysis of those tracks. */
|
|
662
|
+
readonly analysis: ArrangementAnalysis;
|
|
663
|
+
/**
|
|
664
|
+
* Re-analyse after replacing the notes of one or more tracks, reusing
|
|
665
|
+
* everything the edit provably cannot reach.
|
|
666
|
+
*
|
|
667
|
+
* The result equals {@link analyzeArrangement} over the edited tracks — the
|
|
668
|
+
* same segments, confidences, key regions, cadences, annotations and
|
|
669
|
+
* conflicts — so a host may hold a session for as long as it likes without
|
|
670
|
+
* the analysis drifting from what a fresh pass would say.
|
|
671
|
+
*/
|
|
672
|
+
update: (edits: readonly TrackEdit[]) => ArrangementSession;
|
|
673
|
+
};
|
|
674
|
+
/**
|
|
675
|
+
* Open an arrangement analysis that can be updated a few beats at a time.
|
|
676
|
+
*
|
|
677
|
+
* The first pass is an ordinary {@link analyzeArrangement}; what the session
|
|
678
|
+
* adds is the slot-level evidence behind it, so a later {@link
|
|
679
|
+
* ArrangementSession.update} can work out which beats an edit could possibly
|
|
680
|
+
* have changed and leave the rest of the harmony alone. An update returns a new
|
|
681
|
+
* session, so the analysis before an edit stays valid and a host can keep it
|
|
682
|
+
* for undo.
|
|
683
|
+
*
|
|
684
|
+
* What an update recomputes is derived rather than guessed at:
|
|
685
|
+
*
|
|
686
|
+
* - The edited beats are the beats covered by the notes the old and new track
|
|
687
|
+
* arrays disagree on, which is narrower than the track and usually far
|
|
688
|
+
* narrower than the piece.
|
|
689
|
+
* - A slot's evidence sees exactly the notes overlapping it, so the slots to
|
|
690
|
+
* re-derive are the slots overlapping those beats — plus one either side, so
|
|
691
|
+
* that an onset sitting a floating-point hair off a slot boundary cannot
|
|
692
|
+
* leave a stale slot behind.
|
|
693
|
+
* - The boundary search itself has no such margin. It is a shortest-path search
|
|
694
|
+
* over a whole run of slots, so a changed slot can move a boundary anywhere in
|
|
695
|
+
* the run, and its change cost is calibrated on the mean weight of every
|
|
696
|
+
* sounding slot in the piece, which couples the runs to one another. The
|
|
697
|
+
* search is therefore re-run over the whole piece — arithmetic over the
|
|
698
|
+
* carried-over evidence, with nothing allocated per slot — rather than being
|
|
699
|
+
* spliced at a margin that does not exist.
|
|
700
|
+
* - Each settled segment's chord is carried over when its bounds, its key and
|
|
701
|
+
* its slots are all unchanged, since those are the only things chord
|
|
702
|
+
* inference reads.
|
|
703
|
+
*
|
|
704
|
+
* Key regions and the per-note annotations are recomputed in full. Both are
|
|
705
|
+
* whole-piece passes — the key search is a shortest-path search with no reset
|
|
706
|
+
* points, sub-voice lane assignment is greedy from the first note, and every
|
|
707
|
+
* note is judged against the other voices sounding beneath it — so neither has
|
|
708
|
+
* a sound local answer, and reporting one would mean an update that quietly
|
|
709
|
+
* disagreed with a fresh analysis.
|
|
710
|
+
*
|
|
711
|
+
* An update falls back to a full analysis whenever the edit moves the ground the
|
|
712
|
+
* evidence stands on: a changed piece length, a changed first onset (both of
|
|
713
|
+
* which move the slot grid), or an edit to a session whose harmony was supplied
|
|
714
|
+
* through {@link ArrangementOptions.timeline}. The answer is the same either
|
|
715
|
+
* way; only the work differs.
|
|
716
|
+
*
|
|
717
|
+
* @param tracks The tracks to analyse.
|
|
718
|
+
* @param opts Analysis options; see {@link ArrangementOptions}.
|
|
719
|
+
* @returns A session holding the analysis and the evidence behind it.
|
|
720
|
+
* @example
|
|
721
|
+
* ```ts
|
|
722
|
+
* import { createArrangementSession } from '@libraz/libcantus';
|
|
723
|
+
* const melody = [{ pitch: 60, startBeat: 0, durationBeat: 4 }];
|
|
724
|
+
* const session = createArrangementSession([{ role: 'melody', notes: melody }]);
|
|
725
|
+
* const moved = [{ pitch: 62, startBeat: 0, durationBeat: 4 }];
|
|
726
|
+
* const next = session.update([{ trackIndex: 0, notes: moved }]);
|
|
727
|
+
* next.analysis.timeline.segments; // as if analyzed from scratch
|
|
728
|
+
* ```
|
|
729
|
+
* @category Arrangement & Analysis
|
|
730
|
+
*/
|
|
731
|
+
declare function createArrangementSession(tracks: ArrangementTrack[], opts?: ArrangementOptions): ArrangementSession;
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Arrangement tension analysis: a coarse harmonic-tension curve sampled at
|
|
735
|
+
* regular beats across a whole multi-track piece, combining the sounding
|
|
736
|
+
* chord's harmonic function, the dissonance of the sounding notes, and their
|
|
737
|
+
* registral span into a single normalized reading.
|
|
738
|
+
*/
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* A tension reading sampled at a beat.
|
|
742
|
+
*
|
|
743
|
+
* @category Arrangement & Analysis
|
|
744
|
+
*/
|
|
745
|
+
type TensionPoint = {
|
|
746
|
+
beat: number;
|
|
747
|
+
/** Combined tension in [0, 1]. */
|
|
748
|
+
tension: number;
|
|
749
|
+
};
|
|
750
|
+
/**
|
|
751
|
+
* Sample the harmonic tension of an arrangement at regular beats.
|
|
752
|
+
*
|
|
753
|
+
* At each sample beat the tension combines three normalized terms:
|
|
754
|
+
* - the sounding chord's harmonic function (dominant `1`, subdominant `0.5`,
|
|
755
|
+
* tonic or no chord `0`), weighted `FUNCTION_WEIGHT`;
|
|
756
|
+
* - the dissonance of the sounding notes — the larger of the share of sounding
|
|
757
|
+
* pitches that are not chord tones and the share that {@link evaluateSafety}
|
|
758
|
+
* rates {@link NoteSafety.Dissonant} — weighted `DISSONANCE_WEIGHT`;
|
|
759
|
+
* - the registral span of the sounding notes, saturating at
|
|
760
|
+
* `SPAN_SATURATION` semitones, weighted `SPAN_WEIGHT`.
|
|
761
|
+
*
|
|
762
|
+
* The weighted sum is clamped to [0, 1]. The harmony is inferred from all tracks
|
|
763
|
+
* pooled together, so the result is deterministic and self-contained.
|
|
764
|
+
*
|
|
765
|
+
* @param tracks The tracks to sample.
|
|
766
|
+
* @param opts Analysis options plus an optional `step` (default one beat).
|
|
767
|
+
* @returns One {@link TensionPoint} per sampled beat, in beat order.
|
|
768
|
+
* @throws If `step` is not positive.
|
|
769
|
+
* @example
|
|
770
|
+
* ```ts
|
|
771
|
+
* import { tensionCurve } from '@libraz/libcantus';
|
|
772
|
+
* const notes = [{ pitch: 60, startBeat: 0, durationBeat: 4 }];
|
|
773
|
+
* const curve = tensionCurve([{ notes }], { step: 1 });
|
|
774
|
+
* curve; // one { beat, tension } sample per beat, in beat order
|
|
775
|
+
* ```
|
|
776
|
+
* @category Arrangement & Analysis
|
|
777
|
+
*/
|
|
778
|
+
declare function tensionCurve(tracks: ArrangementTrack[], opts?: ArrangementOptions & {
|
|
779
|
+
step?: number;
|
|
780
|
+
}): TensionPoint[];
|
|
781
|
+
/**
|
|
782
|
+
* Sample the tension of an arrangement already analysed by
|
|
783
|
+
* {@link analyzeArrangement}, reusing its harmony instead of inferring it again.
|
|
784
|
+
*
|
|
785
|
+
* `analyzeArrangement` followed by `tensionCurve` runs chord and key inference
|
|
786
|
+
* twice over the same notes. This takes the analysis it already produced, so
|
|
787
|
+
* the curve and the annotations describe the same harmony by construction.
|
|
788
|
+
*
|
|
789
|
+
* @param tracks The same tracks that were analysed.
|
|
790
|
+
* @param analysis The result of {@link analyzeArrangement} for those tracks.
|
|
791
|
+
* @param opts Analysis options plus an optional `step` (default one beat).
|
|
792
|
+
* @returns One {@link TensionPoint} per sampled beat, in beat order.
|
|
793
|
+
* @example
|
|
794
|
+
* ```ts
|
|
795
|
+
* import { analyzeArrangement, tensionCurveFrom } from '@libraz/libcantus';
|
|
796
|
+
* const tracks = [{ notes: [{ pitch: 60, startBeat: 0, durationBeat: 4 }] }];
|
|
797
|
+
* const analysis = analyzeArrangement(tracks);
|
|
798
|
+
* tensionCurveFrom(tracks, analysis, { step: 1 });
|
|
799
|
+
* ```
|
|
800
|
+
* @category Arrangement & Analysis
|
|
801
|
+
*/
|
|
802
|
+
declare function tensionCurveFrom(tracks: ArrangementTrack[], analysis: ArrangementAnalysis, opts?: ArrangementOptions & {
|
|
803
|
+
step?: number;
|
|
804
|
+
}): TensionPoint[];
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Hypermeter: the grouping of bars into hyperbars.
|
|
808
|
+
*
|
|
809
|
+
* Bars are felt in groups the way beats are felt in bars — four bars at a time
|
|
810
|
+
* in most tonal and popular music, but two, three, six or eight elsewhere. The
|
|
811
|
+
* grouping is not written in the score, so it has to be inferred from where the
|
|
812
|
+
* music actually restarts: harmony changes on a hypermetric downbeat, and
|
|
813
|
+
* cadences arrive at the end of a hyperbar.
|
|
814
|
+
*/
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* A bar grouping inferred from the music.
|
|
818
|
+
*
|
|
819
|
+
* @category Arrangement & Analysis
|
|
820
|
+
*/
|
|
821
|
+
type Hypermeter = {
|
|
822
|
+
/**
|
|
823
|
+
* Bars per hyperbar. Falls back to the span's own bar count when the span is
|
|
824
|
+
* too short to hold two of any candidate grouping.
|
|
825
|
+
*/
|
|
826
|
+
groupBars: number;
|
|
827
|
+
/**
|
|
828
|
+
* Absolute beat of each hypermetric downbeat inside the analysed span, in
|
|
829
|
+
* time order. A pickup bar is not a group head, so the first entry is the
|
|
830
|
+
* first full bar that starts a group.
|
|
831
|
+
*/
|
|
832
|
+
downbeats: number[];
|
|
833
|
+
/**
|
|
834
|
+
* How much the music supports the grouping, in [0, 1]: how strongly harmonic
|
|
835
|
+
* change and cadences prefer these bars over the others, discounted when the
|
|
836
|
+
* runner-up grouping explains the music nearly as well.
|
|
837
|
+
*/
|
|
838
|
+
confidence: number;
|
|
839
|
+
/** Why this grouping was chosen. */
|
|
840
|
+
rationale: string;
|
|
841
|
+
};
|
|
842
|
+
/**
|
|
843
|
+
* Options for {@link hypermeter}.
|
|
844
|
+
*
|
|
845
|
+
* @category Arrangement & Analysis
|
|
846
|
+
*/
|
|
847
|
+
type HypermeterOptions = {
|
|
848
|
+
/**
|
|
849
|
+
* End of the analysed span in beats; defaults to the end of the last note.
|
|
850
|
+
*
|
|
851
|
+
* @defaultValue the end of the last note
|
|
852
|
+
*/
|
|
853
|
+
totalBeats?: number;
|
|
854
|
+
/**
|
|
855
|
+
* Beats at which cadences arrive, as {@link CadenceHit.atBeat} reports them.
|
|
856
|
+
*
|
|
857
|
+
* A cadence is the clearest hypermetric evidence there is — it arrives at the
|
|
858
|
+
* end of a hyperbar — but finding one needs a chord timeline and a key, which
|
|
859
|
+
* this function is not given. Callers that already hold cadences should pass
|
|
860
|
+
* them; {@link phrasesFromTimeline} does.
|
|
861
|
+
*
|
|
862
|
+
* @defaultValue none
|
|
863
|
+
*/
|
|
864
|
+
cadenceBeats?: readonly number[];
|
|
865
|
+
/**
|
|
866
|
+
* Upper bound on the work this call may do.
|
|
867
|
+
*
|
|
868
|
+
* @defaultValue {@link DEFAULT_GENERATION_BUDGET}
|
|
869
|
+
*/
|
|
870
|
+
budget?: number;
|
|
871
|
+
};
|
|
872
|
+
/**
|
|
873
|
+
* Infer how a piece's bars group into hyperbars.
|
|
874
|
+
*
|
|
875
|
+
* Groupings of 2, 3, 4, 6, and 8 bars are tried at every phase, and each
|
|
876
|
+
* reading is scored on two counts: how much more harmonic change lands on the
|
|
877
|
+
* bars it calls group heads than on the bars between them, and how many of the
|
|
878
|
+
* cadences it is given arrive where it says a hyperbar ends. The commonest
|
|
879
|
+
* groupings are believed a little more readily than the rare ones, which
|
|
880
|
+
* settles ties without letting a prior overrule the music.
|
|
881
|
+
*
|
|
882
|
+
* The meter is read at the beat in question rather than at the start, so a
|
|
883
|
+
* piece that changes metre keeps being cut at its own bar lines. Bar 0 is the
|
|
884
|
+
* first full bar however much pickup precedes it, so an upbeat is bar -1 and is
|
|
885
|
+
* never taken for a hypermetric downbeat. Whether there is an upbeat at all is
|
|
886
|
+
* decided the way the rest of the analysis decides it: a note a millibeat early
|
|
887
|
+
* is playing the downbeat, not anticipating it.
|
|
888
|
+
*
|
|
889
|
+
* @param notes The notes to read, in any order. Notes that never sound are
|
|
890
|
+
* dropped.
|
|
891
|
+
* @param meter A single time signature, or the piece's meter map; defaults to
|
|
892
|
+
* 4/4 throughout.
|
|
893
|
+
* @param opts Analysis options; see {@link HypermeterOptions}.
|
|
894
|
+
* @returns The grouping, its downbeats, a confidence, and a rationale.
|
|
895
|
+
* @example
|
|
896
|
+
* ```ts
|
|
897
|
+
* import { hypermeter } from '@libraz/libcantus';
|
|
898
|
+
* const notes = [
|
|
899
|
+
* { pitch: 60, startBeat: 0, durationBeat: 16 },
|
|
900
|
+
* { pitch: 65, startBeat: 16, durationBeat: 16 },
|
|
901
|
+
* ];
|
|
902
|
+
* hypermeter(notes).groupBars; // 4
|
|
903
|
+
* ```
|
|
904
|
+
* @category Arrangement & Analysis
|
|
905
|
+
*/
|
|
906
|
+
declare function hypermeter(notes: readonly NoteEvent[], meter?: MeterLike, opts?: HypermeterOptions): Hypermeter;
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* Phrases: the unit of music one bar larger than the chord.
|
|
910
|
+
*
|
|
911
|
+
* Every other analysis in this library is local — a chord against the next
|
|
912
|
+
* chord, a note against the chord under it — and that is the ceiling on what it
|
|
913
|
+
* can say. A cadence is a cadence because it falls at the end of a phrase, so
|
|
914
|
+
* two chords read in isolation cannot settle how much weight one carries. This
|
|
915
|
+
* module draws the boundaries, then ranks the cadences that sit on them.
|
|
916
|
+
*
|
|
917
|
+
* No single signal draws a phrase boundary reliably, so three are combined: the
|
|
918
|
+
* cadences the harmony forms, the rests and long notes the melody breathes at,
|
|
919
|
+
* and the points where the music starts saying something it has said before. A
|
|
920
|
+
* boundary is reported with the evidence behind it and a confidence, because a
|
|
921
|
+
* phrase reading is an interpretation and pretending otherwise would be worse
|
|
922
|
+
* than useless to a caller deciding whether to trust it.
|
|
923
|
+
*/
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* A reason a phrase boundary was placed where it was.
|
|
927
|
+
*
|
|
928
|
+
* @category Arrangement & Analysis
|
|
929
|
+
*/
|
|
930
|
+
type PhraseSignal =
|
|
931
|
+
/** A cadence closed on the beat the boundary falls at. */
|
|
932
|
+
'cadence'
|
|
933
|
+
/** The melody stopped sounding long enough to have taken a breath. */
|
|
934
|
+
| 'rest'
|
|
935
|
+
/** A note was held long enough to read as an arrival. */
|
|
936
|
+
| 'longNote'
|
|
937
|
+
/** The music began repeating material heard immediately before. */
|
|
938
|
+
| 'repetition'
|
|
939
|
+
/** The boundary falls on a hypermetric downbeat. */
|
|
940
|
+
| 'hypermeter'
|
|
941
|
+
/** The music ends here, which closes whatever phrase was open. */
|
|
942
|
+
| 'pieceEnd';
|
|
943
|
+
/**
|
|
944
|
+
* One phrase: a span of music closed by a boundary, and the evidence for it.
|
|
945
|
+
*
|
|
946
|
+
* @category Arrangement & Analysis
|
|
947
|
+
*/
|
|
948
|
+
type Phrase = {
|
|
949
|
+
/** First beat of the phrase; negative when the phrase begins in a pickup. */
|
|
950
|
+
startBeat: number;
|
|
951
|
+
/** End of the phrase, exclusive. */
|
|
952
|
+
endBeat: number;
|
|
953
|
+
/**
|
|
954
|
+
* The cadence closing the phrase, or null when the boundary rests on the
|
|
955
|
+
* melody and the metre alone.
|
|
956
|
+
*/
|
|
957
|
+
cadence: CadenceHit | null;
|
|
958
|
+
/** Why the closing boundary was placed here, strongest evidence first. */
|
|
959
|
+
signals: PhraseSignal[];
|
|
960
|
+
/**
|
|
961
|
+
* How well evidenced the phrase is, in [0, 1]: how strong its closing
|
|
962
|
+
* boundary is, and how close its length is to the length expected of a
|
|
963
|
+
* phrase in this piece.
|
|
964
|
+
*/
|
|
965
|
+
confidence: number;
|
|
966
|
+
/**
|
|
967
|
+
* How much structural weight the closing cadence carries, in [0, 1]; 0 for a
|
|
968
|
+
* phrase that no cadence closes.
|
|
969
|
+
*
|
|
970
|
+
* This is the number that answers "which cadence matters most": it grades the
|
|
971
|
+
* cadence by how conclusive its type is, then by where the phrase it closes
|
|
972
|
+
* sits — a cadence ending the piece or landing on a hypermetric boundary
|
|
973
|
+
* closes more than one in the middle of a hyperbar.
|
|
974
|
+
* {@link structuralCadences} ranks by it.
|
|
975
|
+
*/
|
|
976
|
+
structuralWeight: number;
|
|
977
|
+
/** A short reading of the phrase in words. */
|
|
978
|
+
rationale: string;
|
|
979
|
+
};
|
|
980
|
+
/**
|
|
981
|
+
* Options for {@link phrasesFromTimeline}.
|
|
982
|
+
*
|
|
983
|
+
* @category Arrangement & Analysis
|
|
984
|
+
*/
|
|
985
|
+
type PhraseOptions = {
|
|
986
|
+
/**
|
|
987
|
+
* A single time signature held across the whole span, as sugar for a
|
|
988
|
+
* one-element `meters`; defaults to 4/4. Giving both is an input error.
|
|
989
|
+
*
|
|
990
|
+
* @defaultValue `4/4`
|
|
991
|
+
*/
|
|
992
|
+
ts?: TimeSignature;
|
|
993
|
+
/**
|
|
994
|
+
* The meter as it changes over the span. Bar lines, hypermetric downbeats and
|
|
995
|
+
* the default phrase length all follow the signature in force at the beat in
|
|
996
|
+
* question, so a piece that changes metre is not read in the one it opened in.
|
|
997
|
+
*
|
|
998
|
+
* @defaultValue 4/4 throughout
|
|
999
|
+
*/
|
|
1000
|
+
meters?: MeterMap;
|
|
1001
|
+
/**
|
|
1002
|
+
* Key context for cadence detection: one key for the whole span, or the key
|
|
1003
|
+
* in force at a given beat. Omit it to have the key searched for over time,
|
|
1004
|
+
* so a piece that modulates has each cadence read in the key it arrives in.
|
|
1005
|
+
*
|
|
1006
|
+
* @defaultValue the key inferred from the notes over time
|
|
1007
|
+
*/
|
|
1008
|
+
key?: KeyContext;
|
|
1009
|
+
/**
|
|
1010
|
+
* End of the analysed span in beats; defaults to the end of the last note or
|
|
1011
|
+
* timeline segment.
|
|
1012
|
+
*
|
|
1013
|
+
* @defaultValue the end of the input
|
|
1014
|
+
*/
|
|
1015
|
+
totalBeats?: number;
|
|
1016
|
+
/**
|
|
1017
|
+
* Length a phrase is expected to run, in beats; defaults to one hyperbar.
|
|
1018
|
+
*
|
|
1019
|
+
* This is a prior, not a rule: a boundary is placed where the evidence puts
|
|
1020
|
+
* it, and this decides how much evidence an irregular length has to carry
|
|
1021
|
+
* before it is preferred to a regular one. Set it to two hyperbars for music
|
|
1022
|
+
* whose phrases are periods.
|
|
1023
|
+
*
|
|
1024
|
+
* @defaultValue one hyperbar of the opening meter
|
|
1025
|
+
*/
|
|
1026
|
+
expectedPhraseBeats?: number;
|
|
1027
|
+
/**
|
|
1028
|
+
* Shortest phrase that may be reported, in beats; defaults to one bar of the
|
|
1029
|
+
* opening meter.
|
|
1030
|
+
*
|
|
1031
|
+
* @defaultValue one bar of the opening meter
|
|
1032
|
+
*/
|
|
1033
|
+
minPhraseBeats?: number;
|
|
1034
|
+
/**
|
|
1035
|
+
* Silence at least this long divides phrases; defaults to half a bar of the
|
|
1036
|
+
* opening meter.
|
|
1037
|
+
*
|
|
1038
|
+
* @defaultValue half a bar of the opening meter
|
|
1039
|
+
*/
|
|
1040
|
+
restBeats?: number;
|
|
1041
|
+
/**
|
|
1042
|
+
* A note held at least this long reads as an arrival and closes a phrase;
|
|
1043
|
+
* defaults to one bar of the opening meter.
|
|
1044
|
+
*
|
|
1045
|
+
* @defaultValue one bar of the opening meter
|
|
1046
|
+
*/
|
|
1047
|
+
longNoteBeats?: number;
|
|
1048
|
+
/**
|
|
1049
|
+
* A hypermeter already computed for this piece, to be used instead of
|
|
1050
|
+
* inferring one. Pass it when the caller has already asked for the grouping,
|
|
1051
|
+
* or to impose a grouping the search would not have chosen.
|
|
1052
|
+
*
|
|
1053
|
+
* @defaultValue inferred from the notes and the cadences found
|
|
1054
|
+
*/
|
|
1055
|
+
hypermeter?: Hypermeter;
|
|
1056
|
+
/**
|
|
1057
|
+
* Upper bound on the work this call may do.
|
|
1058
|
+
*
|
|
1059
|
+
* @defaultValue {@link DEFAULT_GENERATION_BUDGET}
|
|
1060
|
+
*/
|
|
1061
|
+
budget?: number;
|
|
1062
|
+
};
|
|
1063
|
+
/**
|
|
1064
|
+
* A cadence ranked by how much of the structure it closes.
|
|
1065
|
+
*
|
|
1066
|
+
* @category Arrangement & Analysis
|
|
1067
|
+
*/
|
|
1068
|
+
type StructuralCadence = {
|
|
1069
|
+
/** Index of the phrase this cadence closes, in the array it came from. */
|
|
1070
|
+
phraseIndex: number;
|
|
1071
|
+
/** The beat the cadence arrives on. */
|
|
1072
|
+
atBeat: number;
|
|
1073
|
+
/** The beat the phrase it closes ends at. */
|
|
1074
|
+
endBeat: number;
|
|
1075
|
+
/** The cadence itself. */
|
|
1076
|
+
cadence: CadenceResult;
|
|
1077
|
+
/** Structural weight in [0, 1]; {@link Phrase.structuralWeight}. */
|
|
1078
|
+
weight: number;
|
|
1079
|
+
/** Why it weighs what it does. */
|
|
1080
|
+
rationale: string;
|
|
1081
|
+
};
|
|
1082
|
+
/**
|
|
1083
|
+
* Split a piece into phrases.
|
|
1084
|
+
*
|
|
1085
|
+
* Three kinds of evidence are gathered independently and then reconciled. The
|
|
1086
|
+
* harmony contributes its cadences, each closing at the end of the chord it
|
|
1087
|
+
* arrives on. The melody contributes the rests it breathes at and the notes it
|
|
1088
|
+
* holds long enough to read as arrivals. Repetition contributes the beats where
|
|
1089
|
+
* the music restarts material it has just played. The metre contributes its
|
|
1090
|
+
* hypermetric downbeats, which confirm a boundary but never declare one on
|
|
1091
|
+
* their own.
|
|
1092
|
+
*
|
|
1093
|
+
* The boundaries are then chosen together rather than one at a time: the
|
|
1094
|
+
* reading that best trades each boundary's evidence against how regular the
|
|
1095
|
+
* phrase lengths come out is selected by dynamic programming, at a fixed cost
|
|
1096
|
+
* per cut. That is what stops a phrase being declared at every plausible beat,
|
|
1097
|
+
* and what lets a well-evidenced irregular phrase survive next to regular ones.
|
|
1098
|
+
*
|
|
1099
|
+
* The result is contiguous: phrases abut, and together they cover the span from
|
|
1100
|
+
* the first note to the end of the music. A cadence found in the middle of a
|
|
1101
|
+
* phrase is not lost — it is still reachable through {@link detectCadences} —
|
|
1102
|
+
* but only the cadences the reading puts at phrase ends are ranked by
|
|
1103
|
+
* {@link structuralCadences}.
|
|
1104
|
+
*
|
|
1105
|
+
* @param timeline The chord timeline the cadences are read from.
|
|
1106
|
+
* @param notes The melody, for its rests, held notes and repetitions. Notes
|
|
1107
|
+
* that never sound are dropped.
|
|
1108
|
+
* @param opts Analysis options; see {@link PhraseOptions}.
|
|
1109
|
+
* @returns The phrases, in time order; one phrase covering the whole span when
|
|
1110
|
+
* the span is too short to divide.
|
|
1111
|
+
* @example
|
|
1112
|
+
* ```ts
|
|
1113
|
+
* import { chordTimelineFromNotes, phrasesFromTimeline } from '@libraz/libcantus';
|
|
1114
|
+
* const notes = [
|
|
1115
|
+
* { pitch: 60, startBeat: 0, durationBeat: 4 },
|
|
1116
|
+
* { pitch: 67, startBeat: 4, durationBeat: 4 },
|
|
1117
|
+
* { pitch: 60, startBeat: 8, durationBeat: 8 },
|
|
1118
|
+
* ];
|
|
1119
|
+
* const { timeline } = chordTimelineFromNotes(notes);
|
|
1120
|
+
* const phrases = phrasesFromTimeline(timeline, notes);
|
|
1121
|
+
* phrases[phrases.length - 1]?.cadence?.cadence.type; // 'authentic', when one closes it
|
|
1122
|
+
* ```
|
|
1123
|
+
* @category Arrangement & Analysis
|
|
1124
|
+
*/
|
|
1125
|
+
declare function phrasesFromTimeline(timeline: ChordTimeline, notes: readonly NoteEvent[], opts?: PhraseOptions): Phrase[];
|
|
1126
|
+
/**
|
|
1127
|
+
* Rank the cadences that close phrases by how much structure they close.
|
|
1128
|
+
*
|
|
1129
|
+
* This is the question the phrase layer exists to answer. Two chords alone
|
|
1130
|
+
* cannot say whether a V-to-I is the end of the piece or a passing confirmation
|
|
1131
|
+
* halfway through a hyperbar; a phrase reading can, because it knows what the
|
|
1132
|
+
* cadence closes and where that sits. Phrases no cadence closes are left out.
|
|
1133
|
+
*
|
|
1134
|
+
* @param phrases The phrases, as {@link phrasesFromTimeline} returns them.
|
|
1135
|
+
* @returns The cadences, heaviest first; ties keep the earlier one.
|
|
1136
|
+
* @example
|
|
1137
|
+
* ```ts
|
|
1138
|
+
* import { chordTimelineFromNotes, phrasesFromTimeline, structuralCadences } from '@libraz/libcantus';
|
|
1139
|
+
* const notes = [
|
|
1140
|
+
* { pitch: 60, startBeat: 0, durationBeat: 4 },
|
|
1141
|
+
* { pitch: 67, startBeat: 4, durationBeat: 4 },
|
|
1142
|
+
* { pitch: 60, startBeat: 8, durationBeat: 8 },
|
|
1143
|
+
* ];
|
|
1144
|
+
* const { timeline } = chordTimelineFromNotes(notes);
|
|
1145
|
+
* const ranked = structuralCadences(phrasesFromTimeline(timeline, notes));
|
|
1146
|
+
* ranked[0]; // the cadence carrying the most structural weight, if any
|
|
1147
|
+
* ```
|
|
1148
|
+
* @category Arrangement & Analysis
|
|
1149
|
+
*/
|
|
1150
|
+
declare function structuralCadences(phrases: readonly Phrase[]): StructuralCadence[];
|
|
1151
|
+
|
|
1152
|
+
/**
|
|
1153
|
+
* Sections: the largest unit this library reads, recovered from repetition.
|
|
1154
|
+
*
|
|
1155
|
+
* A section is found, not named. What the analysis can see is that a span of
|
|
1156
|
+
* music is a restatement of an earlier one, so the spans are labelled by order
|
|
1157
|
+
* of first appearance — A, B, C — and nothing more is claimed. Calling a
|
|
1158
|
+
* repeated span a chorus would be a guess dressed as a finding: repetition
|
|
1159
|
+
* alone cannot tell a chorus from a second verse, and the label a listener
|
|
1160
|
+
* would give depends on lyrics, arrangement and loudness that the notes do not
|
|
1161
|
+
* carry.
|
|
1162
|
+
*/
|
|
1163
|
+
|
|
1164
|
+
/**
|
|
1165
|
+
* A span of music labelled by which earlier span it restates.
|
|
1166
|
+
*
|
|
1167
|
+
* @category Arrangement & Analysis
|
|
1168
|
+
*/
|
|
1169
|
+
type FormSection = {
|
|
1170
|
+
/**
|
|
1171
|
+
* The section's letter, assigned in order of first appearance: the opening
|
|
1172
|
+
* material is A, the first material unlike it is B, and a later restatement
|
|
1173
|
+
* of A is A again. Past Z the letters double (AA, AB), the way spreadsheet
|
|
1174
|
+
* columns do. No functional name — verse, chorus, bridge — is ever inferred.
|
|
1175
|
+
*/
|
|
1176
|
+
label: string;
|
|
1177
|
+
/** First beat of the section; negative when it opens with a pickup. */
|
|
1178
|
+
startBeat: number;
|
|
1179
|
+
/** End of the section, exclusive. */
|
|
1180
|
+
endBeat: number;
|
|
1181
|
+
/** The section's length in bars. */
|
|
1182
|
+
bars: number;
|
|
1183
|
+
/**
|
|
1184
|
+
* Index, in the returned array, of the first section carrying this label; the
|
|
1185
|
+
* section's own index when it is that first appearance.
|
|
1186
|
+
*/
|
|
1187
|
+
firstOccurrence: number;
|
|
1188
|
+
/**
|
|
1189
|
+
* How alike this section and its first occurrence are, in [0, 1]; 1 on the
|
|
1190
|
+
* first occurrence itself. The mean over the units the section was merged
|
|
1191
|
+
* from, each scored against the unit it matched.
|
|
1192
|
+
*/
|
|
1193
|
+
similarity: number;
|
|
1194
|
+
/** How the section came to carry its label. */
|
|
1195
|
+
rationale: string;
|
|
1196
|
+
};
|
|
1197
|
+
/**
|
|
1198
|
+
* Options for {@link sectionsFromNotes}.
|
|
1199
|
+
*
|
|
1200
|
+
* @category Arrangement & Analysis
|
|
1201
|
+
*/
|
|
1202
|
+
type FormSectionOptions = {
|
|
1203
|
+
/**
|
|
1204
|
+
* A single time signature held across the whole span, as sugar for a
|
|
1205
|
+
* one-element `meters`; defaults to 4/4. Giving both is an input error.
|
|
1206
|
+
*
|
|
1207
|
+
* @defaultValue `4/4`
|
|
1208
|
+
*/
|
|
1209
|
+
ts?: TimeSignature;
|
|
1210
|
+
/**
|
|
1211
|
+
* The meter as it changes over the span, so a piece that changes metre is cut
|
|
1212
|
+
* at its own bar lines.
|
|
1213
|
+
*
|
|
1214
|
+
* @defaultValue 4/4 throughout
|
|
1215
|
+
*/
|
|
1216
|
+
meters?: MeterMap;
|
|
1217
|
+
/**
|
|
1218
|
+
* Length of the unit spans are compared in, in bars; defaults to one hyperbar.
|
|
1219
|
+
*
|
|
1220
|
+
* Sections are found by comparing equal units and merging the neighbours that
|
|
1221
|
+
* match, so this is the resolution of the answer rather than the length of a
|
|
1222
|
+
* section: a section is any whole number of units long.
|
|
1223
|
+
*
|
|
1224
|
+
* @defaultValue the inferred hyperbar, in bars
|
|
1225
|
+
*/
|
|
1226
|
+
unitBars?: number;
|
|
1227
|
+
/**
|
|
1228
|
+
* How alike two units must be before the later one is called a restatement of
|
|
1229
|
+
* the earlier, in [0, 1].
|
|
1230
|
+
*
|
|
1231
|
+
* @defaultValue `0.8`
|
|
1232
|
+
*/
|
|
1233
|
+
threshold?: number;
|
|
1234
|
+
/**
|
|
1235
|
+
* End of the analysed span in beats; defaults to the end of the last note.
|
|
1236
|
+
*
|
|
1237
|
+
* @defaultValue the end of the last note
|
|
1238
|
+
*/
|
|
1239
|
+
totalBeats?: number;
|
|
1240
|
+
/**
|
|
1241
|
+
* Upper bound on the work this call may do.
|
|
1242
|
+
*
|
|
1243
|
+
* @defaultValue {@link DEFAULT_GENERATION_BUDGET}
|
|
1244
|
+
*/
|
|
1245
|
+
budget?: number;
|
|
1246
|
+
};
|
|
1247
|
+
/**
|
|
1248
|
+
* Recover a piece's sections from what it repeats.
|
|
1249
|
+
*
|
|
1250
|
+
* The span is cut into equal units of `unitBars` bars, each unit is compared
|
|
1251
|
+
* with every unit before it, and a unit alike enough to an earlier one takes
|
|
1252
|
+
* that unit's label. Neighbouring units carrying the same label are then merged,
|
|
1253
|
+
* so an eight-bar section built of two like four-bar units is reported as one
|
|
1254
|
+
* section rather than two.
|
|
1255
|
+
*
|
|
1256
|
+
* Likeness is measured on the melodic line and on the harmonic content
|
|
1257
|
+
* together — the same tune under different harmony is a variation, not a
|
|
1258
|
+
* restatement — and the labels are letters in order of first appearance.
|
|
1259
|
+
* Nothing beyond "this is a restatement of that" is asserted: see
|
|
1260
|
+
* {@link FormSection.label}.
|
|
1261
|
+
*
|
|
1262
|
+
* A pickup belongs to the section it leads into, so material before the first
|
|
1263
|
+
* bar is folded into the opening unit rather than made a section of its own.
|
|
1264
|
+
*
|
|
1265
|
+
* @param notes The piece's notes, in any order. Notes that never sound are
|
|
1266
|
+
* dropped.
|
|
1267
|
+
* @param opts Analysis options; see {@link FormSectionOptions}.
|
|
1268
|
+
* @returns The sections, in time order.
|
|
1269
|
+
* @example
|
|
1270
|
+
* ```ts
|
|
1271
|
+
* import { sectionsFromNotes } from '@libraz/libcantus';
|
|
1272
|
+
* const bar = (pitch: number, at: number) => ({ pitch, startBeat: at, durationBeat: 4 });
|
|
1273
|
+
* const notes = [bar(60, 0), bar(64, 4), bar(67, 8), bar(60, 12)];
|
|
1274
|
+
* sectionsFromNotes(notes, { unitBars: 2 }).map((section) => section.label);
|
|
1275
|
+
* ```
|
|
1276
|
+
* @category Arrangement & Analysis
|
|
1277
|
+
*/
|
|
1278
|
+
declare function sectionsFromNotes(notes: readonly NoteEvent[], opts?: FormSectionOptions): FormSection[];
|
|
1279
|
+
|
|
1280
|
+
/**
|
|
1281
|
+
* Melody as a shape, rather than as a run of notes.
|
|
1282
|
+
*
|
|
1283
|
+
* A motif is held here as an interval contour plus a rhythmic profile, which is
|
|
1284
|
+
* what makes a restatement at another pitch level the same motif; the absolute
|
|
1285
|
+
* pitches only ever describe one statement of it. On that footing two statements
|
|
1286
|
+
* can be compared and the transformation between them named — repetition,
|
|
1287
|
+
* transposition, inversion, retrograde, augmentation — and phrases that stand in
|
|
1288
|
+
* no such relation can still be scored for similarity.
|
|
1289
|
+
*/
|
|
1290
|
+
|
|
1291
|
+
/**
|
|
1292
|
+
* Step direction between two consecutive notes.
|
|
1293
|
+
*
|
|
1294
|
+
* @category Arrangement & Analysis
|
|
1295
|
+
*/
|
|
1296
|
+
type ContourDirection = 'up' | 'down' | 'same';
|
|
1297
|
+
/**
|
|
1298
|
+
* The overall shape of a line.
|
|
1299
|
+
*
|
|
1300
|
+
* The first four names are the vocabulary the motif generator's contours are
|
|
1301
|
+
* written in, and mean the same here. `'static'` is the shape a generator never
|
|
1302
|
+
* asks for and an analysis regularly meets: a line that does not move. A line
|
|
1303
|
+
* that dips and comes back — an arch upside down — reads as `'wave'`, since the
|
|
1304
|
+
* shared vocabulary has no name of its own for it.
|
|
1305
|
+
*
|
|
1306
|
+
* A wave has to turn more than once to be heard as one, so a cell the generator
|
|
1307
|
+
* writes for `'wave'` reads back as `'wave'` only from three bars up; the one-
|
|
1308
|
+
* and two-bar cells turn once and read as `'arch'`. The other three shapes come
|
|
1309
|
+
* back under their own name at every length.
|
|
1310
|
+
*
|
|
1311
|
+
* @category Arrangement & Analysis
|
|
1312
|
+
*/
|
|
1313
|
+
type MelodicContourShape = 'arch' | 'ascending' | 'descending' | 'wave' | 'static';
|
|
1314
|
+
/**
|
|
1315
|
+
* The abstract shape of a line: its step directions and what they add up to.
|
|
1316
|
+
*
|
|
1317
|
+
* @category Arrangement & Analysis
|
|
1318
|
+
*/
|
|
1319
|
+
type MelodicContour = {
|
|
1320
|
+
/** One direction per adjacent pair of notes, in time order. */
|
|
1321
|
+
directions: ContourDirection[];
|
|
1322
|
+
/** What the directions add up to. */
|
|
1323
|
+
shape: MelodicContourShape;
|
|
1324
|
+
/** Index of the highest note; ties keep the earliest. */
|
|
1325
|
+
peakIndex: number;
|
|
1326
|
+
/** Index of the lowest note; ties keep the earliest. */
|
|
1327
|
+
troughIndex: number;
|
|
1328
|
+
/** Semitone distance from the lowest note to the highest. */
|
|
1329
|
+
range: number;
|
|
1330
|
+
/** Why the line was given that shape. */
|
|
1331
|
+
rationale: string;
|
|
1332
|
+
};
|
|
1333
|
+
/**
|
|
1334
|
+
* One statement of a motif, addressed in the melody it was found in.
|
|
1335
|
+
*
|
|
1336
|
+
* The notes themselves are not repeated here: a statement is the motif's notes
|
|
1337
|
+
* moved by `transpose` and stretched by `timeRatio`, and `noteIndex` addresses
|
|
1338
|
+
* the original array for a caller that wants them exactly.
|
|
1339
|
+
*
|
|
1340
|
+
* @category Arrangement & Analysis
|
|
1341
|
+
*/
|
|
1342
|
+
type MotifOccurrence = {
|
|
1343
|
+
/**
|
|
1344
|
+
* Index of the statement's first note among the sounding notes of the melody,
|
|
1345
|
+
* in time order. Notes that never sound are dropped before the search, so this
|
|
1346
|
+
* is not an index into the caller's array unless the caller's array has none.
|
|
1347
|
+
*/
|
|
1348
|
+
noteIndex: number;
|
|
1349
|
+
/** Onset of the statement's first note. */
|
|
1350
|
+
startBeat: number;
|
|
1351
|
+
/** End of the statement's last note, exclusive. */
|
|
1352
|
+
endBeat: number;
|
|
1353
|
+
/** Semitones this statement stands above the motif's first statement. */
|
|
1354
|
+
transpose: number;
|
|
1355
|
+
/**
|
|
1356
|
+
* How much this statement is stretched against the motif's first: 2 for a
|
|
1357
|
+
* statement in doubled note values, 0.5 for one in halved values, 1 for one in
|
|
1358
|
+
* the same values.
|
|
1359
|
+
*/
|
|
1360
|
+
timeRatio: number;
|
|
1361
|
+
};
|
|
1362
|
+
/**
|
|
1363
|
+
* A recurring melodic cell.
|
|
1364
|
+
*
|
|
1365
|
+
* Identity is the interval sequence plus the rhythmic profile, so every
|
|
1366
|
+
* statement of the motif carries the same `intervals` and `rhythm` however far
|
|
1367
|
+
* it has been transposed or however wide its note values are written. `notes`
|
|
1368
|
+
* holds one statement — the first — exactly as it sounds.
|
|
1369
|
+
*
|
|
1370
|
+
* @category Arrangement & Analysis
|
|
1371
|
+
*/
|
|
1372
|
+
type MotifData = {
|
|
1373
|
+
/** The first statement, exactly as it sounds in the melody. */
|
|
1374
|
+
notes: NoteEvent[];
|
|
1375
|
+
/**
|
|
1376
|
+
* Semitones between consecutive notes. This is the transposition-invariant
|
|
1377
|
+
* identity of the cell: two statements a fifth apart share it.
|
|
1378
|
+
*/
|
|
1379
|
+
intervals: number[];
|
|
1380
|
+
/**
|
|
1381
|
+
* Onset-to-onset gaps as multiples of the first gap, so an even cell reads
|
|
1382
|
+
* `[1, 1, 1]` whatever the tempo or note values it is written in.
|
|
1383
|
+
*/
|
|
1384
|
+
rhythm: number[];
|
|
1385
|
+
/** Every statement found, in time order; the first is `notes`. */
|
|
1386
|
+
occurrences: MotifOccurrence[];
|
|
1387
|
+
/** Why this counts as a motif. */
|
|
1388
|
+
rationale: string;
|
|
1389
|
+
};
|
|
1390
|
+
/**
|
|
1391
|
+
* Options controlling {@link extractMotifs}.
|
|
1392
|
+
*
|
|
1393
|
+
* @category Arrangement & Analysis
|
|
1394
|
+
*/
|
|
1395
|
+
type ExtractMotifsOptions = {
|
|
1396
|
+
/**
|
|
1397
|
+
* Shortest cell to report, in notes.
|
|
1398
|
+
*
|
|
1399
|
+
* @defaultValue 3
|
|
1400
|
+
*/
|
|
1401
|
+
minNotes?: number;
|
|
1402
|
+
/**
|
|
1403
|
+
* Longest cell to report, in notes.
|
|
1404
|
+
*
|
|
1405
|
+
* @defaultValue 8
|
|
1406
|
+
*/
|
|
1407
|
+
maxNotes?: number;
|
|
1408
|
+
/**
|
|
1409
|
+
* How many statements a cell needs before it is reported.
|
|
1410
|
+
*
|
|
1411
|
+
* @defaultValue 2
|
|
1412
|
+
*/
|
|
1413
|
+
minOccurrences?: number;
|
|
1414
|
+
/**
|
|
1415
|
+
* Upper bound on the work this call may do.
|
|
1416
|
+
*
|
|
1417
|
+
* @defaultValue {@link DEFAULT_GENERATION_BUDGET}
|
|
1418
|
+
*/
|
|
1419
|
+
budget?: number;
|
|
1420
|
+
};
|
|
1421
|
+
/**
|
|
1422
|
+
* How one statement of a motif stands to another.
|
|
1423
|
+
*
|
|
1424
|
+
* `'transposition'` is the literal one — every interval preserved, which is what
|
|
1425
|
+
* makes a sequence built on it a *real* sequence. `'tonalTransposition'` shifts
|
|
1426
|
+
* by scale degrees instead and lets the key resize the intervals, which is what
|
|
1427
|
+
* makes a sequence built on it a *tonal* sequence; the two are different devices
|
|
1428
|
+
* and are never reported as one. `'augmentation'` and `'diminution'` name the
|
|
1429
|
+
* relation only when the pitches are untouched, so an inversion in doubled note
|
|
1430
|
+
* values reads as `'inversion'` with a `timeRatio` of 2 rather than losing the
|
|
1431
|
+
* inversion to the stretch.
|
|
1432
|
+
*
|
|
1433
|
+
* @category Arrangement & Analysis
|
|
1434
|
+
*/
|
|
1435
|
+
type MotifRelationKind = 'repetition' | 'transposition' | 'tonalTransposition' | 'inversion' | 'retrograde' | 'retrogradeInversion' | 'augmentation' | 'diminution';
|
|
1436
|
+
/**
|
|
1437
|
+
* The transformation that turns one motif into another.
|
|
1438
|
+
*
|
|
1439
|
+
* @category Arrangement & Analysis
|
|
1440
|
+
*/
|
|
1441
|
+
type MotifRelation = {
|
|
1442
|
+
/** The named transformation. */
|
|
1443
|
+
kind: MotifRelationKind;
|
|
1444
|
+
/**
|
|
1445
|
+
* Whether the second statement begins where the first ends, which is what
|
|
1446
|
+
* separates a sequence from a restatement elsewhere in the piece: a
|
|
1447
|
+
* `'transposition'` that is one is a real sequence, a `'tonalTransposition'`
|
|
1448
|
+
* that is one is a tonal sequence.
|
|
1449
|
+
*/
|
|
1450
|
+
sequence: boolean;
|
|
1451
|
+
/** Semitones from the first statement's first note to the second's. */
|
|
1452
|
+
semitones: number;
|
|
1453
|
+
/** The same distance named as a spelled interval. */
|
|
1454
|
+
interval: SpelledInterval;
|
|
1455
|
+
/** Scale degrees the second statement stands above the first, when tonal. */
|
|
1456
|
+
degrees?: number;
|
|
1457
|
+
/**
|
|
1458
|
+
* How much the second statement is stretched against the first: 2 for doubled
|
|
1459
|
+
* note values, 0.5 for halved, 1 for unchanged.
|
|
1460
|
+
*/
|
|
1461
|
+
timeRatio: number;
|
|
1462
|
+
/** Why the pair was given that name. */
|
|
1463
|
+
rationale: string;
|
|
1464
|
+
};
|
|
1465
|
+
/**
|
|
1466
|
+
* Anything that can be read as a melodic line: a motif, or plain note events.
|
|
1467
|
+
*
|
|
1468
|
+
* @category Arrangement & Analysis
|
|
1469
|
+
*/
|
|
1470
|
+
type MelodicPhrase = MotifData | readonly NoteEvent[];
|
|
1471
|
+
/**
|
|
1472
|
+
* How alike two lines are, and how the answer was reached.
|
|
1473
|
+
*
|
|
1474
|
+
* @category Arrangement & Analysis
|
|
1475
|
+
*/
|
|
1476
|
+
type MelodicComparison = {
|
|
1477
|
+
/** Overall likeness in [0, 1]; 1 for the same line, transposition included. */
|
|
1478
|
+
similarity: number;
|
|
1479
|
+
/** Likeness of the interval sequences alone, in [0, 1]. */
|
|
1480
|
+
pitchSimilarity: number;
|
|
1481
|
+
/** Likeness of the rhythmic profiles alone, in [0, 1]. */
|
|
1482
|
+
rhythmSimilarity: number;
|
|
1483
|
+
/** Why the lines scored what they did. */
|
|
1484
|
+
rationale: string;
|
|
1485
|
+
};
|
|
1486
|
+
/**
|
|
1487
|
+
* Find the cells a melody keeps coming back to.
|
|
1488
|
+
*
|
|
1489
|
+
* Every window of `minNotes` to `maxNotes` consecutive notes is reduced to its
|
|
1490
|
+
* interval sequence and its rhythmic profile, and windows sharing both are
|
|
1491
|
+
* statements of one motif — so a restatement a fifth higher, or one written in
|
|
1492
|
+
* doubled note values, is found as the same cell rather than as a new one.
|
|
1493
|
+
* Overlapping statements of a repetitive figure are counted once, and a cell
|
|
1494
|
+
* whose statements all sit inside the statements of a longer cell that recurs at
|
|
1495
|
+
* least as often is dropped, so the answer names the longest thing that recurs
|
|
1496
|
+
* rather than every fragment of it.
|
|
1497
|
+
*
|
|
1498
|
+
* The melody is expected to be monophonic, as {@link analyzeVoice} expects a
|
|
1499
|
+
* voice to be; split polyphonic material into lines first. Onsets are expected to
|
|
1500
|
+
* be quantised, since two statements must agree on their rhythm to be recognised
|
|
1501
|
+
* as one motif.
|
|
1502
|
+
*
|
|
1503
|
+
* @param notes The melody, in time order. Notes that never sound are dropped.
|
|
1504
|
+
* @param opts Cell-length bounds and the recurrence threshold; see
|
|
1505
|
+
* {@link ExtractMotifsOptions}.
|
|
1506
|
+
* @returns The motifs found, longest first, then by how often they recur, then
|
|
1507
|
+
* by where they first appear. Empty when nothing recurs.
|
|
1508
|
+
* @example
|
|
1509
|
+
* ```ts
|
|
1510
|
+
* import { extractMotifs } from '@libraz/libcantus';
|
|
1511
|
+
* const notes = [
|
|
1512
|
+
* { pitch: 60, startBeat: 0, durationBeat: 1 },
|
|
1513
|
+
* { pitch: 62, startBeat: 1, durationBeat: 1 },
|
|
1514
|
+
* { pitch: 64, startBeat: 2, durationBeat: 1 },
|
|
1515
|
+
* { pitch: 67, startBeat: 4, durationBeat: 1 },
|
|
1516
|
+
* { pitch: 69, startBeat: 5, durationBeat: 1 },
|
|
1517
|
+
* { pitch: 71, startBeat: 6, durationBeat: 1 },
|
|
1518
|
+
* ];
|
|
1519
|
+
* const motifs = extractMotifs(notes);
|
|
1520
|
+
* motifs[0]?.occurrences.map((o) => o.startBeat); // [0, 4]
|
|
1521
|
+
* ```
|
|
1522
|
+
* @category Arrangement & Analysis
|
|
1523
|
+
*/
|
|
1524
|
+
declare function extractMotifs(notes: readonly NoteEvent[], opts?: ExtractMotifsOptions): MotifData[];
|
|
1525
|
+
/**
|
|
1526
|
+
* Read a run of notes as a single-statement motif.
|
|
1527
|
+
*
|
|
1528
|
+
* {@link extractMotifs} answers with motifs it found recurring; this is how a
|
|
1529
|
+
* caller names one it already has — a subject, an answer, a phrase lifted out of
|
|
1530
|
+
* a score — so that it can be handed to {@link relateMotifs}.
|
|
1531
|
+
*
|
|
1532
|
+
* @param notes The cell, in time order. Notes that never sound are dropped.
|
|
1533
|
+
* @returns The motif, carrying exactly one occurrence.
|
|
1534
|
+
* @example
|
|
1535
|
+
* ```ts
|
|
1536
|
+
* import { motifFromNotes, relateMotifs } from '@libraz/libcantus';
|
|
1537
|
+
* const subject = motifFromNotes([
|
|
1538
|
+
* { pitch: 60, startBeat: 0, durationBeat: 1 },
|
|
1539
|
+
* { pitch: 62, startBeat: 1, durationBeat: 1 },
|
|
1540
|
+
* { pitch: 64, startBeat: 2, durationBeat: 1 },
|
|
1541
|
+
* ]);
|
|
1542
|
+
* const answer = motifFromNotes([
|
|
1543
|
+
* { pitch: 67, startBeat: 3, durationBeat: 1 },
|
|
1544
|
+
* { pitch: 69, startBeat: 4, durationBeat: 1 },
|
|
1545
|
+
* { pitch: 71, startBeat: 5, durationBeat: 1 },
|
|
1546
|
+
* ]);
|
|
1547
|
+
* relateMotifs(subject, answer)?.kind; // 'transposition'
|
|
1548
|
+
* ```
|
|
1549
|
+
* @category Arrangement & Analysis
|
|
1550
|
+
*/
|
|
1551
|
+
declare function motifFromNotes(notes: readonly NoteEvent[]): MotifData;
|
|
1552
|
+
/**
|
|
1553
|
+
* Name how two statements of a motif relate.
|
|
1554
|
+
*
|
|
1555
|
+
* The two are compared as shapes, not as pitches: the interval sequences decide
|
|
1556
|
+
* the pitch transformation and the onset gaps decide the timing, so a statement
|
|
1557
|
+
* a fourth higher in doubled note values is recognised for what it is. A pair
|
|
1558
|
+
* whose rhythms do not correspond — the same way round for a repetition or a
|
|
1559
|
+
* transposition, back to front for a retrograde — is not a transformation of the
|
|
1560
|
+
* motif but a different figure, and answers null; {@link melodicSimilarity} is
|
|
1561
|
+
* what scores those.
|
|
1562
|
+
*
|
|
1563
|
+
* With a `key` in hand the search also asks whether the second statement is the
|
|
1564
|
+
* first moved by scale degrees rather than by semitones. That is the tonal
|
|
1565
|
+
* answer to the real one, and the two are kept apart: a real transposition keeps
|
|
1566
|
+
* every interval literally, a tonal one lets the key resize them, so a subject
|
|
1567
|
+
* answered a step higher reads as `'transposition'` when its major second stays
|
|
1568
|
+
* major and as `'tonalTransposition'` when the scale turns it minor.
|
|
1569
|
+
*
|
|
1570
|
+
* @param a The model statement.
|
|
1571
|
+
* @param b The statement to name against it.
|
|
1572
|
+
* @param key Key context for the tonal reading; without it, a statement that
|
|
1573
|
+
* only holds diatonically is not recognised.
|
|
1574
|
+
* @returns The relation, or null when the two stand in none.
|
|
1575
|
+
* @example
|
|
1576
|
+
* ```ts
|
|
1577
|
+
* import { majorKey, motifFromNotes, relateMotifs } from '@libraz/libcantus';
|
|
1578
|
+
* const model = motifFromNotes([
|
|
1579
|
+
* { pitch: 60, startBeat: 0, durationBeat: 1 },
|
|
1580
|
+
* { pitch: 62, startBeat: 1, durationBeat: 1 },
|
|
1581
|
+
* { pitch: 64, startBeat: 2, durationBeat: 1 },
|
|
1582
|
+
* ]);
|
|
1583
|
+
* const answer = motifFromNotes([
|
|
1584
|
+
* { pitch: 62, startBeat: 3, durationBeat: 1 },
|
|
1585
|
+
* { pitch: 64, startBeat: 4, durationBeat: 1 },
|
|
1586
|
+
* { pitch: 65, startBeat: 5, durationBeat: 1 },
|
|
1587
|
+
* ]);
|
|
1588
|
+
* relateMotifs(model, answer, majorKey(0))?.kind; // 'tonalTransposition'
|
|
1589
|
+
* ```
|
|
1590
|
+
* @category Arrangement & Analysis
|
|
1591
|
+
*/
|
|
1592
|
+
declare function relateMotifs(a: MotifData, b: MotifData, key?: KeyScale): MotifRelation | null;
|
|
1593
|
+
/**
|
|
1594
|
+
* Compare two lines and say why they scored what they did.
|
|
1595
|
+
*
|
|
1596
|
+
* The metric is a normalised edit distance over the interval sequence, weighted
|
|
1597
|
+
* against the same over the rhythmic profile. Intervals rather than pitches,
|
|
1598
|
+
* because the same tune in another key is the same tune; edit distance rather
|
|
1599
|
+
* than a step-by-step correlation, because a variant that adds a passing note or
|
|
1600
|
+
* drops one still has to line up with its model, and only an alignment-based
|
|
1601
|
+
* measure can do that. Substituting one interval for another costs by how far
|
|
1602
|
+
* apart they are rather than a flat step, so an answer that widens a third to a
|
|
1603
|
+
* fourth stays close to its model while one that replaces it with a leap does
|
|
1604
|
+
* not. The rhythm carries the smaller share: a melody is recognised mostly by
|
|
1605
|
+
* its pitch shape, but a figure in a wholly different rhythm is a different
|
|
1606
|
+
* figure.
|
|
1607
|
+
*
|
|
1608
|
+
* @param a The first line: a motif, or plain note events.
|
|
1609
|
+
* @param b The second line.
|
|
1610
|
+
* @returns The overall likeness, the two terms behind it, and a rationale.
|
|
1611
|
+
* @example
|
|
1612
|
+
* ```ts
|
|
1613
|
+
* import { compareMelodies } from '@libraz/libcantus';
|
|
1614
|
+
* const line = [
|
|
1615
|
+
* { pitch: 60, startBeat: 0, durationBeat: 1 },
|
|
1616
|
+
* { pitch: 62, startBeat: 1, durationBeat: 1 },
|
|
1617
|
+
* { pitch: 64, startBeat: 2, durationBeat: 1 },
|
|
1618
|
+
* ];
|
|
1619
|
+
* compareMelodies(line, line).similarity; // 1
|
|
1620
|
+
* ```
|
|
1621
|
+
* @category Arrangement & Analysis
|
|
1622
|
+
*/
|
|
1623
|
+
declare function compareMelodies(a: MelodicPhrase, b: MelodicPhrase): MelodicComparison;
|
|
1624
|
+
/**
|
|
1625
|
+
* How alike two lines are, in [0, 1].
|
|
1626
|
+
*
|
|
1627
|
+
* The number {@link compareMelodies} reaches, for callers that only want to rank
|
|
1628
|
+
* phrases: 1 for the same line — a transposition of it included, since the
|
|
1629
|
+
* measure is built on intervals — and lower the further two lines are from being
|
|
1630
|
+
* each other's variant. Symmetric in its arguments, and deterministic.
|
|
1631
|
+
*
|
|
1632
|
+
* @param a The first line: a motif, or plain note events.
|
|
1633
|
+
* @param b The second line.
|
|
1634
|
+
* @returns The likeness in [0, 1].
|
|
1635
|
+
* @example
|
|
1636
|
+
* ```ts
|
|
1637
|
+
* import { melodicSimilarity } from '@libraz/libcantus';
|
|
1638
|
+
* const line = [
|
|
1639
|
+
* { pitch: 60, startBeat: 0, durationBeat: 1 },
|
|
1640
|
+
* { pitch: 62, startBeat: 1, durationBeat: 1 },
|
|
1641
|
+
* { pitch: 64, startBeat: 2, durationBeat: 1 },
|
|
1642
|
+
* ];
|
|
1643
|
+
* melodicSimilarity(line, line); // 1
|
|
1644
|
+
* ```
|
|
1645
|
+
* @category Arrangement & Analysis
|
|
1646
|
+
*/
|
|
1647
|
+
declare function melodicSimilarity(a: MelodicPhrase, b: MelodicPhrase): number;
|
|
1648
|
+
/**
|
|
1649
|
+
* Read the shape of a line.
|
|
1650
|
+
*
|
|
1651
|
+
* The step directions are the literal answer; the shape is what they add up to.
|
|
1652
|
+
* A line is called ascending or descending when most of its motion runs one way,
|
|
1653
|
+
* not only when every step does — a climb with one dip in it is still heard as a
|
|
1654
|
+
* climb — and an arch when it rises to an interior peak and comes back down.
|
|
1655
|
+
* Anything that keeps changing direction is a wave, and a line that does not move
|
|
1656
|
+
* is static.
|
|
1657
|
+
*
|
|
1658
|
+
* @param notes The line: a motif, or plain note events. Notes that never sound
|
|
1659
|
+
* are dropped.
|
|
1660
|
+
* @returns The directions, the shape, the peak and trough, and a rationale.
|
|
1661
|
+
* @example
|
|
1662
|
+
* ```ts
|
|
1663
|
+
* import { melodicContour } from '@libraz/libcantus';
|
|
1664
|
+
* const notes = [
|
|
1665
|
+
* { pitch: 60, startBeat: 0, durationBeat: 1 },
|
|
1666
|
+
* { pitch: 64, startBeat: 1, durationBeat: 1 },
|
|
1667
|
+
* { pitch: 60, startBeat: 2, durationBeat: 1 },
|
|
1668
|
+
* ];
|
|
1669
|
+
* melodicContour(notes).shape; // 'arch'
|
|
1670
|
+
* ```
|
|
1671
|
+
* @category Arrangement & Analysis
|
|
1672
|
+
*/
|
|
1673
|
+
declare function melodicContour(notes: MelodicPhrase): MelodicContour;
|
|
1674
|
+
|
|
1675
|
+
/**
|
|
1676
|
+
* Harmonic reduction: which chords carry the progression and which decorate it.
|
|
1677
|
+
*
|
|
1678
|
+
* `analyzeVoice` can say that a note is a passing tone; a timeline is flat, so
|
|
1679
|
+
* nothing could say the same of a chord. This is that answer one level up — the
|
|
1680
|
+
* middle chord of `Cmaj7 - C#dim7 - Dm7` is an embellishment, and the frame
|
|
1681
|
+
* under `I - IV - #ivo7 - V - I` is `I - IV - V - I`.
|
|
1682
|
+
*
|
|
1683
|
+
* **The position taken here.** What counts as structural is a theoretical
|
|
1684
|
+
* choice, not a fact, and this module makes the functional one: *a chord is
|
|
1685
|
+
* structural unless it is demonstrably an embellishment of its neighbours*. The
|
|
1686
|
+
* frame is what carries harmonic function in the prevailing key — the tonic, the
|
|
1687
|
+
* dominant, and the chords that cadence — so those are never reduced away; a
|
|
1688
|
+
* chord that tonicizes something other than the tonic is prolonging, not
|
|
1689
|
+
* framing, however dominant its sonority. Everything else keeps its place unless
|
|
1690
|
+
* its root traces one of two figures against the chords around it, the same two
|
|
1691
|
+
* figures {@link analyzeVoice} matches at the note level: a step through
|
|
1692
|
+
* (passing) and a step away and back (neighbor). Defaulting to structural, and
|
|
1693
|
+
* demoting only on evidence, is what keeps a reduction from quietly deleting
|
|
1694
|
+
* harmony it merely failed to recognize.
|
|
1695
|
+
*
|
|
1696
|
+
* The salience reading — that the frame is whatever lasts longest, regardless of
|
|
1697
|
+
* function — is the main alternative, and it is a live one for repertoire where
|
|
1698
|
+
* function is weak. {@link ReduceProgressionOptions.basis} switches to it: the
|
|
1699
|
+
* figures stay the same, only what protects a chord from them changes.
|
|
1700
|
+
*
|
|
1701
|
+
* Both readings are deterministic: the same timeline and key always reduce the
|
|
1702
|
+
* same way.
|
|
1703
|
+
*/
|
|
1704
|
+
|
|
1705
|
+
/**
|
|
1706
|
+
* The place a chord holds in a progression.
|
|
1707
|
+
*
|
|
1708
|
+
* `'structural'` is the frame; `'passing'` and `'neighbor'` are the two
|
|
1709
|
+
* embellishing figures, spelled exactly as {@link TheoryLabel} spells them for
|
|
1710
|
+
* a note, so one legend covers both levels of analysis.
|
|
1711
|
+
*
|
|
1712
|
+
* @category Arrangement & Analysis
|
|
1713
|
+
*/
|
|
1714
|
+
type ReductionLevel = 'structural' | 'passing' | 'neighbor';
|
|
1715
|
+
/**
|
|
1716
|
+
* A chord with the place it holds, the beats it holds it for, and a short
|
|
1717
|
+
* rationale.
|
|
1718
|
+
*
|
|
1719
|
+
* One entry per timeline segment, in the same order. The entry carries the
|
|
1720
|
+
* segment's own beat range, so the chords that survive a filter still say when
|
|
1721
|
+
* they sound: holding the structural chords fixed is what a reharmonizer needs,
|
|
1722
|
+
* and it cannot hold a chord at a place the filter threw away.
|
|
1723
|
+
*
|
|
1724
|
+
* @category Arrangement & Analysis
|
|
1725
|
+
*/
|
|
1726
|
+
type ReducedChord = {
|
|
1727
|
+
chord: Chord;
|
|
1728
|
+
startBeat: number;
|
|
1729
|
+
endBeat: number;
|
|
1730
|
+
level: ReductionLevel;
|
|
1731
|
+
rationale: string;
|
|
1732
|
+
};
|
|
1733
|
+
/**
|
|
1734
|
+
* What a reduction treats as the frame.
|
|
1735
|
+
*
|
|
1736
|
+
* `'function'` is the functional reading: the tonic, the dominant and the
|
|
1737
|
+
* chords that cadence are the frame. `'duration'` is the salience reading: the
|
|
1738
|
+
* chords that hold the harmony at least as long as the median chord are the
|
|
1739
|
+
* frame, whatever function they carry.
|
|
1740
|
+
*
|
|
1741
|
+
* @category Arrangement & Analysis
|
|
1742
|
+
*/
|
|
1743
|
+
type ReductionBasis = 'function' | 'duration';
|
|
1744
|
+
/**
|
|
1745
|
+
* Options for {@link reduceProgression}.
|
|
1746
|
+
*
|
|
1747
|
+
* @category Arrangement & Analysis
|
|
1748
|
+
*/
|
|
1749
|
+
type ReduceProgressionOptions = {
|
|
1750
|
+
/**
|
|
1751
|
+
* Which reading decides what the frame is; see {@link ReductionBasis}.
|
|
1752
|
+
*
|
|
1753
|
+
* @defaultValue `'function'`
|
|
1754
|
+
*/
|
|
1755
|
+
basis?: ReductionBasis;
|
|
1756
|
+
};
|
|
1757
|
+
/**
|
|
1758
|
+
* Reduce a chord progression to its frame, labelling every chord.
|
|
1759
|
+
*
|
|
1760
|
+
* One entry comes back per timeline segment, in the same order, so the two read
|
|
1761
|
+
* in step. Filtering the result to `'structural'` is the skeleton view — the
|
|
1762
|
+
* `I - IV - V - I` under a progression that spells far more chords than that —
|
|
1763
|
+
* and holding those chords fixed is what a reharmonizer needs in order to
|
|
1764
|
+
* rewrite the surface without moving the harmony. Each entry carries its own
|
|
1765
|
+
* `startBeat` and `endBeat`, so a filtered chord still knows where it sounds
|
|
1766
|
+
* once the segments it was read against are gone.
|
|
1767
|
+
*
|
|
1768
|
+
* A chord is structural unless one of two figures demotes it: its root stepping
|
|
1769
|
+
* in from the previous chord and on to the next in the same direction (passing),
|
|
1770
|
+
* or stepping away from a harmony that returns (neighbor). Chords carrying the
|
|
1771
|
+
* frame are never demoted, and what carries the frame is the module's stated
|
|
1772
|
+
* theoretical position — see {@link ReductionBasis} for the reading this takes
|
|
1773
|
+
* by default and the one it can be switched to.
|
|
1774
|
+
*
|
|
1775
|
+
* Chords separated by a rest form no figure across it, the way
|
|
1776
|
+
* {@link detectCadences} pairs no chords across one.
|
|
1777
|
+
*
|
|
1778
|
+
* @param timeline The chord timeline to reduce.
|
|
1779
|
+
* @param key The prevailing key, or the key in force at a given beat, for music
|
|
1780
|
+
* that modulates.
|
|
1781
|
+
* @param opts Which reading decides the frame; see
|
|
1782
|
+
* {@link ReduceProgressionOptions}.
|
|
1783
|
+
* @returns One labelled chord per segment, in time order.
|
|
1784
|
+
* @throws If `opts.basis` is not one of the readings {@link ReductionBasis}
|
|
1785
|
+
* names.
|
|
1786
|
+
* @example
|
|
1787
|
+
* ```ts
|
|
1788
|
+
* import { chordTimelineFromChords, majorKey, reduceProgression } from '@libraz/libcantus';
|
|
1789
|
+
* const timeline = chordTimelineFromChords(
|
|
1790
|
+
* [
|
|
1791
|
+
* { rootPc: 0, quality: 'maj7', startBeat: 0 },
|
|
1792
|
+
* { rootPc: 1, quality: 'dim7', startBeat: 4 },
|
|
1793
|
+
* { rootPc: 2, quality: 'min7', startBeat: 8 },
|
|
1794
|
+
* ],
|
|
1795
|
+
* 12,
|
|
1796
|
+
* );
|
|
1797
|
+
* reduceProgression(timeline, majorKey(0)).map((entry) => entry.level);
|
|
1798
|
+
* // ['structural', 'passing', 'structural']
|
|
1799
|
+
* ```
|
|
1800
|
+
* @category Arrangement & Analysis
|
|
1801
|
+
*/
|
|
1802
|
+
declare function reduceProgression(timeline: ChordTimeline, key: KeyContext, opts?: ReduceProgressionOptions): ReducedChord[];
|
|
1803
|
+
|
|
1804
|
+
export { reduceProgression as $, type ArrangementAnalysis as A, type TrackRole as B, type ChordMatch as C, type DetectChordOptions as D, type ExtractMotifsOptions as E, type FormSection as F, analyzeArrangement as G, type Hypermeter as H, compareMelodies as I, createArrangementSession as J, type KeyMatch as K, detectChord as L, MODAL_SCALE_NAMES as M, detectChordBest as N, detectKey as O, type Phrase as P, detectKeyBest as Q, type ReduceProgressionOptions as R, type StructuralCadence as S, type TensionPoint as T, detectKeyFromNotes as U, extractMotifs as V, hypermeter as W, melodicContour as X, melodicSimilarity as Y, motifFromNotes as Z, phrasesFromTimeline as _, type ArrangementOptions as a, relateMotifs as a0, sectionsFromNotes as a1, structuralCadences as a2, tensionCurve as a3, tensionCurveFrom as a4, type ArrangementSession as b, type ArrangementTrack as c, type Conflict as d, type ContourDirection as e, type DetectKeyOptions as f, type FormSectionOptions as g, type HypermeterOptions as h, type KeyVariant as i, type MelodicComparison as j, type MelodicContour as k, type MelodicContourShape as l, type MelodicPhrase as m, type ModalCandidate as n, type ModalScaleName as o, type MotifData as p, type MotifOccurrence as q, type MotifRelation as r, type MotifRelationKind as s, type PhraseOptions as t, type PhraseSignal as u, type ReducedChord as v, type ReductionBasis as w, type ReductionLevel as x, type TrackAnalysis as y, type TrackEdit as z };
|