@libraz/libcantus 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -65
- package/README_ja.md +28 -66
- package/dist/analyze/index.cjs +11 -7
- package/dist/analyze/index.cjs.map +1 -1
- package/dist/analyze/index.d.cts +79 -144
- package/dist/analyze/index.d.ts +79 -144
- package/dist/analyze/index.js +20 -16
- package/dist/chunk-35NBV2FT.cjs +145 -0
- package/dist/chunk-35NBV2FT.cjs.map +1 -0
- package/dist/{chunk-UADYAYAT.cjs → chunk-3R3QVKY5.cjs} +1353 -580
- package/dist/chunk-3R3QVKY5.cjs.map +1 -0
- package/dist/{chunk-WWURE4XT.cjs → chunk-76ZBTRG2.cjs} +624 -729
- package/dist/chunk-76ZBTRG2.cjs.map +1 -0
- package/dist/{chunk-FVSI36UV.js → chunk-7KG3RTYS.js} +197 -110
- package/dist/chunk-7KG3RTYS.js.map +1 -0
- package/dist/{chunk-VTI6FOLB.js → chunk-AGOVW7U6.js} +709 -133
- package/dist/chunk-AGOVW7U6.js.map +1 -0
- package/dist/{chunk-E3F4AHAH.cjs → chunk-CGBADIO5.cjs} +221 -134
- package/dist/chunk-CGBADIO5.cjs.map +1 -0
- package/dist/{chunk-6XH6QZYQ.cjs → chunk-DSMJRXCZ.cjs} +1455 -849
- package/dist/chunk-DSMJRXCZ.cjs.map +1 -0
- package/dist/{chunk-PKFNUHDY.js → chunk-FBH75DAY.js} +1218 -612
- package/dist/chunk-FBH75DAY.js.map +1 -0
- package/dist/chunk-H2KJHBW4.js +3329 -0
- package/dist/chunk-H2KJHBW4.js.map +1 -0
- package/dist/chunk-HPVSH7SF.cjs +2294 -0
- package/dist/chunk-HPVSH7SF.cjs.map +1 -0
- package/dist/{chunk-MXO72PLY.cjs → chunk-HWHGOVO6.cjs} +21 -19
- package/dist/chunk-HWHGOVO6.cjs.map +1 -0
- package/dist/chunk-HYJJ2V2I.js +2294 -0
- package/dist/chunk-HYJJ2V2I.js.map +1 -0
- package/dist/{chunk-AFM2ULJH.cjs → chunk-J7FHVZII.cjs} +1567 -1067
- package/dist/chunk-J7FHVZII.cjs.map +1 -0
- package/dist/chunk-K6OZXIR6.js +1 -0
- package/dist/{chunk-H3VORETI.js → chunk-M5RYMAFI.js} +487 -592
- package/dist/chunk-M5RYMAFI.js.map +1 -0
- package/dist/chunk-NB25P3EH.js +363 -0
- package/dist/chunk-NB25P3EH.js.map +1 -0
- package/dist/{chunk-TTCLJX6D.js → chunk-NFJ4P6ZZ.js} +10 -8
- package/dist/chunk-NFJ4P6ZZ.js.map +1 -0
- package/dist/chunk-NM7UAMC3.cjs +457 -0
- package/dist/chunk-NM7UAMC3.cjs.map +1 -0
- package/dist/chunk-PM4XJE2Y.cjs +363 -0
- package/dist/chunk-PM4XJE2Y.cjs.map +1 -0
- package/dist/chunk-PUKJP2Z5.js +145 -0
- package/dist/chunk-PUKJP2Z5.js.map +1 -0
- package/dist/{chunk-VOHZB45S.js → chunk-Q6LFSAKV.js} +1321 -821
- package/dist/chunk-Q6LFSAKV.js.map +1 -0
- package/dist/{chunk-INHVXP6Z.js → chunk-RFIBF2RR.js} +1203 -430
- package/dist/chunk-RFIBF2RR.js.map +1 -0
- package/dist/{chunk-Q2WGKJNN.cjs → chunk-S5BKZZGM.cjs} +699 -123
- package/dist/chunk-S5BKZZGM.cjs.map +1 -0
- package/dist/chunk-SGLEQVJH.cjs +3329 -0
- package/dist/chunk-SGLEQVJH.cjs.map +1 -0
- package/dist/chunk-UGLFDG7L.cjs +1 -0
- package/dist/chunk-UGLFDG7L.cjs.map +1 -0
- package/dist/{chunk-YH3BIKHH.js → chunk-UMWZ4ZET.js} +146 -49
- package/dist/chunk-UMWZ4ZET.js.map +1 -0
- package/dist/{index-szBv3496.d.ts → coerce-DP0vYu-x.d.ts} +286 -199
- package/dist/{index-C_8o--VE.d.cts → coerce-DRERzdve.d.cts} +286 -199
- package/dist/core/index.cjs +9 -5
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +122 -108
- package/dist/core/index.d.ts +122 -108
- package/dist/core/index.js +32 -28
- package/dist/generate/index.cjs +8 -8
- package/dist/generate/index.d.cts +168 -84
- package/dist/generate/index.d.ts +168 -84
- package/dist/generate/index.js +7 -7
- package/dist/{index-CGpRv5sl.d.cts → index-9eoQs2C1.d.ts} +34 -19
- package/dist/{index-CCNJRG-P.d.cts → index-B9QLziDe.d.cts} +293 -56
- package/dist/{index-Dzr13q3d.d.cts → index-BBWNZS0f.d.cts} +135 -58
- package/dist/{index-9wcMhG73.d.ts → index-BHIUyjhZ.d.ts} +293 -56
- package/dist/{index-BGFW-8P2.d.cts → index-BJgP97N1.d.ts} +196 -114
- package/dist/{index-CunYKLiu.d.ts → index-BL5N2Oyz.d.cts} +1 -1
- package/dist/{index-Cj49F892.d.ts → index-BMweUlm5.d.cts} +63 -23
- package/dist/{index-CgHqxdps.d.ts → index-BNSM-DSk.d.cts} +196 -114
- package/dist/{index-D57QRbVn.d.ts → index-BRMbPkHE.d.ts} +480 -254
- package/dist/{index-Chbst3Jx.d.ts → index-CJBBiAmU.d.ts} +135 -58
- package/dist/{index-BkGmlyI3.d.cts → index-CL4b4gUQ.d.ts} +63 -23
- package/dist/{index-B9MNZboQ.d.ts → index-Cy15oTvh.d.cts} +52 -22
- package/dist/{index-B9MNZboQ.d.cts → index-Cy15oTvh.d.ts} +52 -22
- package/dist/{index-BO6xE9YU.d.cts → index-DGCNKAUr.d.cts} +480 -254
- package/dist/{index-B4o55NDr.d.cts → index-Dw2DP7vm.d.ts} +1 -1
- package/dist/{index-G1uwGGWQ.d.ts → index-cHvbYgJI.d.cts} +34 -19
- package/dist/index.cjs +42 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -14
- package/dist/index.d.ts +14 -14
- package/dist/index.js +82 -54
- package/dist/{masks-DYbE485J.d.cts → masks-Cc7-MzzY.d.ts} +49 -2
- package/dist/{masks-DYbE485J.d.ts → masks-pADfTOiD.d.cts} +49 -2
- package/dist/model/index.cjs +19 -12
- package/dist/model/index.cjs.map +1 -1
- package/dist/model/index.d.cts +504 -182
- package/dist/model/index.d.ts +504 -182
- package/dist/model/index.js +18 -11
- package/dist/{positional-Cptlj-xs.d.ts → positional-DJgs6ltD.d.ts} +85 -8
- package/dist/{positional-DvH_sd8u.d.cts → positional-DMZer-FM.d.cts} +85 -8
- package/dist/theory/index.cjs +35 -6
- package/dist/theory/index.cjs.map +1 -1
- package/dist/theory/index.d.cts +257 -62
- package/dist/theory/index.d.ts +257 -62
- package/dist/theory/index.js +41 -12
- package/package.json +7 -5
- package/dist/chunk-2LCPM3EG.js +0 -3224
- package/dist/chunk-2LCPM3EG.js.map +0 -1
- package/dist/chunk-6XH6QZYQ.cjs.map +0 -1
- package/dist/chunk-6XPTBE7Y.cjs +0 -49
- package/dist/chunk-6XPTBE7Y.cjs.map +0 -1
- package/dist/chunk-AFM2ULJH.cjs.map +0 -1
- package/dist/chunk-B6XNKWZV.cjs +0 -3224
- package/dist/chunk-B6XNKWZV.cjs.map +0 -1
- package/dist/chunk-E3F4AHAH.cjs.map +0 -1
- package/dist/chunk-ELCTZUJJ.js +0 -379
- package/dist/chunk-ELCTZUJJ.js.map +0 -1
- package/dist/chunk-FVSI36UV.js.map +0 -1
- package/dist/chunk-H3VORETI.js.map +0 -1
- package/dist/chunk-H42UHPYW.js +0 -1577
- package/dist/chunk-H42UHPYW.js.map +0 -1
- package/dist/chunk-HFPS7JFD.js +0 -1
- package/dist/chunk-INHVXP6Z.js.map +0 -1
- package/dist/chunk-K4ZJUGF4.cjs +0 -379
- package/dist/chunk-K4ZJUGF4.cjs.map +0 -1
- package/dist/chunk-KNJEKLKV.js +0 -49
- package/dist/chunk-KNJEKLKV.js.map +0 -1
- package/dist/chunk-MXO72PLY.cjs.map +0 -1
- package/dist/chunk-NLEU4QSE.cjs +0 -360
- package/dist/chunk-NLEU4QSE.cjs.map +0 -1
- package/dist/chunk-OE6WK2Y2.cjs +0 -1577
- package/dist/chunk-OE6WK2Y2.cjs.map +0 -1
- package/dist/chunk-PKFNUHDY.js.map +0 -1
- package/dist/chunk-Q2WGKJNN.cjs.map +0 -1
- package/dist/chunk-RHIJ3S4I.cjs +0 -1
- package/dist/chunk-RHIJ3S4I.cjs.map +0 -1
- package/dist/chunk-TTCLJX6D.js.map +0 -1
- package/dist/chunk-UADYAYAT.cjs.map +0 -1
- package/dist/chunk-VOHZB45S.js.map +0 -1
- package/dist/chunk-VTI6FOLB.js.map +0 -1
- package/dist/chunk-WWURE4XT.cjs.map +0 -1
- package/dist/chunk-YH3BIKHH.js.map +0 -1
- /package/dist/{chunk-HFPS7JFD.js.map → chunk-K6OZXIR6.js.map} +0 -0
package/dist/chunk-K4ZJUGF4.cjs
DELETED
|
@@ -1,379 +0,0 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
var _chunkQ2WGKJNNcjs = require('./chunk-Q2WGKJNN.cjs');
|
|
26
|
-
|
|
27
|
-
// src/core/event-index/index.ts
|
|
28
|
-
var EPS = 1e-9;
|
|
29
|
-
function upperBound(values, beat) {
|
|
30
|
-
let low = 0;
|
|
31
|
-
let high = values.length;
|
|
32
|
-
while (low < high) {
|
|
33
|
-
const middle = Math.floor((low + high) / 2);
|
|
34
|
-
if ((_nullishCoalesce(_optionalChain([values, 'access', _ => _[middle], 'optionalAccess', _2 => _2.note, 'access', _3 => _3.startBeat]), () => ( Number.POSITIVE_INFINITY))) <= beat) {
|
|
35
|
-
low = middle + 1;
|
|
36
|
-
} else {
|
|
37
|
-
high = middle;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return low;
|
|
41
|
-
}
|
|
42
|
-
function createNoteEventIndex(events, options = {}) {
|
|
43
|
-
_chunkQ2WGKJNNcjs.assertNoteEvents.call(void 0, events, _nullishCoalesce(options.name, () => ( "note events")), options);
|
|
44
|
-
const tieBreak = _nullishCoalesce(options.tieBreak, () => ( "highest"));
|
|
45
|
-
const notes = Object.freeze(
|
|
46
|
-
events.map((note, originalIndex) => ({
|
|
47
|
-
note: Object.freeze({ ...note }),
|
|
48
|
-
originalIndex,
|
|
49
|
-
endBeat: note.startBeat + note.durationBeat
|
|
50
|
-
})).sort((a, b) => a.note.startBeat - b.note.startBeat || a.originalIndex - b.originalIndex)
|
|
51
|
-
);
|
|
52
|
-
let treeSize = 1;
|
|
53
|
-
while (treeSize < notes.length) treeSize *= 2;
|
|
54
|
-
const maxEndTree = new Array(treeSize * 2).fill(Number.NEGATIVE_INFINITY);
|
|
55
|
-
for (let index = 0; index < notes.length; index += 1) {
|
|
56
|
-
maxEndTree[treeSize + index] = _nullishCoalesce(_optionalChain([notes, 'access', _4 => _4[index], 'optionalAccess', _5 => _5.endBeat]), () => ( Number.NEGATIVE_INFINITY));
|
|
57
|
-
}
|
|
58
|
-
for (let index = treeSize - 1; index > 0; index -= 1) {
|
|
59
|
-
maxEndTree[index] = Math.max(
|
|
60
|
-
_nullishCoalesce(maxEndTree[index * 2], () => ( Number.NEGATIVE_INFINITY)),
|
|
61
|
-
_nullishCoalesce(maxEndTree[index * 2 + 1], () => ( Number.NEGATIVE_INFINITY))
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
const latestActiveIndex = (exclusive, beat) => {
|
|
65
|
-
const find = (node, start, end) => {
|
|
66
|
-
if (start >= exclusive || (_nullishCoalesce(maxEndTree[node], () => ( Number.NEGATIVE_INFINITY))) <= beat + EPS) {
|
|
67
|
-
return -1;
|
|
68
|
-
}
|
|
69
|
-
if (end - start === 1) return start < notes.length ? start : -1;
|
|
70
|
-
const mid = Math.floor((start + end) / 2);
|
|
71
|
-
const right = find(node * 2 + 1, mid, end);
|
|
72
|
-
return right >= 0 ? right : find(node * 2, start, mid);
|
|
73
|
-
};
|
|
74
|
-
return find(1, 0, treeSize);
|
|
75
|
-
};
|
|
76
|
-
return {
|
|
77
|
-
notes,
|
|
78
|
-
at(beat) {
|
|
79
|
-
_chunkQ2WGKJNNcjs.assertFiniteNumber.call(void 0, beat, "beat");
|
|
80
|
-
const index = latestActiveIndex(upperBound(notes, beat + EPS), beat);
|
|
81
|
-
if (index < 0) return void 0;
|
|
82
|
-
const onset = _optionalChain([notes, 'access', _6 => _6[index], 'optionalAccess', _7 => _7.note, 'access', _8 => _8.startBeat]);
|
|
83
|
-
let best;
|
|
84
|
-
for (let candidateIndex = index; candidateIndex >= 0; candidateIndex -= 1) {
|
|
85
|
-
const indexed = notes[candidateIndex];
|
|
86
|
-
if (indexed !== void 0 && indexed.note.startBeat !== onset) break;
|
|
87
|
-
if (indexed === void 0 || indexed.note.durationBeat <= 0 || indexed.note.startBeat - EPS > beat || beat >= indexed.endBeat - EPS) {
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
if (best === void 0) {
|
|
91
|
-
best = indexed;
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
if (tieBreak === "last") continue;
|
|
95
|
-
const higher = indexed.note.pitch > best.note.pitch;
|
|
96
|
-
if (tieBreak === "highest" ? higher : !higher) {
|
|
97
|
-
best = indexed;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
return best;
|
|
101
|
-
},
|
|
102
|
-
attacksAt(beat) {
|
|
103
|
-
_chunkQ2WGKJNNcjs.assertFiniteNumber.call(void 0, beat, "beat");
|
|
104
|
-
let index = upperBound(notes, beat + EPS) - 1;
|
|
105
|
-
while (index >= 0) {
|
|
106
|
-
const candidate = notes[index];
|
|
107
|
-
if (candidate === void 0 || Math.abs(candidate.note.startBeat - beat) >= EPS) {
|
|
108
|
-
return false;
|
|
109
|
-
}
|
|
110
|
-
if (candidate.note.durationBeat > 0) {
|
|
111
|
-
return true;
|
|
112
|
-
}
|
|
113
|
-
index -= 1;
|
|
114
|
-
}
|
|
115
|
-
return false;
|
|
116
|
-
},
|
|
117
|
-
onsetsBetween(startBeat, endBeat) {
|
|
118
|
-
_chunkQ2WGKJNNcjs.assertFiniteNumber.call(void 0, startBeat, "startBeat");
|
|
119
|
-
_chunkQ2WGKJNNcjs.assertFiniteNumber.call(void 0, endBeat, "endBeat");
|
|
120
|
-
const result = [];
|
|
121
|
-
let index = upperBound(notes, startBeat + EPS);
|
|
122
|
-
while (index < notes.length) {
|
|
123
|
-
const indexed = notes[index];
|
|
124
|
-
const onset = _optionalChain([indexed, 'optionalAccess', _9 => _9.note, 'access', _10 => _10.startBeat]);
|
|
125
|
-
if (indexed === void 0 || onset === void 0 || onset >= endBeat - EPS) {
|
|
126
|
-
break;
|
|
127
|
-
}
|
|
128
|
-
index += 1;
|
|
129
|
-
if (indexed.note.durationBeat <= 0) {
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
const previous = result[result.length - 1];
|
|
133
|
-
if (previous === void 0 || Math.abs(previous - onset) >= EPS) {
|
|
134
|
-
result.push(onset);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
return result;
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// src/core/meter/index.ts
|
|
143
|
-
function toMeterData(value, name = "meter") {
|
|
144
|
-
if (typeof value === "string") {
|
|
145
|
-
return parseTimeSignature(value);
|
|
146
|
-
}
|
|
147
|
-
if (typeof value === "object" && value !== null) {
|
|
148
|
-
const data = !Array.isArray(value) && "toJSON" in value && typeof value.toJSON === "function" ? value.toJSON() : value;
|
|
149
|
-
return _chunkQ2WGKJNNcjs.isMeterMap.call(void 0, data) ? _chunkQ2WGKJNNcjs.assertMeterMap.call(void 0, data, name) : _chunkQ2WGKJNNcjs.assertTimeSignature.call(void 0, data, name);
|
|
150
|
-
}
|
|
151
|
-
throw new (0, _chunkQ2WGKJNNcjs.InvalidInputError)(
|
|
152
|
-
`${name} must be a time signature, a meter map, or a signature name; received ${typeof value}`
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
var EPS2 = 1e-9;
|
|
156
|
-
var DEFAULT_TS = { numerator: 4, denominator: 4 };
|
|
157
|
-
function isMultiple(value, unit) {
|
|
158
|
-
if (unit === 0) {
|
|
159
|
-
return false;
|
|
160
|
-
}
|
|
161
|
-
const ratio = value / unit;
|
|
162
|
-
return Math.abs(ratio - Math.round(ratio)) < EPS2;
|
|
163
|
-
}
|
|
164
|
-
function parseTimeSignature(text) {
|
|
165
|
-
return _chunkQ2WGKJNNcjs.unwrapParse.call(void 0, tryParseTimeSignature(text));
|
|
166
|
-
}
|
|
167
|
-
function tryParseTimeSignature(text) {
|
|
168
|
-
try {
|
|
169
|
-
if (typeof text !== "string") {
|
|
170
|
-
throw new (0, _chunkQ2WGKJNNcjs.InvalidInputError)(`time signature must be a string; received ${typeof text}`);
|
|
171
|
-
}
|
|
172
|
-
return { ok: true, value: readTimeSignature(text) };
|
|
173
|
-
} catch (error) {
|
|
174
|
-
return _chunkQ2WGKJNNcjs.parseFailure.call(void 0, error);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
function readTimeSignature(text) {
|
|
178
|
-
const match = /^\s*((?:\d+\s*\+\s*)*\d+)\s*\/\s*(\d+)\s*$/.exec(text);
|
|
179
|
-
if (!match) {
|
|
180
|
-
throw new (0, _chunkQ2WGKJNNcjs.InvalidInputError)(`Invalid time signature: ${text}`);
|
|
181
|
-
}
|
|
182
|
-
const groupTokens = (_nullishCoalesce(match[1], () => ( ""))).split("+").map((token) => Number.parseInt(token.trim(), 10));
|
|
183
|
-
const numerator = groupTokens.reduce((sum, value) => sum + value, 0);
|
|
184
|
-
const denominator = Number.parseInt(_nullishCoalesce(match[2], () => ( "")), 10);
|
|
185
|
-
if (!Number.isSafeInteger(numerator) || !Number.isSafeInteger(denominator) || numerator <= 0 || denominator <= 0) {
|
|
186
|
-
throw new (0, _chunkQ2WGKJNNcjs.InvalidInputError)(`Invalid time signature: ${text}`);
|
|
187
|
-
}
|
|
188
|
-
const grouping = groupTokens.length > 1 ? groupTokens : void 0;
|
|
189
|
-
return _chunkQ2WGKJNNcjs.assertTimeSignature.call(void 0, { numerator, denominator, grouping }, `time signature ${text}`);
|
|
190
|
-
}
|
|
191
|
-
function formatTimeSignature(ts, opts = {}) {
|
|
192
|
-
_chunkQ2WGKJNNcjs.assertTimeSignature.call(void 0, ts);
|
|
193
|
-
if (opts.grouping === true && ts.grouping !== void 0 && _chunkQ2WGKJNNcjs.groupingSumOf.call(void 0, ts) === ts.numerator) {
|
|
194
|
-
return `${ts.grouping.join("+")}/${ts.denominator}`;
|
|
195
|
-
}
|
|
196
|
-
return `${ts.numerator}/${ts.denominator}`;
|
|
197
|
-
}
|
|
198
|
-
function isCompound(ts) {
|
|
199
|
-
_chunkQ2WGKJNNcjs.assertTimeSignature.call(void 0, ts);
|
|
200
|
-
return _chunkQ2WGKJNNcjs.isCompoundNumerator.call(void 0, ts.numerator) && !_chunkQ2WGKJNNcjs.isAdditiveReading.call(void 0, ts);
|
|
201
|
-
}
|
|
202
|
-
function beatsPerBar(ts) {
|
|
203
|
-
_chunkQ2WGKJNNcjs.assertTimeSignature.call(void 0, ts);
|
|
204
|
-
return _chunkQ2WGKJNNcjs.barBeatsOf.call(void 0, ts);
|
|
205
|
-
}
|
|
206
|
-
function meterAt(beatInQuarters, meter) {
|
|
207
|
-
_chunkQ2WGKJNNcjs.assertFiniteNumber.call(void 0, beatInQuarters, "beat");
|
|
208
|
-
return meterAtChecked(beatInQuarters, toMeterData(meter));
|
|
209
|
-
}
|
|
210
|
-
function meterAtChecked(beatInQuarters, meter) {
|
|
211
|
-
if (!_chunkQ2WGKJNNcjs.isMeterMap.call(void 0, meter)) {
|
|
212
|
-
return meter;
|
|
213
|
-
}
|
|
214
|
-
return _nullishCoalesce(_optionalChain([meter, 'access', _11 => _11[_chunkQ2WGKJNNcjs.entryIndexOf.call(void 0, meter, beatInQuarters)], 'optionalAccess', _12 => _12.ts]), () => ( DEFAULT_TS));
|
|
215
|
-
}
|
|
216
|
-
function barStartBeat(beatInQuarters, meter) {
|
|
217
|
-
_chunkQ2WGKJNNcjs.assertFiniteNumber.call(void 0, beatInQuarters, "beat");
|
|
218
|
-
return barStartChecked(beatInQuarters, toMeterData(meter));
|
|
219
|
-
}
|
|
220
|
-
function barStartChecked(beatInQuarters, meter) {
|
|
221
|
-
if (_chunkQ2WGKJNNcjs.isMeterMap.call(void 0, meter)) {
|
|
222
|
-
return _chunkQ2WGKJNNcjs.barStartOf.call(void 0, meter, beatInQuarters);
|
|
223
|
-
}
|
|
224
|
-
const barLen = _chunkQ2WGKJNNcjs.barBeatsOf.call(void 0, meter);
|
|
225
|
-
return Math.floor(beatInQuarters / barLen + EPS2) * barLen;
|
|
226
|
-
}
|
|
227
|
-
function barIndexAt(beatInQuarters, meter) {
|
|
228
|
-
_chunkQ2WGKJNNcjs.assertFiniteNumber.call(void 0, beatInQuarters, "beat");
|
|
229
|
-
return barIndexChecked(beatInQuarters, toMeterData(meter));
|
|
230
|
-
}
|
|
231
|
-
function barIndexChecked(beatInQuarters, meter) {
|
|
232
|
-
if (_chunkQ2WGKJNNcjs.isMeterMap.call(void 0, meter)) {
|
|
233
|
-
return _chunkQ2WGKJNNcjs.barIndexOf.call(void 0, meter, beatInQuarters);
|
|
234
|
-
}
|
|
235
|
-
return Math.floor(beatInQuarters / _chunkQ2WGKJNNcjs.barBeatsOf.call(void 0, meter) + EPS2);
|
|
236
|
-
}
|
|
237
|
-
function beatsPerBarAt(beatInQuarters, meter) {
|
|
238
|
-
return _chunkQ2WGKJNNcjs.barBeatsOf.call(void 0, meterAt(beatInQuarters, meter));
|
|
239
|
-
}
|
|
240
|
-
function resolveMeters(opts, name = "meters") {
|
|
241
|
-
if (opts.ts !== void 0 && opts.meters !== void 0) {
|
|
242
|
-
throw new (0, _chunkQ2WGKJNNcjs.InvalidInputError)(
|
|
243
|
-
`${name} and ts name the same thing; give one or the other, not both`
|
|
244
|
-
);
|
|
245
|
-
}
|
|
246
|
-
if (opts.meters !== void 0) {
|
|
247
|
-
return _chunkQ2WGKJNNcjs.assertMeterMap.call(void 0, opts.meters, name);
|
|
248
|
-
}
|
|
249
|
-
const ts = _nullishCoalesce(opts.ts, () => ( DEFAULT_TS));
|
|
250
|
-
_chunkQ2WGKJNNcjs.assertTimeSignature.call(void 0, ts);
|
|
251
|
-
return [{ startBeat: 0, ts }];
|
|
252
|
-
}
|
|
253
|
-
function pulsesPerBar(ts) {
|
|
254
|
-
_chunkQ2WGKJNNcjs.assertTimeSignature.call(void 0, ts);
|
|
255
|
-
return _chunkQ2WGKJNNcjs.pulseCountOf.call(void 0, ts);
|
|
256
|
-
}
|
|
257
|
-
function isGroupHead(grouping, pulseIndex) {
|
|
258
|
-
let acc = 0;
|
|
259
|
-
for (const g of grouping) {
|
|
260
|
-
if (acc === pulseIndex) {
|
|
261
|
-
return true;
|
|
262
|
-
}
|
|
263
|
-
acc += g;
|
|
264
|
-
}
|
|
265
|
-
return false;
|
|
266
|
-
}
|
|
267
|
-
function isUniformGrouping(grouping) {
|
|
268
|
-
return grouping.every((entry) => entry === grouping[0]);
|
|
269
|
-
}
|
|
270
|
-
function beatToBarPosition(beatInQuarters, meter) {
|
|
271
|
-
_chunkQ2WGKJNNcjs.assertFiniteNumber.call(void 0, beatInQuarters, "beat");
|
|
272
|
-
return barPositionChecked(beatInQuarters, toMeterData(meter));
|
|
273
|
-
}
|
|
274
|
-
function barPositionChecked(beatInQuarters, meter) {
|
|
275
|
-
return {
|
|
276
|
-
bar: barIndexChecked(beatInQuarters, meter),
|
|
277
|
-
beat: beatInQuarters - barStartChecked(beatInQuarters, meter)
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
function pulseBeats(ts) {
|
|
281
|
-
_chunkQ2WGKJNNcjs.assertTimeSignature.call(void 0, ts);
|
|
282
|
-
return _chunkQ2WGKJNNcjs.pulseBeatsOf.call(void 0, ts);
|
|
283
|
-
}
|
|
284
|
-
function barPositionToPulse(pos, meter) {
|
|
285
|
-
_chunkQ2WGKJNNcjs.assertFiniteNumber.call(void 0, pos.beat, "position.beat");
|
|
286
|
-
return pulseChecked(pos, toMeterData(meter));
|
|
287
|
-
}
|
|
288
|
-
function pulseChecked(pos, meter) {
|
|
289
|
-
return pos.beat / _chunkQ2WGKJNNcjs.pulseBeatsOf.call(void 0, meterAtBarChecked(pos.bar, meter)) + 1;
|
|
290
|
-
}
|
|
291
|
-
function meterAtBarChecked(bar, meter) {
|
|
292
|
-
if (!_chunkQ2WGKJNNcjs.isMeterMap.call(void 0, meter)) {
|
|
293
|
-
return meter;
|
|
294
|
-
}
|
|
295
|
-
_chunkQ2WGKJNNcjs.assertFiniteNumber.call(void 0, bar, "position.bar");
|
|
296
|
-
return meterAtChecked(_chunkQ2WGKJNNcjs.beatOfBarIndex.call(void 0, meter, bar), meter);
|
|
297
|
-
}
|
|
298
|
-
function formatBarPosition(beatInQuarters, meter, decimals = 2) {
|
|
299
|
-
_chunkQ2WGKJNNcjs.assertInteger.call(void 0, decimals, "decimals", 0, 100);
|
|
300
|
-
_chunkQ2WGKJNNcjs.assertFiniteNumber.call(void 0, beatInQuarters, "beat");
|
|
301
|
-
const resolved = toMeterData(meter);
|
|
302
|
-
const position = barPositionChecked(beatInQuarters, resolved);
|
|
303
|
-
const ts = meterAtChecked(beatInQuarters, resolved);
|
|
304
|
-
const pulse = pulseChecked(position, ts);
|
|
305
|
-
const rounded = Number(pulse.toFixed(decimals));
|
|
306
|
-
const barLength = _chunkQ2WGKJNNcjs.isMeterMap.call(void 0, resolved) ? _chunkQ2WGKJNNcjs.barLengthOf.call(void 0, resolved, beatInQuarters) : _chunkQ2WGKJNNcjs.barBeatsOf.call(void 0, resolved);
|
|
307
|
-
const pulsesInBar = Math.ceil(barLength / _chunkQ2WGKJNNcjs.pulseBeatsOf.call(void 0, ts) - EPS2);
|
|
308
|
-
if (rounded >= pulsesInBar + 1) {
|
|
309
|
-
return `${barIndexChecked(barStartChecked(beatInQuarters, resolved) + barLength, resolved) + 1}.1`;
|
|
310
|
-
}
|
|
311
|
-
const wholePulse = Math.floor(rounded);
|
|
312
|
-
const fraction = Number((rounded - wholePulse).toFixed(decimals));
|
|
313
|
-
return fraction === 0 ? `${position.bar + 1}.${wholePulse}` : `${position.bar + 1}.${wholePulse}+${fraction}`;
|
|
314
|
-
}
|
|
315
|
-
function barPositionToBeat(pos, meter) {
|
|
316
|
-
_chunkQ2WGKJNNcjs.assertInteger.call(void 0, pos.bar, "bar position bar");
|
|
317
|
-
_chunkQ2WGKJNNcjs.assertFiniteNumber.call(void 0, pos.beat, "bar position beat");
|
|
318
|
-
const resolved = toMeterData(meter);
|
|
319
|
-
if (_chunkQ2WGKJNNcjs.isMeterMap.call(void 0, resolved)) {
|
|
320
|
-
return _chunkQ2WGKJNNcjs.beatOfBarIndex.call(void 0, resolved, pos.bar) + pos.beat;
|
|
321
|
-
}
|
|
322
|
-
return pos.bar * _chunkQ2WGKJNNcjs.barBeatsOf.call(void 0, resolved) + pos.beat;
|
|
323
|
-
}
|
|
324
|
-
function metricWeight(beatInQuarters, meter) {
|
|
325
|
-
_chunkQ2WGKJNNcjs.assertFiniteNumber.call(void 0, beatInQuarters, "beat");
|
|
326
|
-
const resolved = toMeterData(meter);
|
|
327
|
-
const ts = meterAtChecked(beatInQuarters, resolved);
|
|
328
|
-
const beat = beatInQuarters - barStartChecked(beatInQuarters, resolved);
|
|
329
|
-
const pulse = _chunkQ2WGKJNNcjs.pulseBeatsOf.call(void 0, ts);
|
|
330
|
-
if (!isMultiple(beat, pulse)) {
|
|
331
|
-
return 0;
|
|
332
|
-
}
|
|
333
|
-
const pulses = _chunkQ2WGKJNNcjs.pulseCountOf.call(void 0, ts);
|
|
334
|
-
const pulseIndex = Math.round(beat / pulse) % pulses;
|
|
335
|
-
if (pulseIndex === 0) {
|
|
336
|
-
return 3;
|
|
337
|
-
}
|
|
338
|
-
const grouping = _chunkQ2WGKJNNcjs.pulseGroupingOf.call(void 0, ts);
|
|
339
|
-
if (grouping !== void 0 && !isUniformGrouping(grouping)) {
|
|
340
|
-
return isGroupHead(grouping, pulseIndex) ? 2 : 1;
|
|
341
|
-
}
|
|
342
|
-
if (pulses % 2 === 0 && pulseIndex === pulses / 2) {
|
|
343
|
-
return 2;
|
|
344
|
-
}
|
|
345
|
-
return 1;
|
|
346
|
-
}
|
|
347
|
-
function isStrongBeat(beatInQuarters, meter) {
|
|
348
|
-
return metricWeight(beatInQuarters, meter) >= 2;
|
|
349
|
-
}
|
|
350
|
-
function tuplet(totalBeats, count) {
|
|
351
|
-
_chunkQ2WGKJNNcjs.assertRange.call(void 0, totalBeats, 0, Number.MAX_SAFE_INTEGER, "tuplet total beats");
|
|
352
|
-
_chunkQ2WGKJNNcjs.assertPositiveInt.call(void 0, count, "tuplet count");
|
|
353
|
-
return new Array(count).fill(totalBeats / count);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
exports.createNoteEventIndex = createNoteEventIndex; exports.toMeterData = toMeterData; exports.parseTimeSignature = parseTimeSignature; exports.tryParseTimeSignature = tryParseTimeSignature; exports.formatTimeSignature = formatTimeSignature; exports.isCompound = isCompound; exports.beatsPerBar = beatsPerBar; exports.meterAt = meterAt; exports.barStartBeat = barStartBeat; exports.barIndexAt = barIndexAt; exports.beatsPerBarAt = beatsPerBarAt; exports.resolveMeters = resolveMeters; exports.pulsesPerBar = pulsesPerBar; exports.beatToBarPosition = beatToBarPosition; exports.pulseBeats = pulseBeats; exports.barPositionToPulse = barPositionToPulse; exports.formatBarPosition = formatBarPosition; exports.barPositionToBeat = barPositionToBeat; exports.metricWeight = metricWeight; exports.isStrongBeat = isStrongBeat; exports.tuplet = tuplet;
|
|
379
|
-
//# sourceMappingURL=chunk-K4ZJUGF4.cjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/libcantus/libcantus/dist/chunk-K4ZJUGF4.cjs","../src/core/event-index/index.ts","../src/core/meter/index.ts"],"names":["EPS"],"mappings":"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EAAA;AACA,EAAA;AACE,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AAME,IAAA;AACF,EAAA;AAGA,EAAA;AACA,EAAA;AACF;AA4BO;AACL,EAAA;AACA,EAAA;AACE,IAAA;AACF,EAAA;AACA,EAAA;AACF;AAcO;AACL,EAAA;AACA,EAAA;AACF;AASO;AACL,EAAA;AACA,EAAA;AACF;AAmBO;AACL,EAAA;AACA,EAAA;AACF;AAGA;AACE,EAAA;AACE,IAAA;AACF,EAAA;AACA,EAAA;AACF;AAUO;AACL,EAAA;AACA,EAAA;AACF;AAGA;AACE,EAAA;AACE,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAsBO;AACL,EAAA;AACA,EAAA;AACF;AAGA;AACE,EAAA;AACE,IAAA;AACF,EAAA;AACA,EAAA;AACF;AAaO;AACL,EAAA;AACF;AAsBO;AAIL,EAAA;AACE,IAAA;AAAU,MAAA;AAEV,IAAA;AACF,EAAA;AACA,EAAA;AACE,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAgBO;AACL,EAAA;AACA,EAAA;AACF;AAGA;AACE,EAAA;AACA,EAAA;AACE,IAAA;AACE,MAAA;AACF,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AACF;AASA;AACE,EAAA;AACF;AAgBO;AACL,EAAA;AACA,EAAA;AACF;AAGA;AACE,EAAA;AACE,IAAA;AACA,IAAA;AACF,EAAA;AACF;AAkBO;AACL,EAAA;AACA,EAAA;AACF;AAmBO;AACL,EAAA;AACA,EAAA;AACF;AAGA;AACE,EAAA;AACF;AAGA;AACE,EAAA;AACE,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAsBO;AACL,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AAMA,EAAA;AACA,EAAA;AACE,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AAGF;AAWO;AACL,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACF,EAAA;AACA,EAAA;AACF;AAmCO;AACL,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACF,EAAA;AACA,EAAA;AACE,IAAA;AACF,EAAA;AACA,EAAA;AACF;AAYO;AACL,EAAA;AACF;AAYO;AACL,EAAA;AACA,EAAA;AACA,EAAA;AACF;AF7XK;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/libcantus/libcantus/dist/chunk-K4ZJUGF4.cjs","sourcesContent":[null,"import type { NoteEvent } from '../types.js';\nimport { assertFiniteNumber, assertNoteEvents } from '../validation/index.js';\n\n/**\n * A validated note retaining its position in the caller's original array.\n *\n * @category Core\n */\nexport type IndexedNoteEvent = {\n note: Readonly<NoteEvent>;\n originalIndex: number;\n endBeat: number;\n};\n\n/**\n * Which note wins when several sound from the same onset.\n *\n * `'highest'` and `'lowest'` name a voice, so the answer does not depend on the\n * order the caller happened to store the chord in; `'last'` takes the one\n * latest in the input array.\n *\n * @category Core\n */\nexport type OnsetTieBreak = 'highest' | 'lowest' | 'last';\n\n/**\n * Binary-searchable, stable-sorted index over note onsets and active spans.\n *\n * @category Core\n */\nexport type NoteEventIndex = {\n notes: readonly IndexedNoteEvent[];\n /**\n * Latest-onset note sounding at `beat`. Simultaneous onsets are resolved by\n * the index's {@link OnsetTieBreak}.\n *\n * @throws If `beat` is not finite.\n */\n at: (beat: number) => IndexedNoteEvent | undefined;\n /**\n * Whether one or more sounding notes attack at `beat`.\n *\n * @throws If `beat` is not finite.\n */\n attacksAt: (beat: number) => boolean;\n /**\n * Unique sounding-note attack beats strictly inside `(startBeat, endBeat)`.\n *\n * @throws If either bound is not finite.\n */\n onsetsBetween: (startBeat: number, endBeat: number) => number[];\n};\n\nconst EPS = 1e-9;\n\nfunction upperBound(values: readonly IndexedNoteEvent[], beat: number): number {\n let low = 0;\n let high = values.length;\n while (low < high) {\n const middle = Math.floor((low + high) / 2);\n if ((values[middle]?.note.startBeat ?? Number.POSITIVE_INFINITY) <= beat) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n return low;\n}\n\n/**\n * How an index reads the events it is built from.\n *\n * @category Core\n */\nexport type NoteEventIndexOptions = {\n allowNonPositiveDuration?: boolean;\n tieBreak?: OnsetTieBreak;\n budget?: number;\n /** What the events are, for the validation error message. */\n name?: string;\n};\n\n/**\n * Validate and stable-sort note events once, then expose logarithmic onset and\n * active-note lookups. Non-positive-duration notes may be retained for callers\n * that intentionally filter them later, but never count as sounding.\n *\n * @category Core\n */\nexport function createNoteEventIndex(\n events: readonly NoteEvent[],\n options: NoteEventIndexOptions = {},\n): NoteEventIndex {\n assertNoteEvents(events, options.name ?? 'note events', options);\n const tieBreak = options.tieBreak ?? 'highest';\n const notes = Object.freeze(\n events\n .map((note, originalIndex) => ({\n note: Object.freeze({ ...note }),\n originalIndex,\n endBeat: note.startBeat + note.durationBeat,\n }))\n .sort((a, b) => a.note.startBeat - b.note.startBeat || a.originalIndex - b.originalIndex),\n );\n // Segment-tree maxima find the latest active onset in logarithmic time even\n // when an early, very long note would defeat a prefix-max backwards scan.\n let treeSize = 1;\n while (treeSize < notes.length) treeSize *= 2;\n const maxEndTree = new Array<number>(treeSize * 2).fill(Number.NEGATIVE_INFINITY);\n for (let index = 0; index < notes.length; index += 1) {\n maxEndTree[treeSize + index] = notes[index]?.endBeat ?? Number.NEGATIVE_INFINITY;\n }\n for (let index = treeSize - 1; index > 0; index -= 1) {\n maxEndTree[index] = Math.max(\n maxEndTree[index * 2] ?? Number.NEGATIVE_INFINITY,\n maxEndTree[index * 2 + 1] ?? Number.NEGATIVE_INFINITY,\n );\n }\n const latestActiveIndex = (exclusive: number, beat: number): number => {\n const find = (node: number, start: number, end: number): number => {\n if (start >= exclusive || (maxEndTree[node] ?? Number.NEGATIVE_INFINITY) <= beat + EPS) {\n return -1;\n }\n if (end - start === 1) return start < notes.length ? start : -1;\n const mid = Math.floor((start + end) / 2);\n const right = find(node * 2 + 1, mid, end);\n return right >= 0 ? right : find(node * 2, start, mid);\n };\n return find(1, 0, treeSize);\n };\n\n return {\n notes,\n at(beat) {\n // A query beat comes from wherever the host got it — a scrub position, a\n // parsed field — so it is checked like any other entry argument. `NaN`\n // compares false against every bound, which would read as an open window\n // rather than as the missing value it is.\n assertFiniteNumber(beat, 'beat');\n const index = latestActiveIndex(upperBound(notes, beat + EPS), beat);\n if (index < 0) return undefined;\n const onset = notes[index]?.note.startBeat;\n let best: IndexedNoteEvent | undefined;\n for (let candidateIndex = index; candidateIndex >= 0; candidateIndex -= 1) {\n const indexed = notes[candidateIndex];\n if (indexed !== undefined && indexed.note.startBeat !== onset) break;\n if (\n indexed === undefined ||\n indexed.note.durationBeat <= 0 ||\n indexed.note.startBeat - EPS > beat ||\n beat >= indexed.endBeat - EPS\n ) {\n continue;\n }\n if (best === undefined) {\n best = indexed;\n continue;\n }\n if (tieBreak === 'last') continue;\n const higher = indexed.note.pitch > best.note.pitch;\n if (tieBreak === 'highest' ? higher : !higher) {\n best = indexed;\n }\n }\n return best;\n },\n attacksAt(beat) {\n assertFiniteNumber(beat, 'beat');\n // Only a sounding note attacks: a zero-length artefact must not make\n // `attacksAt(b)` true on a beat where `at(b)` finds nothing.\n let index = upperBound(notes, beat + EPS) - 1;\n while (index >= 0) {\n const candidate = notes[index];\n if (candidate === undefined || Math.abs(candidate.note.startBeat - beat) >= EPS) {\n return false;\n }\n if (candidate.note.durationBeat > 0) {\n return true;\n }\n index -= 1;\n }\n return false;\n },\n onsetsBetween(startBeat, endBeat) {\n assertFiniteNumber(startBeat, 'startBeat');\n assertFiniteNumber(endBeat, 'endBeat');\n const result: number[] = [];\n let index = upperBound(notes, startBeat + EPS);\n while (index < notes.length) {\n const indexed = notes[index];\n const onset = indexed?.note.startBeat;\n if (indexed === undefined || onset === undefined || onset >= endBeat - EPS) {\n break;\n }\n index += 1;\n if (indexed.note.durationBeat <= 0) {\n continue;\n }\n const previous = result[result.length - 1];\n if (previous === undefined || Math.abs(previous - onset) >= EPS) {\n result.push(onset);\n }\n }\n return result;\n },\n };\n}\n","/**\n * Meter and time signatures: bar/beat arithmetic, simple vs. compound meters,\n * metric-accent hierarchy, and tuplet subdivision.\n *\n * Positions and durations are measured in quarter-note beats, matching the rest\n * of the library's beat convention (four quarter-note beats per bar in 4/4).\n * Beat 0 is the first downbeat, so a pickup sounds at negative beats, and a\n * piece that changes meter is described by a {@link MeterMap} rather than by\n * one signature.\n */\n\nimport { InvalidInputError, type ParseResult, parseFailure, unwrapParse } from '../errors/index.js';\nimport {\n assertFiniteNumber,\n assertInteger,\n assertMeterMap,\n assertPositiveInt,\n assertRange,\n assertTimeSignature,\n} from '../validation/index.js';\nimport type { MeterData } from './internal.js';\nimport {\n barBeatsOf,\n barIndexOf,\n barLengthOf,\n barStartOf,\n beatOfBarIndex,\n entryIndexOf,\n groupingSumOf,\n isAdditiveReading,\n isCompoundNumerator,\n isMeterMap,\n pulseBeatsOf,\n pulseCountOf,\n pulseGroupingOf,\n} from './internal.js';\n\n/**\n * A time signature as a numerator over a note-value denominator.\n *\n * @category Rhythm & Meter\n */\nexport type TimeSignature = {\n numerator: number;\n denominator: number;\n /**\n * Optional grouping of the bar into felt beats, as positive integers. The\n * entries are group lengths, written either in the meter's main pulses —\n * `[2, 2, 3]` for a 2+2+3 reading of 7/8, `[3, 2]` for 5/8 — or, for a\n * compound numerator, in denominator units. They must sum to\n * {@link pulsesPerBar} in the first spelling and to the numerator in the\n * second; a pulse is one unit outside compound meters, where the two\n * spellings coincide.\n *\n * A compound numerator accepts both readings, and the shape of the grouping\n * says which is meant. Groups of nothing but threes spell the compound\n * division itself, so 9/8 as `[3, 3, 3]` (equivalently `[1, 1, 1]`) is the\n * ordinary three dotted-quarter pulses; any other grouping summing to the\n * numerator reads additively, so 9/8 as `[2, 2, 2, 3]` is nine quaver pulses\n * grouped aksak-fashion. {@link pulsesPerBar}, {@link pulseBeats} and\n * {@link isCompound} follow whichever reading the grouping selects.\n *\n * The head pulse of each group other than the downbeat is a secondary strong\n * pulse to {@link metricWeight}/{@link isStrongBeat}. A grouping whose groups\n * are all the same length states no accent the meter does not already have,\n * so it weighs exactly as an ungrouped bar does.\n */\n grouping?: number[];\n};\n\n/**\n * One time signature and the beat at which it takes effect.\n *\n * @category Rhythm & Meter\n */\nexport type MeterChange = {\n /** Absolute quarter-note beat the signature takes effect at. */\n startBeat: number;\n /** The signature in force from that beat until the next change. */\n ts: TimeSignature;\n};\n\n/**\n * A piece's meter over time: the signatures in force, in beat order.\n *\n * A meter change starts a new bar, so a span that does not divide evenly into\n * its own bars ends with a short bar rather than spilling across the change.\n * The first entry also governs everything before it, which is what reads a\n * pickup written at negative beats in the signature the piece opens in.\n *\n * @category Rhythm & Meter\n */\nexport type MeterMap = MeterChange[];\n\nexport type { MeterData } from './internal.js';\n\n/**\n * Anything that names a meter: a signature text such as `'6/8'`, a plain\n * {@link TimeSignature}, a full {@link MeterMap}, or a value that serializes to\n * one of those such as the `Meter` class.\n *\n * Every meter-aware function takes this, so a piece in one meter reads as\n * `'4/4'` and a piece that changes meter reads as the map, without the caller\n * choosing a different function for each. Only a signature has a text form — a\n * meter map is a run of changes and is given as the data it is.\n *\n * @category Rhythm & Meter\n */\nexport type MeterLike =\n | string\n | TimeSignature\n | MeterMap\n | {\n /** The meter data this value stands for. */\n toJSON(): TimeSignature | MeterMap;\n };\n\n/**\n * Resolve any meter-shaped value to the plain meter data the library reads.\n *\n * The counterpart of {@link toNoteData} for meters: an entry point takes\n * whatever form the caller has — the signature a meter field holds, the data\n * the meter module returns, the map of a piece that changes meter, or a `Meter`\n * instance — and gets one of two shapes back. An instance is accepted through\n * its `toJSON` method rather than by its type, so the core layer can read a\n * class it cannot import.\n *\n * Text is read by {@link parseTimeSignature} and nothing else, so every entry\n * point that takes a meter accepts exactly the signatures that parser accepts,\n * the additive `'2+2+3/8'` included and nothing besides. A meter map has no text\n * form — it is a run of changes, each with the beat it starts at — so text\n * always names a single signature.\n *\n * @param value A signature text, a plain time signature, a meter map, or a\n * value whose `toJSON` returns one of those.\n * @param name What the meter is called in an error message, so a caller that\n * holds several of them hears which one was malformed.\n * @returns The validated signature, or the validated map.\n * @throws If the value names no meter, or the meter it names is malformed.\n */\nexport function toMeterData(value: MeterLike, name = 'meter'): MeterData {\n if (typeof value === 'string') {\n return parseTimeSignature(value);\n }\n if (typeof value === 'object' && value !== null) {\n const data =\n !Array.isArray(value) && 'toJSON' in value && typeof value.toJSON === 'function'\n ? value.toJSON()\n : (value as MeterData);\n return isMeterMap(data) ? assertMeterMap(data, name) : assertTimeSignature(data, name);\n }\n throw new InvalidInputError(\n `${name} must be a time signature, a meter map, or a signature name; received ${typeof value}`,\n );\n}\n\n/**\n * A position expressed as a bar index and a quarter-note offset within the bar.\n *\n * @category Rhythm & Meter\n */\nexport type BarPosition = {\n /**\n * 0-based bar index, counting the first full bar as 0.\n * {@link formatBarPosition} renders it 1-based. A pickup sounds before that\n * bar, so its notes report bar -1.\n */\n bar: number;\n /**\n * Quarter-note offset from the start of the bar — not the felt beat. In 6/8\n * the second felt beat is at `beat: 1.5`; {@link barPositionToPulse} converts\n * to felt-beat numbering.\n */\n beat: number;\n};\n\nconst EPS = 1e-9;\n\n/** The meter assumed when a caller names none. */\nconst DEFAULT_TS: TimeSignature = { numerator: 4, denominator: 4 };\n\n/** Whether `value` is an integer multiple of `unit` (within a float tolerance). */\nfunction isMultiple(value: number, unit: number): boolean {\n if (unit === 0) {\n return false;\n }\n const ratio = value / unit;\n return Math.abs(ratio - Math.round(ratio)) < EPS;\n}\n\n/**\n * Parse a time signature such as `\"4/4\"` or `\"6/8\"`, or the additive `\"2+2+3/8\"`.\n *\n * @param text The signature text.\n * @returns The parsed time signature.\n * @throws If the text is not `n/d` with positive integers, or names a signature\n * the library rejects. Use {@link tryParseTimeSignature} where failure is\n * ordinary, such as a meter field read on every keystroke.\n * @example\n * ```ts\n * import { parseTimeSignature } from '@libraz/libcantus';\n * parseTimeSignature('6/8'); // { numerator: 6, denominator: 8 }\n * ```\n * @category Rhythm & Meter\n */\nexport function parseTimeSignature(text: string): TimeSignature {\n return unwrapParse(tryParseTimeSignature(text));\n}\n\n/**\n * Parse a time signature, reporting failure instead of throwing it.\n *\n * The same reading as {@link parseTimeSignature} — that function is this one\n * with its error thrown — for the callers where text that does not parse yet is\n * the normal state of the input rather than a fault.\n *\n * @param text The signature text.\n * @returns The signature, or the error explaining why the text is not one.\n * @example\n * ```ts\n * import { tryParseTimeSignature } from '@libraz/libcantus';\n * const result = tryParseTimeSignature('7/8');\n * result.ok ? result.value.numerator : result.error.message;\n * ```\n * @category Rhythm & Meter\n */\nexport function tryParseTimeSignature(text: string): ParseResult<TimeSignature> {\n try {\n if (typeof text !== 'string') {\n throw new InvalidInputError(`time signature must be a string; received ${typeof text}`);\n }\n return { ok: true, value: readTimeSignature(text) };\n } catch (error) {\n return parseFailure(error);\n }\n}\n\n/** The reading both parsers share, throwing on anything it cannot read. */\nfunction readTimeSignature(text: string): TimeSignature {\n const match = /^\\s*((?:\\d+\\s*\\+\\s*)*\\d+)\\s*\\/\\s*(\\d+)\\s*$/.exec(text);\n if (!match) {\n throw new InvalidInputError(`Invalid time signature: ${text}`);\n }\n const groupTokens = (match[1] ?? '').split('+').map((token) => Number.parseInt(token.trim(), 10));\n const numerator = groupTokens.reduce((sum, value) => sum + value, 0);\n const denominator = Number.parseInt(match[2] ?? '', 10);\n if (\n !Number.isSafeInteger(numerator) ||\n !Number.isSafeInteger(denominator) ||\n numerator <= 0 ||\n denominator <= 0\n ) {\n throw new InvalidInputError(`Invalid time signature: ${text}`);\n }\n // The same signature every consumer validates, validated here too: parsing\n // a signature the rest of the library rejects only moves the failure.\n const grouping = groupTokens.length > 1 ? groupTokens : undefined;\n return assertTimeSignature({ numerator, denominator, grouping }, `time signature ${text}`);\n}\n\n/**\n * Render a time signature as `\"n/d\"`, or as `\"a+b+c/d\"` when it carries an\n * additive grouping and `grouping` is requested.\n *\n * The additive form is what {@link parseTimeSignature} reads back, and it adds\n * its terms to get the numerator — so only a grouping written in denominator\n * units can be rendered that way. A grouping counted in main pulses (9/8 as\n * `[1, 1, 1]`, 12/8 as `[2, 2]`) would come back as a different bar, so it\n * falls back to the plain form, which those groupings cost nothing: they select\n * the same reading the bare signature does.\n *\n * The plain form drops the grouping, so a round trip through it reads 7/8 as\n * flat rather than as 2+2+3; ask for the additive form to keep it.\n *\n * @param ts The time signature.\n * @param opts Set `grouping: true` to render an additive grouping.\n * @returns The formatted signature.\n * @example\n * ```ts\n * import { formatTimeSignature } from '@libraz/libcantus';\n * const aksak = { numerator: 7, denominator: 8, grouping: [2, 2, 3] };\n * formatTimeSignature(aksak); // '7/8'\n * formatTimeSignature(aksak, { grouping: true }); // '2+2+3/8'\n * ```\n * @category Rhythm & Meter\n */\nexport function formatTimeSignature(ts: TimeSignature, opts: { grouping?: boolean } = {}): string {\n assertTimeSignature(ts);\n if (opts.grouping === true && ts.grouping !== undefined && groupingSumOf(ts) === ts.numerator) {\n return `${ts.grouping.join('+')}/${ts.denominator}`;\n }\n return `${ts.numerator}/${ts.denominator}`;\n}\n\n/**\n * Whether a meter is compound: its main pulses each divide into three, as in\n * 6/8, 9/8, 12/8, or 6/4. Compound meters are those whose numerator is a\n * multiple of three greater than three, independent of the denominator. Meters\n * like 3/8 and 3/4 (simple triples, numerator 3) are not compound, and neither\n * is a signature whose {@link TimeSignature.grouping} selects the additive\n * reading (9/8 as 2+2+2+3).\n *\n * @param ts The time signature.\n * @returns True for compound meters.\n * @category Rhythm & Meter\n */\nexport function isCompound(ts: TimeSignature): boolean {\n assertTimeSignature(ts);\n return isCompoundNumerator(ts.numerator) && !isAdditiveReading(ts);\n}\n\n/**\n * Length of a bar in quarter-note beats.\n *\n * @param ts The time signature.\n * @returns The bar length in quarter notes.\n * @category Rhythm & Meter\n */\nexport function beatsPerBar(ts: TimeSignature): number {\n assertTimeSignature(ts);\n return barBeatsOf(ts);\n}\n\n/**\n * The time signature in force at a beat.\n *\n * @param beatInQuarters Absolute position in quarter-note beats.\n * @param meter A single signature, or the piece's meter map.\n * @returns The signature governing that beat.\n * @example\n * ```ts\n * import { meterAt, parseTimeSignature } from '@libraz/libcantus';\n * const meters = [\n * { startBeat: 0, ts: parseTimeSignature('4/4') },\n * { startBeat: 8, ts: parseTimeSignature('3/4') },\n * ];\n * meterAt(9, meters); // { numerator: 3, denominator: 4 }\n * ```\n * @category Rhythm & Meter\n */\nexport function meterAt(beatInQuarters: number, meter: MeterLike): TimeSignature {\n assertFiniteNumber(beatInQuarters, 'beat');\n return meterAtChecked(beatInQuarters, toMeterData(meter));\n}\n\n/** {@link meterAt} without re-reading an already resolved meter. */\nfunction meterAtChecked(beatInQuarters: number, meter: MeterData): TimeSignature {\n if (!isMeterMap(meter)) {\n return meter;\n }\n return meter[entryIndexOf(meter, beatInQuarters)]?.ts ?? DEFAULT_TS;\n}\n\n/**\n * Absolute beat at which the bar containing `beatInQuarters` begins.\n *\n * @param beatInQuarters Absolute position in quarter-note beats.\n * @param meter A single signature, or the piece's meter map.\n * @returns The bar's first beat, negative inside a pickup.\n * @category Rhythm & Meter\n */\nexport function barStartBeat(beatInQuarters: number, meter: MeterLike): number {\n assertFiniteNumber(beatInQuarters, 'beat');\n return barStartChecked(beatInQuarters, toMeterData(meter));\n}\n\n/** {@link barStartBeat} without re-reading an already resolved meter. */\nfunction barStartChecked(beatInQuarters: number, meter: MeterData): number {\n if (isMeterMap(meter)) {\n return barStartOf(meter, beatInQuarters);\n }\n const barLen = barBeatsOf(meter);\n return Math.floor(beatInQuarters / barLen + EPS) * barLen;\n}\n\n/**\n * Bar index of a beat: 0 for the first full bar, negative inside a pickup.\n *\n * Bars accumulate across meter changes, so the bar after a 4/4-to-3/4 change is\n * the next bar and not the first bar of a new count.\n *\n * @param beatInQuarters Absolute position in quarter-note beats.\n * @param meter A single signature, or the piece's meter map.\n * @returns The 0-based bar index.\n * @example\n * ```ts\n * import { barIndexAt, parseTimeSignature } from '@libraz/libcantus';\n * const meters = [\n * { startBeat: 0, ts: parseTimeSignature('4/4') },\n * { startBeat: 8, ts: parseTimeSignature('3/4') },\n * ];\n * barIndexAt(11, meters); // 3\n * ```\n * @category Rhythm & Meter\n */\nexport function barIndexAt(beatInQuarters: number, meter: MeterLike): number {\n assertFiniteNumber(beatInQuarters, 'beat');\n return barIndexChecked(beatInQuarters, toMeterData(meter));\n}\n\n/** {@link barIndexAt} without re-reading an already resolved meter. */\nfunction barIndexChecked(beatInQuarters: number, meter: MeterData): number {\n if (isMeterMap(meter)) {\n return barIndexOf(meter, beatInQuarters);\n }\n return Math.floor(beatInQuarters / barBeatsOf(meter) + EPS);\n}\n\n/**\n * Length in quarter-note beats of the bar containing a beat.\n *\n * The meter-map counterpart of {@link beatsPerBar}, which answers for one\n * signature and so cannot be asked about a piece that changes meter.\n *\n * @param beatInQuarters Absolute position in quarter-note beats.\n * @param meter A single signature, or the piece's meter map.\n * @returns The bar length in quarter notes.\n * @category Rhythm & Meter\n */\nexport function beatsPerBarAt(beatInQuarters: number, meter: MeterLike): number {\n return barBeatsOf(meterAt(beatInQuarters, meter));\n}\n\n/**\n * Read the meter out of a pair of options that accept either form.\n *\n * Entry points take `meters` for a piece that changes meter and keep `ts` as\n * sugar for a piece that does not, so this is the one place that resolves the\n * pair — including the rule that naming both is an input error rather than one\n * of them silently winning.\n *\n * @param opts The options carrying `ts`, `meters`, or neither.\n * @param name What the options belong to, for the error message.\n * @returns The meter map to analyse against; 4/4 throughout when neither is given.\n * @throws If both `ts` and `meters` are given, or the map is malformed.\n * @example\n * ```ts\n * import { parseTimeSignature, resolveMeters } from '@libraz/libcantus';\n * resolveMeters({ ts: parseTimeSignature('3/4') });\n * // [{ startBeat: 0, ts: { numerator: 3, denominator: 4 } }]\n * ```\n * @category Rhythm & Meter\n */\nexport function resolveMeters(\n opts: { ts?: TimeSignature; meters?: MeterMap },\n name = 'meters',\n): MeterMap {\n if (opts.ts !== undefined && opts.meters !== undefined) {\n throw new InvalidInputError(\n `${name} and ts name the same thing; give one or the other, not both`,\n );\n }\n if (opts.meters !== undefined) {\n return assertMeterMap(opts.meters, name);\n }\n const ts = opts.ts ?? DEFAULT_TS;\n assertTimeSignature(ts);\n return [{ startBeat: 0, ts }];\n}\n\n/**\n * Number of main pulses (felt beats) per bar: the numerator for simple meters,\n * a third of it for compound meters.\n *\n * For additive/irregular meters such as 7/8 or 5/8 this counts the raw pulses\n * (7 and 5 respectively), which are otherwise felt as flat, evenly divided and\n * equally accented pulses. To recover a 2+2+3 or 3+2 felt-beat grouping, set\n * {@link TimeSignature.grouping}; {@link metricWeight} then accents each\n * group's head pulse.\n *\n * @param ts The time signature.\n * @returns The pulse count per bar.\n * @category Rhythm & Meter\n */\nexport function pulsesPerBar(ts: TimeSignature): number {\n assertTimeSignature(ts);\n return pulseCountOf(ts);\n}\n\n/** Whether `pulseIndex` is the head pulse of one of the groups, counted in pulses. */\nfunction isGroupHead(grouping: number[], pulseIndex: number): boolean {\n let acc = 0;\n for (const g of grouping) {\n if (acc === pulseIndex) {\n return true;\n }\n acc += g;\n }\n return false;\n}\n\n/**\n * Whether every group is the same length.\n *\n * Such a grouping divides the bar the way its own meter already divides it, so\n * it names no accent structure: 9/8 as `[1, 1, 1]` is the plain compound bar,\n * not three pulses of equal strength.\n */\nfunction isUniformGrouping(grouping: number[]): boolean {\n return grouping.every((entry) => entry === grouping[0]);\n}\n\n/**\n * Convert an absolute quarter-note position to a bar index and in-bar offset.\n *\n * @param beatInQuarters Absolute position in quarter-note beats.\n * @param meter A single signature, or the piece's meter map.\n * @returns The bar and in-bar quarter-note offset.\n * @example\n * ```ts\n * import { parseTimeSignature, beatToBarPosition } from '@libraz/libcantus';\n * const ts = parseTimeSignature('4/4');\n * beatToBarPosition(5, ts); // { bar: 1, beat: 1 }\n * ```\n * @category Rhythm & Meter\n */\nexport function beatToBarPosition(beatInQuarters: number, meter: MeterLike): BarPosition {\n assertFiniteNumber(beatInQuarters, 'beat');\n return barPositionChecked(beatInQuarters, toMeterData(meter));\n}\n\n/** {@link beatToBarPosition} without re-reading an already resolved meter. */\nfunction barPositionChecked(beatInQuarters: number, meter: MeterData): BarPosition {\n return {\n bar: barIndexChecked(beatInQuarters, meter),\n beat: beatInQuarters - barStartChecked(beatInQuarters, meter),\n };\n}\n\n/**\n * Length of one felt beat (main pulse) in quarter-note beats.\n *\n * A quarter note in simple meters, a dotted quarter in compound ones, and one\n * denominator unit in an additive metre. Position displays, click tracks, and\n * score export are all written against this length.\n *\n * @param ts The time signature.\n * @returns The pulse length in quarter-note beats.\n * @example\n * ```ts\n * import { parseTimeSignature, pulseBeats } from '@libraz/libcantus';\n * pulseBeats(parseTimeSignature('6/8')); // 1.5\n * ```\n * @category Rhythm & Meter\n */\nexport function pulseBeats(ts: TimeSignature): number {\n assertTimeSignature(ts);\n return pulseBeatsOf(ts);\n}\n\n/**\n * The felt-beat number of a bar position, 1-based.\n *\n * {@link BarPosition.beat} is a quarter-note offset, so in 6/8 the second felt\n * beat reads as 1.5. This is the number a musician says and a position display\n * shows.\n *\n * @param pos The bar position.\n * @param meter A single signature, or the piece's meter map.\n * @returns The 1-based felt-beat number, fractional between pulses.\n * @example\n * ```ts\n * import { barPositionToPulse, parseTimeSignature } from '@libraz/libcantus';\n * barPositionToPulse({ bar: 0, beat: 1.5 }, parseTimeSignature('6/8')); // 2\n * ```\n * @category Rhythm & Meter\n */\nexport function barPositionToPulse(pos: BarPosition, meter: MeterLike): number {\n assertFiniteNumber(pos.beat, 'position.beat');\n return pulseChecked(pos, toMeterData(meter));\n}\n\n/** {@link barPositionToPulse} without re-reading an already resolved meter. */\nfunction pulseChecked(pos: BarPosition, meter: MeterData): number {\n return pos.beat / pulseBeatsOf(meterAtBarChecked(pos.bar, meter)) + 1;\n}\n\n/** The signature governing a bar index, on an already resolved meter. */\nfunction meterAtBarChecked(bar: number, meter: MeterData): TimeSignature {\n if (!isMeterMap(meter)) {\n return meter;\n }\n assertFiniteNumber(bar, 'position.bar');\n return meterAtChecked(beatOfBarIndex(meter, bar), meter);\n}\n\n/**\n * Render an absolute position as the `bar.beat` a DAW or a score shows:\n * 1-based bar, 1-based felt beat.\n *\n * The felt beats counted are the ones the bar actually has. A meter change\n * starts a new bar, so the bar it interrupts is short, and a position that\n * rounds past the end of a short bar reads as the downbeat of the next one\n * rather than as a beat the bar never reached.\n *\n * @param beatInQuarters Absolute position in quarter-note beats.\n * @param meter A single signature, or the piece's meter map.\n * @param decimals Digits of the fractional beat to keep.\n * @returns The formatted position, e.g. `'3.2'` for bar 3, felt beat 2.\n * @example\n * ```ts\n * import { formatBarPosition, parseTimeSignature } from '@libraz/libcantus';\n * formatBarPosition(7.5, parseTimeSignature('6/8')); // '3.2'\n * ```\n * @category Rhythm & Meter\n */\nexport function formatBarPosition(beatInQuarters: number, meter: MeterLike, decimals = 2): string {\n assertInteger(decimals, 'decimals', 0, 100);\n assertFiniteNumber(beatInQuarters, 'beat');\n const resolved = toMeterData(meter);\n const position = barPositionChecked(beatInQuarters, resolved);\n const ts = meterAtChecked(beatInQuarters, resolved);\n const pulse = pulseChecked(position, ts);\n const rounded = Number(pulse.toFixed(decimals));\n const barLength = isMeterMap(resolved)\n ? barLengthOf(resolved, beatInQuarters)\n : barBeatsOf(resolved);\n // A short bar has a whole final pulse only in the sense that the pulse it\n // started is cut off, so the count rounds up: 2 beats of 4/4 hold felt beats\n // 1 and 2, and anything past those belongs to the next bar.\n const pulsesInBar = Math.ceil(barLength / pulseBeatsOf(ts) - EPS);\n if (rounded >= pulsesInBar + 1) {\n return `${barIndexChecked(barStartChecked(beatInQuarters, resolved) + barLength, resolved) + 1}.1`;\n }\n const wholePulse = Math.floor(rounded);\n const fraction = Number((rounded - wholePulse).toFixed(decimals));\n return fraction === 0\n ? `${position.bar + 1}.${wholePulse}`\n : `${position.bar + 1}.${wholePulse}+${fraction}`;\n}\n\n/**\n * Convert a bar index and in-bar offset back to an absolute quarter-note\n * position.\n *\n * @param pos The bar position.\n * @param meter A single signature, or the piece's meter map.\n * @returns The absolute position in quarter-note beats.\n * @category Rhythm & Meter\n */\nexport function barPositionToBeat(pos: BarPosition, meter: MeterLike): number {\n assertInteger(pos.bar, 'bar position bar');\n assertFiniteNumber(pos.beat, 'bar position beat');\n const resolved = toMeterData(meter);\n if (isMeterMap(resolved)) {\n return beatOfBarIndex(resolved, pos.bar) + pos.beat;\n }\n return pos.bar * barBeatsOf(resolved) + pos.beat;\n}\n\n/**\n * Metric weight of a position within its bar, on a 0–3 scale:\n * 3 the downbeat, 2 a secondary strong pulse, 1 any other main pulse, and 0 an\n * off-pulse subdivision.\n *\n * The secondary strong pulse is the bar's midpoint in even simple/compound\n * meters. For an additive/irregular meter with {@link TimeSignature.grouping}\n * set (e.g. `[2, 2, 3]` for 7/8), each group's head pulse other than the\n * downbeat is the secondary strong pulse instead; without a grouping every\n * non-downbeat main pulse of such a meter weighs 1 (flat, evenly divided\n * pulses). A grouping of equal-length groups (9/8 as `[1, 1, 1]`, 4/4 as\n * `[1, 1, 1, 1]`) states the division the meter already has, so it weighs as\n * an ungrouped bar rather than accenting every group head.\n *\n * Given a meter map, the weight follows the signature in force at that beat, so\n * beat 5 of a piece is a mid-bar accent in 4/4 and a plain main pulse in 3/4.\n * The downbeat is beat 0 however much pickup precedes it, so a note in the\n * pickup weighs as the upbeat it is rather than as a downbeat.\n *\n * @param beatInQuarters Absolute quarter-note position, or an in-bar offset\n * when the meter is a single signature.\n * @param meter A single signature, or the piece's meter map.\n * @returns The metric weight (0–3).\n * @throws If the meter is malformed, including a grouping that is not a\n * positive-integer list summing to the pulse count or the numerator.\n * @example\n * ```ts\n * import { parseTimeSignature, metricWeight } from '@libraz/libcantus';\n * const ts = parseTimeSignature('4/4');\n * metricWeight(0, ts); // 3 (the downbeat)\n * ```\n * @category Rhythm & Meter\n */\nexport function metricWeight(beatInQuarters: number, meter: MeterLike): number {\n assertFiniteNumber(beatInQuarters, 'beat');\n const resolved = toMeterData(meter);\n const ts = meterAtChecked(beatInQuarters, resolved);\n const beat = beatInQuarters - barStartChecked(beatInQuarters, resolved);\n const pulse = pulseBeatsOf(ts);\n if (!isMultiple(beat, pulse)) {\n return 0;\n }\n const pulses = pulseCountOf(ts);\n const pulseIndex = Math.round(beat / pulse) % pulses;\n if (pulseIndex === 0) {\n return 3;\n }\n const grouping = pulseGroupingOf(ts);\n if (grouping !== undefined && !isUniformGrouping(grouping)) {\n return isGroupHead(grouping, pulseIndex) ? 2 : 1;\n }\n if (pulses % 2 === 0 && pulseIndex === pulses / 2) {\n return 2;\n }\n return 1;\n}\n\n/**\n * Whether a position is metrically accented (weight 2 or more — a downbeat or a\n * secondary strong pulse).\n *\n * @param beatInQuarters Absolute quarter-note position, or an in-bar offset\n * when the meter is a single signature.\n * @param meter A single signature, or the piece's meter map.\n * @returns True on strong beats.\n * @category Rhythm & Meter\n */\nexport function isStrongBeat(beatInQuarters: number, meter: MeterLike): boolean {\n return metricWeight(beatInQuarters, meter) >= 2;\n}\n\n/**\n * Subdivide a span into `count` equal tuplet durations (e.g. an eighth-note\n * triplet is `tuplet(1, 3)`).\n *\n * @param totalBeats Total span in quarter-note beats.\n * @param count Number of equal parts.\n * @returns `count` equal durations summing to `totalBeats`.\n * @throws If `count` is not a positive integer.\n * @category Rhythm & Meter\n */\nexport function tuplet(totalBeats: number, count: number): number[] {\n assertRange(totalBeats, 0, Number.MAX_SAFE_INTEGER, 'tuplet total beats');\n assertPositiveInt(count, 'tuplet count');\n return new Array<number>(count).fill(totalBeats / count);\n}\n"]}
|
package/dist/chunk-KNJEKLKV.js
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
chordSpecOf,
|
|
3
|
-
chordToneRole,
|
|
4
|
-
toChordData
|
|
5
|
-
} from "./chunk-INHVXP6Z.js";
|
|
6
|
-
import {
|
|
7
|
-
pitchClassOf
|
|
8
|
-
} from "./chunk-VTI6FOLB.js";
|
|
9
|
-
|
|
10
|
-
// src/theory/harmony/index.ts
|
|
11
|
-
function suspendedIntervalOf(chord) {
|
|
12
|
-
const sounds = (ic) => chord.intervals.some((interval) => pitchClassOf(interval) === ic);
|
|
13
|
-
if (sounds(3) || sounds(4)) {
|
|
14
|
-
return void 0;
|
|
15
|
-
}
|
|
16
|
-
const spec = chordSpecOf(chord);
|
|
17
|
-
if (spec.base === "sus4") {
|
|
18
|
-
return sounds(5) ? 5 : void 0;
|
|
19
|
-
}
|
|
20
|
-
if (spec.base === "sus2") {
|
|
21
|
-
return sounds(2) ? 2 : void 0;
|
|
22
|
-
}
|
|
23
|
-
return spec.omissions.includes(3) && sounds(5) ? 5 : void 0;
|
|
24
|
-
}
|
|
25
|
-
function roleOf(pitch, chord, chordId = 0) {
|
|
26
|
-
const data = toChordData(chord);
|
|
27
|
-
const interval = (pitchClassOf(pitch) - pitchClassOf(data.rootPc) + 12) % 12;
|
|
28
|
-
const suspended = suspendedIntervalOf(data);
|
|
29
|
-
const isSuspendedTone = suspended !== void 0 && interval === suspended;
|
|
30
|
-
const chordRole = chordToneRole(pitch, data);
|
|
31
|
-
let role;
|
|
32
|
-
let lock;
|
|
33
|
-
if (isSuspendedTone) {
|
|
34
|
-
role = "third";
|
|
35
|
-
lock = "quality";
|
|
36
|
-
} else if (chordRole !== null) {
|
|
37
|
-
role = chordRole;
|
|
38
|
-
lock = chordRole === "root" ? "identity" : chordRole === "third" ? "quality" : "voicing";
|
|
39
|
-
} else {
|
|
40
|
-
role = "tension";
|
|
41
|
-
lock = "voicing";
|
|
42
|
-
}
|
|
43
|
-
return { role, lock, belongsToChordId: chordId };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export {
|
|
47
|
-
roleOf
|
|
48
|
-
};
|
|
49
|
-
//# sourceMappingURL=chunk-KNJEKLKV.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/theory/harmony/index.ts"],"sourcesContent":["import { pitchClassOf } from '../../core/pitch/index.js';\nimport { type Chord, type ChordToneRole, chordSpecOf, chordToneRole } from '../chord/index.js';\nimport { type ChordLike, toChordData } from '../symbol/index.js';\n\n/**\n * Harmonic role a pitch plays within a chord.\n *\n * @category Functional Harmony\n */\nexport type HarmonyRole = ChordToneRole | 'tension' | 'doubling';\n\n/**\n * How firmly a pitch is locked to the chord's identity.\n *\n * - `identity`: moving it produces a different chord (the root).\n * - `quality`: moving it flips the chord quality (the third).\n * - `voicing`: it can move freely without changing chord identity or quality.\n *\n * @category Functional Harmony\n */\nexport type LockLevel = 'identity' | 'quality' | 'voicing';\n\n/**\n * A pitch's role, lock level, and owning chord.\n *\n * @category Functional Harmony\n */\nexport type VoicedRole = {\n role: HarmonyRole;\n lock: LockLevel;\n belongsToChordId: number;\n};\n\n/**\n * The one interval class standing in for a chord's absent third, if any.\n *\n * Read from the chord's structure rather than from its sounding pitch classes:\n * an eleventh chord's tensions fold onto 2 and 5 in pitch-class space, so a set\n * of folded intervals cannot say which of them replaced the third — and both\n * would answer, leaving one chord with two quality-defining tones. The\n * suspension a `sus4` or `sus2` names is its fourth or its second; an eleventh\n * chord's is the eleventh, the tone its omitted third gave way to.\n */\nfunction suspendedIntervalOf(chord: Chord): number | undefined {\n const sounds = (ic: number): boolean =>\n chord.intervals.some((interval) => pitchClassOf(interval) === ic);\n if (sounds(3) || sounds(4)) {\n return undefined;\n }\n const spec = chordSpecOf(chord);\n if (spec.base === 'sus4') {\n return sounds(5) ? 5 : undefined;\n }\n if (spec.base === 'sus2') {\n return sounds(2) ? 2 : undefined;\n }\n return spec.omissions.includes(3) && sounds(5) ? 5 : undefined;\n}\n\n/**\n * Classify a pitch's harmonic role and lock level within a chord.\n *\n * The role comes from the pitch's interval class above the chord root, read\n * against the chord's own template: root (0), third (3/4), fifth (6/7/8),\n * sixth or seventh (9/10/11). Every pitch the template does not claim is\n * `'tension'` — including one that is simply foreign to the chord, which this\n * single-pitch query cannot tell from a colour tone. The root locks the chord\n * identity, the third locks its quality, and everything else is free voicing.\n * Where a chord has no third, the tone standing in its place — the fourth of a\n * `sus4`, the second of a `sus2`, the eleventh of an eleventh chord — takes the\n * third's slot and is locked to `quality`, since moving it changes the chord.\n * At most one interval class of a chord is ever reported as its third, so a\n * consumer reading `lock: 'quality'` as \"this tone identifies the chord\" gets\n * one answer rather than two. Detecting an octave doubling requires the\n * surrounding voicing, which this query does not carry, so `'doubling'` is part\n * of the type — the shared vocabulary of harmonic roles — but is never returned\n * here.\n *\n * @param pitch MIDI pitch or bare pitch class.\n * @param chord The chord providing the root reference, as a chord symbol, chord\n * data, or a `Chord`.\n * @param chordId Identifier stored on the result (defaults to 0).\n * @returns The pitch's role, lock level, and owning chord id.\n * @example\n * ```ts\n * import { roleOf, makeChord } from '@libraz/libcantus';\n * roleOf(64, makeChord(0, 'maj')); // E over C major\n * // { role: 'third', lock: 'quality', belongsToChordId: 0 }\n * ```\n * @category Functional Harmony\n */\nexport function roleOf(pitch: number, chord: ChordLike, chordId = 0): VoicedRole {\n const data = toChordData(chord);\n const interval = (pitchClassOf(pitch) - pitchClassOf(data.rootPc) + 12) % 12;\n const suspended = suspendedIntervalOf(data);\n const isSuspendedTone = suspended !== undefined && interval === suspended;\n const chordRole = chordToneRole(pitch, data);\n let role: HarmonyRole;\n let lock: LockLevel;\n if (isSuspendedTone) {\n role = 'third'; // suspended tone occupies the third's quality-defining slot\n lock = 'quality';\n } else if (chordRole !== null) {\n role = chordRole;\n lock = chordRole === 'root' ? 'identity' : chordRole === 'third' ? 'quality' : 'voicing';\n } else {\n role = 'tension';\n lock = 'voicing';\n }\n return { role, lock, belongsToChordId: chordId };\n}\n"],"mappings":";;;;;;;;;;AA2CA,SAAS,oBAAoB,OAAkC;AAC7D,QAAM,SAAS,CAAC,OACd,MAAM,UAAU,KAAK,CAAC,aAAa,aAAa,QAAQ,MAAM,EAAE;AAClE,MAAI,OAAO,CAAC,KAAK,OAAO,CAAC,GAAG;AAC1B,WAAO;AAAA,EACT;AACA,QAAM,OAAO,YAAY,KAAK;AAC9B,MAAI,KAAK,SAAS,QAAQ;AACxB,WAAO,OAAO,CAAC,IAAI,IAAI;AAAA,EACzB;AACA,MAAI,KAAK,SAAS,QAAQ;AACxB,WAAO,OAAO,CAAC,IAAI,IAAI;AAAA,EACzB;AACA,SAAO,KAAK,UAAU,SAAS,CAAC,KAAK,OAAO,CAAC,IAAI,IAAI;AACvD;AAkCO,SAAS,OAAO,OAAe,OAAkB,UAAU,GAAe;AAC/E,QAAM,OAAO,YAAY,KAAK;AAC9B,QAAM,YAAY,aAAa,KAAK,IAAI,aAAa,KAAK,MAAM,IAAI,MAAM;AAC1E,QAAM,YAAY,oBAAoB,IAAI;AAC1C,QAAM,kBAAkB,cAAc,UAAa,aAAa;AAChE,QAAM,YAAY,cAAc,OAAO,IAAI;AAC3C,MAAI;AACJ,MAAI;AACJ,MAAI,iBAAiB;AACnB,WAAO;AACP,WAAO;AAAA,EACT,WAAW,cAAc,MAAM;AAC7B,WAAO;AACP,WAAO,cAAc,SAAS,aAAa,cAAc,UAAU,YAAY;AAAA,EACjF,OAAO;AACL,WAAO;AACP,WAAO;AAAA,EACT;AACA,SAAO,EAAE,MAAM,MAAM,kBAAkB,QAAQ;AACjD;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/libcantus/libcantus/dist/chunk-MXO72PLY.cjs","../src/analyze/spelling/index.ts"],"names":[],"mappings":"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","file":"/home/runner/work/libcantus/libcantus/dist/chunk-MXO72PLY.cjs","sourcesContent":[null,"/**\n * Line spelling: name every note of a melodic line in one pass.\n *\n * {@link spellPitch} decides one note from the key, the root of the chord under\n * it and its two immediate neighbours. That is enough for a single note, but a\n * line spelled note by note can zig-zag between the two readings of one idea —\n * a rising whole-tone figure comes out F# Ab Bb, with a diminished third in the\n * middle of it.\n *\n * This module solves the whole line at once. The theory layer's own judgment\n * becomes the cost of emitting a spelling, the melodic rules become the cost of\n * moving from one spelling to the next, and a dynamic program takes the\n * cheapest path across the line. Nothing here re-decides what\n * {@link spellPitchClass} and {@link spellChord} already decide: it only weighs\n * their answers against the shape of the line they sit in.\n */\n\nimport type { Note, NoteLike } from '../../core/pitch/index.js';\nimport {\n diatonicLetterOf as mod7,\n pitchClassOf as mod12,\n naturalPitchClassOf as naturalPc,\n toNoteData,\n} from '../../core/pitch/index.js';\nimport type { KeyScale, NoteEvent } from '../../core/types.js';\nimport { assertGenerationBudget, assertNoteEvents } from '../../core/validation/index.js';\nimport type { Chord } from '../../theory/chord/index.js';\nimport { isScaleTone, type KeyLike, spelledKeyOf, toKeyScale } from '../../theory/scale/index.js';\nimport { assertTonicOf, spellChord, spellPitchClass } from '../../theory/spelling/index.js';\nimport type { ChordTimeline } from '../timeline/index.js';\n\n/**\n * The four rules, and what happens where they pull against each other.\n *\n * The costs below are spread far enough apart that a rule can never be\n * outvoted by any number of the rules under it, which fixes the precedence:\n *\n * 1. **The sounding chord.** A note the chord names takes the chord's own\n * spelling of that tone. This is the one place the line departs from\n * {@link spellPitchClass}, which never respells a tone of the key: over a\n * Db7 the seventh has to read Cb even in C major, where B is a scale tone,\n * because a dominant seventh spelled as a sixth is not the chord that was\n * played.\n * 2. **The key's scale.** Anything else the key itself spells — a scale tone,\n * and the tonic — keeps that spelling, exactly as the note-by-note path\n * keeps it. No amount of melodic pressure moves it.\n * 3. **The shape of the line, then its direction.** A chromatic note is spelled\n * so the letters of the intervals around it match the intervals actually\n * sounding, and a stepwise line then prefers sharps rising and flats\n * falling. Shape outranks direction because a rising line spelled with\n * sharps is still wrong if it spells a whole tone as a diminished third.\n * 4. **The key's chromatic convention, then the fewest accidentals.** What the\n * key alone would have said, as the tiebreak it is: the direction rule beats\n * it, which is why a chromatic ascent in C major spells C# D# G# A# rather\n * than the flats the key prefers, but one-sided evidence — a chromatic note\n * with only one stepwise neighbour — does not.\n */\n\n/** Emission cost for a spelling the chord sounding under the note does not name. */\nconst CHORD_MISMATCH = 64;\n\n/** Emission cost for respelling a tone the key's own scale already names. */\nconst SCALE_TONE_MISMATCH = 32;\n\n/** Transition cost per diatonic step an interval's letters miss its written size by. */\nconst LETTER_STEP_DEVIATION = 4;\n\n/**\n * How many steps of deviation are counted before the cost stops growing.\n *\n * Two letters out is already an interval no reading supports, and capping there\n * keeps the whole melodic term below {@link SCALE_TONE_MISMATCH}, so a badly\n * shaped line can never argue a scale tone out of its key spelling.\n */\nconst MAX_COUNTED_DEVIATION = 2;\n\n/** Transition cost for an accidental leaning against the direction of a stepwise line. */\nconst AGAINST_DIRECTION = 3;\n\n/** Emission cost for a chromatic spelling the key alone would not choose. */\nconst KEY_MISMATCH = 4;\n\n/** Emission cost per semitone of alteration: what minimises accidentals overall. */\nconst ACCIDENTAL = 1;\n\n/** Widest melodic interval still counted as a step, where the direction rule applies. */\nconst MAX_STEP = 2;\n\n/** Widest alteration a spelling may carry: a double accidental. */\nconst MAX_ALTER = 2;\n\n/**\n * Spellings each pitch class has within a double accidental.\n *\n * Always exactly three — a natural or its accidental, one letter above, one\n * letter below (C is C, B# and Dbb) — which is what bounds the work the dynamic\n * program is allowed to do.\n */\nconst SPELLINGS_PER_PITCH = 3;\n\n/**\n * The diatonic letter distances a melodic interval of a given size is\n * conventionally written with, indexed by its semitone class.\n *\n * Most sizes read only one way: a whole tone is a second, a minor third is a\n * third, so spelling one as anything else costs. Two sizes read two ways and\n * both are free — a semitone is either a chromatic inflection of one letter\n * (F to F#) or a diatonic step (E to F), and the tritone is either an augmented\n * fourth or a diminished fifth. Direction chooses between those.\n */\nconst CONVENTIONAL_LETTER_STEPS: readonly (readonly number[])[] = [\n [0], // unison\n [0, 1], // chromatic semitone or diatonic semitone\n [1], // second\n [2], // minor third\n [2], // major third\n [3], // fourth\n [3, 4], // augmented fourth or diminished fifth\n [4], // fifth\n [5], // minor sixth\n [5], // major sixth\n [6], // minor seventh\n [6], // major seventh\n];\n\n/**\n * Options for {@link spellLine}.\n *\n * @category Arrangement & Analysis\n */\nexport type SpellLineOptions = {\n /**\n * Tonic spelling to anchor the key, for a caller that already knows how the\n * piece is written. It must sound the key's own root pitch class; anything\n * else is rejected rather than spelled. Defaults to the conventional\n * spelling `spelledKeyOf` gives the key. Accepted as a note name, a MIDI\n * number, note data, or a `Note`.\n */\n tonic?: NoteLike;\n /** Upper bound on the work the line may cost; defaults to the generation budget. */\n budget?: number;\n};\n\n/** Shortest signed alteration (in [-6, 6]) taking a letter's natural pc to `pc`. */\nfunction alterationFor(letter: number, pc: number): number {\n const distance = mod12(pc - naturalPc(letter));\n return distance > 6 ? distance - 12 : distance;\n}\n\n/** The octave that reproduces `pitch` from a letter and its alteration. */\nfunction octaveFor(pitch: number, letter: number, alter: number): number {\n return (pitch - naturalPc(letter) - alter) / 12 - 1;\n}\n\n/** Whether two spellings name the same letter with the same alteration. */\nfunction sameSpelling(a: Note, b: Note): boolean {\n return a.letter === b.letter && a.alter === b.alter;\n}\n\n/**\n * Every spelling of `pitch` within a double accidental, `preferred` first.\n *\n * The order is what settles a tie: two readings that cost the same are equally\n * defensible, and the one the theory layer would have chosen alone is then the\n * one that stands.\n */\nfunction candidatesFor(pitch: number, preferred: Note): Note[] {\n const pc = mod12(pitch);\n const rest: Note[] = [];\n for (let letter = 0; letter < 7; letter += 1) {\n const alter = alterationFor(letter, pc);\n if (Math.abs(alter) > MAX_ALTER || letter === preferred.letter) {\n continue;\n }\n rest.push({ letter, alter, octave: octaveFor(pitch, letter, alter) });\n }\n rest.sort((a, b) => Math.abs(a.alter) - Math.abs(b.alter) || a.letter - b.letter);\n return [\n {\n letter: preferred.letter,\n alter: preferred.alter,\n octave: octaveFor(pitch, preferred.letter, preferred.alter),\n },\n ...rest,\n ];\n}\n\n/**\n * The chord's own spelling of a pitch class, or undefined when the chord does\n * not sound it.\n *\n * The tones come from {@link spellChord}, so a chord carrying its own spellings\n * — a parsed symbol, an augmented sixth — is read by them; a slash bass outside\n * the chord's tones is spelled from its hint when it has one.\n */\nfunction chordSpellingOf(pc: number, chord: Chord, tonic: Note, key: KeyScale): Note | undefined {\n for (const tone of spellChord(chord, tonic, key)) {\n if (mod12(naturalPc(tone.letter) + tone.alter) === pc) {\n return tone;\n }\n }\n const bass = chord.bassPc;\n if (bass === undefined || mod12(bass) !== pc) {\n return undefined;\n }\n const hint = chord.bassSpelling;\n return hint !== undefined && mod12(naturalPc(hint.letter) + hint.alter) === pc\n ? { letter: mod7(hint.letter), alter: hint.alter }\n : spellPitchClass(pc, tonic, key);\n}\n\n/** One note of the line, with the spellings open to it and what each costs. */\ntype LineState = {\n pitch: number;\n /** True when the key does not already spell this pitch, so direction may decide. */\n chromatic: boolean;\n candidates: Note[];\n emissions: number[];\n /** The spelling the note-by-note path would have chosen, used as the fallback. */\n reference: Note;\n};\n\n/**\n * Weigh every spelling open to one note against what the chord and the key say\n * about it.\n *\n * The reference spelling is the theory layer's own answer — the chord's, when\n * the chord names the note, and the key's otherwise — and the cost of departing\n * from it is what ranks the three rules that do not depend on the neighbours.\n */\nfunction stateFor(pitch: number, chord: Chord | null, tonic: Note, key: KeyScale): LineState {\n const pc = mod12(pitch);\n const fromChord = chord === null ? undefined : chordSpellingOf(pc, chord, tonic, key);\n const scaleTone = isScaleTone(pc, key);\n const reference = fromChord ?? spellPitchClass(pc, tonic, key);\n const mismatch =\n fromChord !== undefined ? CHORD_MISMATCH : scaleTone ? SCALE_TONE_MISMATCH : KEY_MISMATCH;\n const candidates = candidatesFor(pitch, reference);\n return {\n pitch,\n chromatic: !scaleTone,\n candidates,\n emissions: candidates.map(\n (candidate) =>\n (sameSpelling(candidate, reference) ? 0 : mismatch) +\n ACCIDENTAL * Math.abs(candidate.alter),\n ),\n reference,\n };\n}\n\n/** A spelling's position on the diatonic ladder, octave included. */\nfunction diatonicIndex(note: Note): number {\n return note.letter + 7 * (note.octave ?? 0);\n}\n\n/**\n * What the letters of a melodic interval cost when they do not match the\n * interval that actually sounds.\n *\n * Both notes are already spelled, so the sounding interval and the written one\n * are both known: a whole tone written F# to Ab is two letters apart where a\n * second is one, and pays for the difference.\n */\nfunction letterStepCost(from: Note, to: Note, semitones: number): number {\n const written = diatonicIndex(to) - diatonicIndex(from);\n const size = Math.abs(semitones);\n const octaves = Math.floor(size / 12);\n const direction = semitones < 0 ? -1 : 1;\n let deviation = Number.POSITIVE_INFINITY;\n for (const steps of CONVENTIONAL_LETTER_STEPS[size % 12] ?? []) {\n deviation = Math.min(deviation, Math.abs(written - direction * (octaves * 7 + steps)));\n }\n if (!Number.isFinite(deviation)) {\n return 0;\n }\n return LETTER_STEP_DEVIATION * Math.min(deviation, MAX_COUNTED_DEVIATION);\n}\n\n/**\n * What an accidental costs for leaning the wrong way in a stepwise line: a\n * rising line is written with sharps and a falling one with flats.\n *\n * Only steps are judged. The rule describes chromatic motion, and a leap onto a\n * flat says nothing about the flat — the key and the chord have the whole story\n * there.\n */\nfunction directionCost(note: Note, chromatic: boolean, semitones: number): number {\n if (!chromatic || semitones === 0 || Math.abs(semitones) > MAX_STEP) {\n return 0;\n }\n const rising = semitones > 0;\n return (rising && note.alter < 0) || (!rising && note.alter > 0) ? AGAINST_DIRECTION : 0;\n}\n\n/**\n * The cost of following one spelling with another.\n *\n * Direction is charged to both notes: a chromatic note is judged by the step it\n * arrives on and by the step it leaves on, so a note inside a run collects the\n * rule twice and a note at the end of one collects it once.\n */\nfunction transitionCost(from: LineState, source: Note, to: LineState, target: Note): number {\n const semitones = to.pitch - from.pitch;\n return (\n letterStepCost(source, target, semitones) +\n directionCost(target, to.chromatic, semitones) +\n directionCost(source, from.chromatic, semitones)\n );\n}\n\n/**\n * Spell a whole melodic line at once, so its notes agree with each other.\n *\n * Every note is spelled together with the rest of the line rather than one at a\n * time: each pitch offers the three spellings a double accidental allows, the\n * chord and the key price them, the intervals between neighbours price the\n * pairs, and the cheapest reading of the whole line wins. Ties fall to the\n * spelling {@link spellPitchClass} would have chosen alone, so the answer never\n * drifts from the theory layer without a reason.\n *\n * Four rules decide, in this order:\n *\n * 1. A note the sounding chord names takes that chord's spelling of it, even\n * when the key spells the pitch otherwise — the seventh of a Db7 reads Cb in\n * C major, not B.\n * 2. Anything else the key's scale spells keeps its key spelling, exactly as\n * the note-by-note path keeps it.\n * 3. A chromatic note is spelled so the written intervals around it match the\n * sounding ones, and then so a rising step carries a sharp and a falling\n * step a flat.\n * 4. Failing all of that, the key's own chromatic spelling stands, and the\n * fewest accidentals win.\n *\n * Without a timeline — pass `null` — rules 2 to 4 still apply in full, so a\n * diatonic line and a chromatic run come out the same. What is lost is rule 1:\n * a chromatic chord can no longer name its own tones, so the tones of a\n * borrowed or altered chord fall back to the key's convention (that Cb becomes\n * B), and an enharmonically ambiguous tone under a chord that would have fixed\n * it is decided by the line's direction instead.\n *\n * @param notes The line, monophonic and in time order. Notes sharing an onset\n * are read as consecutive, so split polyphonic material into voices first.\n * @param timeline The chords sounding under the line, or null when only the key\n * is known. Each note is judged against the chord at its own onset.\n * @param key The key the line is written in, as a key name, a key/scale, or a\n * `Key`.\n * @param opts Optional tonic spelling and work budget.\n * @returns One spelled note per input note, in input order, each carrying the\n * octave that reproduces its pitch.\n * @throws If `opts.tonic` does not sound the key's root pitch class, if a note\n * event is malformed, or if the line is longer than the budget allows.\n * @example\n * ```ts\n * import { chordTimelineFromChords, majorKey, noteNames, spellLine } from '@libraz/libcantus';\n * // A chromatic ascent spells with sharps, whatever the key would say alone.\n * const rising = [60, 61, 62, 63, 64].map((pitch, index) => ({\n * pitch,\n * startBeat: index,\n * durationBeat: 1,\n * }));\n * noteNames(spellLine(rising, null, majorKey(0)));\n * // ['C4', 'C#4', 'D4', 'D#4', 'E4']\n * // Under a Db7 the same B sounds as the chord's seventh.\n * const timeline = chordTimelineFromChords([{ rootPc: 1, quality: 'dom7', startBeat: 0 }], 4);\n * noteNames(spellLine([{ pitch: 71, startBeat: 0, durationBeat: 1 }], timeline, majorKey(0)));\n * // ['Cb5']\n * ```\n * @category Arrangement & Analysis\n */\nexport function spellLine(\n notes: readonly NoteEvent[],\n timeline: ChordTimeline | null,\n key: KeyLike,\n opts: SpellLineOptions = {},\n): Note[] {\n const scale = toKeyScale(key);\n const given = opts.tonic === undefined ? undefined : toNoteData(opts.tonic);\n assertNoteEvents(notes, 'line notes', {\n allowNonPositiveDuration: true,\n budget: opts.budget,\n });\n // The dynamic program compares every spelling of a note with every spelling\n // of the one before it, so the work is bounded before any of it is done.\n assertGenerationBudget(\n notes.length * SPELLINGS_PER_PITCH * SPELLINGS_PER_PITCH,\n 'line spelling states',\n opts.budget,\n );\n if (given !== undefined) {\n assertTonicOf(given, scale, 'spellLine');\n }\n if (notes.length === 0) {\n return [];\n }\n const tonic = given ?? spelledKeyOf(scale).tonic;\n const states = notes.map((note) =>\n stateFor(note.pitch, timeline?.at(note.startBeat) ?? null, tonic, scale),\n );\n\n // Viterbi across the line: `costs` holds the cheapest path reaching each\n // spelling of the current note, and `back` remembers which spelling of the\n // previous note that path came through.\n let costs: number[] = [];\n let previous: LineState | undefined;\n const back: number[][] = [];\n for (const current of states) {\n if (previous === undefined) {\n costs = [...current.emissions];\n back.push(current.candidates.map(() => 0));\n previous = current;\n continue;\n }\n const reached: number[] = [];\n const chosen: number[] = [];\n for (const [target, candidate] of current.candidates.entries()) {\n let best = Number.POSITIVE_INFINITY;\n let bestSource = 0;\n for (const [source, sourceNote] of previous.candidates.entries()) {\n const total =\n (costs[source] ?? Number.POSITIVE_INFINITY) +\n transitionCost(previous, sourceNote, current, candidate);\n if (total < best) {\n best = total;\n bestSource = source;\n }\n }\n reached.push(best + (current.emissions[target] ?? 0));\n chosen.push(bestSource);\n }\n costs = reached;\n back.push(chosen);\n previous = current;\n }\n\n let cursor = 0;\n for (const [index, cost] of costs.entries()) {\n if (cost < (costs[cursor] ?? Number.POSITIVE_INFINITY)) {\n cursor = index;\n }\n }\n const picked: number[] = new Array(states.length).fill(0);\n for (let index = states.length - 1; index >= 0; index -= 1) {\n picked[index] = cursor;\n cursor = back[index]?.[cursor] ?? 0;\n }\n return states.map((state, index) => state.candidates[picked[index] ?? 0] ?? state.reference);\n}\n"]}
|