@libraz/libcantus 0.9.3 → 0.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -20
- package/README_ja.md +268 -0
- package/dist/analyze/index.cjs +39 -1947
- package/dist/analyze/index.cjs.map +1 -1
- package/dist/analyze/index.d.cts +161 -91
- package/dist/analyze/index.d.ts +161 -91
- package/dist/analyze/index.js +19 -8
- package/dist/chunk-2A2QMA74.cjs +261 -0
- package/dist/chunk-2A2QMA74.cjs.map +1 -0
- package/dist/chunk-2PTFGVD7.js +261 -0
- package/dist/chunk-2PTFGVD7.js.map +1 -0
- package/dist/chunk-357ZDPVU.cjs +162 -0
- package/dist/chunk-357ZDPVU.cjs.map +1 -0
- package/dist/chunk-3KX76RLG.js +1548 -0
- package/dist/chunk-3KX76RLG.js.map +1 -0
- package/dist/chunk-4VZ5MZAM.cjs +889 -0
- package/dist/chunk-4VZ5MZAM.cjs.map +1 -0
- package/dist/{chunk-C5XYKUVG.js → chunk-7EONYVI5.js} +471 -171
- package/dist/chunk-7EONYVI5.js.map +1 -0
- package/dist/{chunk-IT2O4XFE.js → chunk-7JEP3MFY.js} +42 -34
- package/dist/chunk-7JEP3MFY.js.map +1 -0
- package/dist/{chunk-4MHA432G.js → chunk-AID75RN3.js} +572 -250
- package/dist/chunk-AID75RN3.js.map +1 -0
- package/dist/chunk-BSLTLGM3.cjs +497 -0
- package/dist/chunk-BSLTLGM3.cjs.map +1 -0
- package/dist/chunk-CU6VG2YM.cjs +109 -0
- package/dist/chunk-CU6VG2YM.cjs.map +1 -0
- package/dist/{chunk-EH6EM37U.js → chunk-DZMHIGI4.js} +81 -59
- package/dist/chunk-DZMHIGI4.js.map +1 -0
- package/dist/{chunk-AKLYPWIN.js → chunk-EDOTEABK.js} +45 -19
- package/dist/chunk-EDOTEABK.js.map +1 -0
- package/dist/{chunk-2ZWMSJH6.js → chunk-HN2OEUN2.js} +7 -4
- package/dist/chunk-HN2OEUN2.js.map +1 -0
- package/dist/{chunk-MIWO425M.js → chunk-KBTTEX7Q.js} +128 -43
- package/dist/chunk-KBTTEX7Q.js.map +1 -0
- package/dist/chunk-KQN2ZYND.cjs +724 -0
- package/dist/chunk-KQN2ZYND.cjs.map +1 -0
- package/dist/chunk-MPTVJIVG.cjs +3176 -0
- package/dist/chunk-MPTVJIVG.cjs.map +1 -0
- package/dist/chunk-MTMZTMWM.cjs +53 -0
- package/dist/chunk-MTMZTMWM.cjs.map +1 -0
- package/dist/{chunk-CHMXELDS.js → chunk-NNB67XA2.js} +372 -197
- package/dist/chunk-NNB67XA2.js.map +1 -0
- package/dist/chunk-OL3IHHSJ.cjs +1204 -0
- package/dist/chunk-OL3IHHSJ.cjs.map +1 -0
- package/dist/chunk-PI3UXRWK.cjs +528 -0
- package/dist/chunk-PI3UXRWK.cjs.map +1 -0
- package/dist/chunk-R2VVFAIN.js +528 -0
- package/dist/chunk-R2VVFAIN.js.map +1 -0
- package/dist/chunk-T64C7ZD7.cjs +1 -0
- package/dist/chunk-T64C7ZD7.cjs.map +1 -0
- package/dist/chunk-WFDXOLUB.cjs +337 -0
- package/dist/chunk-WFDXOLUB.cjs.map +1 -0
- package/dist/chunk-WTAN4ACG.cjs +1548 -0
- package/dist/chunk-WTAN4ACG.cjs.map +1 -0
- package/dist/chunk-YFJXS3AU.cjs +33 -0
- package/dist/chunk-YFJXS3AU.cjs.map +1 -0
- package/dist/{chunk-GOAW4VYL.js → chunk-YUE6TJ4M.js} +315 -145
- package/dist/chunk-YUE6TJ4M.js.map +1 -0
- package/dist/{chunk-62QZKCJQ.js → chunk-ZGLJM4A4.js} +10 -6
- package/dist/chunk-ZGLJM4A4.js.map +1 -0
- package/dist/core/index.cjs +140 -647
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +313 -41
- package/dist/core/index.d.ts +313 -41
- package/dist/core/index.js +64 -16
- package/dist/{function-DVH4Eusr.d.ts → function-CxixTB02.d.cts} +122 -10
- package/dist/{function-DYt4iKsn.d.cts → function-DTCt675z.d.ts} +122 -10
- package/dist/generate/index.cjs +28 -4085
- package/dist/generate/index.cjs.map +1 -1
- package/dist/generate/index.d.cts +374 -66
- package/dist/generate/index.d.ts +374 -66
- package/dist/generate/index.js +23 -9
- package/dist/{index-Jc0Ryi79.d.ts → index-B2Ulxxpo.d.cts} +21 -5
- package/dist/{index-CpOsP1y9.d.cts → index-BkYJ0xMq.d.cts} +18 -17
- package/dist/{index-CxqBQtH1.d.cts → index-BuaB-ypt.d.ts} +21 -5
- package/dist/index-CAN7v-kw.d.ts +199 -0
- package/dist/{index-Bt4tOUD9.d.ts → index-COw1Ky9g.d.ts} +93 -8
- package/dist/{index-Dikst44v.d.cts → index-Cfx8xgH0.d.ts} +14 -11
- package/dist/{index-DxvARIP2.d.ts → index-DRtIhlJj.d.ts} +18 -17
- package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
- package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
- package/dist/index-alrRninU.d.cts +199 -0
- package/dist/{index-BNlGP86A.d.cts → index-oTQbYYIs.d.cts} +93 -8
- package/dist/index-tR9jTn26.d.cts +275 -0
- package/dist/index-tR9jTn26.d.ts +275 -0
- package/dist/{index-Bg5dYMmZ.d.ts → index-x2vcsduE.d.cts} +14 -11
- package/dist/index.cjs +429 -7383
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -15
- package/dist/index.d.ts +15 -15
- package/dist/index.js +121 -27
- package/dist/model/index.cjs +8 -2562
- package/dist/model/index.cjs.map +1 -1
- package/dist/model/index.d.cts +471 -81
- package/dist/model/index.d.ts +471 -81
- package/dist/model/index.js +6 -7
- package/dist/styled-Bl0ZK28h.d.ts +543 -0
- package/dist/styled-Crv6SA4S.d.cts +543 -0
- package/dist/theory/index.cjs +172 -1822
- package/dist/theory/index.cjs.map +1 -1
- package/dist/theory/index.d.cts +151 -233
- package/dist/theory/index.d.ts +151 -233
- package/dist/theory/index.js +30 -7
- package/dist/{types-MGP29r3t.d.cts → types-BN6_Q9Kl.d.cts} +11 -2
- package/dist/{types-MGP29r3t.d.ts → types-BN6_Q9Kl.d.ts} +11 -2
- package/package.json +34 -13
- package/dist/chunk-2ZWMSJH6.js.map +0 -1
- package/dist/chunk-4MHA432G.js.map +0 -1
- package/dist/chunk-62QZKCJQ.js.map +0 -1
- package/dist/chunk-AKLYPWIN.js.map +0 -1
- package/dist/chunk-C5XYKUVG.js.map +0 -1
- package/dist/chunk-CHMXELDS.js.map +0 -1
- package/dist/chunk-DYWFNC4A.js +0 -152
- package/dist/chunk-DYWFNC4A.js.map +0 -1
- package/dist/chunk-EH6EM37U.js.map +0 -1
- package/dist/chunk-FBQK3ZQN.js +0 -131
- package/dist/chunk-FBQK3ZQN.js.map +0 -1
- package/dist/chunk-GOAW4VYL.js.map +0 -1
- package/dist/chunk-I5R7ZBJS.js +0 -202
- package/dist/chunk-I5R7ZBJS.js.map +0 -1
- package/dist/chunk-IT2O4XFE.js.map +0 -1
- package/dist/chunk-MIWO425M.js.map +0 -1
- package/dist/chunk-SGU7SPLK.js +0 -924
- package/dist/chunk-SGU7SPLK.js.map +0 -1
- package/dist/index-CGMZNCx6.d.cts +0 -140
- package/dist/index-CGMZNCx6.d.ts +0 -140
- package/dist/index-CnZCo29u.d.cts +0 -108
- package/dist/index-JnF4v9bx.d.ts +0 -108
- package/dist/styled-DZ7GjShb.d.ts +0 -282
- package/dist/styled-ZTbdaVFM.d.cts +0 -282
- package/src/analyze/arrange/index.ts +0 -21
- package/src/analyze/arrange/internal.ts +0 -127
- package/src/analyze/arrange/tension.ts +0 -199
- package/src/analyze/arrange/tracks.ts +0 -312
- package/src/analyze/detect/index.ts +0 -272
- package/src/analyze/functional/borrowed.ts +0 -84
- package/src/analyze/functional/cadence.ts +0 -66
- package/src/analyze/functional/function.ts +0 -184
- package/src/analyze/functional/index.ts +0 -23
- package/src/analyze/functional/internal.ts +0 -29
- package/src/analyze/functional/roman.ts +0 -402
- package/src/analyze/index.ts +0 -49
- package/src/analyze/timeline/index.ts +0 -451
- package/src/analyze/voice/index.ts +0 -339
- package/src/core/event-index/index.ts +0 -104
- package/src/core/index.ts +0 -61
- package/src/core/interval/index.ts +0 -81
- package/src/core/meter/index.ts +0 -302
- package/src/core/pitch/index.ts +0 -311
- package/src/core/random/index.ts +0 -76
- package/src/core/tuning/index.ts +0 -186
- package/src/core/types.ts +0 -29
- package/src/core/validation/index.ts +0 -119
- package/src/generate/bass/index.ts +0 -373
- package/src/generate/countermelody/index.ts +0 -470
- package/src/generate/drums/beat.ts +0 -266
- package/src/generate/drums/euclid.ts +0 -65
- package/src/generate/drums/fills.ts +0 -356
- package/src/generate/drums/ghost.ts +0 -119
- package/src/generate/drums/hihat.ts +0 -285
- package/src/generate/drums/hit.ts +0 -31
- package/src/generate/drums/index.ts +0 -361
- package/src/generate/drums/internal.ts +0 -257
- package/src/generate/drums/kick.ts +0 -160
- package/src/generate/drums/percussion.ts +0 -121
- package/src/generate/drums/rng.ts +0 -7
- package/src/generate/drums/swing.ts +0 -71
- package/src/generate/groove/index.ts +0 -351
- package/src/generate/harmonize/index.ts +0 -451
- package/src/generate/index.ts +0 -58
- package/src/generate/motif/index.ts +0 -422
- package/src/generate/progression/index.ts +0 -362
- package/src/generate/reharmony/index.ts +0 -323
- package/src/generate/rhythm/index.ts +0 -175
- package/src/index.ts +0 -13
- package/src/model/chord.ts +0 -442
- package/src/model/index.ts +0 -15
- package/src/model/interval.ts +0 -82
- package/src/model/key.ts +0 -266
- package/src/model/note.ts +0 -176
- package/src/model/progression.ts +0 -161
- package/src/model/shared.ts +0 -12
- package/src/theory/chord/index.ts +0 -375
- package/src/theory/chordscale/index.ts +0 -374
- package/src/theory/counterpoint/index.ts +0 -230
- package/src/theory/harmony/index.ts +0 -77
- package/src/theory/index.ts +0 -112
- package/src/theory/safety/index.ts +0 -488
- package/src/theory/scale/degrees.ts +0 -123
- package/src/theory/scale/index.ts +0 -28
- package/src/theory/scale/key.ts +0 -56
- package/src/theory/scale/masks.ts +0 -153
- package/src/theory/spelling/index.ts +0 -249
- package/src/theory/symbol/index.ts +0 -268
- package/src/theory/voicing/index.ts +0 -5
- package/src/theory/voicing/internal.ts +0 -168
- package/src/theory/voicing/leading.ts +0 -112
- package/src/theory/voicing/satb.ts +0 -216
- package/src/theory/voicing/styled.ts +0 -199
- package/tsconfig.json +0 -23
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true});
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
var _chunkPI3UXRWKcjs = require('./chunk-PI3UXRWK.cjs');
|
|
8
|
+
|
|
9
|
+
// src/core/tuning/index.ts
|
|
10
|
+
var TWELVE_TET = Object.freeze({ refStep: 69, refFreq: 440, divisions: 12 });
|
|
11
|
+
function assertTuning(tuning) {
|
|
12
|
+
_chunkPI3UXRWKcjs.assertFiniteNumber.call(void 0, tuning.refStep, "tuning.refStep");
|
|
13
|
+
_chunkPI3UXRWKcjs.assertRange.call(void 0, tuning.refFreq, Number.MIN_VALUE, Number.MAX_VALUE, "tuning.refFreq");
|
|
14
|
+
_chunkPI3UXRWKcjs.assertPositiveInt.call(void 0, tuning.divisions, "tuning.divisions");
|
|
15
|
+
return tuning;
|
|
16
|
+
}
|
|
17
|
+
function edo(n, refFreq = 440, refStep = 69) {
|
|
18
|
+
_chunkPI3UXRWKcjs.assertPositiveInt.call(void 0, n, "EDO divisions");
|
|
19
|
+
_chunkPI3UXRWKcjs.assertRange.call(void 0, refFreq, Number.MIN_VALUE, Number.MAX_VALUE, "reference frequency");
|
|
20
|
+
_chunkPI3UXRWKcjs.assertFiniteNumber.call(void 0, refStep, "reference step");
|
|
21
|
+
return { refStep, refFreq, divisions: n };
|
|
22
|
+
}
|
|
23
|
+
function frequencyOf(step, tuning = TWELVE_TET) {
|
|
24
|
+
_chunkPI3UXRWKcjs.assertFiniteNumber.call(void 0, step, "step");
|
|
25
|
+
assertTuning(tuning);
|
|
26
|
+
const result = tuning.refFreq * 2 ** ((step - tuning.refStep) / tuning.divisions);
|
|
27
|
+
return _chunkPI3UXRWKcjs.assertRange.call(void 0, result, Number.MIN_VALUE, Number.MAX_VALUE, "frequency result");
|
|
28
|
+
}
|
|
29
|
+
function nearestStep(freq, tuning = TWELVE_TET) {
|
|
30
|
+
_chunkPI3UXRWKcjs.assertRange.call(void 0, freq, Number.MIN_VALUE, Number.MAX_VALUE, "frequency");
|
|
31
|
+
assertTuning(tuning);
|
|
32
|
+
return Math.round(tuning.refStep + tuning.divisions * Math.log2(freq / tuning.refFreq));
|
|
33
|
+
}
|
|
34
|
+
function stepOf(freq, tuning = TWELVE_TET) {
|
|
35
|
+
_chunkPI3UXRWKcjs.assertRange.call(void 0, freq, Number.MIN_VALUE, Number.MAX_VALUE, "frequency");
|
|
36
|
+
assertTuning(tuning);
|
|
37
|
+
return tuning.refStep + tuning.divisions * Math.log2(freq / tuning.refFreq);
|
|
38
|
+
}
|
|
39
|
+
function centsFromNearestStep(freq, tuning = TWELVE_TET) {
|
|
40
|
+
const exact = stepOf(freq, tuning);
|
|
41
|
+
return centsOfSteps(exact - Math.round(exact), tuning);
|
|
42
|
+
}
|
|
43
|
+
function stepsOfCents(cents, tuning = TWELVE_TET) {
|
|
44
|
+
_chunkPI3UXRWKcjs.assertFiniteNumber.call(void 0, cents, "cents");
|
|
45
|
+
assertTuning(tuning);
|
|
46
|
+
return _chunkPI3UXRWKcjs.assertFiniteNumber.call(void 0, cents * tuning.divisions / 1200, "steps result");
|
|
47
|
+
}
|
|
48
|
+
function centsToRatio(cents) {
|
|
49
|
+
_chunkPI3UXRWKcjs.assertFiniteNumber.call(void 0, cents, "cents");
|
|
50
|
+
return 2 ** (cents / 1200);
|
|
51
|
+
}
|
|
52
|
+
function centsBetweenFreq(a, b) {
|
|
53
|
+
_chunkPI3UXRWKcjs.assertRange.call(void 0, a, Number.MIN_VALUE, Number.MAX_VALUE, "first frequency");
|
|
54
|
+
_chunkPI3UXRWKcjs.assertRange.call(void 0, b, Number.MIN_VALUE, Number.MAX_VALUE, "second frequency");
|
|
55
|
+
return 1200 * (Math.log2(b) - Math.log2(a));
|
|
56
|
+
}
|
|
57
|
+
function centsOfSteps(steps, tuning = TWELVE_TET) {
|
|
58
|
+
_chunkPI3UXRWKcjs.assertFiniteNumber.call(void 0, steps, "steps");
|
|
59
|
+
assertTuning(tuning);
|
|
60
|
+
return _chunkPI3UXRWKcjs.assertFiniteNumber.call(void 0, steps * 1200 / tuning.divisions, "cents result");
|
|
61
|
+
}
|
|
62
|
+
function ratioToCents(numerator, denominator) {
|
|
63
|
+
_chunkPI3UXRWKcjs.assertRange.call(void 0, numerator, Number.MIN_VALUE, Number.MAX_VALUE, "ratio numerator");
|
|
64
|
+
_chunkPI3UXRWKcjs.assertRange.call(void 0, denominator, Number.MIN_VALUE, Number.MAX_VALUE, "ratio denominator");
|
|
65
|
+
return 1200 * (Math.log2(numerator) - Math.log2(denominator));
|
|
66
|
+
}
|
|
67
|
+
function justRatio(numerator, denominator) {
|
|
68
|
+
return Object.freeze([numerator, denominator]);
|
|
69
|
+
}
|
|
70
|
+
var JUST_RATIOS = Object.freeze({
|
|
71
|
+
0: justRatio(1, 1),
|
|
72
|
+
1: justRatio(16, 15),
|
|
73
|
+
2: justRatio(9, 8),
|
|
74
|
+
3: justRatio(6, 5),
|
|
75
|
+
4: justRatio(5, 4),
|
|
76
|
+
5: justRatio(4, 3),
|
|
77
|
+
6: justRatio(45, 32),
|
|
78
|
+
7: justRatio(3, 2),
|
|
79
|
+
8: justRatio(8, 5),
|
|
80
|
+
9: justRatio(5, 3),
|
|
81
|
+
10: justRatio(9, 5),
|
|
82
|
+
11: justRatio(15, 8),
|
|
83
|
+
12: justRatio(2, 1)
|
|
84
|
+
});
|
|
85
|
+
function justDeviationCents(semitoneClass) {
|
|
86
|
+
_chunkPI3UXRWKcjs.assertInteger.call(void 0, semitoneClass, "semitone class", 0, 12);
|
|
87
|
+
const ratio = JUST_RATIOS[semitoneClass];
|
|
88
|
+
if (!ratio) {
|
|
89
|
+
throw new (0, _chunkPI3UXRWKcjs.InvalidInputError)(`semitone class has no just ratio: ${semitoneClass}`);
|
|
90
|
+
}
|
|
91
|
+
return ratioToCents(ratio[0], ratio[1]) - semitoneClass * 100;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
exports.TWELVE_TET = TWELVE_TET; exports.edo = edo; exports.frequencyOf = frequencyOf; exports.nearestStep = nearestStep; exports.stepOf = stepOf; exports.centsFromNearestStep = centsFromNearestStep; exports.stepsOfCents = stepsOfCents; exports.centsToRatio = centsToRatio; exports.centsBetweenFreq = centsBetweenFreq; exports.centsOfSteps = centsOfSteps; exports.ratioToCents = ratioToCents; exports.JUST_RATIOS = JUST_RATIOS; exports.justDeviationCents = justDeviationCents;
|
|
109
|
+
//# sourceMappingURL=chunk-CU6VG2YM.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/libcantus/libcantus/dist/chunk-CU6VG2YM.cjs","../src/core/tuning/index.ts"],"names":[],"mappings":"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","file":"/home/runner/work/libcantus/libcantus/dist/chunk-CU6VG2YM.cjs","sourcesContent":[null,"/**\n * Tuning and microtonality: frequency conversion, cents, arbitrary equal\n * temperaments (EDO), and just-intonation ratios.\n *\n * The rest of the library reasons in twelve-tone pitch classes, which is a\n * deliberate scope choice for tonal theory. This module is the escape hatch for\n * anyone who needs actual frequencies, cents, non-12 equal temperaments, or the\n * acoustic (just) tuning behind the tempered intervals.\n */\n\nimport { InvalidInputError } from '../errors/index.js';\nimport {\n assertFiniteNumber,\n assertInteger,\n assertPositiveInt,\n assertRange,\n} from '../validation/index.js';\n\n/**\n * An equal-tempered tuning: a reference pitch and a number of equal divisions of\n * the octave. With `divisions === 12` a step index is an ordinary MIDI number.\n *\n * @category Pitch & Intervals\n */\nexport type Tuning = {\n /** Step index (MIDI number when `divisions` is 12) whose frequency is `refFreq`. */\n readonly refStep: number;\n /** Frequency in Hz of `refStep`. */\n readonly refFreq: number;\n /** Equal divisions of the octave (12 standard; e.g. 19 or 31 for microtonal). */\n readonly divisions: number;\n};\n\n/**\n * Standard twelve-tone equal temperament, A4 (MIDI 69) = 440 Hz.\n *\n * @category Pitch & Intervals\n */\nexport const TWELVE_TET: Tuning = Object.freeze({ refStep: 69, refFreq: 440, divisions: 12 });\n\nfunction assertTuning(tuning: Tuning): Tuning {\n assertFiniteNumber(tuning.refStep, 'tuning.refStep');\n assertRange(tuning.refFreq, Number.MIN_VALUE, Number.MAX_VALUE, 'tuning.refFreq');\n assertPositiveInt(tuning.divisions, 'tuning.divisions');\n return tuning;\n}\n\n/**\n * Build an equal temperament with `n` divisions of the octave.\n *\n * @param n Divisions of the octave (e.g. 19, 24, 31).\n * @param refFreq Reference frequency in Hz (default 440).\n * @param refStep Step index of the reference (default 69).\n * @returns The tuning.\n * @example\n * ```ts\n * import { edo, frequencyOf } from '@libraz/libcantus';\n * const et19 = edo(19); // { refStep: 69, refFreq: 440, divisions: 19 }\n * frequencyOf(70, et19); // one 19-EDO step above A4\n * ```\n * @category Pitch & Intervals\n */\nexport function edo(n: number, refFreq = 440, refStep = 69): Tuning {\n assertPositiveInt(n, 'EDO divisions');\n assertRange(refFreq, Number.MIN_VALUE, Number.MAX_VALUE, 'reference frequency');\n assertFiniteNumber(refStep, 'reference step');\n return { refStep, refFreq, divisions: n };\n}\n\n/**\n * Frequency in Hz of a step index under a tuning.\n *\n * @param step Step index (a MIDI number under 12-EDO).\n * @param tuning The tuning (default 12-TET).\n * @returns The frequency in Hz.\n * @example\n * ```ts\n * import { frequencyOf } from '@libraz/libcantus';\n * frequencyOf(69); // 440 (A4 in 12-TET)\n * frequencyOf(60); // middle C in Hz\n * ```\n * @category Pitch & Intervals\n */\nexport function frequencyOf(step: number, tuning: Tuning = TWELVE_TET): number {\n assertFiniteNumber(step, 'step');\n assertTuning(tuning);\n const result = tuning.refFreq * 2 ** ((step - tuning.refStep) / tuning.divisions);\n return assertRange(result, Number.MIN_VALUE, Number.MAX_VALUE, 'frequency result');\n}\n\n/**\n * Nearest step index to a frequency under a tuning (the inverse of\n * {@link frequencyOf}, rounded).\n *\n * @param freq Frequency in Hz.\n * @param tuning The tuning (default 12-TET).\n * @returns The nearest step index.\n * @category Pitch & Intervals\n */\nexport function nearestStep(freq: number, tuning: Tuning = TWELVE_TET): number {\n assertRange(freq, Number.MIN_VALUE, Number.MAX_VALUE, 'frequency');\n assertTuning(tuning);\n return Math.round(tuning.refStep + tuning.divisions * Math.log2(freq / tuning.refFreq));\n}\n\n/**\n * Step index of a frequency under a tuning, unrounded.\n *\n * The exact inverse of {@link frequencyOf}. {@link nearestStep} rounds it,\n * which discards how far off the pitch actually is — the figure a tuner\n * display, a pitch-bend amount, or an intonation check is built on.\n *\n * @param freq Frequency in Hz.\n * @param tuning The tuning (default 12-TET).\n * @returns The fractional step index.\n * @example\n * ```ts\n * import { stepOf } from '@libraz/libcantus';\n * stepOf(442); // slightly above 69, the step for A440\n * ```\n * @category Pitch & Intervals\n */\nexport function stepOf(freq: number, tuning: Tuning = TWELVE_TET): number {\n assertRange(freq, Number.MIN_VALUE, Number.MAX_VALUE, 'frequency');\n assertTuning(tuning);\n return tuning.refStep + tuning.divisions * Math.log2(freq / tuning.refFreq);\n}\n\n/**\n * How far a frequency sits from its nearest step, in cents.\n *\n * Positive means sharp of the step, negative flat.\n *\n * @param freq Frequency in Hz.\n * @param tuning The tuning (default 12-TET).\n * @returns The signed deviation in cents.\n * @example\n * ```ts\n * import { centsFromNearestStep } from '@libraz/libcantus';\n * Math.round(centsFromNearestStep(442)); // 8\n * ```\n * @category Pitch & Intervals\n */\nexport function centsFromNearestStep(freq: number, tuning: Tuning = TWELVE_TET): number {\n const exact = stepOf(freq, tuning);\n return centsOfSteps(exact - Math.round(exact), tuning);\n}\n\n/**\n * Steps spanned by an interval in cents, unrounded — the inverse of\n * {@link centsOfSteps}.\n *\n * @param cents The interval in cents.\n * @param tuning The tuning (default 12-TET).\n * @returns The fractional step count.\n * @category Pitch & Intervals\n */\nexport function stepsOfCents(cents: number, tuning: Tuning = TWELVE_TET): number {\n assertFiniteNumber(cents, 'cents');\n assertTuning(tuning);\n return assertFiniteNumber((cents * tuning.divisions) / 1200, 'steps result');\n}\n\n/**\n * The frequency ratio an interval in cents spans — the inverse of\n * {@link ratioToCents}.\n *\n * Multiply a frequency by this to move it by that many cents, which is how a\n * cents offset (a pitch-bend spec, a tuning table entry) is applied.\n *\n * @param cents The interval in cents.\n * @returns The frequency ratio.\n * @example\n * ```ts\n * import { centsToRatio } from '@libraz/libcantus';\n * 440 * centsToRatio(14); // A440 raised by 14 cents\n * ```\n * @category Pitch & Intervals\n */\nexport function centsToRatio(cents: number): number {\n assertFiniteNumber(cents, 'cents');\n return 2 ** (cents / 1200);\n}\n\n/**\n * Interval in cents between two frequencies.\n *\n * @param a Lower/first frequency in Hz.\n * @param b Upper/second frequency in Hz.\n * @returns Cents from `a` to `b` (negative if `b` is lower).\n * @category Pitch & Intervals\n */\nexport function centsBetweenFreq(a: number, b: number): number {\n assertRange(a, Number.MIN_VALUE, Number.MAX_VALUE, 'first frequency');\n assertRange(b, Number.MIN_VALUE, Number.MAX_VALUE, 'second frequency');\n return 1200 * (Math.log2(b) - Math.log2(a));\n}\n\n/**\n * Cents spanned by a number of equal-temperament steps.\n *\n * @param steps Number of steps.\n * @param tuning The tuning (default 12-TET).\n * @returns The cents.\n * @category Pitch & Intervals\n */\nexport function centsOfSteps(steps: number, tuning: Tuning = TWELVE_TET): number {\n assertFiniteNumber(steps, 'steps');\n assertTuning(tuning);\n return assertFiniteNumber((steps * 1200) / tuning.divisions, 'cents result');\n}\n\n/**\n * Cents of a frequency ratio, e.g. `ratioToCents(3, 2)` ≈ 701.955 for the just\n * perfect fifth.\n *\n * @param numerator Ratio numerator.\n * @param denominator Ratio denominator.\n * @returns The interval in cents.\n * @category Pitch & Intervals\n */\nexport function ratioToCents(numerator: number, denominator: number): number {\n assertRange(numerator, Number.MIN_VALUE, Number.MAX_VALUE, 'ratio numerator');\n assertRange(denominator, Number.MIN_VALUE, Number.MAX_VALUE, 'ratio denominator');\n return 1200 * (Math.log2(numerator) - Math.log2(denominator));\n}\n\n/**\n * Five-limit just-intonation ratios for the twelve interval classes above a\n * unison, indexed by semitone class (0..12).\n *\n * @category Pitch & Intervals\n */\nfunction justRatio(numerator: number, denominator: number): readonly [number, number] {\n return Object.freeze([numerator, denominator] as [number, number]);\n}\n\n/**\n * Five-limit just-intonation ratios for the twelve interval classes above a\n * unison, indexed by semitone class (0..12).\n *\n * @category Pitch & Intervals\n */\nexport const JUST_RATIOS: Readonly<Record<number, readonly [number, number]>> = Object.freeze({\n 0: justRatio(1, 1),\n 1: justRatio(16, 15),\n 2: justRatio(9, 8),\n 3: justRatio(6, 5),\n 4: justRatio(5, 4),\n 5: justRatio(4, 3),\n 6: justRatio(45, 32),\n 7: justRatio(3, 2),\n 8: justRatio(8, 5),\n 9: justRatio(5, 3),\n 10: justRatio(9, 5),\n 11: justRatio(15, 8),\n 12: justRatio(2, 1),\n});\n\n/**\n * Cents by which a five-limit just interval departs from its 12-TET tempering\n * (positive means the just interval is wider).\n *\n * @param semitoneClass Semitone class in [0, 12].\n * @returns The deviation in cents.\n * @throws If `semitoneClass` is not an integer in [0, 12].\n * @category Pitch & Intervals\n */\nexport function justDeviationCents(semitoneClass: number): number {\n assertInteger(semitoneClass, 'semitone class', 0, 12);\n const ratio = JUST_RATIOS[semitoneClass];\n if (!ratio) {\n throw new InvalidInputError(`semitone class has no just ratio: ${semitoneClass}`);\n }\n return ratioToCents(ratio[0], ratio[1]) - semitoneClass * 100;\n}\n"]}
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
|
-
chordPitchClasses,
|
|
3
2
|
createsHiddenParallelPerfect,
|
|
4
3
|
createsParallelPerfect,
|
|
5
4
|
createsVerticalDissonance,
|
|
5
|
+
intervalAboveRoot,
|
|
6
|
+
isChordMember,
|
|
6
7
|
isForbiddenMelodicLeap,
|
|
7
8
|
isScaleTone
|
|
8
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-KBTTEX7Q.js";
|
|
9
10
|
import {
|
|
11
|
+
InvalidInputError,
|
|
10
12
|
assertFiniteNumber,
|
|
11
13
|
assertGenerationBudget,
|
|
12
|
-
assertInteger
|
|
13
|
-
|
|
14
|
+
assertInteger,
|
|
15
|
+
pitchClassOf
|
|
16
|
+
} from "./chunk-R2VVFAIN.js";
|
|
14
17
|
|
|
15
18
|
// src/theory/safety/index.ts
|
|
16
19
|
var NoteSafety = /* @__PURE__ */ ((NoteSafety2) => {
|
|
@@ -36,6 +39,7 @@ var ReasonFlag = /* @__PURE__ */ ((ReasonFlag2) => {
|
|
|
36
39
|
ReasonFlag2[ReasonFlag2["VoiceCrossing"] = 8192] = "VoiceCrossing";
|
|
37
40
|
ReasonFlag2[ReasonFlag2["Suspension"] = 16384] = "Suspension";
|
|
38
41
|
ReasonFlag2[ReasonFlag2["NeedsResolution"] = 32768] = "NeedsResolution";
|
|
42
|
+
ReasonFlag2[ReasonFlag2["MelodicTritone"] = 65536] = "MelodicTritone";
|
|
39
43
|
return ReasonFlag2;
|
|
40
44
|
})(ReasonFlag || {});
|
|
41
45
|
var SUGGESTION_WINDOW = 12;
|
|
@@ -47,6 +51,7 @@ var REASON_SEVERITY = [
|
|
|
47
51
|
[16 /* NonScale */, 2 /* Dissonant */, 2 /* Dissonant */],
|
|
48
52
|
[32 /* OutOfRange */, 1 /* Warning */, 1 /* Warning */],
|
|
49
53
|
[64 /* Tritone */, 2 /* Dissonant */, 2 /* Dissonant */],
|
|
54
|
+
[65536 /* MelodicTritone */, 1 /* Warning */, 2 /* Dissonant */],
|
|
50
55
|
[128 /* LargeLeap */, 1 /* Warning */, 2 /* Dissonant */],
|
|
51
56
|
[1024 /* VerticalDissonance */, 2 /* Dissonant */, 2 /* Dissonant */],
|
|
52
57
|
[2048 /* ParallelPerfect */, 1 /* Warning */, 2 /* Dissonant */],
|
|
@@ -65,21 +70,22 @@ function minimumSafetyForReasons(reasons, profile) {
|
|
|
65
70
|
}
|
|
66
71
|
return minimum;
|
|
67
72
|
}
|
|
68
|
-
function pitchClass(pitch) {
|
|
69
|
-
return (Math.trunc(pitch) % 12 + 12) % 12;
|
|
70
|
-
}
|
|
71
|
-
function intervalAboveRoot(pitch, chord) {
|
|
72
|
-
return ((pitchClass(pitch) - pitchClass(chord.rootPc)) % 12 + 12) % 12;
|
|
73
|
-
}
|
|
74
|
-
function isChordMember(pitch, chord) {
|
|
75
|
-
return chordPitchClasses(chord).includes(pitchClass(pitch));
|
|
76
|
-
}
|
|
77
73
|
function avoidsFourth(chord) {
|
|
78
74
|
return chord.intervals.includes(4) && !chord.intervals.includes(5);
|
|
79
75
|
}
|
|
80
76
|
function avoidsMajorSeventh(chord) {
|
|
81
77
|
return chord.intervals.includes(4) && chord.intervals.includes(10);
|
|
82
78
|
}
|
|
79
|
+
function isStructuralChordDissonance(a, b, chord) {
|
|
80
|
+
const rawClass = pitchClassOf(Math.abs(a - b));
|
|
81
|
+
const aOffset = intervalAboveRoot(a, chord);
|
|
82
|
+
const bOffset = intervalAboveRoot(b, chord);
|
|
83
|
+
const rootSeventh = aOffset === 0 && (bOffset === 10 || bOffset === 11) || bOffset === 0 && (aOffset === 10 || aOffset === 11);
|
|
84
|
+
if (rawClass !== 6 && !rootSeventh) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
return isChordMember(a, chord) && isChordMember(b, chord);
|
|
88
|
+
}
|
|
83
89
|
function stepResolution(pitch, chord) {
|
|
84
90
|
for (let delta = 1; delta <= 2; delta += 1) {
|
|
85
91
|
if (isChordMember(pitch - delta, chord)) {
|
|
@@ -88,17 +94,17 @@ function stepResolution(pitch, chord) {
|
|
|
88
94
|
}
|
|
89
95
|
return void 0;
|
|
90
96
|
}
|
|
91
|
-
function evaluateSafety(q) {
|
|
97
|
+
function evaluateSafety(q, opts = {}) {
|
|
92
98
|
assertSafetyContext(q);
|
|
93
99
|
assertFiniteNumber(q.candidatePitch, "candidatePitch");
|
|
94
|
-
return evaluateInternal(q, true);
|
|
100
|
+
return evaluateInternal(q, opts.suggestions ?? true);
|
|
95
101
|
}
|
|
96
102
|
function assertSafetyContext(q) {
|
|
97
103
|
if (q.prevPitch !== void 0) assertFiniteNumber(q.prevPitch, "prevPitch");
|
|
98
104
|
if (q.vocalLow !== void 0) assertFiniteNumber(q.vocalLow, "vocalLow");
|
|
99
105
|
if (q.vocalHigh !== void 0) assertFiniteNumber(q.vocalHigh, "vocalHigh");
|
|
100
106
|
if (q.vocalLow !== void 0 && q.vocalHigh !== void 0 && q.vocalLow > q.vocalHigh) {
|
|
101
|
-
throw new
|
|
107
|
+
throw new InvalidInputError("vocalLow must not exceed vocalHigh");
|
|
102
108
|
}
|
|
103
109
|
assertGenerationBudget(q.otherVoices.length, "other voices");
|
|
104
110
|
for (let index = 0; index < q.otherVoices.length; index += 1) {
|
|
@@ -128,6 +134,10 @@ function evaluateInternal(q, collectSuggestions) {
|
|
|
128
134
|
}
|
|
129
135
|
const inScale = isScaleTone(pitch, q.key);
|
|
130
136
|
const chord = q.chord;
|
|
137
|
+
if (!inScale && (chord === null || !isChordMember(pitch, chord))) {
|
|
138
|
+
reasons |= 16 /* NonScale */;
|
|
139
|
+
raise(2 /* Dissonant */);
|
|
140
|
+
}
|
|
131
141
|
if (chord && isChordMember(pitch, chord)) {
|
|
132
142
|
reasons |= 1 /* ChordTone */;
|
|
133
143
|
} else if (chord) {
|
|
@@ -148,18 +158,12 @@ 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 && !(
|
|
166
|
+
if (chord && !isChordMember(pitch, chord)) {
|
|
163
167
|
const ic = intervalAboveRoot(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)) {
|
|
@@ -169,8 +173,8 @@ 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
179
|
if (isForbiddenMelodicLeap(q.prevPitch, pitch)) {
|
|
176
180
|
reasons |= 128 /* LargeLeap */;
|
|
@@ -190,6 +194,9 @@ function evaluateInternal(q, collectSuggestions) {
|
|
|
190
194
|
if (!createsVerticalDissonance(pitch, ov.pitch, twoVoice)) {
|
|
191
195
|
continue;
|
|
192
196
|
}
|
|
197
|
+
if (chord && isStructuralChordDissonance(pitch, ov.pitch, chord)) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
193
200
|
reasons |= 1024 /* VerticalDissonance */;
|
|
194
201
|
raise(2 /* Dissonant */);
|
|
195
202
|
if (prev !== void 0 && pitch === prev && ov.prevPitch !== void 0 && !createsVerticalDissonance(prev, ov.prevPitch, twoVoice)) {
|
|
@@ -236,6 +243,9 @@ function findSafeNearby(q, candidate) {
|
|
|
236
243
|
const out = [];
|
|
237
244
|
for (let d = 1; d <= SUGGESTION_WINDOW; d += 1) {
|
|
238
245
|
for (const p of [candidate - d, candidate + d]) {
|
|
246
|
+
if (p < 0 || p > 127) {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
239
249
|
if (evaluateInternal({ ...q, candidatePitch: p }, false).safety === 0 /* Safe */) {
|
|
240
250
|
out.push(p);
|
|
241
251
|
if (out.length >= MAX_SUGGESTIONS) {
|
|
@@ -246,39 +256,47 @@ function findSafeNearby(q, candidate) {
|
|
|
246
256
|
}
|
|
247
257
|
return out;
|
|
248
258
|
}
|
|
259
|
+
var REASON_TEXT = [
|
|
260
|
+
[32 /* OutOfRange */, "Outside the target vocal range"],
|
|
261
|
+
[
|
|
262
|
+
16384 /* Suspension */,
|
|
263
|
+
"Prepared suspension \u2014 a held consonance now dissonant, awaiting resolution"
|
|
264
|
+
],
|
|
265
|
+
[1024 /* VerticalDissonance */, "Dissonant against a sounding voice on a strong beat"],
|
|
266
|
+
[64 /* Tritone */, "Tritone against the sounding harmony"],
|
|
267
|
+
[65536 /* MelodicTritone */, "Melodic tritone leap"],
|
|
268
|
+
[4 /* AvoidNote */, "Avoid note \u2014 clashes with a chord tone a semitone away"],
|
|
269
|
+
[16 /* NonScale */, "Outside the key"],
|
|
270
|
+
[2048 /* ParallelPerfect */, "Parallel perfect interval with another voice"],
|
|
271
|
+
[4096 /* HiddenParallel */, "Hidden perfect interval by similar motion"],
|
|
272
|
+
[8192 /* VoiceCrossing */, "Crosses another voice"],
|
|
273
|
+
[128 /* LargeLeap */, "Leap wider than the profile allows"],
|
|
274
|
+
[32768 /* NeedsResolution */, "Dissonance awaiting a stepwise resolution"],
|
|
275
|
+
[2 /* Tension */, "Chord tension"],
|
|
276
|
+
[8 /* ScaleTone */, "Scale tone, not in the chord"],
|
|
277
|
+
[256 /* MinorSecond */, "Melodic semitone"],
|
|
278
|
+
[512 /* MajorSeventh */, "Melodic major seventh"],
|
|
279
|
+
[1 /* ChordTone */, "Chord tone"]
|
|
280
|
+
];
|
|
281
|
+
function severityOfReason(flag, profile) {
|
|
282
|
+
const entry = REASON_SEVERITY.find(([candidate]) => candidate === flag);
|
|
283
|
+
return entry === void 0 ? 0 /* Safe */ : entry[profile === "strict" ? 2 : 1];
|
|
284
|
+
}
|
|
249
285
|
function describe(reasons, q) {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
if (reasons & 2048 /* ParallelPerfect */) {
|
|
263
|
-
return "Parallel perfect interval with another voice";
|
|
264
|
-
}
|
|
265
|
-
if (reasons & 4096 /* HiddenParallel */) {
|
|
266
|
-
return "Hidden perfect interval by similar motion";
|
|
267
|
-
}
|
|
268
|
-
if (reasons & 8192 /* VoiceCrossing */) {
|
|
269
|
-
return "Crosses another voice";
|
|
270
|
-
}
|
|
271
|
-
if (reasons & 1 /* ChordTone */) {
|
|
272
|
-
return "Chord tone";
|
|
273
|
-
}
|
|
274
|
-
if (reasons & 2 /* Tension */) {
|
|
275
|
-
return "Chord tension";
|
|
276
|
-
}
|
|
277
|
-
if (reasons & 16 /* NonScale */) {
|
|
278
|
-
return "Outside the key";
|
|
286
|
+
let best;
|
|
287
|
+
let bestSeverity = -1;
|
|
288
|
+
for (const [flag, text] of REASON_TEXT) {
|
|
289
|
+
if (!(reasons & flag)) {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const severity = severityOfReason(flag, q.profile);
|
|
293
|
+
if (severity > bestSeverity) {
|
|
294
|
+
bestSeverity = severity;
|
|
295
|
+
best = text;
|
|
296
|
+
}
|
|
279
297
|
}
|
|
280
|
-
if (
|
|
281
|
-
return
|
|
298
|
+
if (best !== void 0) {
|
|
299
|
+
return best;
|
|
282
300
|
}
|
|
283
301
|
return q.chord ? "Placeable" : "No chord context";
|
|
284
302
|
}
|
|
@@ -289,10 +307,14 @@ function enumerateSafePitches(q, pitchLow, pitchHigh) {
|
|
|
289
307
|
assertInteger(pitchLow, "pitchLow");
|
|
290
308
|
assertInteger(pitchHigh, "pitchHigh");
|
|
291
309
|
if (pitchLow > pitchHigh) {
|
|
292
|
-
throw new
|
|
310
|
+
throw new InvalidInputError(
|
|
311
|
+
`pitchLow must not exceed pitchHigh; received ${pitchLow} > ${pitchHigh}`
|
|
312
|
+
);
|
|
293
313
|
}
|
|
294
314
|
assertGenerationBudget(pitchHigh - pitchLow + 1, "safe pitch candidates");
|
|
295
|
-
|
|
315
|
+
const low = Math.max(pitchLow, q.vocalLow ?? pitchLow);
|
|
316
|
+
const high = Math.min(pitchHigh, q.vocalHigh ?? pitchHigh);
|
|
317
|
+
for (let pitch = high; pitch >= low; pitch -= 1) {
|
|
296
318
|
const result = evaluateInternal({ ...q, candidatePitch: pitch }, false);
|
|
297
319
|
if (result.safety === 2 /* Dissonant */) {
|
|
298
320
|
continue;
|
|
@@ -312,4 +334,4 @@ export {
|
|
|
312
334
|
evaluateSafety,
|
|
313
335
|
enumerateSafePitches
|
|
314
336
|
};
|
|
315
|
-
//# sourceMappingURL=chunk-
|
|
337
|
+
//# sourceMappingURL=chunk-DZMHIGI4.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/theory/safety/index.ts"],"sourcesContent":["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"],"mappings":"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","names":["NoteSafety","ReasonFlag"]}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
|
+
InvalidInputError,
|
|
2
3
|
assertFiniteNumber,
|
|
3
4
|
assertInteger,
|
|
4
5
|
assertPositiveInt,
|
|
5
6
|
assertRange
|
|
6
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-R2VVFAIN.js";
|
|
7
8
|
|
|
8
9
|
// src/core/tuning/index.ts
|
|
9
|
-
var TWELVE_TET = { refStep: 69, refFreq: 440, divisions: 12 };
|
|
10
|
+
var TWELVE_TET = Object.freeze({ refStep: 69, refFreq: 440, divisions: 12 });
|
|
10
11
|
function assertTuning(tuning) {
|
|
11
12
|
assertFiniteNumber(tuning.refStep, "tuning.refStep");
|
|
12
13
|
assertRange(tuning.refFreq, Number.MIN_VALUE, Number.MAX_VALUE, "tuning.refFreq");
|
|
@@ -30,6 +31,24 @@ function nearestStep(freq, tuning = TWELVE_TET) {
|
|
|
30
31
|
assertTuning(tuning);
|
|
31
32
|
return Math.round(tuning.refStep + tuning.divisions * Math.log2(freq / tuning.refFreq));
|
|
32
33
|
}
|
|
34
|
+
function stepOf(freq, tuning = TWELVE_TET) {
|
|
35
|
+
assertRange(freq, Number.MIN_VALUE, Number.MAX_VALUE, "frequency");
|
|
36
|
+
assertTuning(tuning);
|
|
37
|
+
return tuning.refStep + tuning.divisions * Math.log2(freq / tuning.refFreq);
|
|
38
|
+
}
|
|
39
|
+
function centsFromNearestStep(freq, tuning = TWELVE_TET) {
|
|
40
|
+
const exact = stepOf(freq, tuning);
|
|
41
|
+
return centsOfSteps(exact - Math.round(exact), tuning);
|
|
42
|
+
}
|
|
43
|
+
function stepsOfCents(cents, tuning = TWELVE_TET) {
|
|
44
|
+
assertFiniteNumber(cents, "cents");
|
|
45
|
+
assertTuning(tuning);
|
|
46
|
+
return assertFiniteNumber(cents * tuning.divisions / 1200, "steps result");
|
|
47
|
+
}
|
|
48
|
+
function centsToRatio(cents) {
|
|
49
|
+
assertFiniteNumber(cents, "cents");
|
|
50
|
+
return 2 ** (cents / 1200);
|
|
51
|
+
}
|
|
33
52
|
function centsBetweenFreq(a, b) {
|
|
34
53
|
assertRange(a, Number.MIN_VALUE, Number.MAX_VALUE, "first frequency");
|
|
35
54
|
assertRange(b, Number.MIN_VALUE, Number.MAX_VALUE, "second frequency");
|
|
@@ -45,26 +64,29 @@ function ratioToCents(numerator, denominator) {
|
|
|
45
64
|
assertRange(denominator, Number.MIN_VALUE, Number.MAX_VALUE, "ratio denominator");
|
|
46
65
|
return 1200 * (Math.log2(numerator) - Math.log2(denominator));
|
|
47
66
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
67
|
+
function justRatio(numerator, denominator) {
|
|
68
|
+
return Object.freeze([numerator, denominator]);
|
|
69
|
+
}
|
|
70
|
+
var JUST_RATIOS = Object.freeze({
|
|
71
|
+
0: justRatio(1, 1),
|
|
72
|
+
1: justRatio(16, 15),
|
|
73
|
+
2: justRatio(9, 8),
|
|
74
|
+
3: justRatio(6, 5),
|
|
75
|
+
4: justRatio(5, 4),
|
|
76
|
+
5: justRatio(4, 3),
|
|
77
|
+
6: justRatio(45, 32),
|
|
78
|
+
7: justRatio(3, 2),
|
|
79
|
+
8: justRatio(8, 5),
|
|
80
|
+
9: justRatio(5, 3),
|
|
81
|
+
10: justRatio(9, 5),
|
|
82
|
+
11: justRatio(15, 8),
|
|
83
|
+
12: justRatio(2, 1)
|
|
84
|
+
});
|
|
63
85
|
function justDeviationCents(semitoneClass) {
|
|
64
86
|
assertInteger(semitoneClass, "semitone class", 0, 12);
|
|
65
87
|
const ratio = JUST_RATIOS[semitoneClass];
|
|
66
88
|
if (!ratio) {
|
|
67
|
-
throw new
|
|
89
|
+
throw new InvalidInputError(`semitone class has no just ratio: ${semitoneClass}`);
|
|
68
90
|
}
|
|
69
91
|
return ratioToCents(ratio[0], ratio[1]) - semitoneClass * 100;
|
|
70
92
|
}
|
|
@@ -74,10 +96,14 @@ export {
|
|
|
74
96
|
edo,
|
|
75
97
|
frequencyOf,
|
|
76
98
|
nearestStep,
|
|
99
|
+
stepOf,
|
|
100
|
+
centsFromNearestStep,
|
|
101
|
+
stepsOfCents,
|
|
102
|
+
centsToRatio,
|
|
77
103
|
centsBetweenFreq,
|
|
78
104
|
centsOfSteps,
|
|
79
105
|
ratioToCents,
|
|
80
106
|
JUST_RATIOS,
|
|
81
107
|
justDeviationCents
|
|
82
108
|
};
|
|
83
|
-
//# sourceMappingURL=chunk-
|
|
109
|
+
//# sourceMappingURL=chunk-EDOTEABK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/tuning/index.ts"],"sourcesContent":["/**\n * Tuning and microtonality: frequency conversion, cents, arbitrary equal\n * temperaments (EDO), and just-intonation ratios.\n *\n * The rest of the library reasons in twelve-tone pitch classes, which is a\n * deliberate scope choice for tonal theory. This module is the escape hatch for\n * anyone who needs actual frequencies, cents, non-12 equal temperaments, or the\n * acoustic (just) tuning behind the tempered intervals.\n */\n\nimport { InvalidInputError } from '../errors/index.js';\nimport {\n assertFiniteNumber,\n assertInteger,\n assertPositiveInt,\n assertRange,\n} from '../validation/index.js';\n\n/**\n * An equal-tempered tuning: a reference pitch and a number of equal divisions of\n * the octave. With `divisions === 12` a step index is an ordinary MIDI number.\n *\n * @category Pitch & Intervals\n */\nexport type Tuning = {\n /** Step index (MIDI number when `divisions` is 12) whose frequency is `refFreq`. */\n readonly refStep: number;\n /** Frequency in Hz of `refStep`. */\n readonly refFreq: number;\n /** Equal divisions of the octave (12 standard; e.g. 19 or 31 for microtonal). */\n readonly divisions: number;\n};\n\n/**\n * Standard twelve-tone equal temperament, A4 (MIDI 69) = 440 Hz.\n *\n * @category Pitch & Intervals\n */\nexport const TWELVE_TET: Tuning = Object.freeze({ refStep: 69, refFreq: 440, divisions: 12 });\n\nfunction assertTuning(tuning: Tuning): Tuning {\n assertFiniteNumber(tuning.refStep, 'tuning.refStep');\n assertRange(tuning.refFreq, Number.MIN_VALUE, Number.MAX_VALUE, 'tuning.refFreq');\n assertPositiveInt(tuning.divisions, 'tuning.divisions');\n return tuning;\n}\n\n/**\n * Build an equal temperament with `n` divisions of the octave.\n *\n * @param n Divisions of the octave (e.g. 19, 24, 31).\n * @param refFreq Reference frequency in Hz (default 440).\n * @param refStep Step index of the reference (default 69).\n * @returns The tuning.\n * @example\n * ```ts\n * import { edo, frequencyOf } from '@libraz/libcantus';\n * const et19 = edo(19); // { refStep: 69, refFreq: 440, divisions: 19 }\n * frequencyOf(70, et19); // one 19-EDO step above A4\n * ```\n * @category Pitch & Intervals\n */\nexport function edo(n: number, refFreq = 440, refStep = 69): Tuning {\n assertPositiveInt(n, 'EDO divisions');\n assertRange(refFreq, Number.MIN_VALUE, Number.MAX_VALUE, 'reference frequency');\n assertFiniteNumber(refStep, 'reference step');\n return { refStep, refFreq, divisions: n };\n}\n\n/**\n * Frequency in Hz of a step index under a tuning.\n *\n * @param step Step index (a MIDI number under 12-EDO).\n * @param tuning The tuning (default 12-TET).\n * @returns The frequency in Hz.\n * @example\n * ```ts\n * import { frequencyOf } from '@libraz/libcantus';\n * frequencyOf(69); // 440 (A4 in 12-TET)\n * frequencyOf(60); // middle C in Hz\n * ```\n * @category Pitch & Intervals\n */\nexport function frequencyOf(step: number, tuning: Tuning = TWELVE_TET): number {\n assertFiniteNumber(step, 'step');\n assertTuning(tuning);\n const result = tuning.refFreq * 2 ** ((step - tuning.refStep) / tuning.divisions);\n return assertRange(result, Number.MIN_VALUE, Number.MAX_VALUE, 'frequency result');\n}\n\n/**\n * Nearest step index to a frequency under a tuning (the inverse of\n * {@link frequencyOf}, rounded).\n *\n * @param freq Frequency in Hz.\n * @param tuning The tuning (default 12-TET).\n * @returns The nearest step index.\n * @category Pitch & Intervals\n */\nexport function nearestStep(freq: number, tuning: Tuning = TWELVE_TET): number {\n assertRange(freq, Number.MIN_VALUE, Number.MAX_VALUE, 'frequency');\n assertTuning(tuning);\n return Math.round(tuning.refStep + tuning.divisions * Math.log2(freq / tuning.refFreq));\n}\n\n/**\n * Step index of a frequency under a tuning, unrounded.\n *\n * The exact inverse of {@link frequencyOf}. {@link nearestStep} rounds it,\n * which discards how far off the pitch actually is — the figure a tuner\n * display, a pitch-bend amount, or an intonation check is built on.\n *\n * @param freq Frequency in Hz.\n * @param tuning The tuning (default 12-TET).\n * @returns The fractional step index.\n * @example\n * ```ts\n * import { stepOf } from '@libraz/libcantus';\n * stepOf(442); // slightly above 69, the step for A440\n * ```\n * @category Pitch & Intervals\n */\nexport function stepOf(freq: number, tuning: Tuning = TWELVE_TET): number {\n assertRange(freq, Number.MIN_VALUE, Number.MAX_VALUE, 'frequency');\n assertTuning(tuning);\n return tuning.refStep + tuning.divisions * Math.log2(freq / tuning.refFreq);\n}\n\n/**\n * How far a frequency sits from its nearest step, in cents.\n *\n * Positive means sharp of the step, negative flat.\n *\n * @param freq Frequency in Hz.\n * @param tuning The tuning (default 12-TET).\n * @returns The signed deviation in cents.\n * @example\n * ```ts\n * import { centsFromNearestStep } from '@libraz/libcantus';\n * Math.round(centsFromNearestStep(442)); // 8\n * ```\n * @category Pitch & Intervals\n */\nexport function centsFromNearestStep(freq: number, tuning: Tuning = TWELVE_TET): number {\n const exact = stepOf(freq, tuning);\n return centsOfSteps(exact - Math.round(exact), tuning);\n}\n\n/**\n * Steps spanned by an interval in cents, unrounded — the inverse of\n * {@link centsOfSteps}.\n *\n * @param cents The interval in cents.\n * @param tuning The tuning (default 12-TET).\n * @returns The fractional step count.\n * @category Pitch & Intervals\n */\nexport function stepsOfCents(cents: number, tuning: Tuning = TWELVE_TET): number {\n assertFiniteNumber(cents, 'cents');\n assertTuning(tuning);\n return assertFiniteNumber((cents * tuning.divisions) / 1200, 'steps result');\n}\n\n/**\n * The frequency ratio an interval in cents spans — the inverse of\n * {@link ratioToCents}.\n *\n * Multiply a frequency by this to move it by that many cents, which is how a\n * cents offset (a pitch-bend spec, a tuning table entry) is applied.\n *\n * @param cents The interval in cents.\n * @returns The frequency ratio.\n * @example\n * ```ts\n * import { centsToRatio } from '@libraz/libcantus';\n * 440 * centsToRatio(14); // A440 raised by 14 cents\n * ```\n * @category Pitch & Intervals\n */\nexport function centsToRatio(cents: number): number {\n assertFiniteNumber(cents, 'cents');\n return 2 ** (cents / 1200);\n}\n\n/**\n * Interval in cents between two frequencies.\n *\n * @param a Lower/first frequency in Hz.\n * @param b Upper/second frequency in Hz.\n * @returns Cents from `a` to `b` (negative if `b` is lower).\n * @category Pitch & Intervals\n */\nexport function centsBetweenFreq(a: number, b: number): number {\n assertRange(a, Number.MIN_VALUE, Number.MAX_VALUE, 'first frequency');\n assertRange(b, Number.MIN_VALUE, Number.MAX_VALUE, 'second frequency');\n return 1200 * (Math.log2(b) - Math.log2(a));\n}\n\n/**\n * Cents spanned by a number of equal-temperament steps.\n *\n * @param steps Number of steps.\n * @param tuning The tuning (default 12-TET).\n * @returns The cents.\n * @category Pitch & Intervals\n */\nexport function centsOfSteps(steps: number, tuning: Tuning = TWELVE_TET): number {\n assertFiniteNumber(steps, 'steps');\n assertTuning(tuning);\n return assertFiniteNumber((steps * 1200) / tuning.divisions, 'cents result');\n}\n\n/**\n * Cents of a frequency ratio, e.g. `ratioToCents(3, 2)` ≈ 701.955 for the just\n * perfect fifth.\n *\n * @param numerator Ratio numerator.\n * @param denominator Ratio denominator.\n * @returns The interval in cents.\n * @category Pitch & Intervals\n */\nexport function ratioToCents(numerator: number, denominator: number): number {\n assertRange(numerator, Number.MIN_VALUE, Number.MAX_VALUE, 'ratio numerator');\n assertRange(denominator, Number.MIN_VALUE, Number.MAX_VALUE, 'ratio denominator');\n return 1200 * (Math.log2(numerator) - Math.log2(denominator));\n}\n\n/**\n * Five-limit just-intonation ratios for the twelve interval classes above a\n * unison, indexed by semitone class (0..12).\n *\n * @category Pitch & Intervals\n */\nfunction justRatio(numerator: number, denominator: number): readonly [number, number] {\n return Object.freeze([numerator, denominator] as [number, number]);\n}\n\n/**\n * Five-limit just-intonation ratios for the twelve interval classes above a\n * unison, indexed by semitone class (0..12).\n *\n * @category Pitch & Intervals\n */\nexport const JUST_RATIOS: Readonly<Record<number, readonly [number, number]>> = Object.freeze({\n 0: justRatio(1, 1),\n 1: justRatio(16, 15),\n 2: justRatio(9, 8),\n 3: justRatio(6, 5),\n 4: justRatio(5, 4),\n 5: justRatio(4, 3),\n 6: justRatio(45, 32),\n 7: justRatio(3, 2),\n 8: justRatio(8, 5),\n 9: justRatio(5, 3),\n 10: justRatio(9, 5),\n 11: justRatio(15, 8),\n 12: justRatio(2, 1),\n});\n\n/**\n * Cents by which a five-limit just interval departs from its 12-TET tempering\n * (positive means the just interval is wider).\n *\n * @param semitoneClass Semitone class in [0, 12].\n * @returns The deviation in cents.\n * @throws If `semitoneClass` is not an integer in [0, 12].\n * @category Pitch & Intervals\n */\nexport function justDeviationCents(semitoneClass: number): number {\n assertInteger(semitoneClass, 'semitone class', 0, 12);\n const ratio = JUST_RATIOS[semitoneClass];\n if (!ratio) {\n throw new InvalidInputError(`semitone class has no just ratio: ${semitoneClass}`);\n }\n return ratioToCents(ratio[0], ratio[1]) - semitoneClass * 100;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
chordToneRole
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-KBTTEX7Q.js";
|
|
4
|
+
import {
|
|
5
|
+
pitchClassOf
|
|
6
|
+
} from "./chunk-R2VVFAIN.js";
|
|
4
7
|
|
|
5
8
|
// src/theory/harmony/index.ts
|
|
6
9
|
function roleOf(pitch, chord, chordId = 0) {
|
|
7
|
-
const interval = ((
|
|
8
|
-
const tones = new Set(chord.intervals.map(
|
|
10
|
+
const interval = (pitchClassOf(pitch) - pitchClassOf(chord.rootPc) + 12) % 12;
|
|
11
|
+
const tones = new Set(chord.intervals.map(pitchClassOf));
|
|
9
12
|
const hasThird = tones.has(3) || tones.has(4);
|
|
10
13
|
const isSuspendedTone = !hasThird && (interval === 5 && tones.has(5) || interval === 2 && tones.has(2));
|
|
11
14
|
const chordRole = chordToneRole(pitch, chord);
|
|
@@ -27,4 +30,4 @@ function roleOf(pitch, chord, chordId = 0) {
|
|
|
27
30
|
export {
|
|
28
31
|
roleOf
|
|
29
32
|
};
|
|
30
|
-
//# sourceMappingURL=chunk-
|
|
33
|
+
//# sourceMappingURL=chunk-HN2OEUN2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/theory/harmony/index.ts"],"sourcesContent":["import { pitchClassOf } from '../../core/pitch/index.js';\nimport { type Chord, type ChordToneRole, chordToneRole } from '../chord/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 * 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 * In a suspended chord (a chord whose intervals include a 4th (5) or 2nd (2)\n * but no third), the suspended tone takes the third's place as the\n * quality-defining tone and is locked to `quality`, since moving it changes the\n * chord. Detecting an octave doubling requires the surrounding voicing, which\n * this query does not carry, so `'doubling'` is part of the type — the shared\n * vocabulary of harmonic roles — but is never returned here.\n *\n * @param pitch MIDI pitch or bare pitch class.\n * @param chord The chord providing the root reference.\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: Chord, chordId = 0): VoicedRole {\n const interval = (pitchClassOf(pitch) - pitchClassOf(chord.rootPc) + 12) % 12;\n const tones = new Set(chord.intervals.map(pitchClassOf));\n const hasThird = tones.has(3) || tones.has(4);\n const isSuspendedTone =\n !hasThird && ((interval === 5 && tones.has(5)) || (interval === 2 && tones.has(2)));\n const chordRole = chordToneRole(pitch, chord);\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":";;;;;;;;AA4DO,SAAS,OAAO,OAAe,OAAc,UAAU,GAAe;AAC3E,QAAM,YAAY,aAAa,KAAK,IAAI,aAAa,MAAM,MAAM,IAAI,MAAM;AAC3E,QAAM,QAAQ,IAAI,IAAI,MAAM,UAAU,IAAI,YAAY,CAAC;AACvD,QAAM,WAAW,MAAM,IAAI,CAAC,KAAK,MAAM,IAAI,CAAC;AAC5C,QAAM,kBACJ,CAAC,aAAc,aAAa,KAAK,MAAM,IAAI,CAAC,KAAO,aAAa,KAAK,MAAM,IAAI,CAAC;AAClF,QAAM,YAAY,cAAc,OAAO,KAAK;AAC5C,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":[]}
|