@libraz/libcantus 0.9.3 → 0.9.5
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 +29 -20
- package/README_ja.md +268 -0
- package/dist/analyze/index.cjs +39 -1947
- package/dist/analyze/index.cjs.map +1 -1
- package/dist/analyze/index.d.cts +161 -91
- package/dist/analyze/index.d.ts +161 -91
- package/dist/analyze/index.js +19 -8
- package/dist/chunk-2A2QMA74.cjs +261 -0
- package/dist/chunk-2A2QMA74.cjs.map +1 -0
- package/dist/chunk-2PTFGVD7.js +261 -0
- package/dist/chunk-2PTFGVD7.js.map +1 -0
- package/dist/chunk-357ZDPVU.cjs +162 -0
- package/dist/chunk-357ZDPVU.cjs.map +1 -0
- package/dist/chunk-3KX76RLG.js +1548 -0
- package/dist/chunk-3KX76RLG.js.map +1 -0
- package/dist/chunk-4VZ5MZAM.cjs +889 -0
- package/dist/chunk-4VZ5MZAM.cjs.map +1 -0
- package/dist/{chunk-C5XYKUVG.js → chunk-7EONYVI5.js} +471 -171
- package/dist/chunk-7EONYVI5.js.map +1 -0
- package/dist/{chunk-IT2O4XFE.js → chunk-7JEP3MFY.js} +42 -34
- package/dist/chunk-7JEP3MFY.js.map +1 -0
- package/dist/{chunk-4MHA432G.js → chunk-AID75RN3.js} +572 -250
- package/dist/chunk-AID75RN3.js.map +1 -0
- package/dist/chunk-BSLTLGM3.cjs +497 -0
- package/dist/chunk-BSLTLGM3.cjs.map +1 -0
- package/dist/chunk-CU6VG2YM.cjs +109 -0
- package/dist/chunk-CU6VG2YM.cjs.map +1 -0
- package/dist/{chunk-EH6EM37U.js → chunk-DZMHIGI4.js} +81 -59
- package/dist/chunk-DZMHIGI4.js.map +1 -0
- package/dist/{chunk-AKLYPWIN.js → chunk-EDOTEABK.js} +45 -19
- package/dist/chunk-EDOTEABK.js.map +1 -0
- package/dist/{chunk-2ZWMSJH6.js → chunk-HN2OEUN2.js} +7 -4
- package/dist/chunk-HN2OEUN2.js.map +1 -0
- package/dist/{chunk-MIWO425M.js → chunk-KBTTEX7Q.js} +128 -43
- package/dist/chunk-KBTTEX7Q.js.map +1 -0
- package/dist/chunk-KQN2ZYND.cjs +724 -0
- package/dist/chunk-KQN2ZYND.cjs.map +1 -0
- package/dist/chunk-MPTVJIVG.cjs +3176 -0
- package/dist/chunk-MPTVJIVG.cjs.map +1 -0
- package/dist/chunk-MTMZTMWM.cjs +53 -0
- package/dist/chunk-MTMZTMWM.cjs.map +1 -0
- package/dist/{chunk-CHMXELDS.js → chunk-NNB67XA2.js} +372 -197
- package/dist/chunk-NNB67XA2.js.map +1 -0
- package/dist/chunk-OL3IHHSJ.cjs +1204 -0
- package/dist/chunk-OL3IHHSJ.cjs.map +1 -0
- package/dist/chunk-PI3UXRWK.cjs +528 -0
- package/dist/chunk-PI3UXRWK.cjs.map +1 -0
- package/dist/chunk-R2VVFAIN.js +528 -0
- package/dist/chunk-R2VVFAIN.js.map +1 -0
- package/dist/chunk-T64C7ZD7.cjs +1 -0
- package/dist/chunk-T64C7ZD7.cjs.map +1 -0
- package/dist/chunk-WFDXOLUB.cjs +337 -0
- package/dist/chunk-WFDXOLUB.cjs.map +1 -0
- package/dist/chunk-WTAN4ACG.cjs +1548 -0
- package/dist/chunk-WTAN4ACG.cjs.map +1 -0
- package/dist/chunk-YFJXS3AU.cjs +33 -0
- package/dist/chunk-YFJXS3AU.cjs.map +1 -0
- package/dist/{chunk-GOAW4VYL.js → chunk-YUE6TJ4M.js} +315 -145
- package/dist/chunk-YUE6TJ4M.js.map +1 -0
- package/dist/{chunk-62QZKCJQ.js → chunk-ZGLJM4A4.js} +10 -6
- package/dist/chunk-ZGLJM4A4.js.map +1 -0
- package/dist/core/index.cjs +140 -647
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +313 -41
- package/dist/core/index.d.ts +313 -41
- package/dist/core/index.js +64 -16
- package/dist/{function-DVH4Eusr.d.ts → function-CxixTB02.d.cts} +122 -10
- package/dist/{function-DYt4iKsn.d.cts → function-DTCt675z.d.ts} +122 -10
- package/dist/generate/index.cjs +28 -4085
- package/dist/generate/index.cjs.map +1 -1
- package/dist/generate/index.d.cts +374 -66
- package/dist/generate/index.d.ts +374 -66
- package/dist/generate/index.js +23 -9
- package/dist/{index-Jc0Ryi79.d.ts → index-B2Ulxxpo.d.cts} +21 -5
- package/dist/{index-CpOsP1y9.d.cts → index-BkYJ0xMq.d.cts} +18 -17
- package/dist/{index-CxqBQtH1.d.cts → index-BuaB-ypt.d.ts} +21 -5
- package/dist/index-CAN7v-kw.d.ts +199 -0
- package/dist/{index-Bt4tOUD9.d.ts → index-COw1Ky9g.d.ts} +93 -8
- package/dist/{index-Dikst44v.d.cts → index-Cfx8xgH0.d.ts} +14 -11
- package/dist/{index-DxvARIP2.d.ts → index-DRtIhlJj.d.ts} +18 -17
- package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
- package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
- package/dist/index-alrRninU.d.cts +199 -0
- package/dist/{index-BNlGP86A.d.cts → index-oTQbYYIs.d.cts} +93 -8
- package/dist/index-tR9jTn26.d.cts +275 -0
- package/dist/index-tR9jTn26.d.ts +275 -0
- package/dist/{index-Bg5dYMmZ.d.ts → index-x2vcsduE.d.cts} +14 -11
- package/dist/index.cjs +429 -7383
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -15
- package/dist/index.d.ts +15 -15
- package/dist/index.js +121 -27
- package/dist/model/index.cjs +8 -2562
- package/dist/model/index.cjs.map +1 -1
- package/dist/model/index.d.cts +471 -81
- package/dist/model/index.d.ts +471 -81
- package/dist/model/index.js +6 -7
- package/dist/styled-Bl0ZK28h.d.ts +543 -0
- package/dist/styled-Crv6SA4S.d.cts +543 -0
- package/dist/theory/index.cjs +172 -1822
- package/dist/theory/index.cjs.map +1 -1
- package/dist/theory/index.d.cts +151 -233
- package/dist/theory/index.d.ts +151 -233
- package/dist/theory/index.js +30 -7
- package/dist/{types-MGP29r3t.d.cts → types-BN6_Q9Kl.d.cts} +11 -2
- package/dist/{types-MGP29r3t.d.ts → types-BN6_Q9Kl.d.ts} +11 -2
- package/package.json +34 -13
- package/dist/chunk-2ZWMSJH6.js.map +0 -1
- package/dist/chunk-4MHA432G.js.map +0 -1
- package/dist/chunk-62QZKCJQ.js.map +0 -1
- package/dist/chunk-AKLYPWIN.js.map +0 -1
- package/dist/chunk-C5XYKUVG.js.map +0 -1
- package/dist/chunk-CHMXELDS.js.map +0 -1
- package/dist/chunk-DYWFNC4A.js +0 -152
- package/dist/chunk-DYWFNC4A.js.map +0 -1
- package/dist/chunk-EH6EM37U.js.map +0 -1
- package/dist/chunk-FBQK3ZQN.js +0 -131
- package/dist/chunk-FBQK3ZQN.js.map +0 -1
- package/dist/chunk-GOAW4VYL.js.map +0 -1
- package/dist/chunk-I5R7ZBJS.js +0 -202
- package/dist/chunk-I5R7ZBJS.js.map +0 -1
- package/dist/chunk-IT2O4XFE.js.map +0 -1
- package/dist/chunk-MIWO425M.js.map +0 -1
- package/dist/chunk-SGU7SPLK.js +0 -924
- package/dist/chunk-SGU7SPLK.js.map +0 -1
- package/dist/index-CGMZNCx6.d.cts +0 -140
- package/dist/index-CGMZNCx6.d.ts +0 -140
- package/dist/index-CnZCo29u.d.cts +0 -108
- package/dist/index-JnF4v9bx.d.ts +0 -108
- package/dist/styled-DZ7GjShb.d.ts +0 -282
- package/dist/styled-ZTbdaVFM.d.cts +0 -282
- package/src/analyze/arrange/index.ts +0 -21
- package/src/analyze/arrange/internal.ts +0 -127
- package/src/analyze/arrange/tension.ts +0 -199
- package/src/analyze/arrange/tracks.ts +0 -312
- package/src/analyze/detect/index.ts +0 -272
- package/src/analyze/functional/borrowed.ts +0 -84
- package/src/analyze/functional/cadence.ts +0 -66
- package/src/analyze/functional/function.ts +0 -184
- package/src/analyze/functional/index.ts +0 -23
- package/src/analyze/functional/internal.ts +0 -29
- package/src/analyze/functional/roman.ts +0 -402
- package/src/analyze/index.ts +0 -49
- package/src/analyze/timeline/index.ts +0 -451
- package/src/analyze/voice/index.ts +0 -339
- package/src/core/event-index/index.ts +0 -104
- package/src/core/index.ts +0 -61
- package/src/core/interval/index.ts +0 -81
- package/src/core/meter/index.ts +0 -302
- package/src/core/pitch/index.ts +0 -311
- package/src/core/random/index.ts +0 -76
- package/src/core/tuning/index.ts +0 -186
- package/src/core/types.ts +0 -29
- package/src/core/validation/index.ts +0 -119
- package/src/generate/bass/index.ts +0 -373
- package/src/generate/countermelody/index.ts +0 -470
- package/src/generate/drums/beat.ts +0 -266
- package/src/generate/drums/euclid.ts +0 -65
- package/src/generate/drums/fills.ts +0 -356
- package/src/generate/drums/ghost.ts +0 -119
- package/src/generate/drums/hihat.ts +0 -285
- package/src/generate/drums/hit.ts +0 -31
- package/src/generate/drums/index.ts +0 -361
- package/src/generate/drums/internal.ts +0 -257
- package/src/generate/drums/kick.ts +0 -160
- package/src/generate/drums/percussion.ts +0 -121
- package/src/generate/drums/rng.ts +0 -7
- package/src/generate/drums/swing.ts +0 -71
- package/src/generate/groove/index.ts +0 -351
- package/src/generate/harmonize/index.ts +0 -451
- package/src/generate/index.ts +0 -58
- package/src/generate/motif/index.ts +0 -422
- package/src/generate/progression/index.ts +0 -362
- package/src/generate/reharmony/index.ts +0 -323
- package/src/generate/rhythm/index.ts +0 -175
- package/src/index.ts +0 -13
- package/src/model/chord.ts +0 -442
- package/src/model/index.ts +0 -15
- package/src/model/interval.ts +0 -82
- package/src/model/key.ts +0 -266
- package/src/model/note.ts +0 -176
- package/src/model/progression.ts +0 -161
- package/src/model/shared.ts +0 -12
- package/src/theory/chord/index.ts +0 -375
- package/src/theory/chordscale/index.ts +0 -374
- package/src/theory/counterpoint/index.ts +0 -230
- package/src/theory/harmony/index.ts +0 -77
- package/src/theory/index.ts +0 -112
- package/src/theory/safety/index.ts +0 -488
- package/src/theory/scale/degrees.ts +0 -123
- package/src/theory/scale/index.ts +0 -28
- package/src/theory/scale/key.ts +0 -56
- package/src/theory/scale/masks.ts +0 -153
- package/src/theory/spelling/index.ts +0 -249
- package/src/theory/symbol/index.ts +0 -268
- package/src/theory/voicing/index.ts +0 -5
- package/src/theory/voicing/internal.ts +0 -168
- package/src/theory/voicing/leading.ts +0 -112
- package/src/theory/voicing/satb.ts +0 -216
- package/src/theory/voicing/styled.ts +0 -199
- package/tsconfig.json +0 -23
package/dist/analyze/index.cjs
CHANGED
|
@@ -1,1970 +1,62 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
-
var __export = (target, all) => {
|
|
7
|
-
for (var name in all)
|
|
8
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
-
};
|
|
10
|
-
var __copyProps = (to, from, except, desc) => {
|
|
11
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
-
for (let key of __getOwnPropNames(from))
|
|
13
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true});
|
|
19
2
|
|
|
20
|
-
// src/analyze/index.ts
|
|
21
|
-
var analyze_exports = {};
|
|
22
|
-
__export(analyze_exports, {
|
|
23
|
-
analyzeArrangement: () => analyzeArrangement,
|
|
24
|
-
analyzeChord: () => analyzeChord,
|
|
25
|
-
analyzeVoice: () => analyzeVoice,
|
|
26
|
-
borrowedSource: () => borrowedSource,
|
|
27
|
-
chordTimelineFromChords: () => chordTimelineFromChords,
|
|
28
|
-
chordTimelineFromNotes: () => chordTimelineFromNotes,
|
|
29
|
-
chordToRoman: () => chordToRoman,
|
|
30
|
-
detectCadence: () => detectCadence,
|
|
31
|
-
detectCadences: () => detectCadences,
|
|
32
|
-
detectChord: () => detectChord,
|
|
33
|
-
detectChordBest: () => detectChordBest,
|
|
34
|
-
detectKey: () => detectKey,
|
|
35
|
-
functionOf: () => functionOf,
|
|
36
|
-
isBorrowedChord: () => isBorrowedChord,
|
|
37
|
-
isDiatonic: () => isDiatonic,
|
|
38
|
-
isMinorKey: () => isMinorKey,
|
|
39
|
-
parallelKey: () => parallelKey,
|
|
40
|
-
romanToChord: () => romanToChord,
|
|
41
|
-
secondaryDominant: () => secondaryDominant,
|
|
42
|
-
tensionCurve: () => tensionCurve
|
|
43
|
-
});
|
|
44
|
-
module.exports = __toCommonJS(analyze_exports);
|
|
45
3
|
|
|
46
|
-
// src/core/validation/index.ts
|
|
47
|
-
var DEFAULT_GENERATION_BUDGET = 1e6;
|
|
48
|
-
function assertFiniteNumber(value, name) {
|
|
49
|
-
if (!Number.isFinite(value)) {
|
|
50
|
-
throw new RangeError(`${name} must be finite; received ${value}`);
|
|
51
|
-
}
|
|
52
|
-
return value;
|
|
53
|
-
}
|
|
54
|
-
function assertInteger(value, name, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) {
|
|
55
|
-
assertFiniteNumber(value, name);
|
|
56
|
-
if (!Number.isSafeInteger(value) || value < min || value > max) {
|
|
57
|
-
throw new RangeError(`${name} must be an integer in [${min}, ${max}]; received ${value}`);
|
|
58
|
-
}
|
|
59
|
-
return value;
|
|
60
|
-
}
|
|
61
|
-
function assertPositiveInt(value, name, max = DEFAULT_GENERATION_BUDGET) {
|
|
62
|
-
return assertInteger(value, name, 1, max);
|
|
63
|
-
}
|
|
64
|
-
function assertRange(value, min, max, name) {
|
|
65
|
-
assertFiniteNumber(value, name);
|
|
66
|
-
if (value < min || value > max) {
|
|
67
|
-
throw new RangeError(`${name} must be in [${min}, ${max}]; received ${value}`);
|
|
68
|
-
}
|
|
69
|
-
return value;
|
|
70
|
-
}
|
|
71
|
-
function assertGenerationBudget(estimated, name, limit = DEFAULT_GENERATION_BUDGET) {
|
|
72
|
-
assertFiniteNumber(estimated, name);
|
|
73
|
-
assertPositiveInt(limit, `${name} limit`, Number.MAX_SAFE_INTEGER);
|
|
74
|
-
if (estimated < 0 || estimated > limit) {
|
|
75
|
-
throw new RangeError(`${name} exceeds the generation budget ${limit}; received ${estimated}`);
|
|
76
|
-
}
|
|
77
|
-
return estimated;
|
|
78
|
-
}
|
|
79
|
-
function assertTimeSignature(ts, name = "time signature") {
|
|
80
|
-
assertPositiveInt(ts.numerator, `${name}.numerator`);
|
|
81
|
-
assertPositiveInt(ts.denominator, `${name}.denominator`);
|
|
82
|
-
if (ts.grouping !== void 0) {
|
|
83
|
-
if (ts.grouping.length === 0) {
|
|
84
|
-
throw new RangeError(`${name}.grouping must not be empty`);
|
|
85
|
-
}
|
|
86
|
-
let sum = 0;
|
|
87
|
-
for (let index = 0; index < ts.grouping.length; index += 1) {
|
|
88
|
-
sum += assertPositiveInt(ts.grouping[index] ?? Number.NaN, `${name}.grouping[${index}]`);
|
|
89
|
-
}
|
|
90
|
-
const compound = ts.numerator % 3 === 0 && ts.numerator > 3;
|
|
91
|
-
const pulses = compound ? ts.numerator / 3 : ts.numerator;
|
|
92
|
-
if (sum !== pulses) {
|
|
93
|
-
throw new RangeError(`${name}.grouping must sum to ${pulses}; received ${sum}`);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return ts;
|
|
97
|
-
}
|
|
98
|
-
function assertNoteEvent(event, name = "note event", options = {}) {
|
|
99
|
-
assertFiniteNumber(event.pitch, `${name}.pitch`);
|
|
100
|
-
assertRange(event.startBeat, 0, Number.MAX_SAFE_INTEGER, `${name}.startBeat`);
|
|
101
|
-
assertRange(
|
|
102
|
-
event.durationBeat,
|
|
103
|
-
options.allowNonPositiveDuration ? -Number.MAX_SAFE_INTEGER : Number.MIN_VALUE,
|
|
104
|
-
Number.MAX_SAFE_INTEGER,
|
|
105
|
-
`${name}.durationBeat`
|
|
106
|
-
);
|
|
107
|
-
if (!options.allowNonPositiveDuration && event.durationBeat <= 0) {
|
|
108
|
-
throw new RangeError(`${name}.durationBeat must be positive; received ${event.durationBeat}`);
|
|
109
|
-
}
|
|
110
|
-
if (event.velocity !== void 0) {
|
|
111
|
-
assertRange(event.velocity, 0, 127, `${name}.velocity`);
|
|
112
|
-
}
|
|
113
|
-
return event;
|
|
114
|
-
}
|
|
115
|
-
function assertNoteEvents(events, name = "note events", options = {}) {
|
|
116
|
-
assertGenerationBudget(events.length, `${name} count`, options.budget);
|
|
117
|
-
for (let index = 0; index < events.length; index += 1) {
|
|
118
|
-
const event = events[index];
|
|
119
|
-
if (event !== void 0) {
|
|
120
|
-
assertNoteEvent(event, `${name}[${index}]`, options);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
return events;
|
|
124
|
-
}
|
|
125
4
|
|
|
126
|
-
// src/core/meter/index.ts
|
|
127
|
-
var EPS = 1e-9;
|
|
128
|
-
function isMultiple(value, unit) {
|
|
129
|
-
if (unit === 0) {
|
|
130
|
-
return false;
|
|
131
|
-
}
|
|
132
|
-
const ratio = value / unit;
|
|
133
|
-
return Math.abs(ratio - Math.round(ratio)) < EPS;
|
|
134
|
-
}
|
|
135
|
-
function parseTimeSignature(text) {
|
|
136
|
-
const match = /^\s*(\d+)\s*\/\s*(\d+)\s*$/.exec(text);
|
|
137
|
-
if (!match) {
|
|
138
|
-
throw new Error(`Invalid time signature: ${text}`);
|
|
139
|
-
}
|
|
140
|
-
const numerator = Number.parseInt(match[1] ?? "", 10);
|
|
141
|
-
const denominator = Number.parseInt(match[2] ?? "", 10);
|
|
142
|
-
if (!Number.isSafeInteger(numerator) || !Number.isSafeInteger(denominator) || numerator <= 0 || denominator <= 0) {
|
|
143
|
-
throw new Error(`Invalid time signature: ${text}`);
|
|
144
|
-
}
|
|
145
|
-
return { numerator, denominator };
|
|
146
|
-
}
|
|
147
|
-
function formatTimeSignature(ts) {
|
|
148
|
-
assertTimeSignature(ts);
|
|
149
|
-
return `${ts.numerator}/${ts.denominator}`;
|
|
150
|
-
}
|
|
151
|
-
function isCompound(ts) {
|
|
152
|
-
assertTimeSignature(ts);
|
|
153
|
-
return ts.numerator % 3 === 0 && ts.numerator > 3;
|
|
154
|
-
}
|
|
155
|
-
function unitBeats(ts) {
|
|
156
|
-
return 4 / ts.denominator;
|
|
157
|
-
}
|
|
158
|
-
function beatsPerBar(ts) {
|
|
159
|
-
assertTimeSignature(ts);
|
|
160
|
-
return ts.numerator * unitBeats(ts);
|
|
161
|
-
}
|
|
162
|
-
function pulseBeats(ts) {
|
|
163
|
-
return isCompound(ts) ? 3 * unitBeats(ts) : unitBeats(ts);
|
|
164
|
-
}
|
|
165
|
-
function pulsesPerBar(ts) {
|
|
166
|
-
return beatsPerBar(ts) / pulseBeats(ts);
|
|
167
|
-
}
|
|
168
|
-
function isValidGrouping(grouping, pulses) {
|
|
169
|
-
if (grouping.length === 0) {
|
|
170
|
-
return false;
|
|
171
|
-
}
|
|
172
|
-
let sum = 0;
|
|
173
|
-
for (const g of grouping) {
|
|
174
|
-
if (!Number.isInteger(g) || g <= 0) {
|
|
175
|
-
return false;
|
|
176
|
-
}
|
|
177
|
-
sum += g;
|
|
178
|
-
}
|
|
179
|
-
return sum === pulses;
|
|
180
|
-
}
|
|
181
|
-
function isGroupHead(grouping, pulseIndex) {
|
|
182
|
-
let acc = 0;
|
|
183
|
-
for (const g of grouping) {
|
|
184
|
-
if (acc === pulseIndex) {
|
|
185
|
-
return true;
|
|
186
|
-
}
|
|
187
|
-
acc += g;
|
|
188
|
-
}
|
|
189
|
-
return false;
|
|
190
|
-
}
|
|
191
|
-
function beatToBarPosition(beatInQuarters, ts) {
|
|
192
|
-
assertFiniteNumber(beatInQuarters, "beat");
|
|
193
|
-
const barLen = beatsPerBar(ts);
|
|
194
|
-
const bar = Math.floor(beatInQuarters / barLen);
|
|
195
|
-
return { bar, beat: beatInQuarters - bar * barLen };
|
|
196
|
-
}
|
|
197
|
-
function metricWeight(beatInQuarters, ts) {
|
|
198
|
-
assertFiniteNumber(beatInQuarters, "beat");
|
|
199
|
-
assertTimeSignature(ts);
|
|
200
|
-
const { beat } = beatToBarPosition(beatInQuarters, ts);
|
|
201
|
-
const pulse = pulseBeats(ts);
|
|
202
|
-
if (!isMultiple(beat, pulse)) {
|
|
203
|
-
return 0;
|
|
204
|
-
}
|
|
205
|
-
const pulses = pulsesPerBar(ts);
|
|
206
|
-
const pulseIndex = Math.round(beat / pulse) % pulses;
|
|
207
|
-
if (pulseIndex === 0) {
|
|
208
|
-
return 3;
|
|
209
|
-
}
|
|
210
|
-
const grouping = ts.grouping;
|
|
211
|
-
if (grouping !== void 0) {
|
|
212
|
-
if (!isValidGrouping(grouping, pulses)) {
|
|
213
|
-
throw new Error(
|
|
214
|
-
`Invalid grouping [${grouping.join(", ")}] for ${formatTimeSignature(ts)}: entries must be positive integers summing to ${pulses}`
|
|
215
|
-
);
|
|
216
|
-
}
|
|
217
|
-
return isGroupHead(grouping, pulseIndex) ? 2 : 1;
|
|
218
|
-
}
|
|
219
|
-
if (pulses % 2 === 0 && pulseIndex === pulses / 2) {
|
|
220
|
-
return 2;
|
|
221
|
-
}
|
|
222
|
-
return 1;
|
|
223
|
-
}
|
|
224
|
-
function isStrongBeat(beatInQuarters, ts) {
|
|
225
|
-
return metricWeight(beatInQuarters, ts) >= 2;
|
|
226
|
-
}
|
|
227
5
|
|
|
228
|
-
// src/theory/scale/degrees.ts
|
|
229
|
-
function pitchClass(pitch) {
|
|
230
|
-
return (Math.trunc(pitch) % 12 + 12) % 12;
|
|
231
|
-
}
|
|
232
|
-
function scaleOffset(pitch, key) {
|
|
233
|
-
return (pitchClass(pitch) - pitchClass(key.rootPc) + 12) % 12;
|
|
234
|
-
}
|
|
235
|
-
function isScaleTone(pitch, key) {
|
|
236
|
-
return (key.modeMask12 >> scaleOffset(pitch, key) & 1) === 1;
|
|
237
|
-
}
|
|
238
|
-
function scaleTonesInDegreeOrder(key) {
|
|
239
|
-
const root = pitchClass(key.rootPc);
|
|
240
|
-
const pcs = [];
|
|
241
|
-
for (let n = 0; n < 12; n += 1) {
|
|
242
|
-
if ((key.modeMask12 >> n & 1) === 1) {
|
|
243
|
-
pcs.push((root + n) % 12);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
return pcs;
|
|
247
|
-
}
|
|
248
6
|
|
|
249
|
-
// src/theory/scale/masks.ts
|
|
250
|
-
function maskFromOffsets(offsets) {
|
|
251
|
-
let mask = 1;
|
|
252
|
-
for (const offset of offsets) {
|
|
253
|
-
mask |= 1 << (offset % 12 + 12) % 12;
|
|
254
|
-
}
|
|
255
|
-
return mask;
|
|
256
|
-
}
|
|
257
|
-
var MAJOR_MASK = 2741;
|
|
258
|
-
var NATURAL_MINOR_MASK = 1453;
|
|
259
|
-
var HARMONIC_MINOR_MASK = maskFromOffsets([0, 2, 3, 5, 7, 8, 11]);
|
|
260
|
-
var MELODIC_MINOR_MASK = maskFromOffsets([0, 2, 3, 5, 7, 9, 11]);
|
|
261
|
-
var DORIAN_MASK = maskFromOffsets([0, 2, 3, 5, 7, 9, 10]);
|
|
262
|
-
var PHRYGIAN_MASK = maskFromOffsets([0, 1, 3, 5, 7, 8, 10]);
|
|
263
|
-
var LYDIAN_MASK = maskFromOffsets([0, 2, 4, 6, 7, 9, 11]);
|
|
264
|
-
var MIXOLYDIAN_MASK = maskFromOffsets([0, 2, 4, 5, 7, 9, 10]);
|
|
265
|
-
var LOCRIAN_MASK = maskFromOffsets([0, 1, 3, 5, 6, 8, 10]);
|
|
266
|
-
var MAJOR_PENTATONIC_MASK = maskFromOffsets([0, 2, 4, 7, 9]);
|
|
267
|
-
var MINOR_PENTATONIC_MASK = maskFromOffsets([0, 3, 5, 7, 10]);
|
|
268
|
-
var BLUES_MASK = maskFromOffsets([0, 3, 5, 6, 7, 10]);
|
|
269
|
-
var WHOLE_TONE_MASK = maskFromOffsets([0, 2, 4, 6, 8, 10]);
|
|
270
|
-
var OCTATONIC_HALF_WHOLE_MASK = maskFromOffsets([0, 1, 3, 4, 6, 7, 9, 10]);
|
|
271
|
-
var OCTATONIC_WHOLE_HALF_MASK = maskFromOffsets([0, 2, 3, 5, 6, 8, 9, 11]);
|
|
272
7
|
|
|
273
|
-
// src/theory/scale/key.ts
|
|
274
|
-
function majorKey(rootPc) {
|
|
275
|
-
return { rootPc: (rootPc % 12 + 12) % 12, modeMask12: MAJOR_MASK };
|
|
276
|
-
}
|
|
277
|
-
function minorKey(rootPc) {
|
|
278
|
-
return { rootPc: (rootPc % 12 + 12) % 12, modeMask12: NATURAL_MINOR_MASK };
|
|
279
|
-
}
|
|
280
8
|
|
|
281
|
-
// src/theory/chord/index.ts
|
|
282
|
-
var QUALITY_INTERVALS = {
|
|
283
|
-
maj: [0, 4, 7],
|
|
284
|
-
min: [0, 3, 7],
|
|
285
|
-
dim: [0, 3, 6],
|
|
286
|
-
aug: [0, 4, 8],
|
|
287
|
-
majb5: [0, 4, 6],
|
|
288
|
-
maj7: [0, 4, 7, 11],
|
|
289
|
-
min7: [0, 3, 7, 10],
|
|
290
|
-
dom7: [0, 4, 7, 10],
|
|
291
|
-
dim7: [0, 3, 6, 9],
|
|
292
|
-
m7b5: [0, 3, 6, 10],
|
|
293
|
-
minMaj7: [0, 3, 7, 11],
|
|
294
|
-
aug7: [0, 4, 8, 10],
|
|
295
|
-
augMaj7: [0, 4, 8, 11],
|
|
296
|
-
"6": [0, 4, 7, 9],
|
|
297
|
-
min6: [0, 3, 7, 9],
|
|
298
|
-
"6/9": [0, 4, 7, 9, 14],
|
|
299
|
-
sus2: [0, 2, 7],
|
|
300
|
-
sus4: [0, 5, 7],
|
|
301
|
-
add9: [0, 4, 7, 14],
|
|
302
|
-
add11: [0, 4, 7, 17],
|
|
303
|
-
maj9: [0, 4, 7, 11, 14],
|
|
304
|
-
min9: [0, 3, 7, 10, 14],
|
|
305
|
-
dom9: [0, 4, 7, 10, 14],
|
|
306
|
-
"7b9": [0, 4, 7, 10, 13],
|
|
307
|
-
"7#9": [0, 4, 7, 10, 15],
|
|
308
|
-
"7#11": [0, 4, 7, 10, 18],
|
|
309
|
-
"7b13": [0, 4, 7, 10, 20],
|
|
310
|
-
"11": [0, 7, 10, 14, 17],
|
|
311
|
-
"13": [0, 4, 7, 10, 14, 21],
|
|
312
|
-
"5": [0, 7]
|
|
313
|
-
};
|
|
314
|
-
function pitchClass2(value) {
|
|
315
|
-
return (Math.trunc(value) % 12 + 12) % 12;
|
|
316
|
-
}
|
|
317
|
-
function chordQualities() {
|
|
318
|
-
return Object.keys(QUALITY_INTERVALS);
|
|
319
|
-
}
|
|
320
|
-
function makeChord(rootPc, quality, bassPc) {
|
|
321
|
-
const chord = {
|
|
322
|
-
rootPc: pitchClass2(rootPc),
|
|
323
|
-
quality,
|
|
324
|
-
intervals: [...QUALITY_INTERVALS[quality]]
|
|
325
|
-
};
|
|
326
|
-
if (bassPc !== void 0) {
|
|
327
|
-
chord.bassPc = pitchClass2(bassPc);
|
|
328
|
-
}
|
|
329
|
-
return chord;
|
|
330
|
-
}
|
|
331
|
-
function chordPitchClasses(chord) {
|
|
332
|
-
const set = /* @__PURE__ */ new Set();
|
|
333
|
-
for (const interval of chord.intervals) {
|
|
334
|
-
set.add(pitchClass2(chord.rootPc + interval));
|
|
335
|
-
}
|
|
336
|
-
return [...set].sort((a, b) => a - b);
|
|
337
|
-
}
|
|
338
|
-
function chordToneOffsets(chord) {
|
|
339
|
-
return new Set(chord.intervals.map((i) => (i % 12 + 12) % 12));
|
|
340
|
-
}
|
|
341
|
-
function chordToneRole(pitch, chord) {
|
|
342
|
-
const interval = (pitchClass2(pitch) - pitchClass2(chord.rootPc) + 12) % 12;
|
|
343
|
-
const tones = chordToneOffsets(chord);
|
|
344
|
-
if (interval === 0) {
|
|
345
|
-
return "root";
|
|
346
|
-
}
|
|
347
|
-
if (interval === 3 || interval === 4) {
|
|
348
|
-
return tones.has(interval) ? "third" : null;
|
|
349
|
-
}
|
|
350
|
-
if (interval === 7) {
|
|
351
|
-
return tones.has(7) ? "fifth" : null;
|
|
352
|
-
}
|
|
353
|
-
if (interval === 6) {
|
|
354
|
-
return tones.has(6) && !tones.has(7) ? "fifth" : null;
|
|
355
|
-
}
|
|
356
|
-
if (interval === 8) {
|
|
357
|
-
return tones.has(8) && !tones.has(7) ? "fifth" : null;
|
|
358
|
-
}
|
|
359
|
-
if (interval === 9) {
|
|
360
|
-
const hasHigherSeventh = tones.has(10) || tones.has(11);
|
|
361
|
-
const isDiminishedSeventh = tones.has(3) && tones.has(6) && !hasHigherSeventh;
|
|
362
|
-
if (isDiminishedSeventh) {
|
|
363
|
-
return "seventh";
|
|
364
|
-
}
|
|
365
|
-
if (tones.has(9) && !hasHigherSeventh) {
|
|
366
|
-
return "sixth";
|
|
367
|
-
}
|
|
368
|
-
return null;
|
|
369
|
-
}
|
|
370
|
-
if (interval === 10 || interval === 11) {
|
|
371
|
-
return tones.has(interval) ? "seventh" : null;
|
|
372
|
-
}
|
|
373
|
-
return null;
|
|
374
|
-
}
|
|
375
9
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
return Math.abs(semitones) % 12;
|
|
379
|
-
}
|
|
380
|
-
function isConsonantInterval(semitones, twoVoice = true) {
|
|
381
|
-
const pc = simpleInterval(semitones);
|
|
382
|
-
if (pc === 5) {
|
|
383
|
-
return !twoVoice;
|
|
384
|
-
}
|
|
385
|
-
return pc === 0 || pc === 3 || pc === 4 || pc === 7 || pc === 8 || pc === 9;
|
|
386
|
-
}
|
|
10
|
+
var _chunk4VZ5MZAMcjs = require('../chunk-4VZ5MZAM.cjs');
|
|
11
|
+
require('../chunk-2A2QMA74.cjs');
|
|
387
12
|
|
|
388
|
-
// src/theory/counterpoint/index.ts
|
|
389
|
-
function pitchClass3(pitch) {
|
|
390
|
-
return (Math.trunc(pitch) % 12 + 12) % 12;
|
|
391
|
-
}
|
|
392
|
-
function createsVerticalDissonance(a, b, twoVoice) {
|
|
393
|
-
return !isConsonantInterval(a - b, twoVoice);
|
|
394
|
-
}
|
|
395
|
-
function isForbiddenMelodicLeap(prev, cur) {
|
|
396
|
-
const semis = Math.abs(cur - prev);
|
|
397
|
-
return semis === 6 || semis === 10 || semis === 11 || semis > 12;
|
|
398
|
-
}
|
|
399
|
-
function simpleClass(semitones) {
|
|
400
|
-
return Math.abs(semitones) % 12;
|
|
401
|
-
}
|
|
402
|
-
function isPerfectClass(cls) {
|
|
403
|
-
return cls === 0 || cls === 7;
|
|
404
|
-
}
|
|
405
|
-
function similarMotion(aMove, bMove) {
|
|
406
|
-
return aMove !== 0 && bMove !== 0 && aMove > 0 === bMove > 0;
|
|
407
|
-
}
|
|
408
|
-
function bothVoicesMove(aMove, bMove) {
|
|
409
|
-
return aMove !== 0 && bMove !== 0;
|
|
410
|
-
}
|
|
411
|
-
function createsParallelPerfect(aPrev, aCur, bPrev, bCur) {
|
|
412
|
-
if (!bothVoicesMove(aCur - aPrev, bCur - bPrev)) {
|
|
413
|
-
return false;
|
|
414
|
-
}
|
|
415
|
-
const nowClass = simpleClass(aCur - bCur);
|
|
416
|
-
const prevClass = simpleClass(aPrev - bPrev);
|
|
417
|
-
return isPerfectClass(nowClass) && nowClass === prevClass;
|
|
418
|
-
}
|
|
419
|
-
function createsHiddenParallelPerfect(aPrev, aCur, bPrev, bCur) {
|
|
420
|
-
const aMove = aCur - aPrev;
|
|
421
|
-
const bMove = bCur - bPrev;
|
|
422
|
-
if (!similarMotion(aMove, bMove)) {
|
|
423
|
-
return false;
|
|
424
|
-
}
|
|
425
|
-
const nowClass = simpleClass(aCur - bCur);
|
|
426
|
-
const prevClass = simpleClass(aPrev - bPrev);
|
|
427
|
-
if (!isPerfectClass(nowClass) || prevClass === nowClass) {
|
|
428
|
-
return false;
|
|
429
|
-
}
|
|
430
|
-
const upperMove = aCur >= bCur ? aMove : bMove;
|
|
431
|
-
if (Math.abs(upperMove) <= 2) {
|
|
432
|
-
return false;
|
|
433
|
-
}
|
|
434
|
-
return true;
|
|
435
|
-
}
|
|
436
|
-
function isLeadingToneResolution(prev, cur, key) {
|
|
437
|
-
const tonic = pitchClass3(key.rootPc);
|
|
438
|
-
const leading = (tonic + 11) % 12;
|
|
439
|
-
if (pitchClass3(prev) !== leading || pitchClass3(cur) !== tonic) {
|
|
440
|
-
return false;
|
|
441
|
-
}
|
|
442
|
-
return cur > prev && cur - prev <= 2;
|
|
443
|
-
}
|
|
444
13
|
|
|
445
|
-
// src/theory/safety/index.ts
|
|
446
|
-
var SUGGESTION_WINDOW = 12;
|
|
447
|
-
var MAX_SUGGESTIONS = 3;
|
|
448
|
-
var REASON_SEVERITY = [
|
|
449
|
-
[2 /* Tension */, 1 /* Warning */, 1 /* Warning */],
|
|
450
|
-
[4 /* AvoidNote */, 1 /* Warning */, 2 /* Dissonant */],
|
|
451
|
-
[8 /* ScaleTone */, 1 /* Warning */, 1 /* Warning */],
|
|
452
|
-
[16 /* NonScale */, 2 /* Dissonant */, 2 /* Dissonant */],
|
|
453
|
-
[32 /* OutOfRange */, 1 /* Warning */, 1 /* Warning */],
|
|
454
|
-
[64 /* Tritone */, 2 /* Dissonant */, 2 /* Dissonant */],
|
|
455
|
-
[128 /* LargeLeap */, 1 /* Warning */, 2 /* Dissonant */],
|
|
456
|
-
[1024 /* VerticalDissonance */, 2 /* Dissonant */, 2 /* Dissonant */],
|
|
457
|
-
[2048 /* ParallelPerfect */, 1 /* Warning */, 2 /* Dissonant */],
|
|
458
|
-
[4096 /* HiddenParallel */, 1 /* Warning */, 2 /* Dissonant */],
|
|
459
|
-
[8192 /* VoiceCrossing */, 1 /* Warning */, 2 /* Dissonant */],
|
|
460
|
-
[16384 /* Suspension */, 2 /* Dissonant */, 2 /* Dissonant */],
|
|
461
|
-
[32768 /* NeedsResolution */, 1 /* Warning */, 2 /* Dissonant */]
|
|
462
|
-
];
|
|
463
|
-
function minimumSafetyForReasons(reasons, profile) {
|
|
464
|
-
let minimum = 0 /* Safe */;
|
|
465
|
-
const severityIndex = profile === "strict" ? 2 : 1;
|
|
466
|
-
for (const entry of REASON_SEVERITY) {
|
|
467
|
-
if (reasons & entry[0]) {
|
|
468
|
-
minimum = Math.max(minimum, entry[severityIndex]);
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
return minimum;
|
|
472
|
-
}
|
|
473
|
-
function pitchClass4(pitch) {
|
|
474
|
-
return (Math.trunc(pitch) % 12 + 12) % 12;
|
|
475
|
-
}
|
|
476
|
-
function intervalAboveRoot(pitch, chord) {
|
|
477
|
-
return ((pitchClass4(pitch) - pitchClass4(chord.rootPc)) % 12 + 12) % 12;
|
|
478
|
-
}
|
|
479
|
-
function isChordMember(pitch, chord) {
|
|
480
|
-
return chordPitchClasses(chord).includes(pitchClass4(pitch));
|
|
481
|
-
}
|
|
482
|
-
function avoidsFourth(chord) {
|
|
483
|
-
return chord.intervals.includes(4) && !chord.intervals.includes(5);
|
|
484
|
-
}
|
|
485
|
-
function avoidsMajorSeventh(chord) {
|
|
486
|
-
return chord.intervals.includes(4) && chord.intervals.includes(10);
|
|
487
|
-
}
|
|
488
|
-
function stepResolution(pitch, chord) {
|
|
489
|
-
for (let delta = 1; delta <= 2; delta += 1) {
|
|
490
|
-
if (isChordMember(pitch - delta, chord)) {
|
|
491
|
-
return pitch - delta;
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
return void 0;
|
|
495
|
-
}
|
|
496
|
-
function evaluateSafety(q) {
|
|
497
|
-
assertSafetyContext(q);
|
|
498
|
-
assertFiniteNumber(q.candidatePitch, "candidatePitch");
|
|
499
|
-
return evaluateInternal(q, true);
|
|
500
|
-
}
|
|
501
|
-
function assertSafetyContext(q) {
|
|
502
|
-
if (q.prevPitch !== void 0) assertFiniteNumber(q.prevPitch, "prevPitch");
|
|
503
|
-
if (q.vocalLow !== void 0) assertFiniteNumber(q.vocalLow, "vocalLow");
|
|
504
|
-
if (q.vocalHigh !== void 0) assertFiniteNumber(q.vocalHigh, "vocalHigh");
|
|
505
|
-
if (q.vocalLow !== void 0 && q.vocalHigh !== void 0 && q.vocalLow > q.vocalHigh) {
|
|
506
|
-
throw new RangeError("vocalLow must not exceed vocalHigh");
|
|
507
|
-
}
|
|
508
|
-
assertGenerationBudget(q.otherVoices.length, "other voices");
|
|
509
|
-
for (let index = 0; index < q.otherVoices.length; index += 1) {
|
|
510
|
-
const voice = q.otherVoices[index];
|
|
511
|
-
if (!voice) continue;
|
|
512
|
-
assertFiniteNumber(voice.pitch, `otherVoices[${index}].pitch`);
|
|
513
|
-
if (voice.prevPitch !== void 0) {
|
|
514
|
-
assertFiniteNumber(voice.prevPitch, `otherVoices[${index}].prevPitch`);
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
function evaluateInternal(q, collectSuggestions) {
|
|
519
|
-
let reasons = 0;
|
|
520
|
-
let level = 0 /* Safe */;
|
|
521
|
-
let resolveTo;
|
|
522
|
-
const raise = (l) => {
|
|
523
|
-
if (l > level) {
|
|
524
|
-
level = l;
|
|
525
|
-
}
|
|
526
|
-
};
|
|
527
|
-
const avoidSeverity = q.profile === "strict" ? 2 /* Dissonant */ : 1 /* Warning */;
|
|
528
|
-
const parallelSeverity = avoidSeverity;
|
|
529
|
-
const pitch = q.candidatePitch;
|
|
530
|
-
if (q.vocalLow !== void 0 && pitch < q.vocalLow || q.vocalHigh !== void 0 && pitch > q.vocalHigh) {
|
|
531
|
-
reasons |= 32 /* OutOfRange */;
|
|
532
|
-
raise(1 /* Warning */);
|
|
533
|
-
}
|
|
534
|
-
const inScale = isScaleTone(pitch, q.key);
|
|
535
|
-
const chord = q.chord;
|
|
536
|
-
if (chord && isChordMember(pitch, chord)) {
|
|
537
|
-
reasons |= 1 /* ChordTone */;
|
|
538
|
-
} else if (chord) {
|
|
539
|
-
const ic = intervalAboveRoot(pitch, chord);
|
|
540
|
-
const isTension = ic === 2 || ic === 5 || ic === 9;
|
|
541
|
-
const avoid = ic === 5 && avoidsFourth(chord) || ic === 11 && avoidsMajorSeventh(chord);
|
|
542
|
-
if (avoid) {
|
|
543
|
-
reasons |= 4 /* AvoidNote */;
|
|
544
|
-
if (isTension) {
|
|
545
|
-
reasons |= 2 /* Tension */;
|
|
546
|
-
}
|
|
547
|
-
reasons |= 32768 /* NeedsResolution */;
|
|
548
|
-
resolveTo = stepResolution(pitch, chord);
|
|
549
|
-
raise(avoidSeverity);
|
|
550
|
-
} else if (isTension && inScale) {
|
|
551
|
-
reasons |= 2 /* Tension */;
|
|
552
|
-
raise(1 /* Warning */);
|
|
553
|
-
} else if (inScale) {
|
|
554
|
-
reasons |= 8 /* ScaleTone */;
|
|
555
|
-
raise(1 /* Warning */);
|
|
556
|
-
} else {
|
|
557
|
-
reasons |= 16 /* NonScale */;
|
|
558
|
-
raise(2 /* Dissonant */);
|
|
559
|
-
}
|
|
560
|
-
} else if (inScale) {
|
|
561
|
-
reasons |= 8 /* ScaleTone */;
|
|
562
|
-
raise(1 /* Warning */);
|
|
563
|
-
} else {
|
|
564
|
-
reasons |= 16 /* NonScale */;
|
|
565
|
-
raise(2 /* Dissonant */);
|
|
566
|
-
}
|
|
567
|
-
if (chord && !(chord.intervals.includes(4) && chord.intervals.includes(10))) {
|
|
568
|
-
const ic = intervalAboveRoot(pitch, chord);
|
|
569
|
-
const toneOffsets = chord.intervals.map((i) => (i % 12 + 12) % 12);
|
|
570
|
-
if (toneOffsets.some((t) => Math.abs(ic - t) === 6)) {
|
|
571
|
-
reasons |= 64 /* Tritone */;
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
if (q.prevPitch !== void 0) {
|
|
575
|
-
const pc = Math.abs(pitch - q.prevPitch) % 12;
|
|
576
|
-
if (pc === 6) {
|
|
577
|
-
reasons |= 64 /* Tritone */;
|
|
578
|
-
raise(2 /* Dissonant */);
|
|
579
|
-
}
|
|
580
|
-
if (isForbiddenMelodicLeap(q.prevPitch, pitch)) {
|
|
581
|
-
reasons |= 128 /* LargeLeap */;
|
|
582
|
-
raise(parallelSeverity);
|
|
583
|
-
}
|
|
584
|
-
if (pc === 1) {
|
|
585
|
-
reasons |= 256 /* MinorSecond */;
|
|
586
|
-
}
|
|
587
|
-
if (pc === 11) {
|
|
588
|
-
reasons |= 512 /* MajorSeventh */;
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
if (q.strongBeat) {
|
|
592
|
-
const twoVoice = q.otherVoices.length === 1;
|
|
593
|
-
const prev = q.prevPitch;
|
|
594
|
-
for (const ov of q.otherVoices) {
|
|
595
|
-
if (!createsVerticalDissonance(pitch, ov.pitch, twoVoice)) {
|
|
596
|
-
continue;
|
|
597
|
-
}
|
|
598
|
-
reasons |= 1024 /* VerticalDissonance */;
|
|
599
|
-
raise(2 /* Dissonant */);
|
|
600
|
-
if (prev !== void 0 && pitch === prev && ov.prevPitch !== void 0 && !createsVerticalDissonance(prev, ov.prevPitch, twoVoice)) {
|
|
601
|
-
reasons |= 16384 /* Suspension */;
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
if (q.prevPitch !== void 0) {
|
|
606
|
-
const prev = q.prevPitch;
|
|
607
|
-
for (const ov of q.otherVoices) {
|
|
608
|
-
if (ov.prevPitch === void 0) {
|
|
609
|
-
continue;
|
|
610
|
-
}
|
|
611
|
-
if (createsParallelPerfect(prev, pitch, ov.prevPitch, ov.pitch)) {
|
|
612
|
-
reasons |= 2048 /* ParallelPerfect */;
|
|
613
|
-
raise(parallelSeverity);
|
|
614
|
-
}
|
|
615
|
-
if (createsHiddenParallelPerfect(prev, pitch, ov.prevPitch, ov.pitch)) {
|
|
616
|
-
reasons |= 4096 /* HiddenParallel */;
|
|
617
|
-
raise(parallelSeverity);
|
|
618
|
-
}
|
|
619
|
-
const crossedNow = pitch - ov.pitch;
|
|
620
|
-
const crossedPrev = prev - ov.prevPitch;
|
|
621
|
-
if (crossedNow !== 0 && crossedPrev !== 0 && crossedNow > 0 !== crossedPrev > 0) {
|
|
622
|
-
reasons |= 8192 /* VoiceCrossing */;
|
|
623
|
-
raise(parallelSeverity);
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
raise(minimumSafetyForReasons(reasons, q.profile));
|
|
628
|
-
const result = { safety: level, reasons, rationale: describe(reasons, q) };
|
|
629
|
-
if (resolveTo !== void 0) {
|
|
630
|
-
result.resolveTo = resolveTo;
|
|
631
|
-
}
|
|
632
|
-
if (collectSuggestions && level !== 0 /* Safe */) {
|
|
633
|
-
const suggestions = findSafeNearby(q, pitch);
|
|
634
|
-
if (suggestions.length > 0) {
|
|
635
|
-
result.suggestions = suggestions;
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
return result;
|
|
639
|
-
}
|
|
640
|
-
function findSafeNearby(q, candidate) {
|
|
641
|
-
const out = [];
|
|
642
|
-
for (let d = 1; d <= SUGGESTION_WINDOW; d += 1) {
|
|
643
|
-
for (const p of [candidate - d, candidate + d]) {
|
|
644
|
-
if (evaluateInternal({ ...q, candidatePitch: p }, false).safety === 0 /* Safe */) {
|
|
645
|
-
out.push(p);
|
|
646
|
-
if (out.length >= MAX_SUGGESTIONS) {
|
|
647
|
-
return out;
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
return out;
|
|
653
|
-
}
|
|
654
|
-
function describe(reasons, q) {
|
|
655
|
-
if (reasons & 32 /* OutOfRange */) {
|
|
656
|
-
return "Outside the target vocal range";
|
|
657
|
-
}
|
|
658
|
-
if (reasons & 16384 /* Suspension */) {
|
|
659
|
-
return "Prepared suspension \u2014 a held consonance now dissonant, awaiting resolution";
|
|
660
|
-
}
|
|
661
|
-
if (reasons & 1024 /* VerticalDissonance */) {
|
|
662
|
-
return "Dissonant against a sounding voice on a strong beat";
|
|
663
|
-
}
|
|
664
|
-
if (reasons & 4 /* AvoidNote */) {
|
|
665
|
-
return "Avoid note \u2014 clashes with a chord tone a semitone away";
|
|
666
|
-
}
|
|
667
|
-
if (reasons & 2048 /* ParallelPerfect */) {
|
|
668
|
-
return "Parallel perfect interval with another voice";
|
|
669
|
-
}
|
|
670
|
-
if (reasons & 4096 /* HiddenParallel */) {
|
|
671
|
-
return "Hidden perfect interval by similar motion";
|
|
672
|
-
}
|
|
673
|
-
if (reasons & 8192 /* VoiceCrossing */) {
|
|
674
|
-
return "Crosses another voice";
|
|
675
|
-
}
|
|
676
|
-
if (reasons & 1 /* ChordTone */) {
|
|
677
|
-
return "Chord tone";
|
|
678
|
-
}
|
|
679
|
-
if (reasons & 2 /* Tension */) {
|
|
680
|
-
return "Chord tension";
|
|
681
|
-
}
|
|
682
|
-
if (reasons & 16 /* NonScale */) {
|
|
683
|
-
return "Outside the key";
|
|
684
|
-
}
|
|
685
|
-
if (reasons & 8 /* ScaleTone */) {
|
|
686
|
-
return "Scale tone, not in the chord";
|
|
687
|
-
}
|
|
688
|
-
return q.chord ? "Placeable" : "No chord context";
|
|
689
|
-
}
|
|
690
14
|
|
|
691
|
-
// src/analyze/functional/internal.ts
|
|
692
|
-
function mod12(n) {
|
|
693
|
-
return (n % 12 + 12) % 12;
|
|
694
|
-
}
|
|
695
|
-
function degreeRootPc(degreeNumber, key) {
|
|
696
|
-
const tones = scaleTonesInDegreeOrder(key);
|
|
697
|
-
if (tones.length === 0) {
|
|
698
|
-
return mod12(key.rootPc);
|
|
699
|
-
}
|
|
700
|
-
return tones[(degreeNumber - 1) % tones.length] ?? mod12(key.rootPc);
|
|
701
|
-
}
|
|
702
|
-
function isNeapolitan(chord, key) {
|
|
703
|
-
return mod12(chord.rootPc - key.rootPc) === 1 && chord.quality === "maj";
|
|
704
|
-
}
|
|
705
15
|
|
|
706
|
-
// src/analyze/functional/roman.ts
|
|
707
|
-
var ROMAN = ["I", "II", "III", "IV", "V", "VI", "VII"];
|
|
708
|
-
var ROMAN_TO_DEGREE = {
|
|
709
|
-
I: 1,
|
|
710
|
-
II: 2,
|
|
711
|
-
III: 3,
|
|
712
|
-
IV: 4,
|
|
713
|
-
V: 5,
|
|
714
|
-
VI: 6,
|
|
715
|
-
VII: 7
|
|
716
|
-
};
|
|
717
|
-
var OFFSET_SPELLING = [
|
|
718
|
-
[1, ""],
|
|
719
|
-
[2, "b"],
|
|
720
|
-
[2, ""],
|
|
721
|
-
[3, "b"],
|
|
722
|
-
[3, ""],
|
|
723
|
-
[4, ""],
|
|
724
|
-
[4, "#"],
|
|
725
|
-
[5, ""],
|
|
726
|
-
[6, "b"],
|
|
727
|
-
[6, ""],
|
|
728
|
-
[7, "b"],
|
|
729
|
-
[7, ""]
|
|
730
|
-
];
|
|
731
|
-
function seventhQuality(base, halfDim) {
|
|
732
|
-
if (halfDim) {
|
|
733
|
-
return "m7b5";
|
|
734
|
-
}
|
|
735
|
-
if (base === "dim") {
|
|
736
|
-
return "dim7";
|
|
737
|
-
}
|
|
738
|
-
if (base === "aug") {
|
|
739
|
-
return "aug7";
|
|
740
|
-
}
|
|
741
|
-
return base === "maj" ? "dom7" : "min7";
|
|
742
|
-
}
|
|
743
|
-
var INVERSION_FIGURES = /* @__PURE__ */ new Set(["6", "64", "65", "43", "42", "2", "7"]);
|
|
744
|
-
var ROMAN_STYLE = {
|
|
745
|
-
maj: { lower: false, suffix: "" },
|
|
746
|
-
min: { lower: true, suffix: "" },
|
|
747
|
-
dim: { lower: true, suffix: "o" },
|
|
748
|
-
aug: { lower: false, suffix: "+" },
|
|
749
|
-
maj7: { lower: false, suffix: "maj7" },
|
|
750
|
-
min7: { lower: true, suffix: "7" },
|
|
751
|
-
dom7: { lower: false, suffix: "7" },
|
|
752
|
-
dim7: { lower: true, suffix: "o7" },
|
|
753
|
-
m7b5: { lower: true, suffix: "\xF87" },
|
|
754
|
-
minMaj7: { lower: true, suffix: "maj7" },
|
|
755
|
-
aug7: { lower: false, suffix: "+7" },
|
|
756
|
-
augMaj7: { lower: false, suffix: "+maj7" },
|
|
757
|
-
majb5: { lower: false, suffix: "b5" },
|
|
758
|
-
"6": { lower: false, suffix: "add6" },
|
|
759
|
-
min6: { lower: true, suffix: "add6" },
|
|
760
|
-
"6/9": { lower: false, suffix: "69" },
|
|
761
|
-
sus2: { lower: false, suffix: "sus2" },
|
|
762
|
-
sus4: { lower: false, suffix: "sus4" },
|
|
763
|
-
add9: { lower: false, suffix: "add9" },
|
|
764
|
-
add11: { lower: false, suffix: "add11" },
|
|
765
|
-
maj9: { lower: false, suffix: "maj9" },
|
|
766
|
-
min9: { lower: true, suffix: "9" },
|
|
767
|
-
dom9: { lower: false, suffix: "9" },
|
|
768
|
-
"7b9": { lower: false, suffix: "7b9" },
|
|
769
|
-
"7#9": { lower: false, suffix: "7#9" },
|
|
770
|
-
"7#11": { lower: false, suffix: "7#11" },
|
|
771
|
-
"7b13": { lower: false, suffix: "7b13" },
|
|
772
|
-
"11": { lower: false, suffix: "11" },
|
|
773
|
-
"13": { lower: false, suffix: "13" },
|
|
774
|
-
"5": { lower: false, suffix: "5" }
|
|
775
|
-
};
|
|
776
|
-
var SUFFIX_QUALITY = new Map(
|
|
777
|
-
Object.entries(ROMAN_STYLE).map(
|
|
778
|
-
([quality, style]) => [`${style.lower ? "l" : "u"}:${style.suffix}`, quality]
|
|
779
|
-
)
|
|
780
|
-
);
|
|
781
|
-
function parseInversion(figures) {
|
|
782
|
-
if (figures === "65") {
|
|
783
|
-
return { inversion: 1, seventh: true };
|
|
784
|
-
}
|
|
785
|
-
if (figures === "43") {
|
|
786
|
-
return { inversion: 2, seventh: true };
|
|
787
|
-
}
|
|
788
|
-
if (figures === "42" || figures === "2") {
|
|
789
|
-
return { inversion: 3, seventh: true };
|
|
790
|
-
}
|
|
791
|
-
if (figures === "7") {
|
|
792
|
-
return { inversion: 0, seventh: true };
|
|
793
|
-
}
|
|
794
|
-
if (figures === "64") {
|
|
795
|
-
return { inversion: 2, seventh: false };
|
|
796
|
-
}
|
|
797
|
-
if (figures === "6") {
|
|
798
|
-
return { inversion: 1, seventh: false };
|
|
799
|
-
}
|
|
800
|
-
return { inversion: 0, seventh: false };
|
|
801
|
-
}
|
|
802
|
-
function parseSimpleRoman(text, key) {
|
|
803
|
-
const match = /^([b#]?)([iIvV]+)(.*)$/.exec(text.trim());
|
|
804
|
-
if (!match) {
|
|
805
|
-
throw new Error(`Invalid Roman numeral: ${text}`);
|
|
806
|
-
}
|
|
807
|
-
const accidental = match[1] === "b" ? -1 : match[1] === "#" ? 1 : 0;
|
|
808
|
-
const glyph = (match[2] ?? "").toUpperCase();
|
|
809
|
-
const degreeNumber = ROMAN_TO_DEGREE[glyph];
|
|
810
|
-
if (degreeNumber === void 0) {
|
|
811
|
-
throw new Error(`Invalid Roman numeral: ${text}`);
|
|
812
|
-
}
|
|
813
|
-
const isUpper = (match[2] ?? "")[0] === (match[2] ?? "")[0]?.toUpperCase();
|
|
814
|
-
const suffix = match[3] ?? "";
|
|
815
|
-
const rootPc = mod12(degreeRootPc(degreeNumber, key) + accidental);
|
|
816
|
-
const canonical = SUFFIX_QUALITY.get(`${isUpper ? "u" : "l"}:${suffix}`);
|
|
817
|
-
if (canonical !== void 0) {
|
|
818
|
-
return { rootPc, quality: canonical, inversion: 0 };
|
|
819
|
-
}
|
|
820
|
-
const parsedSuffix = /^(o|°|dim|ø|\+|aug)?(maj7|M7)?(65|64|43|42|7|6|2)?$/.exec(suffix);
|
|
821
|
-
if (!parsedSuffix) {
|
|
822
|
-
throw new Error(`Unsupported suffix "${suffix}" in Roman numeral: ${text}`);
|
|
823
|
-
}
|
|
824
|
-
const qualityMarker = parsedSuffix[1] ?? "";
|
|
825
|
-
const isDim = qualityMarker === "o" || qualityMarker === "\xB0" || qualityMarker === "dim";
|
|
826
|
-
const isAug = qualityMarker === "+" || qualityMarker === "aug";
|
|
827
|
-
const isHalfDim = qualityMarker === "\xF8";
|
|
828
|
-
const explicitMaj7 = parsedSuffix[2] !== void 0;
|
|
829
|
-
const figures = parsedSuffix[3] ?? "";
|
|
830
|
-
if (figures !== "" && !INVERSION_FIGURES.has(figures)) {
|
|
831
|
-
throw new Error(`Unsupported figured-bass or extension "${figures}" in Roman numeral: ${text}`);
|
|
832
|
-
}
|
|
833
|
-
const { inversion, seventh } = parseInversion(figures);
|
|
834
|
-
const hasSeventh = explicitMaj7 || seventh || isHalfDim;
|
|
835
|
-
let base;
|
|
836
|
-
if (isDim || isHalfDim) {
|
|
837
|
-
base = "dim";
|
|
838
|
-
} else if (isAug) {
|
|
839
|
-
base = "aug";
|
|
840
|
-
} else {
|
|
841
|
-
base = isUpper ? "maj" : "min";
|
|
842
|
-
}
|
|
843
|
-
let quality;
|
|
844
|
-
if (explicitMaj7) {
|
|
845
|
-
quality = base === "min" ? "minMaj7" : base === "aug" ? "augMaj7" : "maj7";
|
|
846
|
-
} else if (hasSeventh) {
|
|
847
|
-
quality = seventhQuality(base, isHalfDim);
|
|
848
|
-
} else if (base === "dim") {
|
|
849
|
-
quality = "dim";
|
|
850
|
-
} else if (base === "aug") {
|
|
851
|
-
quality = "aug";
|
|
852
|
-
} else {
|
|
853
|
-
quality = base;
|
|
854
|
-
}
|
|
855
|
-
return { rootPc, quality, inversion };
|
|
856
|
-
}
|
|
857
|
-
function chordFromParsed(parsed) {
|
|
858
|
-
const chord = makeChord(parsed.rootPc, parsed.quality);
|
|
859
|
-
if (parsed.inversion > 0 && parsed.inversion < chord.intervals.length) {
|
|
860
|
-
chord.bassPc = mod12(parsed.rootPc + (chord.intervals[parsed.inversion] ?? 0));
|
|
861
|
-
}
|
|
862
|
-
return chord;
|
|
863
|
-
}
|
|
864
|
-
function romanToChord(text, key) {
|
|
865
|
-
const trimmed = text.trim();
|
|
866
|
-
const slash = trimmed.indexOf("/");
|
|
867
|
-
if (slash >= 0) {
|
|
868
|
-
const applied = trimmed.slice(0, slash);
|
|
869
|
-
const target = trimmed.slice(slash + 1);
|
|
870
|
-
const targetRoot = parseSimpleRoman(target, key).rootPc;
|
|
871
|
-
const localKey = majorKey(targetRoot);
|
|
872
|
-
return chordFromParsed(parseSimpleRoman(applied, localKey));
|
|
873
|
-
}
|
|
874
|
-
return chordFromParsed(parseSimpleRoman(trimmed, key));
|
|
875
|
-
}
|
|
876
|
-
function romanStyle(quality) {
|
|
877
|
-
return ROMAN_STYLE[quality];
|
|
878
|
-
}
|
|
879
|
-
function romanSpelling(rootPc, key) {
|
|
880
|
-
const tones = scaleTonesInDegreeOrder(key);
|
|
881
|
-
const diatonic = tones.indexOf(mod12(rootPc));
|
|
882
|
-
if (diatonic >= 0) {
|
|
883
|
-
return { degreeNumber: diatonic + 1, accidental: "" };
|
|
884
|
-
}
|
|
885
|
-
for (let i = 0; i < tones.length; i += 1) {
|
|
886
|
-
const degreeNumber2 = i + 1;
|
|
887
|
-
if (degreeNumber2 === 1 || degreeNumber2 === 5) {
|
|
888
|
-
continue;
|
|
889
|
-
}
|
|
890
|
-
if (mod12((tones[i] ?? 0) - 1) === mod12(rootPc)) {
|
|
891
|
-
return { degreeNumber: degreeNumber2, accidental: "b" };
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
for (let i = 0; i < tones.length; i += 1) {
|
|
895
|
-
if (mod12((tones[i] ?? 0) + 1) === mod12(rootPc)) {
|
|
896
|
-
return { degreeNumber: i + 1, accidental: "#" };
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
const [degreeNumber, accidental] = OFFSET_SPELLING[mod12(rootPc - key.rootPc)] ?? [1, ""];
|
|
900
|
-
return { degreeNumber, accidental };
|
|
901
|
-
}
|
|
902
|
-
function baseMarker(quality) {
|
|
903
|
-
if (quality === "dim" || quality === "dim7") {
|
|
904
|
-
return "o";
|
|
905
|
-
}
|
|
906
|
-
if (quality === "m7b5") {
|
|
907
|
-
return "\xF8";
|
|
908
|
-
}
|
|
909
|
-
if (quality === "aug" || quality === "aug7") {
|
|
910
|
-
return "+";
|
|
911
|
-
}
|
|
912
|
-
if (quality === "augMaj7") {
|
|
913
|
-
return "+maj7";
|
|
914
|
-
}
|
|
915
|
-
if (quality === "maj7" || quality === "minMaj7") {
|
|
916
|
-
return "maj7";
|
|
917
|
-
}
|
|
918
|
-
return "";
|
|
919
|
-
}
|
|
920
|
-
var TRIAD_FIGURES = { 1: "6", 2: "64" };
|
|
921
|
-
var SEVENTH_FIGURES = { 1: "65", 2: "43", 3: "42" };
|
|
922
|
-
var TRIAD_FIGURE_QUALITIES = /* @__PURE__ */ new Set(["maj", "min", "dim", "aug"]);
|
|
923
|
-
var SEVENTH_FIGURE_QUALITIES = /* @__PURE__ */ new Set([
|
|
924
|
-
"maj7",
|
|
925
|
-
"min7",
|
|
926
|
-
"dom7",
|
|
927
|
-
"dim7",
|
|
928
|
-
"m7b5",
|
|
929
|
-
"minMaj7",
|
|
930
|
-
"aug7",
|
|
931
|
-
"augMaj7"
|
|
932
|
-
]);
|
|
933
|
-
function chordToRoman(chord, key) {
|
|
934
|
-
const { degreeNumber, accidental } = romanSpelling(chord.rootPc, key);
|
|
935
|
-
const { lower, suffix } = romanStyle(chord.quality);
|
|
936
|
-
const numeral = ROMAN[degreeNumber - 1] ?? "I";
|
|
937
|
-
const cased = lower ? numeral.toLowerCase() : numeral;
|
|
938
|
-
let inversion = 0;
|
|
939
|
-
if (chord.bassPc !== void 0) {
|
|
940
|
-
const idx = chord.intervals.findIndex((iv) => mod12(chord.rootPc + iv) === chord.bassPc);
|
|
941
|
-
if (idx > 0) {
|
|
942
|
-
inversion = idx;
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
if (inversion > 0) {
|
|
946
|
-
const figure = SEVENTH_FIGURE_QUALITIES.has(chord.quality) ? SEVENTH_FIGURES[inversion] : TRIAD_FIGURE_QUALITIES.has(chord.quality) ? TRIAD_FIGURES[inversion] : void 0;
|
|
947
|
-
if (figure !== void 0) {
|
|
948
|
-
return `${accidental}${cased}${baseMarker(chord.quality)}${figure}`;
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
return `${accidental}${cased}${suffix}`;
|
|
952
|
-
}
|
|
953
16
|
|
|
954
|
-
// src/analyze/functional/function.ts
|
|
955
|
-
var FUNCTION_BY_OFFSET = [
|
|
956
|
-
"tonic",
|
|
957
|
-
// 0 I
|
|
958
|
-
"subdominant",
|
|
959
|
-
// 1 bII (Neapolitan)
|
|
960
|
-
"subdominant",
|
|
961
|
-
// 2 ii
|
|
962
|
-
"tonic",
|
|
963
|
-
// 3 bIII
|
|
964
|
-
"tonic",
|
|
965
|
-
// 4 iii
|
|
966
|
-
"subdominant",
|
|
967
|
-
// 5 IV
|
|
968
|
-
"dominant",
|
|
969
|
-
// 6 #IV / bV
|
|
970
|
-
"dominant",
|
|
971
|
-
// 7 V
|
|
972
|
-
"subdominant",
|
|
973
|
-
// 8 bVI
|
|
974
|
-
"tonic",
|
|
975
|
-
// 9 vi
|
|
976
|
-
"subdominant",
|
|
977
|
-
// 10 bVII
|
|
978
|
-
"dominant"
|
|
979
|
-
// 11 vii
|
|
980
|
-
];
|
|
981
|
-
function isMinorKey(key) {
|
|
982
|
-
const hasMinorThird = key.modeMask12 >> 3 & 1;
|
|
983
|
-
const hasMajorThird2 = key.modeMask12 >> 4 & 1;
|
|
984
|
-
return Boolean(hasMinorThird) && !hasMajorThird2;
|
|
985
|
-
}
|
|
986
|
-
function functionOf(chord, key) {
|
|
987
|
-
return FUNCTION_BY_OFFSET[mod12(chord.rootPc - key.rootPc)] ?? "tonic";
|
|
988
|
-
}
|
|
989
|
-
function isDiatonic(chord, key) {
|
|
990
|
-
return chordPitchClasses(chord).every((pc) => isScaleTone(pc, key));
|
|
991
|
-
}
|
|
992
|
-
function parallelKey(key) {
|
|
993
|
-
return {
|
|
994
|
-
rootPc: mod12(key.rootPc),
|
|
995
|
-
modeMask12: isMinorKey(key) ? MAJOR_MASK : NATURAL_MINOR_MASK
|
|
996
|
-
};
|
|
997
|
-
}
|
|
998
|
-
function isDiminishedQuality(quality) {
|
|
999
|
-
return quality === "dim" || quality === "dim7" || quality === "m7b5";
|
|
1000
|
-
}
|
|
1001
|
-
function hasMajorThird(chord) {
|
|
1002
|
-
return chord.intervals.some((interval) => mod12(interval) === 4);
|
|
1003
|
-
}
|
|
1004
|
-
function qualityAwareFunction(chord, key) {
|
|
1005
|
-
const offset = mod12(chord.rootPc - key.rootPc);
|
|
1006
|
-
if (isDiminishedQuality(chord.quality) && (offset === 11 || offset === 6)) {
|
|
1007
|
-
return "dominant";
|
|
1008
|
-
}
|
|
1009
|
-
if (isNeapolitan(chord, key)) {
|
|
1010
|
-
return "subdominant";
|
|
1011
|
-
}
|
|
1012
|
-
if (!isMinorKey(key) && hasMajorThird(chord) && (offset === 8 || offset === 10)) {
|
|
1013
|
-
return "subdominant";
|
|
1014
|
-
}
|
|
1015
|
-
return functionOf(chord, key);
|
|
1016
|
-
}
|
|
1017
|
-
function analyzeChord(chord, key) {
|
|
1018
|
-
const source = borrowedSource(chord, key);
|
|
1019
|
-
return {
|
|
1020
|
-
function: qualityAwareFunction(chord, key),
|
|
1021
|
-
borrowed: source !== null,
|
|
1022
|
-
source,
|
|
1023
|
-
roman: chordToRoman(chord, key)
|
|
1024
|
-
};
|
|
1025
|
-
}
|
|
1026
|
-
function secondaryDominant(targetDegree, key) {
|
|
1027
|
-
const targetRoot = degreeRootPc(targetDegree + 1, key);
|
|
1028
|
-
return makeChord(mod12(targetRoot + 7), "dom7");
|
|
1029
|
-
}
|
|
1030
17
|
|
|
1031
|
-
// src/analyze/functional/borrowed.ts
|
|
1032
|
-
function isHarmonicMinorAlteration(chord, key) {
|
|
1033
|
-
return isMinorKey(key) && isDiatonic(chord, { rootPc: mod12(key.rootPc), modeMask12: HARMONIC_MINOR_MASK });
|
|
1034
|
-
}
|
|
1035
|
-
function isBorrowedChord(chord, key) {
|
|
1036
|
-
if (isDiatonic(chord, key)) {
|
|
1037
|
-
return false;
|
|
1038
|
-
}
|
|
1039
|
-
if (isHarmonicMinorAlteration(chord, key)) {
|
|
1040
|
-
return false;
|
|
1041
|
-
}
|
|
1042
|
-
return isDiatonic(chord, parallelKey(key));
|
|
1043
|
-
}
|
|
1044
|
-
function borrowedSource(chord, key) {
|
|
1045
|
-
if (isDiatonic(chord, key)) {
|
|
1046
|
-
return null;
|
|
1047
|
-
}
|
|
1048
|
-
if (isNeapolitan(chord, key)) {
|
|
1049
|
-
return "neapolitan";
|
|
1050
|
-
}
|
|
1051
|
-
if (isBorrowedChord(chord, key)) {
|
|
1052
|
-
return isMinorKey(key) ? "parallel-major" : "parallel-minor";
|
|
1053
|
-
}
|
|
1054
|
-
return null;
|
|
1055
|
-
}
|
|
1056
18
|
|
|
1057
|
-
// src/analyze/functional/cadence.ts
|
|
1058
|
-
function detectCadence(from, to, key) {
|
|
1059
|
-
const tonic = mod12(key.rootPc);
|
|
1060
|
-
const fromOffset = mod12(from.rootPc - tonic);
|
|
1061
|
-
const toOffset = mod12(to.rootPc - tonic);
|
|
1062
|
-
const deceptiveTargets = isMinorKey(key) ? [8] : [8, 9];
|
|
1063
|
-
if (fromOffset === 7 && toOffset === 0) {
|
|
1064
|
-
return "authentic";
|
|
1065
|
-
}
|
|
1066
|
-
if (fromOffset === 5 && toOffset === 0) {
|
|
1067
|
-
return "plagal";
|
|
1068
|
-
}
|
|
1069
|
-
if (fromOffset === 7 && deceptiveTargets.includes(toOffset)) {
|
|
1070
|
-
return "deceptive";
|
|
1071
|
-
}
|
|
1072
|
-
if (toOffset === 7 && fromOffset !== 7) {
|
|
1073
|
-
return "half";
|
|
1074
|
-
}
|
|
1075
|
-
return null;
|
|
1076
|
-
}
|
|
1077
19
|
|
|
1078
|
-
// src/core/event-index/index.ts
|
|
1079
|
-
var EPS2 = 1e-9;
|
|
1080
|
-
function upperBound(values, beat) {
|
|
1081
|
-
let low = 0;
|
|
1082
|
-
let high = values.length;
|
|
1083
|
-
while (low < high) {
|
|
1084
|
-
const middle = Math.floor((low + high) / 2);
|
|
1085
|
-
if ((values[middle]?.note.startBeat ?? Number.POSITIVE_INFINITY) <= beat) {
|
|
1086
|
-
low = middle + 1;
|
|
1087
|
-
} else {
|
|
1088
|
-
high = middle;
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
return low;
|
|
1092
|
-
}
|
|
1093
|
-
function createNoteEventIndex(events, options = {}) {
|
|
1094
|
-
assertNoteEvents(events, "note events", options);
|
|
1095
|
-
const notes = events.map((note, originalIndex) => ({
|
|
1096
|
-
note,
|
|
1097
|
-
originalIndex,
|
|
1098
|
-
endBeat: note.startBeat + note.durationBeat
|
|
1099
|
-
})).sort((a, b) => a.note.startBeat - b.note.startBeat || a.originalIndex - b.originalIndex);
|
|
1100
|
-
const prefixMaxEnd = [];
|
|
1101
|
-
let maxEnd = Number.NEGATIVE_INFINITY;
|
|
1102
|
-
for (const indexed of notes) {
|
|
1103
|
-
maxEnd = Math.max(maxEnd, indexed.endBeat);
|
|
1104
|
-
prefixMaxEnd.push(maxEnd);
|
|
1105
|
-
}
|
|
1106
|
-
return {
|
|
1107
|
-
notes,
|
|
1108
|
-
at(beat) {
|
|
1109
|
-
let index = upperBound(notes, beat + EPS2) - 1;
|
|
1110
|
-
while (index >= 0) {
|
|
1111
|
-
if ((prefixMaxEnd[index] ?? Number.NEGATIVE_INFINITY) <= beat + EPS2) {
|
|
1112
|
-
return void 0;
|
|
1113
|
-
}
|
|
1114
|
-
const indexed = notes[index];
|
|
1115
|
-
if (indexed && indexed.note.durationBeat > 0 && indexed.note.startBeat - EPS2 <= beat && beat < indexed.endBeat - EPS2) {
|
|
1116
|
-
return indexed;
|
|
1117
|
-
}
|
|
1118
|
-
index -= 1;
|
|
1119
|
-
}
|
|
1120
|
-
return void 0;
|
|
1121
|
-
},
|
|
1122
|
-
attacksAt(beat) {
|
|
1123
|
-
const firstAfter = upperBound(notes, beat + EPS2);
|
|
1124
|
-
const candidate = notes[firstAfter - 1];
|
|
1125
|
-
return candidate !== void 0 && Math.abs(candidate.note.startBeat - beat) < EPS2;
|
|
1126
|
-
},
|
|
1127
|
-
onsetsBetween(startBeat, endBeat) {
|
|
1128
|
-
const result = [];
|
|
1129
|
-
let index = upperBound(notes, startBeat + EPS2);
|
|
1130
|
-
while (index < notes.length) {
|
|
1131
|
-
const onset = notes[index]?.note.startBeat;
|
|
1132
|
-
if (onset === void 0 || onset >= endBeat - EPS2) {
|
|
1133
|
-
break;
|
|
1134
|
-
}
|
|
1135
|
-
if (result[result.length - 1] !== onset) {
|
|
1136
|
-
result.push(onset);
|
|
1137
|
-
}
|
|
1138
|
-
index += 1;
|
|
1139
|
-
}
|
|
1140
|
-
return result;
|
|
1141
|
-
}
|
|
1142
|
-
};
|
|
1143
|
-
}
|
|
1144
20
|
|
|
1145
|
-
// src/analyze/detect/index.ts
|
|
1146
|
-
function pitchClass5(value) {
|
|
1147
|
-
return (Math.trunc(value) % 12 + 12) % 12;
|
|
1148
|
-
}
|
|
1149
|
-
function uniquePitchClasses(pitches) {
|
|
1150
|
-
return [...new Set(pitches.map(pitchClass5))].sort((a, b) => a - b);
|
|
1151
|
-
}
|
|
1152
|
-
function assertPitches(pitches) {
|
|
1153
|
-
assertGenerationBudget(pitches.length, "detection pitches");
|
|
1154
|
-
for (let index = 0; index < pitches.length; index += 1) {
|
|
1155
|
-
assertFiniteNumber(pitches[index] ?? Number.NaN, `pitches[${index}]`);
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
function detectChord(pitches, opts = {}) {
|
|
1159
|
-
assertPitches(pitches);
|
|
1160
|
-
if (opts.bassPc !== void 0) assertFiniteNumber(opts.bassPc, "bassPc");
|
|
1161
|
-
const input = uniquePitchClasses(pitches);
|
|
1162
|
-
if (input.length === 0) {
|
|
1163
|
-
return [];
|
|
1164
|
-
}
|
|
1165
|
-
const inputKind = opts.input === void 0 || opts.input === "auto" ? pitches.every((pitch) => Number.isInteger(pitch) && pitch >= 0 && pitch <= 11) ? "pitchClass" : "midi" : opts.input;
|
|
1166
|
-
const bassPc = opts.bassPc !== void 0 ? pitchClass5(opts.bassPc) : inputKind === "midi" ? pitchClass5(pitches.reduce((lowest, pitch) => Math.min(lowest, pitch), Infinity)) : void 0;
|
|
1167
|
-
const inputSet = new Set(input);
|
|
1168
|
-
const matches = [];
|
|
1169
|
-
const qualities = chordQualities();
|
|
1170
|
-
for (const rootPc of input) {
|
|
1171
|
-
for (const quality of qualities) {
|
|
1172
|
-
const chord = makeChord(rootPc, quality);
|
|
1173
|
-
const tones = chordPitchClasses(chord);
|
|
1174
|
-
const toneSet = new Set(tones);
|
|
1175
|
-
const missingPcs = tones.filter((pc) => !inputSet.has(pc));
|
|
1176
|
-
const fifthPc = pitchClass5(rootPc + 7);
|
|
1177
|
-
if (missingPcs.length > 1 || missingPcs.length === 1 && missingPcs[0] !== fifthPc) {
|
|
1178
|
-
continue;
|
|
1179
|
-
}
|
|
1180
|
-
const extraPcs = input.filter((pc) => !toneSet.has(pc));
|
|
1181
|
-
const bassIndex = bassPc === void 0 ? -1 : chord.intervals.findIndex((iv) => pitchClass5(rootPc + iv) === bassPc);
|
|
1182
|
-
const inversion = bassPc === void 0 ? null : bassIndex > 0 ? bassIndex : 0;
|
|
1183
|
-
const match = {
|
|
1184
|
-
rootPc,
|
|
1185
|
-
quality,
|
|
1186
|
-
missingPcs,
|
|
1187
|
-
extraPcs,
|
|
1188
|
-
exact: extraPcs.length === 0 && missingPcs.length === 0,
|
|
1189
|
-
inversion
|
|
1190
|
-
};
|
|
1191
|
-
if (bassPc !== void 0 && bassPc !== rootPc) {
|
|
1192
|
-
match.bassPc = bassPc;
|
|
1193
|
-
}
|
|
1194
|
-
matches.push(match);
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
|
-
matches.sort((a, b) => {
|
|
1198
|
-
if (a.extraPcs.length !== b.extraPcs.length) {
|
|
1199
|
-
return a.extraPcs.length - b.extraPcs.length;
|
|
1200
|
-
}
|
|
1201
|
-
if (a.missingPcs.length !== b.missingPcs.length) {
|
|
1202
|
-
return a.missingPcs.length - b.missingPcs.length;
|
|
1203
|
-
}
|
|
1204
|
-
if (a.inversion === 0 !== (b.inversion === 0)) {
|
|
1205
|
-
return a.inversion === 0 ? -1 : 1;
|
|
1206
|
-
}
|
|
1207
|
-
const aSize = chordPitchClasses(makeChord(a.rootPc, a.quality)).length;
|
|
1208
|
-
const bSize = chordPitchClasses(makeChord(b.rootPc, b.quality)).length;
|
|
1209
|
-
return bSize - aSize;
|
|
1210
|
-
});
|
|
1211
|
-
return matches;
|
|
1212
|
-
}
|
|
1213
|
-
function detectChordBest(pitches, opts = {}) {
|
|
1214
|
-
const best = detectChord(pitches, opts)[0];
|
|
1215
|
-
if (!best) {
|
|
1216
|
-
return null;
|
|
1217
|
-
}
|
|
1218
|
-
return makeChord(best.rootPc, best.quality, best.bassPc);
|
|
1219
|
-
}
|
|
1220
|
-
var MINOR_MASK_VARIANTS = [NATURAL_MINOR_MASK, HARMONIC_MINOR_MASK, MELODIC_MINOR_MASK];
|
|
1221
|
-
function detectKey(pitches) {
|
|
1222
|
-
assertPitches(pitches);
|
|
1223
|
-
const input = uniquePitchClasses(pitches);
|
|
1224
|
-
if (input.length === 0) {
|
|
1225
|
-
return [];
|
|
1226
|
-
}
|
|
1227
|
-
const counts = /* @__PURE__ */ new Map();
|
|
1228
|
-
for (const pc of pitches.map(pitchClass5)) {
|
|
1229
|
-
counts.set(pc, (counts.get(pc) ?? 0) + 1);
|
|
1230
|
-
}
|
|
1231
|
-
const total = pitches.length;
|
|
1232
|
-
const results = [];
|
|
1233
|
-
for (let tonic = 0; tonic < 12; tonic += 1) {
|
|
1234
|
-
for (const mode of ["major", "minor"]) {
|
|
1235
|
-
const key = mode === "major" ? majorKey(tonic) : minorKey(tonic);
|
|
1236
|
-
const masks = mode === "major" ? [MAJOR_MASK] : MINOR_MASK_VARIANTS;
|
|
1237
|
-
let inScale = 0;
|
|
1238
|
-
let weighted = Number.NEGATIVE_INFINITY;
|
|
1239
|
-
for (const mask of masks) {
|
|
1240
|
-
let variantInScale = 0;
|
|
1241
|
-
let variantWeighted = 0;
|
|
1242
|
-
for (const pc of input) {
|
|
1243
|
-
const offset = (pc - tonic + 12) % 12;
|
|
1244
|
-
if (mask >> offset & 1) {
|
|
1245
|
-
variantInScale += 1;
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
for (const [pc, count] of counts) {
|
|
1249
|
-
const offset = (pc - tonic + 12) % 12;
|
|
1250
|
-
if (mask >> offset & 1) {
|
|
1251
|
-
variantWeighted += count;
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
if (variantWeighted > weighted) {
|
|
1255
|
-
weighted = variantWeighted;
|
|
1256
|
-
inScale = variantInScale;
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
weighted += (counts.get(tonic) ?? 0) * 0.5;
|
|
1260
|
-
results.push({
|
|
1261
|
-
key,
|
|
1262
|
-
mode,
|
|
1263
|
-
fit: inScale / input.length,
|
|
1264
|
-
score: weighted / total
|
|
1265
|
-
});
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
results.sort((a, b) => b.score - a.score);
|
|
1269
|
-
return results.map(({ key, mode, fit }) => ({ key, mode, fit }));
|
|
1270
|
-
}
|
|
1271
21
|
|
|
1272
|
-
// src/analyze/timeline/index.ts
|
|
1273
|
-
function chordTimelineFromChords(chords, totalBeats) {
|
|
1274
|
-
assertRange(totalBeats, 0, Number.MAX_SAFE_INTEGER, "timeline totalBeats");
|
|
1275
|
-
assertGenerationBudget(chords.length, "timeline chords");
|
|
1276
|
-
for (let index = 0; index < chords.length; index += 1) {
|
|
1277
|
-
assertRange(
|
|
1278
|
-
chords[index]?.startBeat ?? Number.NaN,
|
|
1279
|
-
0,
|
|
1280
|
-
Number.MAX_SAFE_INTEGER,
|
|
1281
|
-
`chords[${index}].startBeat`
|
|
1282
|
-
);
|
|
1283
|
-
}
|
|
1284
|
-
const sorted = [...chords].filter((chord) => chord.startBeat < totalBeats).sort((a, b) => a.startBeat - b.startBeat);
|
|
1285
|
-
const segments = sorted.map((gc, i) => {
|
|
1286
|
-
const next = sorted[i + 1];
|
|
1287
|
-
const nextBeat = next ? next.startBeat : totalBeats;
|
|
1288
|
-
const endBeat = Math.min(totalBeats, Math.max(gc.startBeat, nextBeat));
|
|
1289
|
-
return {
|
|
1290
|
-
startBeat: gc.startBeat,
|
|
1291
|
-
endBeat,
|
|
1292
|
-
chord: makeChord(gc.rootPc, gc.quality, gc.bassPc)
|
|
1293
|
-
};
|
|
1294
|
-
}).filter((seg) => seg.endBeat > seg.startBeat);
|
|
1295
|
-
return { at: segmentLookup(segments), segments };
|
|
1296
|
-
}
|
|
1297
|
-
function segmentLookup(segments) {
|
|
1298
|
-
return (beat) => {
|
|
1299
|
-
assertFiniteNumber(beat, "timeline query beat");
|
|
1300
|
-
for (const seg of segments) {
|
|
1301
|
-
if (beat >= seg.startBeat && beat < seg.endBeat) {
|
|
1302
|
-
return seg.chord;
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
return null;
|
|
1306
|
-
};
|
|
1307
|
-
}
|
|
1308
|
-
var EPS3 = 1e-9;
|
|
1309
|
-
var NOISE_THRESHOLD_RATIO = 0.2;
|
|
1310
|
-
var MAX_DETECTION_PCS = 6;
|
|
1311
|
-
var FALLBACK_PCS = 3;
|
|
1312
|
-
var INEXACT_CONFIDENCE_FACTOR = 0.85;
|
|
1313
|
-
var DIATONIC_BONUS = 0.5;
|
|
1314
|
-
var EXACT_BONUS = 0.5;
|
|
1315
|
-
var MISMATCH_PENALTY = 0.3;
|
|
1316
|
-
function pitchClass6(value) {
|
|
1317
|
-
return (Math.trunc(value) % 12 + 12) % 12;
|
|
1318
|
-
}
|
|
1319
|
-
function scoreMatch(match, weights, maxWeight, key) {
|
|
1320
|
-
const rootWeight = maxWeight > 0 ? (weights[match.rootPc] ?? 0) / maxWeight : 0;
|
|
1321
|
-
const tones = chordPitchClasses(makeChord(match.rootPc, match.quality));
|
|
1322
|
-
let score = rootWeight;
|
|
1323
|
-
if (tones.every((pc) => isScaleTone(pc, key))) {
|
|
1324
|
-
score += DIATONIC_BONUS;
|
|
1325
|
-
}
|
|
1326
|
-
if (match.exact) {
|
|
1327
|
-
score += EXACT_BONUS;
|
|
1328
|
-
}
|
|
1329
|
-
score -= MISMATCH_PENALTY * (match.extraPcs.length + match.missingPcs.length);
|
|
1330
|
-
return score;
|
|
1331
|
-
}
|
|
1332
|
-
function chordConfidence(chord, weights, totalWeight, exact) {
|
|
1333
|
-
const tones = new Set(chordPitchClasses(chord));
|
|
1334
|
-
let chordWeight = 0;
|
|
1335
|
-
for (let pc = 0; pc < 12; pc += 1) {
|
|
1336
|
-
if (tones.has(pc)) {
|
|
1337
|
-
chordWeight += weights[pc] ?? 0;
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
const raw = totalWeight > 0 ? chordWeight / totalWeight : 0;
|
|
1341
|
-
const confidence = Math.min(1, Math.max(0, raw));
|
|
1342
|
-
return exact ? confidence : confidence * INEXACT_CONFIDENCE_FACTOR;
|
|
1343
|
-
}
|
|
1344
|
-
function analyzeWindow(notes, windowStart, windowEnd, ts, key) {
|
|
1345
|
-
const weights = new Array(12).fill(0);
|
|
1346
|
-
let lowestPitch = Number.POSITIVE_INFINITY;
|
|
1347
|
-
for (const note of notes) {
|
|
1348
|
-
const noteEnd = note.startBeat + note.durationBeat;
|
|
1349
|
-
const overlap = Math.min(noteEnd, windowEnd) - Math.max(note.startBeat, windowStart);
|
|
1350
|
-
if (overlap <= EPS3) {
|
|
1351
|
-
continue;
|
|
1352
|
-
}
|
|
1353
|
-
const velocityFactor = note.velocity !== void 0 ? note.velocity / 127 : 1;
|
|
1354
|
-
const onsetInWindow = note.startBeat >= windowStart - EPS3 && note.startBeat < windowEnd - EPS3;
|
|
1355
|
-
const accent = onsetInWindow ? 1 + metricWeight(note.startBeat, ts) / 3 : 1;
|
|
1356
|
-
const pc = pitchClass6(note.pitch);
|
|
1357
|
-
weights[pc] = (weights[pc] ?? 0) + overlap * velocityFactor * accent;
|
|
1358
|
-
if (note.pitch < lowestPitch) {
|
|
1359
|
-
lowestPitch = note.pitch;
|
|
1360
|
-
}
|
|
1361
|
-
}
|
|
1362
|
-
let totalWeight = 0;
|
|
1363
|
-
let maxWeight = 0;
|
|
1364
|
-
for (const w of weights) {
|
|
1365
|
-
totalWeight += w;
|
|
1366
|
-
maxWeight = Math.max(maxWeight, w);
|
|
1367
|
-
}
|
|
1368
|
-
if (totalWeight <= EPS3) {
|
|
1369
|
-
return null;
|
|
1370
|
-
}
|
|
1371
|
-
const ranked = weights.map((weight, pc) => ({ pc, weight })).filter(({ weight }) => weight > 0).sort((a, b) => b.weight - a.weight || a.pc - b.pc);
|
|
1372
|
-
const selected = ranked.filter(({ weight }) => weight >= maxWeight * NOISE_THRESHOLD_RATIO).slice(0, MAX_DETECTION_PCS).map(({ pc }) => pc);
|
|
1373
|
-
const bassPc = pitchClass6(lowestPitch);
|
|
1374
|
-
const anchorPc = selected.includes(bassPc) ? bassPc : selected[0] ?? bassPc;
|
|
1375
|
-
const detectionPitches = selected.map((pc) => pc === anchorPc ? pc : pc + 12);
|
|
1376
|
-
const matches = detectChord(detectionPitches);
|
|
1377
|
-
let bestMatch = null;
|
|
1378
|
-
let bestScore = Number.NEGATIVE_INFINITY;
|
|
1379
|
-
for (const match of matches) {
|
|
1380
|
-
const score = scoreMatch(match, weights, maxWeight, key);
|
|
1381
|
-
if (score > bestScore) {
|
|
1382
|
-
bestScore = score;
|
|
1383
|
-
bestMatch = match;
|
|
1384
|
-
}
|
|
1385
|
-
}
|
|
1386
|
-
if (bestMatch) {
|
|
1387
|
-
const chord = makeChord(bestMatch.rootPc, bestMatch.quality, bestMatch.bassPc);
|
|
1388
|
-
return { chord, confidence: chordConfidence(chord, weights, totalWeight, bestMatch.exact) };
|
|
1389
|
-
}
|
|
1390
|
-
const fallback = detectChordBest(selected.slice(0, FALLBACK_PCS));
|
|
1391
|
-
if (fallback) {
|
|
1392
|
-
return { chord: fallback, confidence: chordConfidence(fallback, weights, totalWeight, false) };
|
|
1393
|
-
}
|
|
1394
|
-
return null;
|
|
1395
|
-
}
|
|
1396
|
-
function sameChord(a, b) {
|
|
1397
|
-
return a.rootPc === b.rootPc && a.quality === b.quality && a.bassPc === b.bassPc;
|
|
1398
|
-
}
|
|
1399
|
-
function chordTimelineFromNotes(notes, opts = {}) {
|
|
1400
|
-
const ts = opts.ts ?? parseTimeSignature("4/4");
|
|
1401
|
-
assertTimeSignature(ts);
|
|
1402
|
-
const harmonicRhythm = opts.harmonicRhythm ?? beatsPerBar(ts);
|
|
1403
|
-
assertRange(harmonicRhythm, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "harmonic rhythm");
|
|
1404
|
-
assertNoteEvents(notes, "timeline notes", { allowNonPositiveDuration: true });
|
|
1405
|
-
const soundingIndex = createNoteEventIndex(notes.filter((note) => note.durationBeat > 0));
|
|
1406
|
-
const sounding = soundingIndex.notes.map(({ note }) => note);
|
|
1407
|
-
const lastNoteEnd = sounding.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);
|
|
1408
|
-
const totalBeats = opts.totalBeats ?? lastNoteEnd;
|
|
1409
|
-
assertRange(totalBeats, 0, Number.MAX_SAFE_INTEGER, "timeline totalBeats");
|
|
1410
|
-
const key = opts.key ?? detectKey(sounding.map((n) => n.pitch))[0]?.key ?? majorKey(0);
|
|
1411
|
-
const segments = [];
|
|
1412
|
-
const segmentConfidence = [];
|
|
1413
|
-
const windowCount = Math.max(0, Math.ceil(totalBeats / harmonicRhythm - EPS3));
|
|
1414
|
-
assertGenerationBudget(windowCount, "timeline windows");
|
|
1415
|
-
const windowNotes = Array.from({ length: windowCount }, () => []);
|
|
1416
|
-
let memberships = 0;
|
|
1417
|
-
for (const indexed of soundingIndex.notes) {
|
|
1418
|
-
const first = Math.max(0, Math.floor(indexed.note.startBeat / harmonicRhythm));
|
|
1419
|
-
const lastExclusive = Math.min(windowCount, Math.ceil(indexed.endBeat / harmonicRhythm));
|
|
1420
|
-
memberships += Math.max(0, lastExclusive - first);
|
|
1421
|
-
assertGenerationBudget(memberships, "timeline note-to-window memberships");
|
|
1422
|
-
for (let window = first; window < lastExclusive; window += 1) {
|
|
1423
|
-
windowNotes[window]?.push(indexed.note);
|
|
1424
|
-
}
|
|
1425
|
-
}
|
|
1426
|
-
for (let i = 0; i < windowCount; i += 1) {
|
|
1427
|
-
const start = i * harmonicRhythm;
|
|
1428
|
-
const end = Math.min(start + harmonicRhythm, totalBeats);
|
|
1429
|
-
const inferred = analyzeWindow(windowNotes[i] ?? [], start, end, ts, key);
|
|
1430
|
-
if (!inferred) {
|
|
1431
|
-
continue;
|
|
1432
|
-
}
|
|
1433
|
-
const last = segments[segments.length - 1];
|
|
1434
|
-
if (last && sameChord(last.chord, inferred.chord) && Math.abs(last.endBeat - start) < EPS3) {
|
|
1435
|
-
const lastLength = last.endBeat - last.startBeat;
|
|
1436
|
-
const length = end - start;
|
|
1437
|
-
const lastConfidence = segmentConfidence[segmentConfidence.length - 1] ?? 0;
|
|
1438
|
-
segmentConfidence[segmentConfidence.length - 1] = (lastConfidence * lastLength + inferred.confidence * length) / (lastLength + length);
|
|
1439
|
-
last.endBeat = end;
|
|
1440
|
-
} else {
|
|
1441
|
-
segments.push({ startBeat: start, endBeat: end, chord: inferred.chord });
|
|
1442
|
-
segmentConfidence.push(inferred.confidence);
|
|
1443
|
-
}
|
|
1444
|
-
}
|
|
1445
|
-
return {
|
|
1446
|
-
timeline: { at: segmentLookup(segments), segments },
|
|
1447
|
-
key,
|
|
1448
|
-
segmentConfidence
|
|
1449
|
-
};
|
|
1450
|
-
}
|
|
1451
|
-
function detectCadences(timeline, key) {
|
|
1452
|
-
const hits = [];
|
|
1453
|
-
for (let i = 1; i < timeline.segments.length; i += 1) {
|
|
1454
|
-
const prev = timeline.segments[i - 1];
|
|
1455
|
-
const cur = timeline.segments[i];
|
|
1456
|
-
if (!prev || !cur) {
|
|
1457
|
-
continue;
|
|
1458
|
-
}
|
|
1459
|
-
if (Math.abs(cur.startBeat - prev.endBeat) > EPS3) {
|
|
1460
|
-
continue;
|
|
1461
|
-
}
|
|
1462
|
-
const type = detectCadence(prev.chord, cur.chord, key);
|
|
1463
|
-
if (type !== null) {
|
|
1464
|
-
hits.push({ atBeat: cur.startBeat, type, from: prev.chord, to: cur.chord });
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
return hits;
|
|
1468
|
-
}
|
|
1469
22
|
|
|
1470
|
-
// src/analyze/arrange/internal.ts
|
|
1471
|
-
var EPS4 = 1e-9;
|
|
1472
|
-
function covers(note, beat) {
|
|
1473
|
-
return beat >= note.startBeat - EPS4 && beat < note.endBeat - EPS4;
|
|
1474
|
-
}
|
|
1475
|
-
function poolNotes(tracks) {
|
|
1476
|
-
const notes = [];
|
|
1477
|
-
for (const track of tracks) {
|
|
1478
|
-
for (const note of track.notes) {
|
|
1479
|
-
if (note.durationBeat > 0) {
|
|
1480
|
-
notes.push(note);
|
|
1481
|
-
}
|
|
1482
|
-
}
|
|
1483
|
-
}
|
|
1484
|
-
return notes;
|
|
1485
|
-
}
|
|
1486
|
-
function splitIntoSubVoices(ordered) {
|
|
1487
|
-
const lanes = [];
|
|
1488
|
-
for (const note of ordered) {
|
|
1489
|
-
const lane = lanes.find((candidate) => {
|
|
1490
|
-
const last = candidate[candidate.length - 1];
|
|
1491
|
-
return last !== void 0 && last.startBeat + last.durationBeat <= note.startBeat + EPS4;
|
|
1492
|
-
});
|
|
1493
|
-
if (lane) {
|
|
1494
|
-
lane.push(note);
|
|
1495
|
-
} else {
|
|
1496
|
-
lanes.push([note]);
|
|
1497
|
-
}
|
|
1498
|
-
}
|
|
1499
|
-
return lanes;
|
|
1500
|
-
}
|
|
1501
|
-
function prepareTracks(tracks) {
|
|
1502
|
-
let nextId = 0;
|
|
1503
|
-
return tracks.map((track, index) => {
|
|
1504
|
-
const ordered = track.notes.filter((note) => note.durationBeat > 0).sort((a, b) => a.startBeat - b.startBeat || a.pitch - b.pitch).map((note) => ({ ...note, id: nextId++ }));
|
|
1505
|
-
const voices = splitIntoSubVoices(ordered).map((voice) => ({
|
|
1506
|
-
voice,
|
|
1507
|
-
sounding: voice.map((note, i) => {
|
|
1508
|
-
const prev = i > 0 ? voice[i - 1] : void 0;
|
|
1509
|
-
const prevEnd = prev === void 0 ? void 0 : prev.startBeat + prev.durationBeat;
|
|
1510
|
-
const contiguous = prevEnd !== void 0 && Math.abs(prevEnd - note.startBeat) <= EPS4;
|
|
1511
|
-
return {
|
|
1512
|
-
pitch: note.pitch,
|
|
1513
|
-
prevPitch: contiguous ? prev?.pitch : void 0,
|
|
1514
|
-
startBeat: note.startBeat,
|
|
1515
|
-
endBeat: note.startBeat + note.durationBeat
|
|
1516
|
-
};
|
|
1517
|
-
})
|
|
1518
|
-
}));
|
|
1519
|
-
return {
|
|
1520
|
-
name: track.name ?? `track ${index + 1}`,
|
|
1521
|
-
role: track.role ?? "other",
|
|
1522
|
-
voices
|
|
1523
|
-
};
|
|
1524
|
-
});
|
|
1525
|
-
}
|
|
1526
23
|
|
|
1527
|
-
// src/analyze/arrange/tension.ts
|
|
1528
|
-
var FUNCTION_TENSION = {
|
|
1529
|
-
tonic: 0,
|
|
1530
|
-
subdominant: 0.5,
|
|
1531
|
-
dominant: 1
|
|
1532
|
-
};
|
|
1533
|
-
var FUNCTION_WEIGHT = 0.5;
|
|
1534
|
-
var DISSONANCE_WEIGHT = 0.35;
|
|
1535
|
-
var SPAN_WEIGHT = 0.15;
|
|
1536
|
-
var SPAN_SATURATION = 24;
|
|
1537
|
-
function pitchClass7(pitch) {
|
|
1538
|
-
return (Math.trunc(pitch) % 12 + 12) % 12;
|
|
1539
|
-
}
|
|
1540
|
-
function clamp01(value) {
|
|
1541
|
-
return Math.min(1, Math.max(0, value));
|
|
1542
|
-
}
|
|
1543
|
-
function tensionCurve(tracks, opts = {}) {
|
|
1544
|
-
const ts = opts.ts ?? parseTimeSignature("4/4");
|
|
1545
|
-
assertTimeSignature(ts);
|
|
1546
|
-
assertGenerationBudget(tracks.length, "arrangement tracks");
|
|
1547
|
-
for (let index = 0; index < tracks.length; index += 1) {
|
|
1548
|
-
assertNoteEvents(tracks[index]?.notes ?? [], `tracks[${index}].notes`, {
|
|
1549
|
-
allowNonPositiveDuration: true
|
|
1550
|
-
});
|
|
1551
|
-
}
|
|
1552
|
-
const profile = opts.profile ?? "pop";
|
|
1553
|
-
const step = opts.step ?? 1;
|
|
1554
|
-
assertRange(step, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, "tension sampling step");
|
|
1555
|
-
const pooled = poolNotes(tracks);
|
|
1556
|
-
const totalBeats = pooled.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);
|
|
1557
|
-
const { timeline, key } = chordTimelineFromNotes(pooled, {
|
|
1558
|
-
key: opts.key,
|
|
1559
|
-
ts,
|
|
1560
|
-
harmonicRhythm: opts.harmonicRhythm
|
|
1561
|
-
});
|
|
1562
|
-
const prepared = prepareTracks(tracks);
|
|
1563
|
-
const points = [];
|
|
1564
|
-
const sampleCount = Math.max(0, Math.ceil(totalBeats / step - EPS4));
|
|
1565
|
-
assertGenerationBudget(sampleCount, "tension samples");
|
|
1566
|
-
for (let i = 0; i < sampleCount; i += 1) {
|
|
1567
|
-
const beat = i * step;
|
|
1568
|
-
points.push({ beat, tension: sampleTension(prepared, timeline, key, ts, profile, beat) });
|
|
1569
|
-
}
|
|
1570
|
-
return points;
|
|
1571
|
-
}
|
|
1572
|
-
function allSounding(prepared, beat) {
|
|
1573
|
-
const out = [];
|
|
1574
|
-
for (let t = 0; t < prepared.length; t += 1) {
|
|
1575
|
-
const track = prepared[t];
|
|
1576
|
-
if (!track) {
|
|
1577
|
-
continue;
|
|
1578
|
-
}
|
|
1579
|
-
for (const subVoice of track.voices) {
|
|
1580
|
-
for (const note of subVoice.sounding) {
|
|
1581
|
-
if (covers(note, beat)) {
|
|
1582
|
-
out.push({ pitch: note.pitch, track: t });
|
|
1583
|
-
}
|
|
1584
|
-
}
|
|
1585
|
-
}
|
|
1586
|
-
}
|
|
1587
|
-
return out;
|
|
1588
|
-
}
|
|
1589
|
-
function sampleTension(prepared, timeline, key, ts, profile, beat) {
|
|
1590
|
-
const sounding = allSounding(prepared, beat);
|
|
1591
|
-
if (sounding.length === 0) {
|
|
1592
|
-
return 0;
|
|
1593
|
-
}
|
|
1594
|
-
const chord = timeline.at(beat);
|
|
1595
|
-
const functionScore = chord ? FUNCTION_TENSION[functionOf(chord, key)] : 0;
|
|
1596
|
-
const chordPcs = chord ? new Set(chordPitchClasses(chord)) : null;
|
|
1597
|
-
const strongBeat = isStrongBeat(beat, ts);
|
|
1598
|
-
let nonChord = 0;
|
|
1599
|
-
let dissonant = 0;
|
|
1600
|
-
for (let i = 0; i < sounding.length; i += 1) {
|
|
1601
|
-
const voice = sounding[i];
|
|
1602
|
-
if (!voice) {
|
|
1603
|
-
continue;
|
|
1604
|
-
}
|
|
1605
|
-
if (chordPcs && !chordPcs.has(pitchClass7(voice.pitch))) {
|
|
1606
|
-
nonChord += 1;
|
|
1607
|
-
}
|
|
1608
|
-
const others = sounding.filter((_, j) => j !== i).map((s) => ({ pitch: s.pitch }));
|
|
1609
|
-
const result = evaluateSafety({
|
|
1610
|
-
profile,
|
|
1611
|
-
candidatePitch: voice.pitch,
|
|
1612
|
-
chord,
|
|
1613
|
-
key,
|
|
1614
|
-
otherVoices: others,
|
|
1615
|
-
strongBeat
|
|
1616
|
-
});
|
|
1617
|
-
if (result.safety === 2 /* Dissonant */) {
|
|
1618
|
-
dissonant += 1;
|
|
1619
|
-
}
|
|
1620
|
-
}
|
|
1621
|
-
const dissonanceScore = Math.max(nonChord, dissonant) / sounding.length;
|
|
1622
|
-
let low = Number.POSITIVE_INFINITY;
|
|
1623
|
-
let high = Number.NEGATIVE_INFINITY;
|
|
1624
|
-
for (const voice of sounding) {
|
|
1625
|
-
low = Math.min(low, voice.pitch);
|
|
1626
|
-
high = Math.max(high, voice.pitch);
|
|
1627
|
-
}
|
|
1628
|
-
const spanScore = Math.min(1, (high - low) / SPAN_SATURATION);
|
|
1629
|
-
return clamp01(
|
|
1630
|
-
FUNCTION_WEIGHT * functionScore + DISSONANCE_WEIGHT * dissonanceScore + SPAN_WEIGHT * spanScore
|
|
1631
|
-
);
|
|
1632
|
-
}
|
|
1633
24
|
|
|
1634
|
-
// src/analyze/voice/index.ts
|
|
1635
|
-
var EPS5 = 1e-9;
|
|
1636
|
-
function pitchClass8(pitch) {
|
|
1637
|
-
return (Math.trunc(pitch) % 12 + 12) % 12;
|
|
1638
|
-
}
|
|
1639
|
-
function intervalAboveRoot2(pitch, chord) {
|
|
1640
|
-
return ((pitchClass8(pitch) - pitchClass8(chord.rootPc)) % 12 + 12) % 12;
|
|
1641
|
-
}
|
|
1642
|
-
function intervalAboveBass(pitch, others, chord) {
|
|
1643
|
-
let bass = Number.POSITIVE_INFINITY;
|
|
1644
|
-
for (const ov of others) {
|
|
1645
|
-
bass = Math.min(bass, ov.pitch);
|
|
1646
|
-
}
|
|
1647
|
-
if (Number.isFinite(bass) && bass < pitch) {
|
|
1648
|
-
return ((pitch - bass) % 12 + 12) % 12;
|
|
1649
|
-
}
|
|
1650
|
-
const ref = chord.bassPc ?? chord.rootPc;
|
|
1651
|
-
return ((pitchClass8(pitch) - pitchClass8(ref)) % 12 + 12) % 12;
|
|
1652
|
-
}
|
|
1653
|
-
function tensionDegree(ic) {
|
|
1654
|
-
if (ic === 1 || ic === 2) {
|
|
1655
|
-
return 9;
|
|
1656
|
-
}
|
|
1657
|
-
if (ic === 5 || ic === 6) {
|
|
1658
|
-
return 11;
|
|
1659
|
-
}
|
|
1660
|
-
return 13;
|
|
1661
|
-
}
|
|
1662
|
-
function isChordMember2(pitch, chord) {
|
|
1663
|
-
return chord ? chordPitchClasses(chord).includes(pitchClass8(pitch)) : false;
|
|
1664
|
-
}
|
|
1665
|
-
function isStep(a, b) {
|
|
1666
|
-
const d = Math.abs(a - b);
|
|
1667
|
-
return d === 1 || d === 2;
|
|
1668
|
-
}
|
|
1669
|
-
function suspensionType(ic, delta) {
|
|
1670
|
-
let type;
|
|
1671
|
-
if (delta > 0) {
|
|
1672
|
-
type = "sus2-3";
|
|
1673
|
-
} else if (ic === 2) {
|
|
1674
|
-
type = "sus9-8";
|
|
1675
|
-
} else if (ic === 10 || ic === 11) {
|
|
1676
|
-
type = "sus7-6";
|
|
1677
|
-
} else {
|
|
1678
|
-
type = "sus4-3";
|
|
1679
|
-
}
|
|
1680
|
-
return { kind: "suspension", type, resolveTo: 0 };
|
|
1681
|
-
}
|
|
1682
|
-
function stepResolution2(pitch, chord) {
|
|
1683
|
-
for (let delta = 1; delta <= 2; delta += 1) {
|
|
1684
|
-
if (isChordMember2(pitch - delta, chord)) {
|
|
1685
|
-
return pitch - delta;
|
|
1686
|
-
}
|
|
1687
|
-
}
|
|
1688
|
-
return void 0;
|
|
1689
|
-
}
|
|
1690
|
-
function analyzeVoice(voice, chordAtBeat, key, otherVoicesAtBeat) {
|
|
1691
|
-
assertNoteEvents(voice, "voice notes", { allowNonPositiveDuration: true });
|
|
1692
|
-
const result = [];
|
|
1693
|
-
for (let i = 0; i < voice.length; i += 1) {
|
|
1694
|
-
const note = voice[i];
|
|
1695
|
-
if (!note) {
|
|
1696
|
-
continue;
|
|
1697
|
-
}
|
|
1698
|
-
const prevNote = i > 0 ? voice[i - 1] : void 0;
|
|
1699
|
-
const nextNote = i + 1 < voice.length ? voice[i + 1] : void 0;
|
|
1700
|
-
const prevEnd = prevNote === void 0 ? Number.NEGATIVE_INFINITY : prevNote.startBeat + prevNote.durationBeat;
|
|
1701
|
-
const noteEnd = note.startBeat + note.durationBeat;
|
|
1702
|
-
const prev = prevNote !== void 0 && Math.abs(prevEnd - note.startBeat) <= EPS5 ? prevNote : void 0;
|
|
1703
|
-
const next = nextNote !== void 0 && Math.abs(noteEnd - nextNote.startBeat) <= EPS5 ? nextNote : void 0;
|
|
1704
|
-
const chord = chordAtBeat(note.startBeat);
|
|
1705
|
-
const labels = [];
|
|
1706
|
-
const member = isChordMember2(note.pitch, chord);
|
|
1707
|
-
let handled = false;
|
|
1708
|
-
if (chord && member) {
|
|
1709
|
-
const role = chordToneRole(note.pitch, chord);
|
|
1710
|
-
if (role) {
|
|
1711
|
-
labels.push({ kind: "chordTone", role });
|
|
1712
|
-
} else {
|
|
1713
|
-
const ic = intervalAboveRoot2(note.pitch, chord);
|
|
1714
|
-
labels.push({ kind: "tension", degree: tensionDegree(ic) });
|
|
1715
|
-
}
|
|
1716
|
-
handled = true;
|
|
1717
|
-
}
|
|
1718
|
-
if (!handled && chord && prev && next) {
|
|
1719
|
-
const prevChord = chordAtBeat(prev.startBeat);
|
|
1720
|
-
const prepared = prev.pitch === note.pitch && isChordMember2(prev.pitch, prevChord);
|
|
1721
|
-
const resolves = isStep(next.pitch, note.pitch);
|
|
1722
|
-
const others = otherVoicesAtBeat(note.startBeat);
|
|
1723
|
-
const verticallyDissonant = others.some(
|
|
1724
|
-
(ov) => createsVerticalDissonance(note.pitch, ov.pitch, true)
|
|
1725
|
-
);
|
|
1726
|
-
if (prepared && verticallyDissonant && resolves) {
|
|
1727
|
-
const ic = intervalAboveBass(note.pitch, others, chord);
|
|
1728
|
-
const sus = suspensionType(ic, next.pitch - note.pitch);
|
|
1729
|
-
sus.resolveTo = next.pitch;
|
|
1730
|
-
labels.push(sus);
|
|
1731
|
-
handled = true;
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1734
|
-
if (!handled && chord && prev && next) {
|
|
1735
|
-
const prevMember = isChordMember2(prev.pitch, chordAtBeat(prev.startBeat));
|
|
1736
|
-
const nextMember = isChordMember2(next.pitch, chordAtBeat(next.startBeat));
|
|
1737
|
-
const up = note.pitch - prev.pitch > 0;
|
|
1738
|
-
const contInto = next.pitch - note.pitch > 0;
|
|
1739
|
-
const sameDir = note.pitch !== prev.pitch && next.pitch !== note.pitch && up === contInto;
|
|
1740
|
-
if (prevMember && nextMember && !member && isStep(note.pitch, prev.pitch) && isStep(next.pitch, note.pitch) && sameDir) {
|
|
1741
|
-
labels.push({ kind: "passing" });
|
|
1742
|
-
handled = true;
|
|
1743
|
-
}
|
|
1744
|
-
}
|
|
1745
|
-
if (!handled && chord && prev && next && !member && prev.pitch === next.pitch && isChordMember2(prev.pitch, chordAtBeat(prev.startBeat)) && isStep(note.pitch, prev.pitch)) {
|
|
1746
|
-
labels.push({ kind: "neighbor" });
|
|
1747
|
-
handled = true;
|
|
1748
|
-
}
|
|
1749
|
-
if (!handled && chord && next && !member && note.pitch === next.pitch) {
|
|
1750
|
-
const nextChord = chordAtBeat(next.startBeat);
|
|
1751
|
-
if (isChordMember2(next.pitch, nextChord)) {
|
|
1752
|
-
labels.push({ kind: "anticipation" });
|
|
1753
|
-
handled = true;
|
|
1754
|
-
}
|
|
1755
|
-
}
|
|
1756
|
-
if (!handled && chord && prev && next && !member) {
|
|
1757
|
-
const prevMember = isChordMember2(prev.pitch, chordAtBeat(prev.startBeat));
|
|
1758
|
-
const nextMember = isChordMember2(next.pitch, chordAtBeat(next.startBeat));
|
|
1759
|
-
const stepFromPrev = isStep(note.pitch, prev.pitch);
|
|
1760
|
-
const leapToNext = Math.abs(next.pitch - note.pitch) >= 3;
|
|
1761
|
-
const opposite = note.pitch - prev.pitch > 0 !== next.pitch - note.pitch > 0;
|
|
1762
|
-
if (prevMember && nextMember && stepFromPrev && leapToNext && opposite) {
|
|
1763
|
-
labels.push({ kind: "escape" });
|
|
1764
|
-
handled = true;
|
|
1765
|
-
}
|
|
1766
|
-
}
|
|
1767
|
-
if (!handled && chord && !member) {
|
|
1768
|
-
const ic = intervalAboveRoot2(note.pitch, chord);
|
|
1769
|
-
const isTension = ic === 2 || ic === 5 || ic === 9;
|
|
1770
|
-
const avoid = ic === 5 && chord.intervals.includes(4) && !chord.intervals.includes(5) || ic === 11 && chord.intervals.includes(4) && chord.intervals.includes(10);
|
|
1771
|
-
if (avoid) {
|
|
1772
|
-
labels.push({ kind: "avoid" });
|
|
1773
|
-
const resolveTo = stepResolution2(note.pitch, chord);
|
|
1774
|
-
if (resolveTo !== void 0) {
|
|
1775
|
-
labels.push({ kind: "needsResolution", resolveTo });
|
|
1776
|
-
}
|
|
1777
|
-
} else if (isTension) {
|
|
1778
|
-
labels.push({ kind: "tension", degree: tensionDegree(ic) });
|
|
1779
|
-
} else {
|
|
1780
|
-
const resolveTo = stepResolution2(note.pitch, chord);
|
|
1781
|
-
if (resolveTo !== void 0) {
|
|
1782
|
-
labels.push({ kind: "needsResolution", resolveTo });
|
|
1783
|
-
}
|
|
1784
|
-
}
|
|
1785
|
-
}
|
|
1786
|
-
if (next && isLeadingToneResolution(note.pitch, next.pitch, key)) {
|
|
1787
|
-
labels.push({ kind: "leadingTone", resolveTo: next.pitch });
|
|
1788
|
-
}
|
|
1789
|
-
result.push({ noteId: note.id, labels, rationale: describe2(labels) });
|
|
1790
|
-
}
|
|
1791
|
-
return result;
|
|
1792
|
-
}
|
|
1793
|
-
function describe2(labels) {
|
|
1794
|
-
const primary = labels[0];
|
|
1795
|
-
if (!primary) {
|
|
1796
|
-
return "Unclassified note";
|
|
1797
|
-
}
|
|
1798
|
-
switch (primary.kind) {
|
|
1799
|
-
case "chordTone":
|
|
1800
|
-
return `Chord ${primary.role}`;
|
|
1801
|
-
case "tension":
|
|
1802
|
-
return `${primary.degree}th tension`;
|
|
1803
|
-
case "avoid":
|
|
1804
|
-
return "Avoid note";
|
|
1805
|
-
case "passing":
|
|
1806
|
-
return "Passing tone between chord tones";
|
|
1807
|
-
case "neighbor":
|
|
1808
|
-
return "Neighbor tone";
|
|
1809
|
-
case "suspension":
|
|
1810
|
-
return `Suspension (${primary.type})`;
|
|
1811
|
-
case "anticipation":
|
|
1812
|
-
return "Anticipation of the next chord";
|
|
1813
|
-
case "escape":
|
|
1814
|
-
return "Escape tone";
|
|
1815
|
-
case "needsResolution":
|
|
1816
|
-
return "Unresolved dissonance";
|
|
1817
|
-
case "leadingTone":
|
|
1818
|
-
return "Leading tone resolving to the tonic";
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
25
|
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
beats.push(segment.startBeat);
|
|
1859
|
-
}
|
|
1860
|
-
}
|
|
1861
|
-
return beats;
|
|
1862
|
-
}
|
|
1863
|
-
function analyzeArrangement(tracks, opts = {}) {
|
|
1864
|
-
const ts = opts.ts ?? parseTimeSignature("4/4");
|
|
1865
|
-
assertTimeSignature(ts);
|
|
1866
|
-
assertGenerationBudget(tracks.length, "arrangement tracks");
|
|
1867
|
-
for (let index = 0; index < tracks.length; index += 1) {
|
|
1868
|
-
assertNoteEvents(tracks[index]?.notes ?? [], `tracks[${index}].notes`, {
|
|
1869
|
-
allowNonPositiveDuration: true
|
|
1870
|
-
});
|
|
1871
|
-
}
|
|
1872
|
-
const profile = opts.profile ?? "pop";
|
|
1873
|
-
const pooled = poolNotes(tracks);
|
|
1874
|
-
const { timeline, key, segmentConfidence } = chordTimelineFromNotes(pooled, {
|
|
1875
|
-
key: opts.key,
|
|
1876
|
-
ts,
|
|
1877
|
-
harmonicRhythm: opts.harmonicRhythm
|
|
1878
|
-
});
|
|
1879
|
-
const cadences = detectCadences(timeline, key);
|
|
1880
|
-
const prepared = prepareTracks(tracks);
|
|
1881
|
-
const trackAnalyses = [];
|
|
1882
|
-
const conflicts = [];
|
|
1883
|
-
for (let t = 0; t < prepared.length; t += 1) {
|
|
1884
|
-
const track = prepared[t];
|
|
1885
|
-
if (!track) {
|
|
1886
|
-
continue;
|
|
1887
|
-
}
|
|
1888
|
-
const notes = [];
|
|
1889
|
-
for (let v = 0; v < track.voices.length; v += 1) {
|
|
1890
|
-
const subVoice = track.voices[v];
|
|
1891
|
-
if (!subVoice) {
|
|
1892
|
-
continue;
|
|
1893
|
-
}
|
|
1894
|
-
notes.push(
|
|
1895
|
-
...analyzeVoice(
|
|
1896
|
-
subVoice.voice,
|
|
1897
|
-
timeline.at,
|
|
1898
|
-
key,
|
|
1899
|
-
(beat) => otherVoicesSounding(prepared, t, v, beat)
|
|
1900
|
-
)
|
|
1901
|
-
);
|
|
1902
|
-
for (let noteIndex = 0; noteIndex < subVoice.voice.length; noteIndex += 1) {
|
|
1903
|
-
const note = subVoice.voice[noteIndex];
|
|
1904
|
-
const preparedNote = subVoice.sounding[noteIndex];
|
|
1905
|
-
if (note === void 0 || preparedNote === void 0) {
|
|
1906
|
-
continue;
|
|
1907
|
-
}
|
|
1908
|
-
for (const beat of evaluationBeats(note, timeline)) {
|
|
1909
|
-
const atOnset = Math.abs(beat - note.startBeat) <= EPS4;
|
|
1910
|
-
const result = evaluateSafety({
|
|
1911
|
-
profile,
|
|
1912
|
-
candidatePitch: note.pitch,
|
|
1913
|
-
prevPitch: atOnset ? preparedNote.prevPitch : note.pitch,
|
|
1914
|
-
chord: timeline.at(beat),
|
|
1915
|
-
key,
|
|
1916
|
-
otherVoices: otherVoicesSounding(prepared, t, v, beat),
|
|
1917
|
-
strongBeat: isStrongBeat(beat, ts)
|
|
1918
|
-
});
|
|
1919
|
-
if (result.safety !== 0 /* Safe */) {
|
|
1920
|
-
const conflict = {
|
|
1921
|
-
beat,
|
|
1922
|
-
trackName: track.name,
|
|
1923
|
-
pitch: note.pitch,
|
|
1924
|
-
safety: result.safety,
|
|
1925
|
-
reasons: result.reasons
|
|
1926
|
-
};
|
|
1927
|
-
if (result.rationale !== void 0) {
|
|
1928
|
-
conflict.rationale = result.rationale;
|
|
1929
|
-
}
|
|
1930
|
-
if (result.resolveTo !== void 0) {
|
|
1931
|
-
conflict.resolveTo = result.resolveTo;
|
|
1932
|
-
}
|
|
1933
|
-
if (result.suggestions !== void 0) {
|
|
1934
|
-
conflict.suggestions = [...result.suggestions];
|
|
1935
|
-
}
|
|
1936
|
-
conflicts.push(conflict);
|
|
1937
|
-
}
|
|
1938
|
-
}
|
|
1939
|
-
}
|
|
1940
|
-
}
|
|
1941
|
-
notes.sort((a, b) => a.noteId - b.noteId);
|
|
1942
|
-
trackAnalyses.push({ name: track.name, role: track.role, notes });
|
|
1943
|
-
}
|
|
1944
|
-
conflicts.sort((a, b) => b.safety - a.safety || a.beat - b.beat);
|
|
1945
|
-
return { key, timeline, segmentConfidence, cadences, tracks: trackAnalyses, conflicts };
|
|
1946
|
-
}
|
|
1947
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
1948
|
-
0 && (module.exports = {
|
|
1949
|
-
analyzeArrangement,
|
|
1950
|
-
analyzeChord,
|
|
1951
|
-
analyzeVoice,
|
|
1952
|
-
borrowedSource,
|
|
1953
|
-
chordTimelineFromChords,
|
|
1954
|
-
chordTimelineFromNotes,
|
|
1955
|
-
chordToRoman,
|
|
1956
|
-
detectCadence,
|
|
1957
|
-
detectCadences,
|
|
1958
|
-
detectChord,
|
|
1959
|
-
detectChordBest,
|
|
1960
|
-
detectKey,
|
|
1961
|
-
functionOf,
|
|
1962
|
-
isBorrowedChord,
|
|
1963
|
-
isDiatonic,
|
|
1964
|
-
isMinorKey,
|
|
1965
|
-
parallelKey,
|
|
1966
|
-
romanToChord,
|
|
1967
|
-
secondaryDominant,
|
|
1968
|
-
tensionCurve
|
|
1969
|
-
});
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
var _chunkKQN2ZYNDcjs = require('../chunk-KQN2ZYND.cjs');
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
var _chunkWFDXOLUBcjs = require('../chunk-WFDXOLUB.cjs');
|
|
33
|
+
require('../chunk-BSLTLGM3.cjs');
|
|
34
|
+
require('../chunk-PI3UXRWK.cjs');
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
exports.NoteSafety = _chunkWFDXOLUBcjs.NoteSafety; exports.analyzeArrangement = _chunk4VZ5MZAMcjs.analyzeArrangement; exports.analyzeChord = _chunkKQN2ZYNDcjs.analyzeChord; exports.analyzeVoice = _chunk4VZ5MZAMcjs.analyzeVoice; exports.borrowedSource = _chunkKQN2ZYNDcjs.borrowedSource; exports.chordTimelineFromChords = _chunk4VZ5MZAMcjs.chordTimelineFromChords; exports.chordTimelineFromNotes = _chunk4VZ5MZAMcjs.chordTimelineFromNotes; exports.chordToRoman = _chunkKQN2ZYNDcjs.chordToRoman; exports.detectCadence = _chunkKQN2ZYNDcjs.detectCadence; exports.detectCadences = _chunk4VZ5MZAMcjs.detectCadences; exports.detectChord = _chunkKQN2ZYNDcjs.detectChord; exports.detectChordBest = _chunkKQN2ZYNDcjs.detectChordBest; exports.detectKey = _chunkKQN2ZYNDcjs.detectKey; exports.detectKeyBest = _chunkKQN2ZYNDcjs.detectKeyBest; exports.detectKeyFromNotes = _chunkKQN2ZYNDcjs.detectKeyFromNotes; exports.functionOf = _chunkKQN2ZYNDcjs.functionOf; exports.isBorrowedChord = _chunkKQN2ZYNDcjs.isBorrowedChord; exports.isDiatonic = _chunkKQN2ZYNDcjs.isDiatonic; exports.isMinorKey = _chunkKQN2ZYNDcjs.isMinorKey; exports.parallelKey = _chunkKQN2ZYNDcjs.parallelKey; exports.romanToChord = _chunkKQN2ZYNDcjs.romanToChord; exports.secondaryDominant = _chunkKQN2ZYNDcjs.secondaryDominant; exports.tensionCurve = _chunk4VZ5MZAMcjs.tensionCurve; exports.tensionCurveFrom = _chunk4VZ5MZAMcjs.tensionCurveFrom; exports.toVoiceNotes = _chunk4VZ5MZAMcjs.toVoiceNotes;
|
|
1970
62
|
//# sourceMappingURL=index.cjs.map
|