@libraz/libcantus 0.9.4 → 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 +27 -18
- package/README_ja.md +268 -0
- package/dist/analyze/index.cjs +7 -7
- package/dist/analyze/index.d.cts +9 -99
- package/dist/analyze/index.d.ts +9 -99
- package/dist/analyze/index.js +6 -6
- package/dist/{chunk-36N73KZ3.cjs → chunk-2A2QMA74.cjs} +80 -65
- package/dist/chunk-2A2QMA74.cjs.map +1 -0
- package/dist/{chunk-YLEMGEFH.js → chunk-2PTFGVD7.js} +50 -35
- package/dist/chunk-2PTFGVD7.js.map +1 -0
- package/dist/{chunk-ZEJR4TKL.cjs → chunk-357ZDPVU.cjs} +36 -47
- package/dist/chunk-357ZDPVU.cjs.map +1 -0
- package/dist/{chunk-EHVEBWM5.js → chunk-3KX76RLG.js} +136 -37
- package/dist/chunk-3KX76RLG.js.map +1 -0
- package/dist/{chunk-RC6GBMCD.cjs → chunk-4VZ5MZAM.cjs} +155 -111
- package/dist/chunk-4VZ5MZAM.cjs.map +1 -0
- package/dist/{chunk-RK35RIN7.js → chunk-7EONYVI5.js} +115 -28
- package/dist/chunk-7EONYVI5.js.map +1 -0
- package/dist/{chunk-GQSGS2N4.js → chunk-7JEP3MFY.js} +5 -16
- package/dist/chunk-7JEP3MFY.js.map +1 -0
- package/dist/{chunk-7NKDPF74.js → chunk-AID75RN3.js} +203 -104
- package/dist/chunk-AID75RN3.js.map +1 -0
- package/dist/{chunk-FNDWXRRI.cjs → chunk-BSLTLGM3.cjs} +55 -37
- package/dist/chunk-BSLTLGM3.cjs.map +1 -0
- package/dist/{chunk-NP4NA2FB.cjs → chunk-CU6VG2YM.cjs} +39 -36
- package/dist/chunk-CU6VG2YM.cjs.map +1 -0
- package/dist/{chunk-NYYWMV2C.js → chunk-DZMHIGI4.js} +27 -17
- package/dist/chunk-DZMHIGI4.js.map +1 -0
- package/dist/{chunk-I24MV65U.js → chunk-EDOTEABK.js} +18 -15
- package/dist/{chunk-I24MV65U.js.map → chunk-EDOTEABK.js.map} +1 -1
- package/dist/{chunk-NKIBQ6PT.js → chunk-HN2OEUN2.js} +7 -4
- package/dist/chunk-HN2OEUN2.js.map +1 -0
- package/dist/{chunk-D2FAJBYL.js → chunk-KBTTEX7Q.js} +30 -12
- package/dist/chunk-KBTTEX7Q.js.map +1 -0
- package/dist/{chunk-CAN76NCU.cjs → chunk-KQN2ZYND.cjs} +147 -102
- package/dist/chunk-KQN2ZYND.cjs.map +1 -0
- package/dist/{chunk-4HLLZPXT.cjs → chunk-MPTVJIVG.cjs} +350 -251
- package/dist/chunk-MPTVJIVG.cjs.map +1 -0
- package/dist/{chunk-RUMU3UA6.cjs → chunk-MTMZTMWM.cjs} +12 -12
- package/dist/{chunk-RUMU3UA6.cjs.map → chunk-MTMZTMWM.cjs.map} +1 -1
- package/dist/{chunk-KMHOICIY.js → chunk-NNB67XA2.js} +90 -45
- package/dist/chunk-NNB67XA2.js.map +1 -0
- package/dist/{chunk-LW53AHQU.cjs → chunk-OL3IHHSJ.cjs} +228 -141
- package/dist/chunk-OL3IHHSJ.cjs.map +1 -0
- package/dist/{chunk-7MGZ7D62.cjs → chunk-PI3UXRWK.cjs} +43 -13
- package/dist/chunk-PI3UXRWK.cjs.map +1 -0
- package/dist/{chunk-XEBG3G53.js → chunk-R2VVFAIN.js} +42 -12
- package/dist/chunk-R2VVFAIN.js.map +1 -0
- package/dist/{chunk-Q6OP4U43.cjs → chunk-WFDXOLUB.cjs} +50 -40
- package/dist/chunk-WFDXOLUB.cjs.map +1 -0
- package/dist/{chunk-NVE34YGZ.cjs → chunk-WTAN4ACG.cjs} +212 -113
- package/dist/chunk-WTAN4ACG.cjs.map +1 -0
- package/dist/{chunk-FCANXBAT.cjs → chunk-YFJXS3AU.cjs} +8 -5
- package/dist/chunk-YFJXS3AU.cjs.map +1 -0
- package/dist/{chunk-Q2XUP227.js → chunk-YUE6TJ4M.js} +89 -45
- package/dist/chunk-YUE6TJ4M.js.map +1 -0
- package/dist/{chunk-HKGYCQ34.js → chunk-ZGLJM4A4.js} +2 -2
- package/dist/core/index.cjs +15 -5
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +14 -4
- package/dist/core/index.d.ts +14 -4
- package/dist/core/index.js +14 -4
- package/dist/{function-DNugAOrt.d.cts → function-CxixTB02.d.cts} +98 -7
- package/dist/{function-BtiZb9i9.d.ts → function-DTCt675z.d.ts} +98 -7
- package/dist/generate/index.cjs +10 -10
- package/dist/generate/index.d.cts +21 -21
- package/dist/generate/index.d.ts +21 -21
- package/dist/generate/index.js +9 -9
- package/dist/{index-tJhkmQgE.d.cts → index-B2Ulxxpo.d.cts} +3 -2
- package/dist/{index-Be74mcon.d.cts → index-BkYJ0xMq.d.cts} +2 -2
- package/dist/{index-6VR8-kit.d.ts → index-BuaB-ypt.d.ts} +3 -2
- package/dist/{index-70p168VM.d.ts → index-CAN7v-kw.d.ts} +9 -2
- package/dist/{index-C83mZI1P.d.ts → index-COw1Ky9g.d.ts} +1 -1
- package/dist/{index-tSQh-xwY.d.ts → index-Cfx8xgH0.d.ts} +1 -1
- package/dist/{index-C5UBbQ4G.d.ts → index-DRtIhlJj.d.ts} +2 -2
- package/dist/{index-BtXT6UQ0.d.cts → index-alrRninU.d.cts} +9 -2
- package/dist/{index-y_ak9_Hi.d.cts → index-oTQbYYIs.d.cts} +1 -1
- package/dist/{index-OG65ocOr.d.ts → index-tR9jTn26.d.cts} +4 -1
- package/dist/{index-OG65ocOr.d.cts → index-tR9jTn26.d.ts} +4 -1
- package/dist/{index-D6b6na69.d.cts → index-x2vcsduE.d.cts} +1 -1
- package/dist/index.cjs +24 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -11
- package/dist/index.d.ts +11 -11
- package/dist/index.js +23 -13
- package/dist/model/index.cjs +7 -7
- package/dist/model/index.d.cts +48 -16
- package/dist/model/index.d.ts +48 -16
- package/dist/model/index.js +6 -6
- package/dist/{styled-DIRkk9Uz.d.ts → styled-Bl0ZK28h.d.ts} +40 -3
- package/dist/{styled-DJbkJQfj.d.cts → styled-Crv6SA4S.d.cts} +40 -3
- package/dist/theory/index.cjs +6 -6
- package/dist/theory/index.d.cts +9 -9
- package/dist/theory/index.d.ts +9 -9
- package/dist/theory/index.js +5 -5
- package/package.json +12 -9
- package/dist/chunk-36N73KZ3.cjs.map +0 -1
- package/dist/chunk-4HLLZPXT.cjs.map +0 -1
- package/dist/chunk-7MGZ7D62.cjs.map +0 -1
- package/dist/chunk-7NKDPF74.js.map +0 -1
- package/dist/chunk-CAN76NCU.cjs.map +0 -1
- package/dist/chunk-D2FAJBYL.js.map +0 -1
- package/dist/chunk-EHVEBWM5.js.map +0 -1
- package/dist/chunk-FCANXBAT.cjs.map +0 -1
- package/dist/chunk-FNDWXRRI.cjs.map +0 -1
- package/dist/chunk-GQSGS2N4.js.map +0 -1
- package/dist/chunk-KMHOICIY.js.map +0 -1
- package/dist/chunk-LW53AHQU.cjs.map +0 -1
- package/dist/chunk-NKIBQ6PT.js.map +0 -1
- package/dist/chunk-NP4NA2FB.cjs.map +0 -1
- package/dist/chunk-NVE34YGZ.cjs.map +0 -1
- package/dist/chunk-NYYWMV2C.js.map +0 -1
- package/dist/chunk-Q2XUP227.js.map +0 -1
- package/dist/chunk-Q6OP4U43.cjs.map +0 -1
- package/dist/chunk-RC6GBMCD.cjs.map +0 -1
- package/dist/chunk-RK35RIN7.js.map +0 -1
- package/dist/chunk-XEBG3G53.js.map +0 -1
- package/dist/chunk-YLEMGEFH.js.map +0 -1
- package/dist/chunk-ZEJR4TKL.cjs.map +0 -1
- /package/dist/{chunk-HKGYCQ34.js.map → chunk-ZGLJM4A4.js.map} +0 -0
|
@@ -21,7 +21,11 @@ var BudgetExceededError = class extends RangeError {
|
|
|
21
21
|
name = "BudgetExceededError";
|
|
22
22
|
};
|
|
23
23
|
function isLibcantusError(value) {
|
|
24
|
-
|
|
24
|
+
if (typeof value !== "object" || value === null || !("code" in value)) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
const code = value.code;
|
|
28
|
+
return code === "INVALID_INPUT" || code === "NO_SOLUTION" || code === "BUDGET_EXCEEDED";
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
// src/core/meter/internal.ts
|
|
@@ -76,9 +80,23 @@ function assertInteger(value, name, min = Number.MIN_SAFE_INTEGER, max = Number.
|
|
|
76
80
|
}
|
|
77
81
|
return value;
|
|
78
82
|
}
|
|
83
|
+
function assertMidiPitch(value, name = "MIDI pitch") {
|
|
84
|
+
return assertInteger(value, name, 0, 127);
|
|
85
|
+
}
|
|
86
|
+
function clampToMidi(value, name = "MIDI pitch") {
|
|
87
|
+
assertInteger(value, name);
|
|
88
|
+
return Math.min(127, Math.max(0, value));
|
|
89
|
+
}
|
|
90
|
+
function assertDegree(value, name = "degree") {
|
|
91
|
+
return assertInteger(value, name, -1e3, 1e3);
|
|
92
|
+
}
|
|
93
|
+
function assertFiniteSemitones(value, name = "semitones") {
|
|
94
|
+
return assertFiniteNumber(value, name);
|
|
95
|
+
}
|
|
79
96
|
function dropSilentNotes(events) {
|
|
80
97
|
return events.filter((event) => event !== void 0 && event.durationBeat > 0);
|
|
81
98
|
}
|
|
99
|
+
var soundingNotesOnly = dropSilentNotes;
|
|
82
100
|
function assertOneOf(value, allowed, name) {
|
|
83
101
|
if (typeof value !== "string" || !allowed.includes(value)) {
|
|
84
102
|
throw new InvalidInputError(
|
|
@@ -131,7 +149,7 @@ function assertTimeSignature(ts, name = "time signature") {
|
|
|
131
149
|
return ts;
|
|
132
150
|
}
|
|
133
151
|
function assertNoteEvent(event, name = "note event", options = {}) {
|
|
134
|
-
|
|
152
|
+
assertMidiPitch(event.pitch, `${name}.pitch`);
|
|
135
153
|
assertRange(event.startBeat, 0, Number.MAX_SAFE_INTEGER, `${name}.startBeat`);
|
|
136
154
|
assertFiniteNumber(event.durationBeat, `${name}.durationBeat`);
|
|
137
155
|
if (!options.allowNonPositiveDuration && event.durationBeat <= 0) {
|
|
@@ -152,7 +170,7 @@ function assertNoteEvent(event, name = "note event", options = {}) {
|
|
|
152
170
|
}
|
|
153
171
|
function assertNoteEvents(events, name = "note events", options = {}) {
|
|
154
172
|
if (!Array.isArray(events)) {
|
|
155
|
-
throw new
|
|
173
|
+
throw new InvalidInputError(`${name} must be an array; received ${typeof events}`);
|
|
156
174
|
}
|
|
157
175
|
assertGenerationBudget(events.length, `${name} count`, options.budget);
|
|
158
176
|
for (let index = 0; index < events.length; index += 1) {
|
|
@@ -183,6 +201,7 @@ function pitchClassOf(value) {
|
|
|
183
201
|
}
|
|
184
202
|
var mod12 = pitchClassOf;
|
|
185
203
|
function diatonicLetterOf(value) {
|
|
204
|
+
assertFiniteNumber(value, "diatonic letter");
|
|
186
205
|
return (Math.round(value) % 7 + 7) % 7;
|
|
187
206
|
}
|
|
188
207
|
function naturalPitchClassOf(letter) {
|
|
@@ -190,6 +209,9 @@ function naturalPitchClassOf(letter) {
|
|
|
190
209
|
}
|
|
191
210
|
var mod7 = diatonicLetterOf;
|
|
192
211
|
function parseNote(text) {
|
|
212
|
+
if (typeof text !== "string") {
|
|
213
|
+
throw new InvalidInputError(`note must be a string; received ${typeof text}`);
|
|
214
|
+
}
|
|
193
215
|
const match = /^([A-Ga-g])([#x]*|b*)(-?\d+)?$/.exec(text.trim());
|
|
194
216
|
if (!match) {
|
|
195
217
|
throw new InvalidInputError(`Invalid note: ${text}`);
|
|
@@ -286,8 +308,11 @@ function transposeNote(note, semitones, opts) {
|
|
|
286
308
|
if (opts?.spelling !== void 0) {
|
|
287
309
|
return note.octave === void 0 ? bareOf(midiToNote(60 + mod12(noteToPitchClass(note) + steps), opts.spelling)) : midiToNote(noteToMidi(note) + steps, opts.spelling);
|
|
288
310
|
}
|
|
289
|
-
const
|
|
290
|
-
const
|
|
311
|
+
const direction = steps < 0 ? -1 : 1;
|
|
312
|
+
const magnitude = Math.abs(steps);
|
|
313
|
+
const octaves = Math.floor(magnitude / 12);
|
|
314
|
+
const withinOctave = magnitude % 12;
|
|
315
|
+
const letterSteps = direction * ((LETTER_STEPS_BY_SEMITONE[withinOctave] ?? 0) + 7 * octaves);
|
|
291
316
|
const absoluteLetter = mod7(note.letter) + letterSteps;
|
|
292
317
|
const letter = mod7(absoluteLetter);
|
|
293
318
|
const natural = LETTER_SEMITONES[letter] ?? 0;
|
|
@@ -354,7 +379,7 @@ function intervalSemitones(numberValue, quality) {
|
|
|
354
379
|
return reference + quality.length;
|
|
355
380
|
}
|
|
356
381
|
if (/^d+$/.test(quality)) {
|
|
357
|
-
return reference - quality.length - (perfect ? 0 : 1);
|
|
382
|
+
return Math.abs(reference - quality.length - (perfect ? 0 : 1));
|
|
358
383
|
}
|
|
359
384
|
throw new InvalidInputError(`unknown interval quality ${JSON.stringify(quality)}`);
|
|
360
385
|
}
|
|
@@ -371,9 +396,9 @@ function parseInterval(name) {
|
|
|
371
396
|
}
|
|
372
397
|
function transposeByInterval(note, interval) {
|
|
373
398
|
assertNote(note, "note");
|
|
374
|
-
assertInteger(interval.number, "interval.number", 1
|
|
399
|
+
assertInteger(interval.number, "interval.number", 1);
|
|
375
400
|
assertFiniteNumber(interval.semitones, "interval.semitones");
|
|
376
|
-
const descending = interval.semitones < 0;
|
|
401
|
+
const descending = interval.descending ?? interval.semitones < 0;
|
|
377
402
|
const letterSteps = (interval.number - 1) * (descending ? -1 : 1);
|
|
378
403
|
const absoluteLetter = mod7(note.letter) + letterSteps;
|
|
379
404
|
const letter = mod7(absoluteLetter);
|
|
@@ -417,7 +442,8 @@ function spelledInterval(a, b) {
|
|
|
417
442
|
const number = absSteps + 1;
|
|
418
443
|
const directedSpan = letterSteps === 0 ? Math.abs(semitones) : letterSteps > 0 ? semitones : -semitones;
|
|
419
444
|
const quality = qualityFromSpan(number, directedSpan);
|
|
420
|
-
|
|
445
|
+
const descending = letterSteps < 0 || letterSteps === 0 && semitones < 0;
|
|
446
|
+
return { number, quality, semitones, descending };
|
|
421
447
|
}
|
|
422
448
|
|
|
423
449
|
// src/core/interval/index.ts
|
|
@@ -428,8 +454,7 @@ var ConsonanceClass = /* @__PURE__ */ ((ConsonanceClass2) => {
|
|
|
428
454
|
return ConsonanceClass2;
|
|
429
455
|
})(ConsonanceClass || {});
|
|
430
456
|
function simpleInterval(semitones) {
|
|
431
|
-
|
|
432
|
-
return Math.abs(Math.round(semitones)) % 12;
|
|
457
|
+
return pitchClassOf(Math.abs(semitones));
|
|
433
458
|
}
|
|
434
459
|
function classifyInterval(semitones, twoVoice = true) {
|
|
435
460
|
const pc = simpleInterval(semitones);
|
|
@@ -469,7 +494,12 @@ export {
|
|
|
469
494
|
DEFAULT_GENERATION_BUDGET,
|
|
470
495
|
assertFiniteNumber,
|
|
471
496
|
assertInteger,
|
|
497
|
+
assertMidiPitch,
|
|
498
|
+
clampToMidi,
|
|
499
|
+
assertDegree,
|
|
500
|
+
assertFiniteSemitones,
|
|
472
501
|
dropSilentNotes,
|
|
502
|
+
soundingNotesOnly,
|
|
473
503
|
assertOneOf,
|
|
474
504
|
assertPositiveInt,
|
|
475
505
|
assertRange,
|
|
@@ -495,4 +525,4 @@ export {
|
|
|
495
525
|
isPerfectInterval,
|
|
496
526
|
isConsonantInterval
|
|
497
527
|
};
|
|
498
|
-
//# sourceMappingURL=chunk-
|
|
528
|
+
//# sourceMappingURL=chunk-R2VVFAIN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/errors/index.ts","../src/core/meter/internal.ts","../src/core/validation/index.ts","../src/core/pitch/index.ts","../src/core/interval/index.ts"],"sourcesContent":["/**\n * The library's error taxonomy.\n *\n * Every failure this library raises falls into one of three kinds, and the kind\n * decides what a caller can do about it. Batch-voicing a lead sheet, for\n * instance, has to tell \"fix the input\" apart from \"loosen the constraints\",\n * and a message string is not a contract.\n *\n * The classes extend the built-in error types a caller would otherwise catch —\n * a rejected argument is still a `RangeError` — so the `code` narrows an\n * existing category rather than replacing it.\n */\n\n/**\n * What kind of failure an error reports.\n *\n * - `INVALID_INPUT`: an argument is malformed or out of range. The caller's\n * input must change.\n * - `NO_SOLUTION`: the input is valid but nothing satisfies the constraints.\n * The constraints must change, or the failure must be accepted.\n * - `BUDGET_EXCEEDED`: the requested work is larger than the synchronous\n * generation budget. The request must be smaller, or the budget larger.\n *\n * @category Core\n */\nexport type LibcantusErrorCode = 'INVALID_INPUT' | 'NO_SOLUTION' | 'BUDGET_EXCEEDED';\n\n/**\n * A rejected argument: malformed, out of range, or naming something unknown.\n *\n * @category Core\n */\nexport class InvalidInputError extends RangeError {\n readonly code = 'INVALID_INPUT' as const;\n override readonly name = 'InvalidInputError';\n}\n\n/**\n * A valid request with no answer: no voicing fits the given ranges, no chord\n * matches the pitches, no preset claims the style.\n *\n * @category Core\n */\nexport class NoSolutionError extends Error {\n readonly code = 'NO_SOLUTION' as const;\n override readonly name = 'NoSolutionError';\n\n /**\n * Where in a sequence the failure occurred, when the operation was working\n * through one — the chord index of a progression, say.\n */\n readonly at: number | undefined;\n\n constructor(message: string, options: { at?: number } = {}) {\n super(message);\n this.at = options.at;\n }\n}\n\n/**\n * Work that would exceed the synchronous generation budget.\n *\n * @category Core\n */\nexport class BudgetExceededError extends RangeError {\n readonly code = 'BUDGET_EXCEEDED' as const;\n override readonly name = 'BudgetExceededError';\n}\n\n/**\n * Whether a caught value is one of this library's coded errors.\n *\n * @param value The caught value.\n * @returns True when it carries a {@link LibcantusErrorCode}.\n * @example\n * ```ts\n * import { isLibcantusError, voiceChord, parseChordSymbol } from '@libraz/libcantus';\n * try {\n * voiceChord(parseChordSymbol('C'), { ranges: [] });\n * } catch (error) {\n * if (isLibcantusError(error) && error.code === 'NO_SOLUTION') {\n * // loosen the constraints rather than blaming the input\n * }\n * }\n * ```\n * @category Core\n */\nexport function isLibcantusError(\n value: unknown,\n): value is InvalidInputError | NoSolutionError | BudgetExceededError {\n if (typeof value !== 'object' || value === null || !('code' in value)) {\n return false;\n }\n const code = value.code;\n return code === 'INVALID_INPUT' || code === 'NO_SOLUTION' || code === 'BUDGET_EXCEEDED';\n}\n","/**\n * Meter arithmetic shared by the meter module and the validation guards.\n *\n * These helpers never validate: they are the single definition of how a time\n * signature is read, so the guard that rejects a malformed signature and the\n * functions that use a validated one cannot drift apart. Everything here is\n * integer arithmetic on the numerator, so a bar's pulse count is exact rather\n * than the result of dividing two floats.\n */\n\nimport type { TimeSignature } from './index.js';\n\n/** Length of one denominator unit in quarter-note beats. */\nexport function unitBeatsOf(ts: TimeSignature): number {\n return 4 / ts.denominator;\n}\n\n/**\n * Whether a numerator reads as compound: a multiple of three greater than\n * three, so its main pulses each divide into three.\n */\nexport function isCompoundNumerator(numerator: number): boolean {\n return numerator % 3 === 0 && numerator > 3;\n}\n\n/** Sum of an additive grouping, or undefined when there is none. */\nexport function groupingSumOf(ts: TimeSignature): number | undefined {\n const grouping = ts.grouping;\n if (grouping === undefined) {\n return undefined;\n }\n let sum = 0;\n for (const entry of grouping) {\n sum += entry;\n }\n return sum;\n}\n\n/**\n * Whether the signature's grouping counts denominator units rather than\n * compound pulses.\n *\n * A compound numerator normally groups into pulses of three — 9/8 is three\n * dotted-quarter pulses — but the same signature is also how additive metres\n * are written, where 9/8 means 2+2+2+3 quavers. A grouping that sums to the\n * numerator selects that additive reading.\n */\nexport function isAdditiveReading(ts: TimeSignature): boolean {\n const sum = groupingSumOf(ts);\n return (\n sum !== undefined && isCompoundNumerator(ts.numerator) && sum === ts.numerator && sum !== 0\n );\n}\n\n/** Number of main pulses per bar under the signature's effective reading. */\nexport function pulseCountOf(ts: TimeSignature): number {\n if (isAdditiveReading(ts)) {\n return ts.numerator;\n }\n return isCompoundNumerator(ts.numerator) ? ts.numerator / 3 : ts.numerator;\n}\n\n/** Length of one main pulse in quarter-note beats, under the same reading. */\nexport function pulseBeatsOf(ts: TimeSignature): number {\n return (ts.numerator * unitBeatsOf(ts)) / pulseCountOf(ts);\n}\n\n/** Length of a bar in quarter-note beats. */\nexport function barBeatsOf(ts: TimeSignature): number {\n return ts.numerator * unitBeatsOf(ts);\n}\n","import { BudgetExceededError, InvalidInputError } from '../errors/index.js';\nimport type { TimeSignature } from '../meter/index.js';\nimport { isCompoundNumerator } from '../meter/internal.js';\nimport type { NoteEvent } from '../types.js';\n\n/**\n * Default upper bound for synchronous event/window/candidate generation.\n *\n * @category Core\n */\nexport const DEFAULT_GENERATION_BUDGET = 1_000_000;\n\n/**\n * Require a finite JavaScript number and return it unchanged.\n *\n * @category Core\n */\nexport function assertFiniteNumber(value: number, name: string): number {\n if (!Number.isFinite(value)) {\n throw new InvalidInputError(`${name} must be finite; received ${value}`);\n }\n return value;\n}\n\n/**\n * Require an integer in the inclusive range `[min, max]`.\n *\n * @category Core\n */\nexport function assertInteger(\n value: number,\n name: string,\n min = Number.MIN_SAFE_INTEGER,\n max = Number.MAX_SAFE_INTEGER,\n): number {\n assertFiniteNumber(value, name);\n if (!Number.isSafeInteger(value) || value < min || value > max) {\n throw new InvalidInputError(\n `${name} must be an integer in [${min}, ${max}]; received ${value}`,\n );\n }\n return value;\n}\n\n/** Require an integer MIDI pitch in the inclusive range 0..127. */\nexport function assertMidiPitch(value: number, name = 'MIDI pitch'): number {\n return assertInteger(value, name, 0, 127);\n}\n\n/** Clamp an integer MIDI pitch to the inclusive range 0..127. */\nexport function clampToMidi(value: number, name = 'MIDI pitch'): number {\n assertInteger(value, name);\n return Math.min(127, Math.max(0, value));\n}\n\n/** Require a bounded integer scale/chord degree. */\nexport function assertDegree(value: number, name = 'degree'): number {\n return assertInteger(value, name, -1000, 1000);\n}\n\n/** Require a finite semitone offset before pitch-class reduction. */\nexport function assertFiniteSemitones(value: number, name = 'semitones'): number {\n return assertFiniteNumber(value, name);\n}\n\n/**\n * Drop the notes that never sound.\n *\n * A MIDI or DAW import routinely carries zero-length artefacts. Every entry\n * point in this library ignores them, but a caller holding such an array still\n * has to decide what to do with it before comparing note counts or indices;\n * this is the one filter to apply.\n *\n * @param events The note events to filter.\n * @returns A new array containing only the notes with a positive duration.\n * @example\n * ```ts\n * import { dropSilentNotes } from '@libraz/libcantus';\n * dropSilentNotes([{ pitch: 60, startBeat: 0, durationBeat: 0 }]); // []\n * ```\n * @category Core\n */\nexport function dropSilentNotes(events: readonly NoteEvent[]): NoteEvent[] {\n return events.filter((event) => event !== undefined && event.durationBeat > 0);\n}\n\n/** Alias that makes the shared positive-duration policy explicit at API entrances. */\nexport const soundingNotesOnly = dropSilentNotes;\n\n/**\n * Require a value to be one of a fixed set of names.\n *\n * TypeScript checks string-union options at compile time only, so a value that\n * arrives from JSON, a config file, or a JavaScript caller reaches the engine\n * unchecked and is then read against a table that has no entry for it —\n * producing a silent `undefined`, a NaN velocity, or a crash far from the call.\n *\n * @param value The value to check.\n * @param allowed Every accepted name.\n * @param name What the value is, for the error message.\n * @returns The value, narrowed to the allowed union.\n * @throws If the value is not one of `allowed`.\n *\n * @category Core\n */\nexport function assertOneOf<const T extends string>(\n value: unknown,\n allowed: readonly T[],\n name: string,\n): T {\n if (typeof value !== 'string' || !(allowed as readonly string[]).includes(value)) {\n throw new InvalidInputError(\n `${name} must be one of ${allowed.join(', ')}; received ${JSON.stringify(value)}`,\n );\n }\n return value as T;\n}\n\n/**\n * Require a positive safe integer, optionally capped by `max`.\n *\n * @category Core\n */\nexport function assertPositiveInt(\n value: number,\n name: string,\n max = DEFAULT_GENERATION_BUDGET,\n): number {\n return assertInteger(value, name, 1, max);\n}\n\n/**\n * Require a finite number in the inclusive range `[min, max]`.\n *\n * @category Core\n */\nexport function assertRange(value: number, min: number, max: number, name: string): number {\n assertFiniteNumber(value, name);\n if (value < min || value > max) {\n throw new InvalidInputError(`${name} must be in [${min}, ${max}]; received ${value}`);\n }\n return value;\n}\n\n/**\n * Reject work estimates that would exceed a synchronous generation budget.\n *\n * @category Core\n */\nexport function assertGenerationBudget(\n estimated: number,\n name: string,\n limit: number | undefined = DEFAULT_GENERATION_BUDGET,\n): number {\n const cap = limit ?? DEFAULT_GENERATION_BUDGET;\n assertFiniteNumber(estimated, name);\n assertPositiveInt(cap, `${name} limit`, Number.MAX_SAFE_INTEGER);\n if (estimated < 0) {\n throw new InvalidInputError(`${name} must not be negative; received ${estimated}`);\n }\n if (estimated > cap) {\n throw new BudgetExceededError(\n `${name} exceeds the generation budget ${cap}; received ${estimated}`,\n );\n }\n return estimated;\n}\n\n/**\n * Validate a time signature, including additive grouping, before any early return.\n *\n * @category Core\n */\nexport function assertTimeSignature(ts: TimeSignature, name = 'time signature'): TimeSignature {\n assertPositiveInt(ts.numerator, `${name}.numerator`);\n assertPositiveInt(ts.denominator, `${name}.denominator`);\n if (ts.grouping !== undefined) {\n if (ts.grouping.length === 0) {\n throw new InvalidInputError(`${name}.grouping must not be empty`);\n }\n let sum = 0;\n for (let index = 0; index < ts.grouping.length; index += 1) {\n sum += assertPositiveInt(ts.grouping[index] ?? Number.NaN, `${name}.grouping[${index}]`);\n }\n // A compound numerator accepts either reading: grouping its dotted pulses\n // (9/8 as [1, 1, 1]) or grouping its denominator units additively (9/8 as\n // [2, 2, 2, 3]), which is how aksak and other additive metres are written.\n const pulses = isCompoundNumerator(ts.numerator) ? ts.numerator / 3 : ts.numerator;\n if (sum !== pulses && sum !== ts.numerator) {\n const accepted =\n pulses === ts.numerator ? `${pulses}` : `${pulses} (pulses) or ${ts.numerator} (units)`;\n throw new InvalidInputError(`${name}.grouping must sum to ${accepted}; received ${sum}`);\n }\n }\n return ts;\n}\n\n/**\n * How strictly {@link assertNoteEvent} and {@link assertNoteEvents} read an\n * event array.\n *\n * @category Core\n */\nexport type NoteEventAssertOptions = {\n /**\n * Accept notes that never sound. The analysis side does, so an array from a\n * MIDI import can be validated before the zero-length artefacts are dropped.\n */\n allowNonPositiveDuration?: boolean;\n /** Upper bound on the event count; defaults to the generation budget. */\n budget?: number;\n};\n\n/**\n * Validate the finite fields of one timeline note event.\n *\n * @category Core\n */\nexport function assertNoteEvent(\n event: NoteEvent,\n name = 'note event',\n options: NoteEventAssertOptions = {},\n): NoteEvent {\n assertMidiPitch(event.pitch, `${name}.pitch`);\n assertRange(event.startBeat, 0, Number.MAX_SAFE_INTEGER, `${name}.startBeat`);\n // The positivity check comes first so the common failure — a zero-length note\n // from a MIDI import — reads as such instead of naming a denormal lower bound.\n assertFiniteNumber(event.durationBeat, `${name}.durationBeat`);\n if (!options.allowNonPositiveDuration && event.durationBeat <= 0) {\n throw new InvalidInputError(\n `${name}.durationBeat must be positive; received ${event.durationBeat}`,\n );\n }\n assertRange(\n event.durationBeat,\n -Number.MAX_SAFE_INTEGER,\n Number.MAX_SAFE_INTEGER,\n `${name}.durationBeat`,\n );\n if (event.velocity !== undefined) {\n assertRange(event.velocity, 0, 127, `${name}.velocity`);\n }\n return event;\n}\n\n/**\n * Validate an event array and its allocation budget without copying it.\n *\n * @category Core\n */\nexport function assertNoteEvents(\n events: readonly NoteEvent[],\n name = 'note events',\n options: NoteEventAssertOptions = {},\n): NoteEvent[] {\n if (!Array.isArray(events)) {\n throw new InvalidInputError(`${name} must be an array; received ${typeof events}`);\n }\n assertGenerationBudget(events.length, `${name} count`, options.budget);\n for (let index = 0; index < events.length; index += 1) {\n const event = events[index];\n // A hole in a sparse array and an explicit undefined are both rejected\n // here: letting either through only moves the failure to a later\n // TypeError, or drops the note silently.\n if (event === undefined) {\n throw new InvalidInputError(`${name}[${index}] must be a note event; received undefined`);\n }\n assertNoteEvent(event, `${name}[${index}]`, options);\n }\n return events;\n}\n","/**\n * Pitch spelling: letter-name notes, enharmonic-aware conversion, and spelled\n * intervals.\n *\n * The rest of the library works in pitch classes (0..11), which cannot tell a\n * G# from an Ab or an augmented fourth from a diminished fifth. This module adds\n * an explicit spelling layer — a note is a diatonic letter plus a chromatic\n * alteration — so interval quality (P/M/m/A/d) and enharmonic identity are\n * preserved.\n */\n\nimport { InvalidInputError } from '../errors/index.js';\nimport { assertFiniteNumber, assertInteger } from '../validation/index.js';\n\n/** Semitone offset of each natural letter above C: C D E F G A B. */\nconst LETTER_SEMITONES = [0, 2, 4, 5, 7, 9, 11] as const;\n\n/**\n * Widest alteration a note may carry.\n *\n * Six accidentals already exceeds anything common practice writes, and the\n * bound is what keeps an unvalidated number from reaching `String.repeat`.\n */\nconst MAX_ALTER = 6;\n\n/** Validate the fields of a spelled note once, at a public entry point. */\nfunction assertNote(note: Note, name: string): Note {\n assertInteger(note.letter, `${name}.letter`, -1000, 1000);\n assertInteger(note.alter, `${name}.alter`, -MAX_ALTER, MAX_ALTER);\n if (note.octave !== undefined) {\n assertInteger(note.octave, `${name}.octave`, -100, 100);\n }\n return note;\n}\n\n/** Letter glyphs indexed by letter number (0 = C .. 6 = B). */\nconst LETTER_NAMES = ['C', 'D', 'E', 'F', 'G', 'A', 'B'] as const;\n\n/**\n * A spelled note: a diatonic letter, a chromatic alteration, and an optional\n * octave.\n *\n * - `letter` is 0..6 for C..B.\n * - `alter` is the number of semitones of alteration: -2 double-flat, -1 flat,\n * 0 natural, +1 sharp, +2 double-sharp.\n * - `octave` follows scientific pitch notation (middle C = C4 = MIDI 60). When\n * absent, the note denotes a bare pitch class.\n *\n * @category Pitch & Intervals\n */\nexport type Note = {\n letter: number;\n alter: number;\n octave?: number;\n};\n\n/**\n * The quality label of a spelled interval: perfect, major, minor, or one or\n * more augmentations or diminutions.\n *\n * This names how an interval is *spelled*. It is a different notion from\n * {@link ConsonanceClass}, which classifies how an interval *sounds* against\n * another voice.\n *\n * @see {@link ConsonanceClass}\n * @category Pitch & Intervals\n */\nexport type IntervalQualityLabel = 'P' | 'M' | 'm' | `A${string}` | `d${string}`;\n\n/**\n * A spelled interval: a diatonic number, a quality label, and its semitone span.\n *\n * @category Pitch & Intervals\n */\nexport type SpelledInterval = {\n /** Diatonic size: 1 = unison, 2 = second, ... 8 = octave, and beyond. */\n number: number;\n /** Quality label: 'P', 'M', 'm', or repeated 'A'/'d' for (multiply) aug/dim. */\n quality: IntervalQualityLabel;\n /** Signed semitone distance from the first note to the second. */\n semitones: number;\n /** True when the diatonic letter movement is descending despite a zero span. */\n descending?: boolean;\n};\n\n/**\n * Reduce a MIDI pitch, a bare pitch class, or any signed offset to a pitch\n * class in [0, 11].\n *\n * This is the library's single definition of pitch-class arithmetic: every\n * layer routes through it so a fractional pitch reduces the same way\n * everywhere. A non-integral input is rounded to the nearest semitone first, so\n * a microtonal 60.6 reads as C# rather than C.\n *\n * @param value A MIDI pitch, pitch class, or signed semitone offset.\n * @returns The pitch class in [0, 11].\n * @example\n * ```ts\n * import { pitchClassOf } from '@libraz/libcantus';\n * pitchClassOf(61); // 1\n * pitchClassOf(-1); // 11\n * pitchClassOf(60.6); // 1\n * ```\n * @category Pitch & Intervals\n */\nexport function pitchClassOf(value: number): number {\n assertFiniteNumber(value, 'pitch');\n return ((Math.round(value) % 12) + 12) % 12;\n}\n\n/** Reduce any integer to a pitch class in [0, 11]. */\nconst mod12 = pitchClassOf;\n\n/**\n * Reduce a letter number or letter offset to a diatonic letter in [0, 6]\n * (0 = C .. 6 = B).\n *\n * @param value A letter number or signed letter offset.\n * @returns The diatonic letter in [0, 6].\n * @category Pitch & Intervals\n */\nexport function diatonicLetterOf(value: number): number {\n assertFiniteNumber(value, 'diatonic letter');\n return ((Math.round(value) % 7) + 7) % 7;\n}\n\n/**\n * The pitch class of a diatonic letter with no accidental (C = 0, D = 2, ...).\n *\n * @param letter A letter number or signed letter offset; reduced to [0, 6].\n * @returns The natural pitch class of that letter.\n * @category Pitch & Intervals\n */\nexport function naturalPitchClassOf(letter: number): number {\n return LETTER_SEMITONES[diatonicLetterOf(letter)] ?? 0;\n}\n\n/** Reduce any integer to [0, 7). */\nconst mod7 = diatonicLetterOf;\n\n/**\n * Parse scientific pitch notation into a {@link Note}.\n *\n * Accepts a letter (case-insensitive) followed by any number of same-direction\n * accidentals and an optional octave, e.g. `\"C\"`, `\"C#4\"`, `\"Bb\"`, `\"F##3\"`,\n * `\"Ebb2\"`. Both `#` and `x` (double-sharp) are accepted for sharps; `b` for\n * flats. Mixing sharps and flats (e.g. `\"C#b\"`) is rejected.\n *\n * @param text The note text.\n * @returns The parsed note.\n * @throws If the text is not a valid note.\n * @example\n * ```ts\n * import { parseNote, noteToMidi } from '@libraz/libcantus';\n * const n = parseNote('C#4');\n * noteToMidi(n); // 61\n * ```\n * @category Pitch & Intervals\n */\nexport function parseNote(text: string): Note {\n if (typeof text !== 'string') {\n throw new InvalidInputError(`note must be a string; received ${typeof text}`);\n }\n const match = /^([A-Ga-g])([#x]*|b*)(-?\\d+)?$/.exec(text.trim());\n if (!match) {\n throw new InvalidInputError(`Invalid note: ${text}`);\n }\n const letterGlyph = (match[1] ?? '').toUpperCase();\n const letter = LETTER_NAMES.indexOf(letterGlyph as (typeof LETTER_NAMES)[number]);\n let alter = 0;\n for (const ch of match[2] ?? '') {\n if (ch === '#') {\n alter += 1;\n } else if (ch === 'x') {\n alter += 2;\n } else if (ch === 'b') {\n alter -= 1;\n }\n }\n const note: Note = { letter, alter };\n if (match[3] !== undefined) {\n note.octave = Number.parseInt(match[3], 10);\n }\n return assertNote(note, `note ${text}`);\n}\n\n/** Render an alteration as accidental glyphs (`##`, `b`, empty for natural). */\nfunction formatAlter(alter: number): string {\n if (alter > 0) {\n return '#'.repeat(alter);\n }\n if (alter < 0) {\n return 'b'.repeat(-alter);\n }\n return '';\n}\n\n/**\n * Render a {@link Note} as scientific pitch notation.\n *\n * @param note The note to format.\n * @returns The note text, including the octave when present.\n * @example\n * ```ts\n * import { formatNote, parseNote } from '@libraz/libcantus';\n * formatNote(parseNote('C#4')); // 'C#4'\n * ```\n * @category Pitch & Intervals\n */\nexport function formatNote(note: Note): string {\n assertNote(note, 'note');\n const glyph = LETTER_NAMES[mod7(note.letter)] ?? 'C';\n const octave = note.octave === undefined ? '' : String(note.octave);\n return `${glyph}${formatAlter(note.alter)}${octave}`;\n}\n\n/**\n * Pitch class (0..11) of a note, ignoring octave.\n *\n * @param note The note.\n * @returns The pitch class.\n * @example\n * ```ts\n * import { noteToPitchClass, parseNote } from '@libraz/libcantus';\n * noteToPitchClass(parseNote('Db')); // 1\n * ```\n * @category Pitch & Intervals\n */\nexport function noteToPitchClass(note: Note): number {\n assertNote(note, 'note');\n const natural = LETTER_SEMITONES[mod7(note.letter)] ?? 0;\n return mod12(natural + note.alter);\n}\n\n/**\n * MIDI number of a note (middle C = C4 = 60).\n *\n * @param note The note; must carry an octave.\n * @returns The MIDI number.\n * @throws If the note has no octave.\n * @example\n * ```ts\n * import { noteToMidi, parseNote } from '@libraz/libcantus';\n * noteToMidi(parseNote('A4')); // 69\n * ```\n * @category Pitch & Intervals\n */\nexport function noteToMidi(note: Note): number {\n assertNote(note, 'note');\n if (note.octave === undefined) {\n throw new InvalidInputError('noteToMidi requires an octave');\n }\n const natural = LETTER_SEMITONES[mod7(note.letter)] ?? 0;\n return (note.octave + 1) * 12 + natural + note.alter;\n}\n\n/** Preferred spelling when naming a black key from a bare MIDI number. */\nconst SHARP_SPELLING: readonly [number, number][] = [\n [0, 0],\n [0, 1],\n [1, 0],\n [1, 1],\n [2, 0],\n [3, 0],\n [3, 1],\n [4, 0],\n [4, 1],\n [5, 0],\n [5, 1],\n [6, 0],\n];\n\nconst FLAT_SPELLING: readonly [number, number][] = [\n [0, 0],\n [1, -1],\n [1, 0],\n [2, -1],\n [2, 0],\n [3, 0],\n [4, -1],\n [4, 0],\n [5, -1],\n [5, 0],\n [6, -1],\n [6, 0],\n];\n\n/**\n * Name a MIDI number as a {@link Note}, choosing sharp or flat spelling.\n *\n * The input is rounded to the nearest integer but is not clamped to the MIDI\n * range 0..127: out-of-range values extrapolate linearly (e.g. `-1` -> `B-2`,\n * `128` -> `G#9`) and remain an exact inverse of {@link noteToMidi}.\n *\n * @param midi The MIDI number.\n * @param spelling Whether to prefer sharps or flats for black keys.\n * @returns The spelled note, with octave.\n * @example\n * ```ts\n * import { midiToNote, formatNote } from '@libraz/libcantus';\n * formatNote(midiToNote(61)); // 'C#4'\n * formatNote(midiToNote(61, 'flat')); // 'Db4'\n * ```\n * @category Pitch & Intervals\n */\nexport function midiToNote(midi: number, spelling: 'sharp' | 'flat' = 'sharp'): Note {\n assertFiniteNumber(midi, 'midi');\n const rounded = Math.round(midi);\n const pc = mod12(rounded);\n const octave = Math.floor(rounded / 12) - 1;\n const table = spelling === 'sharp' ? SHARP_SPELLING : FLAT_SPELLING;\n const entry = table[pc] ?? [0, 0];\n return { letter: entry[0], alter: entry[1], octave };\n}\n\n/**\n * Diatonic letter distance of the conventional ascending interval for each\n * semitone step within an octave: P1, m2, M2, m3, M3, P4, A4, P5, m6, M6, m7, M7.\n */\nconst LETTER_STEPS_BY_SEMITONE = [0, 1, 1, 2, 2, 3, 3, 4, 5, 5, 6, 6] as const;\n\n/**\n * Transpose a note by a signed number of semitones, keeping its spelling.\n *\n * The letter moves by the diatonic distance of the conventional interval for\n * that many semitones and the accidental is recomputed for the new letter, so\n * enharmonic identity survives: `Ab4` up a major second is `Bb4`, not `A#4`, and\n * the operation is an exact inverse of itself. An octave-less note stays\n * octave-less.\n *\n * A bare semitone count cannot distinguish an augmented fourth from a diminished\n * fifth; pass `spelling` to override the letter distance when the enharmonic\n * choice matters.\n *\n * @param note The note to transpose.\n * @param semitones The signed semitone offset.\n * @param opts `spelling` forces the result onto the sharp or flat side instead\n * of following the source note's letter.\n * @returns The transposed note.\n * @example\n * ```ts\n * import { transposeNote, formatNote, parseNote } from '@libraz/libcantus';\n * formatNote(transposeNote(parseNote('Ab4'), 2)); // 'Bb4'\n * formatNote(transposeNote(parseNote('Ab4'), 2, { spelling: 'sharp' })); // 'A#4'\n * ```\n * @category Pitch & Intervals\n */\nexport function transposeNote(\n note: Note,\n semitones: number,\n opts?: { spelling?: 'sharp' | 'flat' },\n): Note {\n if (!Number.isFinite(semitones)) {\n throw new InvalidInputError(`semitones must be a finite number; received ${semitones}`);\n }\n const steps = Math.round(semitones);\n if (opts?.spelling !== undefined) {\n return note.octave === undefined\n ? bareOf(midiToNote(60 + mod12(noteToPitchClass(note) + steps), opts.spelling))\n : midiToNote(noteToMidi(note) + steps, opts.spelling);\n }\n const direction = steps < 0 ? -1 : 1;\n const magnitude = Math.abs(steps);\n const octaves = Math.floor(magnitude / 12);\n const withinOctave = magnitude % 12;\n // Descending motion reverses the conventional ascending letter distance.\n // Deriving it from floor(negative / 12) treated -6 as a descending fourth,\n // so transpose(+n) followed by transpose(-n) changed the letter spelling.\n const letterSteps = direction * ((LETTER_STEPS_BY_SEMITONE[withinOctave] ?? 0) + 7 * octaves);\n const absoluteLetter = mod7(note.letter) + letterSteps;\n const letter = mod7(absoluteLetter);\n const natural = LETTER_SEMITONES[letter] ?? 0;\n if (note.octave === undefined) {\n return { letter, alter: alterFor(natural, mod12(noteToPitchClass(note) + steps)) };\n }\n const octave = note.octave + Math.floor(absoluteLetter / 7);\n return { letter, alter: noteToMidi(note) + steps - ((octave + 1) * 12 + natural), octave };\n}\n\n/** Drop the octave from a spelled note. */\nfunction bareOf(note: Note): Note {\n return { letter: note.letter, alter: note.alter };\n}\n\n/** Shortest signed alteration taking a letter's natural pitch class to `pc`. */\nfunction alterFor(natural: number, pc: number): number {\n const d = mod12(pc - natural);\n return d > 6 ? d - 12 : d;\n}\n\n/** Reference semitone span of a perfect/major simple interval by diatonic number. */\nconst SIMPLE_REFERENCE = [0, 0, 2, 4, 5, 7, 9, 11] as const;\n\n/** Whether a simple diatonic number (1..7 within an octave) is a perfect kind. */\nfunction isPerfectNumber(simpleNumber: number): boolean {\n return simpleNumber === 1 || simpleNumber === 4 || simpleNumber === 5;\n}\n\n/** Quality label from a diatonic number and its actual semitone span. */\nfunction qualityFromSpan(numberValue: number, semitones: number): IntervalQualityLabel {\n const octaves = Math.floor((numberValue - 1) / 7);\n const simple = numberValue - 7 * octaves;\n const reference = (SIMPLE_REFERENCE[simple] ?? 0) + 12 * octaves;\n const delta = semitones - reference;\n const repeat = (glyph: 'A' | 'd', count: number): IntervalQualityLabel =>\n `${glyph}${glyph.repeat(Math.min(count, MAX_ALTER * 2) - 1)}`;\n if (isPerfectNumber(simple)) {\n if (delta === 0) {\n return 'P';\n }\n return delta > 0 ? repeat('A', delta) : repeat('d', -delta);\n }\n if (delta === 0) {\n return 'M';\n }\n if (delta === -1) {\n return 'm';\n }\n return delta > 0 ? repeat('A', delta) : repeat('d', -delta - 1);\n}\n\n/**\n * The semitone span of the interval named by a diatonic number and a quality.\n *\n * The inverse of the quality derivation in {@link spelledInterval}: it turns a\n * name back into a distance, which is what transposing by a named interval or\n * parsing one from text needs.\n *\n * @param numberValue Diatonic size: 1 = unison, 2 = second, ... 8 = octave.\n * @param quality Quality label: `'P'`, `'M'`, `'m'`, or repeated `'A'`/`'d'`.\n * @returns The unsigned semitone span.\n * @throws If the quality cannot apply to the number, such as a major fifth.\n * @example\n * ```ts\n * import { intervalSemitones } from '@libraz/libcantus';\n * intervalSemitones(5, 'P'); // 7\n * ```\n * @category Pitch & Intervals\n */\nexport function intervalSemitones(numberValue: number, quality: IntervalQualityLabel): number {\n assertInteger(numberValue, 'interval number', 1, 64);\n const octaves = Math.floor((numberValue - 1) / 7);\n const simple = numberValue - 7 * octaves;\n const reference = (SIMPLE_REFERENCE[simple] ?? 0) + 12 * octaves;\n const perfect = isPerfectNumber(simple);\n if (quality === 'P' || quality === 'M') {\n if (perfect !== (quality === 'P')) {\n throw new InvalidInputError(\n `a ${numberValue === 1 ? 'unison' : `${numberValue}th`} cannot be ${quality === 'P' ? 'perfect' : 'major'}`,\n );\n }\n return reference;\n }\n if (quality === 'm') {\n if (perfect) {\n throw new InvalidInputError(\n `a perfect-class interval cannot be minor; received ${numberValue}`,\n );\n }\n return reference - 1;\n }\n if (/^A+$/.test(quality)) {\n return reference + quality.length;\n }\n if (/^d+$/.test(quality)) {\n return Math.abs(reference - quality.length - (perfect ? 0 : 1));\n }\n throw new InvalidInputError(`unknown interval quality ${JSON.stringify(quality)}`);\n}\n\n/**\n * Parse an interval name such as `'P5'`, `'m3'`, or `'AA4'`.\n *\n * @param name The interval name: a quality label followed by a diatonic number.\n * @returns The spelled interval. Diminished unisons have a descending\n * (negative) semitone span; all other supported names are ascending.\n * @throws If the name is not a quality label followed by a number, or the two\n * cannot describe the same interval.\n * @example\n * ```ts\n * import { parseInterval } from '@libraz/libcantus';\n * parseInterval('m3'); // { number: 3, quality: 'm', semitones: 3 }\n * ```\n * @category Pitch & Intervals\n */\nexport function parseInterval(name: string): SpelledInterval {\n const match = /^(P|M|m|A+|d+)(\\d+)$/.exec(name);\n const quality = match?.[1] as IntervalQualityLabel | undefined;\n const numberValue = Number(match?.[2]);\n if (quality === undefined || !Number.isFinite(numberValue)) {\n throw new InvalidInputError(\n `interval must be a quality followed by a number, such as 'P5'; received ${JSON.stringify(name)}`,\n );\n }\n return { number: numberValue, quality, semitones: intervalSemitones(numberValue, quality) };\n}\n\n/**\n * Transpose a note by a spelled interval, keeping the spelling the interval\n * names.\n *\n * Unlike {@link transposeNote}, which picks a letter from the semitone count,\n * the diatonic number decides the letter here: transposing C by an augmented\n * second gives D#, not Eb.\n *\n * @param note The note to transpose.\n * @param interval The interval to apply; a negative span transposes downward.\n * @returns The transposed note, octave-less if the source was.\n * @example\n * ```ts\n * import { formatNote, parseInterval, parseNote, transposeByInterval } from '@libraz/libcantus';\n * formatNote(transposeByInterval(parseNote('C4'), parseInterval('A2'))); // 'D#4'\n * ```\n * @category Pitch & Intervals\n */\nexport function transposeByInterval(note: Note, interval: SpelledInterval): Note {\n assertNote(note, 'note');\n // `spelledInterval` accepts the full supported octave range, so accepting\n // only 64 here made a value produced by that sibling public function\n // impossible to apply back to its source note.\n assertInteger(interval.number, 'interval.number', 1);\n assertFiniteNumber(interval.semitones, 'interval.semitones');\n const descending = interval.descending ?? interval.semitones < 0;\n const letterSteps = (interval.number - 1) * (descending ? -1 : 1);\n const absoluteLetter = mod7(note.letter) + letterSteps;\n const letter = mod7(absoluteLetter);\n const natural = LETTER_SEMITONES[letter] ?? 0;\n const steps = Math.round(interval.semitones);\n if (note.octave === undefined) {\n return { letter, alter: alterFor(natural, mod12(noteToPitchClass(note) + steps)) };\n }\n const octave = note.octave + Math.floor(absoluteLetter / 7);\n return { letter, alter: noteToMidi(note) + steps - ((octave + 1) * 12 + natural), octave };\n}\n\n/** Diatonic ladder index of a note (letter + 7 * octave when octave-bearing). */\nfunction diatonicIndex(note: Note): number {\n return mod7(note.letter) + 7 * (note.octave ?? 0);\n}\n\n/**\n * The spelled interval from one note to another.\n *\n * When both notes carry octaves, the interval reflects their full signed\n * distance (so `C4 -> C5` is a perfect octave). When either lacks an octave the\n * interval is measured within a single ascending octave from `a` to `b`.\n *\n * @param a The lower/first note.\n * @param b The upper/second note.\n * @returns The diatonic number, quality, and semitone span.\n * @example\n * ```ts\n * import { spelledInterval, parseNote } from '@libraz/libcantus';\n * spelledInterval(parseNote('C4'), parseNote('G4'));\n * // { number: 5, quality: 'P', semitones: 7 }\n * ```\n * @category Pitch & Intervals\n */\nexport function spelledInterval(a: Note, b: Note): SpelledInterval {\n assertNote(a, 'a');\n assertNote(b, 'b');\n // Two octave-bearing notes measure their full signed distance; two\n // octave-less ones measure a single ascending octave. One of each has no\n // defined answer, and silently picking either reading gives a number the\n // caller cannot interpret.\n if ((a.octave === undefined) !== (b.octave === undefined)) {\n throw new InvalidInputError(\n 'spelledInterval needs both notes to carry an octave or neither to; received one of each',\n );\n }\n const octaved = a.octave !== undefined && b.octave !== undefined;\n let letterSteps: number;\n let semitones: number;\n if (octaved) {\n letterSteps = diatonicIndex(b) - diatonicIndex(a);\n // Signed pitch distance from a to b, so a note below a yields a negative span.\n semitones = noteToMidi(b) - noteToMidi(a);\n } else {\n letterSteps = mod7(b.letter - a.letter);\n let rawSemis = mod12(noteToPitchClass(b) - noteToPitchClass(a));\n // Lift the chromatic span into the octave nearest the diatonic reference so\n // wraparound intervals stay consistent with the diatonic number instead of\n // collapsing modulo 12. The correction is bidirectional: an upward wrap\n // (e.g. Ab -> G# = augmented seventh) adds an octave, while a same-letter or\n // downward chromatic step (e.g. E -> Eb, F# -> F, C -> Cb = descending\n // diminished/augmented unison) subtracts one so the sign follows pitch\n // direction instead of returning a garbage stack of augmentations.\n const reference = SIMPLE_REFERENCE[letterSteps + 1] ?? 0;\n if (reference - rawSemis > 6) {\n rawSemis += 12;\n } else if (rawSemis - reference > 6) {\n rawSemis -= 12;\n }\n semitones = rawSemis;\n }\n const absSteps = Math.abs(letterSteps);\n const number = absSteps + 1;\n // The span is measured in the direction the letters move, not as a magnitude:\n // C# up to Dbb steps up one letter but down one semitone, which is a doubly\n // diminished second, not a minor one. `semitones` keeps its own sign.\n //\n // A unison is the exception: the letters do not move, so there is no\n // direction to measure against and the quality names the chromatic\n // alteration alone — C down to Cb is a descending augmented unison.\n const directedSpan =\n letterSteps === 0 ? Math.abs(semitones) : letterSteps > 0 ? semitones : -semitones;\n const quality = qualityFromSpan(number, directedSpan);\n const descending = letterSteps < 0 || (letterSteps === 0 && semitones < 0);\n return { number, quality, semitones, descending };\n}\n","import { pitchClassOf } from '../pitch/index.js';\n\n/**\n * Counterpoint classification of a harmonic interval by consonance.\n *\n * Distinct from a spelled interval's quality label — see\n * {@link IntervalQualityLabel}, which names the `'P'` / `'M'` / `'m'` / `'A'` /\n * `'d'` spelling of an interval rather than how it sounds against another\n * voice.\n *\n * @see {@link IntervalQualityLabel}\n * @category Pitch & Intervals\n */\nexport enum ConsonanceClass {\n PerfectConsonance = 0,\n ImperfectConsonance = 1,\n Dissonance = 2,\n}\n\n/**\n * Reduce an interval to a simple interval class in the range [0, 11].\n *\n * Every public predicate in this module funnels through here, so a non-finite\n * interval is rejected once rather than silently classifying as a dissonance.\n */\nfunction simpleInterval(semitones: number): number {\n return pitchClassOf(Math.abs(semitones));\n}\n\n/**\n * Classify an interval for counterpoint evaluation.\n *\n * The interval is reduced modulo 12, so compound intervals classify as their\n * simple equivalents. The perfect fourth (5) is context-dependent: with\n * `twoVoice` (the default) it classifies as a dissonance, matching\n * {@link isConsonantInterval}; otherwise it is an imperfect consonance.\n *\n * @param semitones Interval size in semitones (may be negative or compound).\n * @param twoVoice When true, the perfect fourth is treated as dissonant.\n * @returns The counterpoint quality of the interval.\n * @example\n * ```ts\n * import { classifyInterval, ConsonanceClass } from '@libraz/libcantus';\n * classifyInterval(7); // ConsonanceClass.PerfectConsonance\n * classifyInterval(5); // ConsonanceClass.Dissonance (two-voice)\n * classifyInterval(5, false); // ConsonanceClass.ImperfectConsonance\n * ```\n * @category Pitch & Intervals\n */\nexport function classifyInterval(semitones: number, twoVoice = true): ConsonanceClass {\n const pc = simpleInterval(semitones);\n if (pc === 0 || pc === 7) {\n return ConsonanceClass.PerfectConsonance;\n }\n if (pc === 3 || pc === 4 || pc === 8 || pc === 9) {\n return ConsonanceClass.ImperfectConsonance;\n }\n if (pc === 5) {\n return twoVoice ? ConsonanceClass.Dissonance : ConsonanceClass.ImperfectConsonance;\n }\n return ConsonanceClass.Dissonance;\n}\n\n/**\n * Test whether an interval is a perfect consonance: the unison/octave or the\n * fifth.\n *\n * The perfect fourth is deliberately excluded. This is the predicate the\n * parallel-motion rules are written against, where the fourth behaves as a\n * dissonance between two voices; {@link classifyInterval} is the general\n * classifier, and it treats the fourth as context-dependent.\n *\n * @param semitones Interval size in semitones (may be negative or compound).\n * @returns True for the unison/octave (0) and the perfect fifth (7) mod 12.\n * @category Pitch & Intervals\n */\nexport function isPerfectInterval(semitones: number): boolean {\n const pc = simpleInterval(semitones);\n return pc === 0 || pc === 7;\n}\n\n/**\n * Test whether an interval is consonant.\n *\n * The consonant set is {0, 3, 4, 5, 7, 8, 9} mod 12. In two-voice counterpoint\n * the perfect fourth (5) is treated as a dissonance, so `twoVoice` set to true\n * excludes it.\n *\n * @param semitones Interval size in semitones (may be negative or compound).\n * @param twoVoice When true, the perfect fourth is treated as dissonant.\n * @returns True if the interval is consonant in the given context.\n * @category Pitch & Intervals\n */\nexport function isConsonantInterval(semitones: number, twoVoice = true): boolean {\n const pc = simpleInterval(semitones);\n if (pc === 5) {\n return !twoVoice;\n }\n return pc === 0 || pc === 3 || pc === 4 || pc === 7 || pc === 8 || pc === 9;\n}\n"],"mappings":"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","names":["ConsonanceClass"]}
|
|
@@ -6,13 +6,14 @@
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
var
|
|
9
|
+
var _chunkBSLTLGM3cjs = require('./chunk-BSLTLGM3.cjs');
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
|
|
16
|
+
var _chunkPI3UXRWKcjs = require('./chunk-PI3UXRWK.cjs');
|
|
16
17
|
|
|
17
18
|
// src/theory/safety/index.ts
|
|
18
19
|
var NoteSafety = /* @__PURE__ */ ((NoteSafety2) => {
|
|
@@ -38,6 +39,7 @@ var ReasonFlag = /* @__PURE__ */ ((ReasonFlag2) => {
|
|
|
38
39
|
ReasonFlag2[ReasonFlag2["VoiceCrossing"] = 8192] = "VoiceCrossing";
|
|
39
40
|
ReasonFlag2[ReasonFlag2["Suspension"] = 16384] = "Suspension";
|
|
40
41
|
ReasonFlag2[ReasonFlag2["NeedsResolution"] = 32768] = "NeedsResolution";
|
|
42
|
+
ReasonFlag2[ReasonFlag2["MelodicTritone"] = 65536] = "MelodicTritone";
|
|
41
43
|
return ReasonFlag2;
|
|
42
44
|
})(ReasonFlag || {});
|
|
43
45
|
var SUGGESTION_WINDOW = 12;
|
|
@@ -49,6 +51,7 @@ var REASON_SEVERITY = [
|
|
|
49
51
|
[16 /* NonScale */, 2 /* Dissonant */, 2 /* Dissonant */],
|
|
50
52
|
[32 /* OutOfRange */, 1 /* Warning */, 1 /* Warning */],
|
|
51
53
|
[64 /* Tritone */, 2 /* Dissonant */, 2 /* Dissonant */],
|
|
54
|
+
[65536 /* MelodicTritone */, 1 /* Warning */, 2 /* Dissonant */],
|
|
52
55
|
[128 /* LargeLeap */, 1 /* Warning */, 2 /* Dissonant */],
|
|
53
56
|
[1024 /* VerticalDissonance */, 2 /* Dissonant */, 2 /* Dissonant */],
|
|
54
57
|
[2048 /* ParallelPerfect */, 1 /* Warning */, 2 /* Dissonant */],
|
|
@@ -74,15 +77,18 @@ function avoidsMajorSeventh(chord) {
|
|
|
74
77
|
return chord.intervals.includes(4) && chord.intervals.includes(10);
|
|
75
78
|
}
|
|
76
79
|
function isStructuralChordDissonance(a, b, chord) {
|
|
77
|
-
const
|
|
78
|
-
|
|
80
|
+
const rawClass = _chunkPI3UXRWKcjs.pitchClassOf.call(void 0, Math.abs(a - b));
|
|
81
|
+
const aOffset = _chunkBSLTLGM3cjs.intervalAboveRoot.call(void 0, a, chord);
|
|
82
|
+
const bOffset = _chunkBSLTLGM3cjs.intervalAboveRoot.call(void 0, b, chord);
|
|
83
|
+
const rootSeventh = aOffset === 0 && (bOffset === 10 || bOffset === 11) || bOffset === 0 && (aOffset === 10 || aOffset === 11);
|
|
84
|
+
if (rawClass !== 6 && !rootSeventh) {
|
|
79
85
|
return false;
|
|
80
86
|
}
|
|
81
|
-
return
|
|
87
|
+
return _chunkBSLTLGM3cjs.isChordMember.call(void 0, a, chord) && _chunkBSLTLGM3cjs.isChordMember.call(void 0, b, chord);
|
|
82
88
|
}
|
|
83
89
|
function stepResolution(pitch, chord) {
|
|
84
90
|
for (let delta = 1; delta <= 2; delta += 1) {
|
|
85
|
-
if (
|
|
91
|
+
if (_chunkBSLTLGM3cjs.isChordMember.call(void 0, pitch - delta, chord)) {
|
|
86
92
|
return pitch - delta;
|
|
87
93
|
}
|
|
88
94
|
}
|
|
@@ -90,23 +96,23 @@ function stepResolution(pitch, chord) {
|
|
|
90
96
|
}
|
|
91
97
|
function evaluateSafety(q, opts = {}) {
|
|
92
98
|
assertSafetyContext(q);
|
|
93
|
-
|
|
99
|
+
_chunkPI3UXRWKcjs.assertFiniteNumber.call(void 0, q.candidatePitch, "candidatePitch");
|
|
94
100
|
return evaluateInternal(q, _nullishCoalesce(opts.suggestions, () => ( true)));
|
|
95
101
|
}
|
|
96
102
|
function assertSafetyContext(q) {
|
|
97
|
-
if (q.prevPitch !== void 0)
|
|
98
|
-
if (q.vocalLow !== void 0)
|
|
99
|
-
if (q.vocalHigh !== void 0)
|
|
103
|
+
if (q.prevPitch !== void 0) _chunkPI3UXRWKcjs.assertFiniteNumber.call(void 0, q.prevPitch, "prevPitch");
|
|
104
|
+
if (q.vocalLow !== void 0) _chunkPI3UXRWKcjs.assertFiniteNumber.call(void 0, q.vocalLow, "vocalLow");
|
|
105
|
+
if (q.vocalHigh !== void 0) _chunkPI3UXRWKcjs.assertFiniteNumber.call(void 0, q.vocalHigh, "vocalHigh");
|
|
100
106
|
if (q.vocalLow !== void 0 && q.vocalHigh !== void 0 && q.vocalLow > q.vocalHigh) {
|
|
101
|
-
throw new (0,
|
|
107
|
+
throw new (0, _chunkPI3UXRWKcjs.InvalidInputError)("vocalLow must not exceed vocalHigh");
|
|
102
108
|
}
|
|
103
|
-
|
|
109
|
+
_chunkPI3UXRWKcjs.assertGenerationBudget.call(void 0, q.otherVoices.length, "other voices");
|
|
104
110
|
for (let index = 0; index < q.otherVoices.length; index += 1) {
|
|
105
111
|
const voice = q.otherVoices[index];
|
|
106
112
|
if (!voice) continue;
|
|
107
|
-
|
|
113
|
+
_chunkPI3UXRWKcjs.assertFiniteNumber.call(void 0, voice.pitch, `otherVoices[${index}].pitch`);
|
|
108
114
|
if (voice.prevPitch !== void 0) {
|
|
109
|
-
|
|
115
|
+
_chunkPI3UXRWKcjs.assertFiniteNumber.call(void 0, voice.prevPitch, `otherVoices[${index}].prevPitch`);
|
|
110
116
|
}
|
|
111
117
|
}
|
|
112
118
|
}
|
|
@@ -126,12 +132,16 @@ function evaluateInternal(q, collectSuggestions) {
|
|
|
126
132
|
reasons |= 32 /* OutOfRange */;
|
|
127
133
|
raise(1 /* Warning */);
|
|
128
134
|
}
|
|
129
|
-
const inScale =
|
|
135
|
+
const inScale = _chunkBSLTLGM3cjs.isScaleTone.call(void 0, pitch, q.key);
|
|
130
136
|
const chord = q.chord;
|
|
131
|
-
if (
|
|
137
|
+
if (!inScale && (chord === null || !_chunkBSLTLGM3cjs.isChordMember.call(void 0, pitch, chord))) {
|
|
138
|
+
reasons |= 16 /* NonScale */;
|
|
139
|
+
raise(2 /* Dissonant */);
|
|
140
|
+
}
|
|
141
|
+
if (chord && _chunkBSLTLGM3cjs.isChordMember.call(void 0, pitch, chord)) {
|
|
132
142
|
reasons |= 1 /* ChordTone */;
|
|
133
143
|
} else if (chord) {
|
|
134
|
-
const ic =
|
|
144
|
+
const ic = _chunkBSLTLGM3cjs.intervalAboveRoot.call(void 0, pitch, chord);
|
|
135
145
|
const isTension = ic === 2 || ic === 5 || ic === 9;
|
|
136
146
|
const avoid = ic === 5 && avoidsFourth(chord) || ic === 11 && avoidsMajorSeventh(chord);
|
|
137
147
|
if (avoid) {
|
|
@@ -148,19 +158,13 @@ function evaluateInternal(q, collectSuggestions) {
|
|
|
148
158
|
} else if (inScale) {
|
|
149
159
|
reasons |= 8 /* ScaleTone */;
|
|
150
160
|
raise(1 /* Warning */);
|
|
151
|
-
} else {
|
|
152
|
-
reasons |= 16 /* NonScale */;
|
|
153
|
-
raise(2 /* Dissonant */);
|
|
154
161
|
}
|
|
155
162
|
} else if (inScale) {
|
|
156
163
|
reasons |= 8 /* ScaleTone */;
|
|
157
164
|
raise(1 /* Warning */);
|
|
158
|
-
} else {
|
|
159
|
-
reasons |= 16 /* NonScale */;
|
|
160
|
-
raise(2 /* Dissonant */);
|
|
161
165
|
}
|
|
162
|
-
if (chord && !
|
|
163
|
-
const ic =
|
|
166
|
+
if (chord && !_chunkBSLTLGM3cjs.isChordMember.call(void 0, pitch, chord)) {
|
|
167
|
+
const ic = _chunkBSLTLGM3cjs.intervalAboveRoot.call(void 0, pitch, chord);
|
|
164
168
|
const toneOffsets = chord.intervals.map((i) => (i % 12 + 12) % 12);
|
|
165
169
|
if (toneOffsets.some((t) => Math.abs(ic - t) === 6)) {
|
|
166
170
|
reasons |= 64 /* Tritone */;
|
|
@@ -169,10 +173,10 @@ function evaluateInternal(q, collectSuggestions) {
|
|
|
169
173
|
if (q.prevPitch !== void 0) {
|
|
170
174
|
const pc = Math.abs(pitch - q.prevPitch) % 12;
|
|
171
175
|
if (pc === 6) {
|
|
172
|
-
reasons |=
|
|
173
|
-
raise(2 /* Dissonant */);
|
|
176
|
+
reasons |= 65536 /* MelodicTritone */;
|
|
177
|
+
raise(q.profile === "strict" ? 2 /* Dissonant */ : 1 /* Warning */);
|
|
174
178
|
}
|
|
175
|
-
if (
|
|
179
|
+
if (_chunkBSLTLGM3cjs.isForbiddenMelodicLeap.call(void 0, q.prevPitch, pitch)) {
|
|
176
180
|
reasons |= 128 /* LargeLeap */;
|
|
177
181
|
raise(parallelSeverity);
|
|
178
182
|
}
|
|
@@ -187,7 +191,7 @@ function evaluateInternal(q, collectSuggestions) {
|
|
|
187
191
|
const twoVoice = q.otherVoices.length === 1;
|
|
188
192
|
const prev = q.prevPitch;
|
|
189
193
|
for (const ov of q.otherVoices) {
|
|
190
|
-
if (!
|
|
194
|
+
if (!_chunkBSLTLGM3cjs.createsVerticalDissonance.call(void 0, pitch, ov.pitch, twoVoice)) {
|
|
191
195
|
continue;
|
|
192
196
|
}
|
|
193
197
|
if (chord && isStructuralChordDissonance(pitch, ov.pitch, chord)) {
|
|
@@ -195,7 +199,7 @@ function evaluateInternal(q, collectSuggestions) {
|
|
|
195
199
|
}
|
|
196
200
|
reasons |= 1024 /* VerticalDissonance */;
|
|
197
201
|
raise(2 /* Dissonant */);
|
|
198
|
-
if (prev !== void 0 && pitch === prev && ov.prevPitch !== void 0 && !
|
|
202
|
+
if (prev !== void 0 && pitch === prev && ov.prevPitch !== void 0 && !_chunkBSLTLGM3cjs.createsVerticalDissonance.call(void 0, prev, ov.prevPitch, twoVoice)) {
|
|
199
203
|
reasons |= 16384 /* Suspension */;
|
|
200
204
|
}
|
|
201
205
|
}
|
|
@@ -206,11 +210,11 @@ function evaluateInternal(q, collectSuggestions) {
|
|
|
206
210
|
if (ov.prevPitch === void 0) {
|
|
207
211
|
continue;
|
|
208
212
|
}
|
|
209
|
-
if (
|
|
213
|
+
if (_chunkBSLTLGM3cjs.createsParallelPerfect.call(void 0, prev, pitch, ov.prevPitch, ov.pitch)) {
|
|
210
214
|
reasons |= 2048 /* ParallelPerfect */;
|
|
211
215
|
raise(parallelSeverity);
|
|
212
216
|
}
|
|
213
|
-
if (
|
|
217
|
+
if (_chunkBSLTLGM3cjs.createsHiddenParallelPerfect.call(void 0, prev, pitch, ov.prevPitch, ov.pitch)) {
|
|
214
218
|
reasons |= 4096 /* HiddenParallel */;
|
|
215
219
|
raise(parallelSeverity);
|
|
216
220
|
}
|
|
@@ -239,6 +243,9 @@ function findSafeNearby(q, candidate) {
|
|
|
239
243
|
const out = [];
|
|
240
244
|
for (let d = 1; d <= SUGGESTION_WINDOW; d += 1) {
|
|
241
245
|
for (const p of [candidate - d, candidate + d]) {
|
|
246
|
+
if (p < 0 || p > 127) {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
242
249
|
if (evaluateInternal({ ...q, candidatePitch: p }, false).safety === 0 /* Safe */) {
|
|
243
250
|
out.push(p);
|
|
244
251
|
if (out.length >= MAX_SUGGESTIONS) {
|
|
@@ -257,6 +264,7 @@ var REASON_TEXT = [
|
|
|
257
264
|
],
|
|
258
265
|
[1024 /* VerticalDissonance */, "Dissonant against a sounding voice on a strong beat"],
|
|
259
266
|
[64 /* Tritone */, "Tritone against the sounding harmony"],
|
|
267
|
+
[65536 /* MelodicTritone */, "Melodic tritone leap"],
|
|
260
268
|
[4 /* AvoidNote */, "Avoid note \u2014 clashes with a chord tone a semitone away"],
|
|
261
269
|
[16 /* NonScale */, "Outside the key"],
|
|
262
270
|
[2048 /* ParallelPerfect */, "Parallel perfect interval with another voice"],
|
|
@@ -266,8 +274,8 @@ var REASON_TEXT = [
|
|
|
266
274
|
[32768 /* NeedsResolution */, "Dissonance awaiting a stepwise resolution"],
|
|
267
275
|
[2 /* Tension */, "Chord tension"],
|
|
268
276
|
[8 /* ScaleTone */, "Scale tone, not in the chord"],
|
|
269
|
-
[256 /* MinorSecond */, "
|
|
270
|
-
[512 /* MajorSeventh */, "
|
|
277
|
+
[256 /* MinorSecond */, "Melodic semitone"],
|
|
278
|
+
[512 /* MajorSeventh */, "Melodic major seventh"],
|
|
271
279
|
[1 /* ChordTone */, "Chord tone"]
|
|
272
280
|
];
|
|
273
281
|
function severityOfReason(flag, profile) {
|
|
@@ -296,15 +304,17 @@ function enumerateSafePitches(q, pitchLow, pitchHigh) {
|
|
|
296
304
|
assertSafetyContext(q);
|
|
297
305
|
const chordTones = [];
|
|
298
306
|
const others = [];
|
|
299
|
-
|
|
300
|
-
|
|
307
|
+
_chunkPI3UXRWKcjs.assertInteger.call(void 0, pitchLow, "pitchLow");
|
|
308
|
+
_chunkPI3UXRWKcjs.assertInteger.call(void 0, pitchHigh, "pitchHigh");
|
|
301
309
|
if (pitchLow > pitchHigh) {
|
|
302
|
-
throw new (0,
|
|
310
|
+
throw new (0, _chunkPI3UXRWKcjs.InvalidInputError)(
|
|
303
311
|
`pitchLow must not exceed pitchHigh; received ${pitchLow} > ${pitchHigh}`
|
|
304
312
|
);
|
|
305
313
|
}
|
|
306
|
-
|
|
307
|
-
|
|
314
|
+
_chunkPI3UXRWKcjs.assertGenerationBudget.call(void 0, pitchHigh - pitchLow + 1, "safe pitch candidates");
|
|
315
|
+
const low = Math.max(pitchLow, _nullishCoalesce(q.vocalLow, () => ( pitchLow)));
|
|
316
|
+
const high = Math.min(pitchHigh, _nullishCoalesce(q.vocalHigh, () => ( pitchHigh)));
|
|
317
|
+
for (let pitch = high; pitch >= low; pitch -= 1) {
|
|
308
318
|
const result = evaluateInternal({ ...q, candidatePitch: pitch }, false);
|
|
309
319
|
if (result.safety === 2 /* Dissonant */) {
|
|
310
320
|
continue;
|
|
@@ -324,4 +334,4 @@ function enumerateSafePitches(q, pitchLow, pitchHigh) {
|
|
|
324
334
|
|
|
325
335
|
|
|
326
336
|
exports.NoteSafety = NoteSafety; exports.ReasonFlag = ReasonFlag; exports.evaluateSafety = evaluateSafety; exports.enumerateSafePitches = enumerateSafePitches;
|
|
327
|
-
//# sourceMappingURL=chunk-
|
|
337
|
+
//# sourceMappingURL=chunk-WFDXOLUB.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/libcantus/libcantus/dist/chunk-WFDXOLUB.cjs","../src/theory/safety/index.ts"],"names":["NoteSafety","ReasonFlag"],"mappings":"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","file":"/home/runner/work/libcantus/libcantus/dist/chunk-WFDXOLUB.cjs","sourcesContent":[null,"import { InvalidInputError } from '../../core/errors/index.js';\nimport { pitchClassOf as pitchClass } from '../../core/pitch/index.js';\nimport type { KeyScale } from '../../core/types.js';\nimport {\n assertFiniteNumber,\n assertGenerationBudget,\n assertInteger,\n} from '../../core/validation/index.js';\nimport type { Chord } from '../chord/index.js';\nimport { intervalAboveRoot, isChordMember } from '../chord/index.js';\nimport {\n createsHiddenParallelPerfect,\n createsParallelPerfect,\n createsVerticalDissonance,\n isForbiddenMelodicLeap,\n} from '../counterpoint/index.js';\nimport { isScaleTone } from '../scale/index.js';\n\n/**\n * Severity policy: `strict` counterpoint vs. lenient `pop` voice-leading.\n *\n * @category Arrangement & Analysis\n */\nexport type SafetyProfile = 'strict' | 'pop';\n\n/**\n * Overall placeability verdict for a candidate pitch.\n *\n * @category Arrangement & Analysis\n */\nexport enum NoteSafety {\n Safe = 0,\n Warning = 1,\n Dissonant = 2,\n}\n\n/**\n * Bit flags describing why a pitch received its verdict.\n *\n * @category Arrangement & Analysis\n */\nexport enum ReasonFlag {\n ChordTone = 1 << 0,\n Tension = 1 << 1,\n AvoidNote = 1 << 2,\n ScaleTone = 1 << 3,\n NonScale = 1 << 4,\n OutOfRange = 1 << 5,\n Tritone = 1 << 6,\n LargeLeap = 1 << 7,\n MinorSecond = 1 << 8,\n MajorSeventh = 1 << 9,\n VerticalDissonance = 1 << 10,\n ParallelPerfect = 1 << 11,\n HiddenParallel = 1 << 12,\n VoiceCrossing = 1 << 13,\n Suspension = 1 << 14,\n NeedsResolution = 1 << 15,\n MelodicTritone = 1 << 16,\n}\n\n/**\n * A neighbouring voice's current and previous pitch.\n *\n * @category Arrangement & Analysis\n */\nexport type VoiceSnapshot = {\n pitch: number;\n prevPitch?: number;\n};\n\n/**\n * Everything needed to evaluate one candidate pitch.\n *\n * @category Arrangement & Analysis\n */\nexport type SafetyQuery = {\n profile: SafetyProfile;\n candidatePitch: number;\n prevPitch?: number;\n chord: Chord | null;\n key: KeyScale;\n otherVoices: VoiceSnapshot[];\n strongBeat: boolean;\n vocalLow?: number;\n vocalHigh?: number;\n};\n\n/**\n * The verdict, reason flags, and optional resolution guidance.\n *\n * @category Arrangement & Analysis\n */\nexport type SafetyResult = {\n safety: NoteSafety;\n reasons: number;\n resolveTo?: number;\n suggestions?: number[];\n rationale?: string;\n};\n\n/** Semitone radius searched around a rejected candidate for safe alternatives. */\nconst SUGGESTION_WINDOW = 12;\n/** Maximum number of alternative pitches returned in `SafetyResult.suggestions`. */\nconst MAX_SUGGESTIONS = 3;\n\n/** Canonical minimum verdict contributed by each non-informational reason. */\nconst REASON_SEVERITY: ReadonlyArray<\n readonly [flag: ReasonFlag, pop: NoteSafety, strict: NoteSafety]\n> = [\n [ReasonFlag.Tension, NoteSafety.Warning, NoteSafety.Warning],\n [ReasonFlag.AvoidNote, NoteSafety.Warning, NoteSafety.Dissonant],\n [ReasonFlag.ScaleTone, NoteSafety.Warning, NoteSafety.Warning],\n [ReasonFlag.NonScale, NoteSafety.Dissonant, NoteSafety.Dissonant],\n [ReasonFlag.OutOfRange, NoteSafety.Warning, NoteSafety.Warning],\n [ReasonFlag.Tritone, NoteSafety.Dissonant, NoteSafety.Dissonant],\n [ReasonFlag.MelodicTritone, NoteSafety.Warning, NoteSafety.Dissonant],\n [ReasonFlag.LargeLeap, NoteSafety.Warning, NoteSafety.Dissonant],\n [ReasonFlag.VerticalDissonance, NoteSafety.Dissonant, NoteSafety.Dissonant],\n [ReasonFlag.ParallelPerfect, NoteSafety.Warning, NoteSafety.Dissonant],\n [ReasonFlag.HiddenParallel, NoteSafety.Warning, NoteSafety.Dissonant],\n [ReasonFlag.VoiceCrossing, NoteSafety.Warning, NoteSafety.Dissonant],\n [ReasonFlag.Suspension, NoteSafety.Dissonant, NoteSafety.Dissonant],\n [ReasonFlag.NeedsResolution, NoteSafety.Warning, NoteSafety.Dissonant],\n];\n\nfunction minimumSafetyForReasons(reasons: number, profile: SafetyProfile): NoteSafety {\n let minimum = NoteSafety.Safe;\n const severityIndex = profile === 'strict' ? 2 : 1;\n for (const entry of REASON_SEVERITY) {\n if (reasons & entry[0]) {\n minimum = Math.max(minimum, entry[severityIndex]) as NoteSafety;\n }\n }\n return minimum;\n}\n\n/** Whether the perfect fourth is an avoid note over this chord. */\nfunction avoidsFourth(chord: Chord): boolean {\n return chord.intervals.includes(4) && !chord.intervals.includes(5);\n}\n\n/** Whether the major seventh is an avoid note over this chord (dominant). */\nfunction avoidsMajorSeventh(chord: Chord): boolean {\n return chord.intervals.includes(4) && chord.intervals.includes(10);\n}\n\n/**\n * Whether a dissonance between two chord tones is one the chord is built from.\n *\n * A seventh chord states a seventh against its root and a dominant states the\n * tritone between its third and seventh; those intervals are the chord's\n * identity, not a clash to be prepared and resolved. A second between two chord\n * tones (a ninth voiced against the root, say) is a spacing problem rather than\n * a property of the harmony, so it stays reportable.\n */\nfunction isStructuralChordDissonance(a: number, b: number, chord: Chord): boolean {\n const rawClass = pitchClass(Math.abs(a - b));\n const aOffset = intervalAboveRoot(a, chord);\n const bOffset = intervalAboveRoot(b, chord);\n const rootSeventh =\n (aOffset === 0 && (bOffset === 10 || bOffset === 11)) ||\n (bOffset === 0 && (aOffset === 10 || aOffset === 11));\n if (rawClass !== 6 && !rootSeventh) {\n return false;\n }\n return isChordMember(a, chord) && isChordMember(b, chord);\n}\n\n/** Nearest chord tone at or below the candidate, within two semitones. */\nfunction stepResolution(pitch: number, chord: Chord): number | undefined {\n for (let delta = 1; delta <= 2; delta += 1) {\n if (isChordMember(pitch - delta, chord)) {\n return pitch - delta;\n }\n }\n return undefined;\n}\n\n/**\n * Evaluate the safety of placing a candidate pitch in a voice.\n *\n * Reasons are merged from harmonic context (chord tone / tension / avoid / scale)\n * and counterpoint (vertical dissonance, parallels, voice crossing). The `strict`\n * profile treats avoid notes and parallels as dissonant; `pop` treats them as\n * warnings. The final verdict is the worst severity among the reasons.\n *\n * @param q The candidate and its harmonic/voice-leading context.\n * @returns The verdict, reason bitmask, and optional resolution guidance.\n * @example\n * ```ts\n * import { evaluateSafety, makeChord, majorKey, NoteSafety } from '@libraz/libcantus';\n * const result = evaluateSafety({\n * profile: 'pop',\n * candidatePitch: 60, // C over a C major chord\n * chord: makeChord(0, 'maj'),\n * key: majorKey(0),\n * otherVoices: [],\n * strongBeat: true,\n * });\n * result.safety === NoteSafety.Safe; // true — C is a chord tone\n * ```\n * @category Arrangement & Analysis\n */\nexport function evaluateSafety(q: SafetyQuery, opts: EvaluateSafetyOptions = {}): SafetyResult {\n assertSafetyContext(q);\n assertFiniteNumber(q.candidatePitch, 'candidatePitch');\n return evaluateInternal(q, opts.suggestions ?? true);\n}\n\n/**\n * Options for {@link evaluateSafety}.\n *\n * @category Arrangement & Analysis\n */\nexport type EvaluateSafetyOptions = {\n /**\n * Whether to search for safe replacement pitches when the verdict is not\n * safe. Turning it off skips up to two dozen extra evaluations per rejected\n * pitch, which is worth doing when only the verdict is wanted.\n *\n * @defaultValue true\n */\n suggestions?: boolean;\n};\n\n/** Validate all numeric context fields once at a public safety entry point. */\nfunction assertSafetyContext(q: Omit<SafetyQuery, 'candidatePitch'>): void {\n if (q.prevPitch !== undefined) assertFiniteNumber(q.prevPitch, 'prevPitch');\n if (q.vocalLow !== undefined) assertFiniteNumber(q.vocalLow, 'vocalLow');\n if (q.vocalHigh !== undefined) assertFiniteNumber(q.vocalHigh, 'vocalHigh');\n if (q.vocalLow !== undefined && q.vocalHigh !== undefined && q.vocalLow > q.vocalHigh) {\n throw new InvalidInputError('vocalLow must not exceed vocalHigh');\n }\n assertGenerationBudget(q.otherVoices.length, 'other voices');\n for (let index = 0; index < q.otherVoices.length; index += 1) {\n const voice = q.otherVoices[index];\n if (!voice) continue;\n assertFiniteNumber(voice.pitch, `otherVoices[${index}].pitch`);\n if (voice.prevPitch !== undefined) {\n assertFiniteNumber(voice.prevPitch, `otherVoices[${index}].prevPitch`);\n }\n }\n}\n\n/**\n * Core safety evaluation.\n *\n * @param q The candidate and its harmonic/voice-leading context.\n * @param collectSuggestions Whether to search for safe alternatives on a\n * non-Safe verdict. Set false during that search itself to bound recursion.\n * @returns The verdict, reason bitmask, and optional guidance.\n */\nfunction evaluateInternal(q: SafetyQuery, collectSuggestions: boolean): SafetyResult {\n let reasons = 0;\n let level = NoteSafety.Safe;\n let resolveTo: number | undefined;\n const raise = (l: NoteSafety) => {\n if (l > level) {\n level = l;\n }\n };\n const avoidSeverity = q.profile === 'strict' ? NoteSafety.Dissonant : NoteSafety.Warning;\n const parallelSeverity = avoidSeverity;\n const pitch = q.candidatePitch;\n\n if (\n (q.vocalLow !== undefined && pitch < q.vocalLow) ||\n (q.vocalHigh !== undefined && pitch > q.vocalHigh)\n ) {\n reasons |= ReasonFlag.OutOfRange;\n raise(NoteSafety.Warning);\n }\n\n const inScale = isScaleTone(pitch, q.key);\n const chord = q.chord;\n\n // A chromatic avoid note remains chromatic. The avoid-note branch below\n // refines the resolution guidance, but must not erase the key relationship\n // or downgrade a non-scale pitch to a merely stylistic warning.\n if (!inScale && (chord === null || !isChordMember(pitch, chord))) {\n reasons |= ReasonFlag.NonScale;\n raise(NoteSafety.Dissonant);\n }\n\n if (chord && isChordMember(pitch, chord)) {\n reasons |= ReasonFlag.ChordTone;\n } else if (chord) {\n const ic = intervalAboveRoot(pitch, chord);\n const isTension = ic === 2 || ic === 5 || ic === 9;\n const avoid = (ic === 5 && avoidsFourth(chord)) || (ic === 11 && avoidsMajorSeventh(chord));\n if (avoid) {\n reasons |= ReasonFlag.AvoidNote;\n if (isTension) {\n reasons |= ReasonFlag.Tension;\n }\n reasons |= ReasonFlag.NeedsResolution;\n resolveTo = stepResolution(pitch, chord);\n raise(avoidSeverity);\n } else if (isTension && inScale) {\n reasons |= ReasonFlag.Tension;\n raise(NoteSafety.Warning);\n } else if (inScale) {\n reasons |= ReasonFlag.ScaleTone;\n raise(NoteSafety.Warning);\n }\n } else if (inScale) {\n reasons |= ReasonFlag.ScaleTone;\n raise(NoteSafety.Warning);\n }\n\n // Harmonic tritone against a chord tone. A chord tone is never flagged for a\n // tritone its own chord spells: the interval is the chord's identity, not a\n // clash with it. This covers dominant sevenths (third against seventh) and\n // equally the diminished, half-diminished, diminished-seventh and minor-sixth\n // families, where the tritone falls between two chord tones as well.\n if (chord && !isChordMember(pitch, chord)) {\n const ic = intervalAboveRoot(pitch, chord);\n const toneOffsets = chord.intervals.map((i) => ((i % 12) + 12) % 12);\n if (toneOffsets.some((t) => Math.abs(ic - t) === 6)) {\n reasons |= ReasonFlag.Tritone;\n }\n }\n\n if (q.prevPitch !== undefined) {\n // Quality flags reduce the melodic interval to its pitch-class interval so\n // compound intervals are treated uniformly with their simple form. The\n // forbidden-leap flag defers to the counterpoint rule, which preserves the\n // octave boundary (an octave leap is allowed, wider leaps are not).\n const pc = Math.abs(pitch - q.prevPitch) % 12;\n if (pc === 6) {\n reasons |= ReasonFlag.MelodicTritone;\n raise(q.profile === 'strict' ? NoteSafety.Dissonant : NoteSafety.Warning);\n }\n if (isForbiddenMelodicLeap(q.prevPitch, pitch)) {\n reasons |= ReasonFlag.LargeLeap;\n raise(parallelSeverity);\n }\n if (pc === 1) {\n reasons |= ReasonFlag.MinorSecond;\n }\n if (pc === 11) {\n reasons |= ReasonFlag.MajorSeventh;\n }\n }\n\n if (q.strongBeat) {\n const twoVoice = q.otherVoices.length === 1;\n const prev = q.prevPitch;\n for (const ov of q.otherVoices) {\n if (!createsVerticalDissonance(pitch, ov.pitch, twoVoice)) {\n continue;\n }\n if (chord && isStructuralChordDissonance(pitch, ov.pitch, chord)) {\n continue;\n }\n reasons |= ReasonFlag.VerticalDissonance;\n raise(NoteSafety.Dissonant);\n // A held pitch that was consonant on the previous step and is now\n // dissonant against the same voice is a prepared suspension.\n if (\n prev !== undefined &&\n pitch === prev &&\n ov.prevPitch !== undefined &&\n !createsVerticalDissonance(prev, ov.prevPitch, twoVoice)\n ) {\n reasons |= ReasonFlag.Suspension;\n }\n }\n }\n\n if (q.prevPitch !== undefined) {\n const prev = q.prevPitch;\n for (const ov of q.otherVoices) {\n if (ov.prevPitch === undefined) {\n continue;\n }\n // createsParallelPerfect covers every perfect class (unison/octave and\n // fifth), so no separate octave check is needed.\n if (createsParallelPerfect(prev, pitch, ov.prevPitch, ov.pitch)) {\n reasons |= ReasonFlag.ParallelPerfect;\n raise(parallelSeverity);\n }\n if (createsHiddenParallelPerfect(prev, pitch, ov.prevPitch, ov.pitch)) {\n reasons |= ReasonFlag.HiddenParallel;\n raise(parallelSeverity);\n }\n const crossedNow = pitch - ov.pitch;\n const crossedPrev = prev - ov.prevPitch;\n if (crossedNow !== 0 && crossedPrev !== 0 && crossedNow > 0 !== crossedPrev > 0) {\n reasons |= ReasonFlag.VoiceCrossing;\n raise(parallelSeverity);\n }\n }\n }\n\n // Keep reason production and verdict policy as one invariant even when a new\n // call path skips an earlier local `raise`: informational flags (ChordTone,\n // MinorSecond, MajorSeventh) may coexist with Safe; every policy flag above\n // contributes at least its table severity.\n raise(minimumSafetyForReasons(reasons, q.profile));\n\n const result: SafetyResult = { safety: level, reasons, rationale: describe(reasons, q) };\n if (resolveTo !== undefined) {\n result.resolveTo = resolveTo;\n }\n if (collectSuggestions && level !== NoteSafety.Safe) {\n const suggestions = findSafeNearby(q, pitch);\n if (suggestions.length > 0) {\n result.suggestions = suggestions;\n }\n }\n return result;\n}\n\n/**\n * Find safe alternative pitches near a rejected candidate.\n *\n * Searches outward from the candidate within `SUGGESTION_WINDOW` semitones,\n * probing the pitch below before the one above at each distance so the result\n * is deterministic and ordered by nearness.\n *\n * @param q The safety context of the rejected candidate.\n * @param candidate The candidate pitch that was not Safe.\n * @returns Up to `MAX_SUGGESTIONS` safe pitches, nearest first.\n */\nfunction findSafeNearby(q: SafetyQuery, candidate: number): number[] {\n const out: number[] = [];\n for (let d = 1; d <= SUGGESTION_WINDOW; d += 1) {\n for (const p of [candidate - d, candidate + d]) {\n if (p < 0 || p > 127) {\n continue;\n }\n if (evaluateInternal({ ...q, candidatePitch: p }, false).safety === NoteSafety.Safe) {\n out.push(p);\n if (out.length >= MAX_SUGGESTIONS) {\n return out;\n }\n }\n }\n }\n return out;\n}\n\n/**\n * Wording for every reason flag, most to least specific.\n *\n * The list is exhaustive by construction: {@link describe} picks from it by\n * severity, so a flag with no wording here would let a rejected pitch be\n * explained by a milder reason — the seventh of a dominant reading as a plain\n * \"Chord tone\" while its verdict says dissonant.\n */\nconst REASON_TEXT: ReadonlyArray<readonly [ReasonFlag, string]> = [\n [ReasonFlag.OutOfRange, 'Outside the target vocal range'],\n [\n ReasonFlag.Suspension,\n 'Prepared suspension — a held consonance now dissonant, awaiting resolution',\n ],\n [ReasonFlag.VerticalDissonance, 'Dissonant against a sounding voice on a strong beat'],\n [ReasonFlag.Tritone, 'Tritone against the sounding harmony'],\n [ReasonFlag.MelodicTritone, 'Melodic tritone leap'],\n [ReasonFlag.AvoidNote, 'Avoid note — clashes with a chord tone a semitone away'],\n [ReasonFlag.NonScale, 'Outside the key'],\n [ReasonFlag.ParallelPerfect, 'Parallel perfect interval with another voice'],\n [ReasonFlag.HiddenParallel, 'Hidden perfect interval by similar motion'],\n [ReasonFlag.VoiceCrossing, 'Crosses another voice'],\n [ReasonFlag.LargeLeap, 'Leap wider than the profile allows'],\n [ReasonFlag.NeedsResolution, 'Dissonance awaiting a stepwise resolution'],\n [ReasonFlag.Tension, 'Chord tension'],\n [ReasonFlag.ScaleTone, 'Scale tone, not in the chord'],\n [ReasonFlag.MinorSecond, 'Melodic semitone'],\n [ReasonFlag.MajorSeventh, 'Melodic major seventh'],\n [ReasonFlag.ChordTone, 'Chord tone'],\n];\n\n/** The severity a single flag carries under a profile; Safe when purely informational. */\nfunction severityOfReason(flag: ReasonFlag, profile: SafetyProfile): NoteSafety {\n const entry = REASON_SEVERITY.find(([candidate]) => candidate === flag);\n return entry === undefined ? NoteSafety.Safe : entry[profile === 'strict' ? 2 : 1];\n}\n\n/**\n * Build a short human explanation from the reason flags.\n *\n * The flag driving the verdict wins: the one whose severity under this profile\n * is highest, ties going to the more specific wording. Explaining a rejected\n * pitch by an informational flag it also carries would tell the caller nothing\n * about what to fix.\n */\nfunction describe(reasons: number, q: SafetyQuery): string {\n let best: string | undefined;\n let bestSeverity = -1;\n for (const [flag, text] of REASON_TEXT) {\n if (!(reasons & flag)) {\n continue;\n }\n const severity = severityOfReason(flag, q.profile);\n if (severity > bestSeverity) {\n bestSeverity = severity;\n best = text;\n }\n }\n if (best !== undefined) {\n return best;\n }\n return q.chord ? 'Placeable' : 'No chord context';\n}\n\n/**\n * Enumerate placeable pitches in a range, chord tones first, descending.\n *\n * @param q The safety context minus the candidate pitch.\n * @param pitchLow Lowest MIDI pitch to consider (inclusive).\n * @param pitchHigh Highest MIDI pitch to consider (inclusive).\n * @returns Placeable pitches (non-dissonant), chord tones before others, each group descending.\n * @throws If either bound is non-finite/non-integral, reversed, or exceeds the generation budget.\n * @example\n * ```ts\n * import { enumerateSafePitches, makeChord, majorKey } from '@libraz/libcantus';\n * const pitches = enumerateSafePitches(\n * { profile: 'pop', chord: makeChord(0, 'maj'), key: majorKey(0), otherVoices: [], strongBeat: true },\n * 60,\n * 72,\n * );\n * pitches; // placeable pitches in [60, 72], chord tones first, each group descending\n * ```\n * @category Arrangement & Analysis\n */\nexport function enumerateSafePitches(\n q: Omit<SafetyQuery, 'candidatePitch'>,\n pitchLow: number,\n pitchHigh: number,\n): number[] {\n assertSafetyContext(q);\n const chordTones: number[] = [];\n const others: number[] = [];\n assertInteger(pitchLow, 'pitchLow');\n assertInteger(pitchHigh, 'pitchHigh');\n if (pitchLow > pitchHigh) {\n throw new InvalidInputError(\n `pitchLow must not exceed pitchHigh; received ${pitchLow} > ${pitchHigh}`,\n );\n }\n assertGenerationBudget(pitchHigh - pitchLow + 1, 'safe pitch candidates');\n const low = Math.max(pitchLow, q.vocalLow ?? pitchLow);\n const high = Math.min(pitchHigh, q.vocalHigh ?? pitchHigh);\n for (let pitch = high; pitch >= low; pitch -= 1) {\n const result = evaluateInternal({ ...q, candidatePitch: pitch }, false);\n if (result.safety === NoteSafety.Dissonant) {\n continue;\n }\n if (result.reasons & ReasonFlag.ChordTone) {\n chordTones.push(pitch);\n } else {\n others.push(pitch);\n }\n }\n return [...chordTones, ...others];\n}\n"]}
|