@libraz/libcantus 0.9.4 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/README.md +61 -261
  2. package/README_ja.md +115 -0
  3. package/dist/analyze/index.cjs +54 -7
  4. package/dist/analyze/index.cjs.map +1 -1
  5. package/dist/analyze/index.d.cts +213 -456
  6. package/dist/analyze/index.d.ts +213 -456
  7. package/dist/analyze/index.js +56 -9
  8. package/dist/chunk-3JXCW6DX.cjs +3220 -0
  9. package/dist/chunk-3JXCW6DX.cjs.map +1 -0
  10. package/dist/chunk-4A2RMUCC.js +7647 -0
  11. package/dist/chunk-4A2RMUCC.js.map +1 -0
  12. package/dist/chunk-6XPTBE7Y.cjs +49 -0
  13. package/dist/chunk-6XPTBE7Y.cjs.map +1 -0
  14. package/dist/chunk-AFM2ULJH.cjs +5121 -0
  15. package/dist/chunk-AFM2ULJH.cjs.map +1 -0
  16. package/dist/chunk-E3F4AHAH.cjs +690 -0
  17. package/dist/chunk-E3F4AHAH.cjs.map +1 -0
  18. package/dist/chunk-ELCTZUJJ.js +379 -0
  19. package/dist/chunk-ELCTZUJJ.js.map +1 -0
  20. package/dist/chunk-FVSI36UV.js +690 -0
  21. package/dist/chunk-FVSI36UV.js.map +1 -0
  22. package/dist/chunk-GEDTJHAC.cjs +1 -0
  23. package/dist/chunk-GEDTJHAC.cjs.map +1 -0
  24. package/dist/chunk-GOZG57ER.js +3220 -0
  25. package/dist/chunk-GOZG57ER.js.map +1 -0
  26. package/dist/chunk-H3VORETI.js +2243 -0
  27. package/dist/chunk-H3VORETI.js.map +1 -0
  28. package/dist/chunk-H42UHPYW.js +1577 -0
  29. package/dist/chunk-H42UHPYW.js.map +1 -0
  30. package/dist/chunk-HFPS7JFD.js +1 -0
  31. package/dist/chunk-INHVXP6Z.js +3053 -0
  32. package/dist/chunk-INHVXP6Z.js.map +1 -0
  33. package/dist/chunk-K4ZJUGF4.cjs +379 -0
  34. package/dist/chunk-K4ZJUGF4.cjs.map +1 -0
  35. package/dist/chunk-KNJEKLKV.js +49 -0
  36. package/dist/chunk-KNJEKLKV.js.map +1 -0
  37. package/dist/chunk-M4CWTJWJ.cjs +7647 -0
  38. package/dist/chunk-M4CWTJWJ.cjs.map +1 -0
  39. package/dist/chunk-MXO72PLY.cjs +211 -0
  40. package/dist/chunk-MXO72PLY.cjs.map +1 -0
  41. package/dist/chunk-NLEU4QSE.cjs +360 -0
  42. package/dist/chunk-NLEU4QSE.cjs.map +1 -0
  43. package/dist/chunk-OE6WK2Y2.cjs +1577 -0
  44. package/dist/chunk-OE6WK2Y2.cjs.map +1 -0
  45. package/dist/chunk-Q2WGKJNN.cjs +1064 -0
  46. package/dist/chunk-Q2WGKJNN.cjs.map +1 -0
  47. package/dist/chunk-QYQQ5J6E.js +1 -0
  48. package/dist/chunk-QYQQ5J6E.js.map +1 -0
  49. package/dist/chunk-RHIJ3S4I.cjs +1 -0
  50. package/dist/chunk-RHIJ3S4I.cjs.map +1 -0
  51. package/dist/chunk-TTCLJX6D.js +211 -0
  52. package/dist/chunk-TTCLJX6D.js.map +1 -0
  53. package/dist/chunk-UADYAYAT.cjs +3053 -0
  54. package/dist/chunk-UADYAYAT.cjs.map +1 -0
  55. package/dist/chunk-VOHZB45S.js +5121 -0
  56. package/dist/chunk-VOHZB45S.js.map +1 -0
  57. package/dist/chunk-VTI6FOLB.js +1064 -0
  58. package/dist/chunk-VTI6FOLB.js.map +1 -0
  59. package/dist/chunk-WWURE4XT.cjs +2243 -0
  60. package/dist/chunk-WWURE4XT.cjs.map +1 -0
  61. package/dist/chunk-YH3BIKHH.js +360 -0
  62. package/dist/chunk-YH3BIKHH.js.map +1 -0
  63. package/dist/core/index.cjs +103 -6
  64. package/dist/core/index.cjs.map +1 -1
  65. package/dist/core/index.d.cts +96 -345
  66. package/dist/core/index.d.ts +96 -345
  67. package/dist/core/index.js +104 -7
  68. package/dist/generate/index.cjs +89 -10
  69. package/dist/generate/index.cjs.map +1 -1
  70. package/dist/generate/index.d.cts +437 -1095
  71. package/dist/generate/index.d.ts +437 -1095
  72. package/dist/generate/index.js +94 -15
  73. package/dist/index-9wcMhG73.d.ts +1095 -0
  74. package/dist/{index-D6b6na69.d.cts → index-B4o55NDr.d.cts} +13 -9
  75. package/dist/index-B9MNZboQ.d.cts +770 -0
  76. package/dist/index-B9MNZboQ.d.ts +770 -0
  77. package/dist/index-BGFW-8P2.d.cts +2307 -0
  78. package/dist/index-BO6xE9YU.d.cts +1804 -0
  79. package/dist/index-BkGmlyI3.d.cts +470 -0
  80. package/dist/index-CCNJRG-P.d.cts +1095 -0
  81. package/dist/index-CGpRv5sl.d.cts +643 -0
  82. package/dist/index-C_8o--VE.d.cts +1242 -0
  83. package/dist/index-CgHqxdps.d.ts +2307 -0
  84. package/dist/index-Chbst3Jx.d.ts +1343 -0
  85. package/dist/index-Cj49F892.d.ts +470 -0
  86. package/dist/{index-tSQh-xwY.d.ts → index-CunYKLiu.d.ts} +13 -9
  87. package/dist/index-D57QRbVn.d.ts +1804 -0
  88. package/dist/index-DInQqT8Q.d.cts +66 -0
  89. package/dist/index-DInQqT8Q.d.ts +66 -0
  90. package/dist/index-Dzr13q3d.d.cts +1343 -0
  91. package/dist/index-G1uwGGWQ.d.ts +643 -0
  92. package/dist/index-szBv3496.d.ts +1242 -0
  93. package/dist/index.cjs +358 -15
  94. package/dist/index.cjs.map +1 -1
  95. package/dist/index.d.cts +17 -15
  96. package/dist/index.d.ts +17 -15
  97. package/dist/index.js +407 -64
  98. package/dist/masks-DYbE485J.d.cts +445 -0
  99. package/dist/masks-DYbE485J.d.ts +445 -0
  100. package/dist/model/index.cjs +39 -7
  101. package/dist/model/index.cjs.map +1 -1
  102. package/dist/model/index.d.cts +4685 -243
  103. package/dist/model/index.d.ts +4685 -243
  104. package/dist/model/index.js +40 -8
  105. package/dist/positional-Cptlj-xs.d.ts +287 -0
  106. package/dist/positional-DvH_sd8u.d.cts +287 -0
  107. package/dist/theory/index.cjs +101 -7
  108. package/dist/theory/index.cjs.map +1 -1
  109. package/dist/theory/index.d.cts +513 -173
  110. package/dist/theory/index.d.ts +513 -173
  111. package/dist/theory/index.js +118 -24
  112. package/package.json +16 -11
  113. package/dist/chunk-36N73KZ3.cjs +0 -246
  114. package/dist/chunk-36N73KZ3.cjs.map +0 -1
  115. package/dist/chunk-4HLLZPXT.cjs +0 -3077
  116. package/dist/chunk-4HLLZPXT.cjs.map +0 -1
  117. package/dist/chunk-7MGZ7D62.cjs +0 -498
  118. package/dist/chunk-7MGZ7D62.cjs.map +0 -1
  119. package/dist/chunk-7NKDPF74.js +0 -3077
  120. package/dist/chunk-7NKDPF74.js.map +0 -1
  121. package/dist/chunk-CAN76NCU.cjs +0 -679
  122. package/dist/chunk-CAN76NCU.cjs.map +0 -1
  123. package/dist/chunk-D2FAJBYL.js +0 -479
  124. package/dist/chunk-D2FAJBYL.js.map +0 -1
  125. package/dist/chunk-EHVEBWM5.js +0 -1449
  126. package/dist/chunk-EHVEBWM5.js.map +0 -1
  127. package/dist/chunk-FCANXBAT.cjs +0 -30
  128. package/dist/chunk-FCANXBAT.cjs.map +0 -1
  129. package/dist/chunk-FNDWXRRI.cjs +0 -479
  130. package/dist/chunk-FNDWXRRI.cjs.map +0 -1
  131. package/dist/chunk-GQSGS2N4.js +0 -173
  132. package/dist/chunk-GQSGS2N4.js.map +0 -1
  133. package/dist/chunk-HKGYCQ34.js +0 -53
  134. package/dist/chunk-HKGYCQ34.js.map +0 -1
  135. package/dist/chunk-I24MV65U.js +0 -106
  136. package/dist/chunk-I24MV65U.js.map +0 -1
  137. package/dist/chunk-KMHOICIY.js +0 -679
  138. package/dist/chunk-KMHOICIY.js.map +0 -1
  139. package/dist/chunk-LW53AHQU.cjs +0 -1117
  140. package/dist/chunk-LW53AHQU.cjs.map +0 -1
  141. package/dist/chunk-N6IHQUOX.js +0 -1
  142. package/dist/chunk-NKIBQ6PT.js +0 -30
  143. package/dist/chunk-NKIBQ6PT.js.map +0 -1
  144. package/dist/chunk-NP4NA2FB.cjs +0 -106
  145. package/dist/chunk-NP4NA2FB.cjs.map +0 -1
  146. package/dist/chunk-NVE34YGZ.cjs +0 -1449
  147. package/dist/chunk-NVE34YGZ.cjs.map +0 -1
  148. package/dist/chunk-NYYWMV2C.js +0 -327
  149. package/dist/chunk-NYYWMV2C.js.map +0 -1
  150. package/dist/chunk-Q2XUP227.js +0 -845
  151. package/dist/chunk-Q2XUP227.js.map +0 -1
  152. package/dist/chunk-Q6OP4U43.cjs +0 -327
  153. package/dist/chunk-Q6OP4U43.cjs.map +0 -1
  154. package/dist/chunk-RC6GBMCD.cjs +0 -845
  155. package/dist/chunk-RC6GBMCD.cjs.map +0 -1
  156. package/dist/chunk-RK35RIN7.js +0 -1117
  157. package/dist/chunk-RK35RIN7.js.map +0 -1
  158. package/dist/chunk-RUMU3UA6.cjs +0 -53
  159. package/dist/chunk-RUMU3UA6.cjs.map +0 -1
  160. package/dist/chunk-T64C7ZD7.cjs +0 -1
  161. package/dist/chunk-T64C7ZD7.cjs.map +0 -1
  162. package/dist/chunk-XEBG3G53.js +0 -498
  163. package/dist/chunk-XEBG3G53.js.map +0 -1
  164. package/dist/chunk-YLEMGEFH.js +0 -246
  165. package/dist/chunk-YLEMGEFH.js.map +0 -1
  166. package/dist/chunk-ZEJR4TKL.cjs +0 -173
  167. package/dist/chunk-ZEJR4TKL.cjs.map +0 -1
  168. package/dist/function-BtiZb9i9.d.ts +0 -208
  169. package/dist/function-DNugAOrt.d.cts +0 -208
  170. package/dist/index-6VR8-kit.d.ts +0 -143
  171. package/dist/index-70p168VM.d.ts +0 -192
  172. package/dist/index-Be74mcon.d.cts +0 -152
  173. package/dist/index-BtXT6UQ0.d.cts +0 -192
  174. package/dist/index-C5UBbQ4G.d.ts +0 -152
  175. package/dist/index-C83mZI1P.d.ts +0 -242
  176. package/dist/index-DXd9JikN.d.cts +0 -246
  177. package/dist/index-DXd9JikN.d.ts +0 -246
  178. package/dist/index-OG65ocOr.d.cts +0 -272
  179. package/dist/index-OG65ocOr.d.ts +0 -272
  180. package/dist/index-tJhkmQgE.d.cts +0 -143
  181. package/dist/index-y_ak9_Hi.d.cts +0 -242
  182. package/dist/styled-DIRkk9Uz.d.ts +0 -506
  183. package/dist/styled-DJbkJQfj.d.cts +0 -506
  184. package/dist/types-BN6_Q9Kl.d.cts +0 -39
  185. package/dist/types-BN6_Q9Kl.d.ts +0 -39
  186. /package/dist/{chunk-N6IHQUOX.js.map → chunk-HFPS7JFD.js.map} +0 -0
@@ -1,246 +0,0 @@
1
- import {
2
- InvalidInputError,
3
- assertFiniteNumber,
4
- assertInteger,
5
- assertNoteEvents,
6
- assertPositiveInt,
7
- assertRange,
8
- assertTimeSignature,
9
- barBeatsOf,
10
- isAdditiveReading,
11
- isCompoundNumerator,
12
- pulseBeatsOf,
13
- pulseCountOf
14
- } from "./chunk-XEBG3G53.js";
15
-
16
- // src/core/meter/index.ts
17
- var EPS = 1e-9;
18
- function isMultiple(value, unit) {
19
- if (unit === 0) {
20
- return false;
21
- }
22
- const ratio = value / unit;
23
- return Math.abs(ratio - Math.round(ratio)) < EPS;
24
- }
25
- function parseTimeSignature(text) {
26
- const match = /^\s*(\d+)\s*\/\s*(\d+)\s*$/.exec(text);
27
- if (!match) {
28
- throw new InvalidInputError(`Invalid time signature: ${text}`);
29
- }
30
- const numerator = Number.parseInt(match[1] ?? "", 10);
31
- const denominator = Number.parseInt(match[2] ?? "", 10);
32
- if (!Number.isSafeInteger(numerator) || !Number.isSafeInteger(denominator) || numerator <= 0 || denominator <= 0) {
33
- throw new InvalidInputError(`Invalid time signature: ${text}`);
34
- }
35
- return assertTimeSignature({ numerator, denominator }, `time signature ${text}`);
36
- }
37
- function formatTimeSignature(ts, opts = {}) {
38
- assertTimeSignature(ts);
39
- if (opts.grouping === true && ts.grouping !== void 0) {
40
- return `${ts.grouping.join("+")}/${ts.denominator}`;
41
- }
42
- return `${ts.numerator}/${ts.denominator}`;
43
- }
44
- function isCompound(ts) {
45
- assertTimeSignature(ts);
46
- return isCompoundNumerator(ts.numerator) && !isAdditiveReading(ts);
47
- }
48
- function beatsPerBar(ts) {
49
- assertTimeSignature(ts);
50
- return barBeatsOf(ts);
51
- }
52
- function pulsesPerBar(ts) {
53
- assertTimeSignature(ts);
54
- return pulseCountOf(ts);
55
- }
56
- function isGroupHead(grouping, pulseIndex) {
57
- let acc = 0;
58
- for (const g of grouping) {
59
- if (acc === pulseIndex) {
60
- return true;
61
- }
62
- acc += g;
63
- }
64
- return false;
65
- }
66
- function beatToBarPosition(beatInQuarters, ts) {
67
- assertFiniteNumber(beatInQuarters, "beat");
68
- assertTimeSignature(ts);
69
- const barLen = barBeatsOf(ts);
70
- const bar = Math.floor(beatInQuarters / barLen);
71
- return { bar, beat: beatInQuarters - bar * barLen };
72
- }
73
- function pulseBeats(ts) {
74
- assertTimeSignature(ts);
75
- return pulseBeatsOf(ts);
76
- }
77
- function barPositionToPulse(pos, ts) {
78
- assertFiniteNumber(pos.beat, "position.beat");
79
- assertTimeSignature(ts);
80
- return pos.beat / pulseBeatsOf(ts) + 1;
81
- }
82
- function formatBarPosition(beatInQuarters, ts, decimals = 2) {
83
- const position = beatToBarPosition(beatInQuarters, ts);
84
- const pulse = barPositionToPulse(position, ts);
85
- const rounded = Number(pulse.toFixed(decimals));
86
- return `${position.bar + 1}.${rounded}`;
87
- }
88
- function barPositionToBeat(pos, ts) {
89
- assertInteger(pos.bar, "bar position bar");
90
- assertFiniteNumber(pos.beat, "bar position beat");
91
- assertTimeSignature(ts);
92
- return pos.bar * barBeatsOf(ts) + pos.beat;
93
- }
94
- function metricWeight(beatInQuarters, ts) {
95
- assertFiniteNumber(beatInQuarters, "beat");
96
- assertTimeSignature(ts);
97
- const barLen = barBeatsOf(ts);
98
- const beat = beatInQuarters - Math.floor(beatInQuarters / barLen) * barLen;
99
- const pulse = pulseBeatsOf(ts);
100
- if (!isMultiple(beat, pulse)) {
101
- return 0;
102
- }
103
- const pulses = pulseCountOf(ts);
104
- const pulseIndex = Math.round(beat / pulse) % pulses;
105
- if (pulseIndex === 0) {
106
- return 3;
107
- }
108
- const grouping = ts.grouping;
109
- if (grouping !== void 0) {
110
- return isGroupHead(grouping, pulseIndex) ? 2 : 1;
111
- }
112
- if (pulses % 2 === 0 && pulseIndex === pulses / 2) {
113
- return 2;
114
- }
115
- return 1;
116
- }
117
- function isStrongBeat(beatInQuarters, ts) {
118
- return metricWeight(beatInQuarters, ts) >= 2;
119
- }
120
- function tuplet(totalBeats, count) {
121
- assertRange(totalBeats, 0, Number.MAX_SAFE_INTEGER, "tuplet total beats");
122
- assertPositiveInt(count, "tuplet count");
123
- return new Array(count).fill(totalBeats / count);
124
- }
125
-
126
- // src/core/event-index/index.ts
127
- var EPS2 = 1e-9;
128
- function upperBound(values, beat) {
129
- let low = 0;
130
- let high = values.length;
131
- while (low < high) {
132
- const middle = Math.floor((low + high) / 2);
133
- if ((values[middle]?.note.startBeat ?? Number.POSITIVE_INFINITY) <= beat) {
134
- low = middle + 1;
135
- } else {
136
- high = middle;
137
- }
138
- }
139
- return low;
140
- }
141
- function createNoteEventIndex(events, options = {}) {
142
- assertNoteEvents(events, options.name ?? "note events", options);
143
- const tieBreak = options.tieBreak ?? "highest";
144
- const notes = events.map((note, originalIndex) => ({
145
- note,
146
- originalIndex,
147
- endBeat: note.startBeat + note.durationBeat
148
- })).sort((a, b) => a.note.startBeat - b.note.startBeat || a.originalIndex - b.originalIndex);
149
- let prefixMaxEnd;
150
- const maxEndUpTo = (index) => {
151
- if (prefixMaxEnd === void 0) {
152
- const prefix = [];
153
- let maxEnd = Number.NEGATIVE_INFINITY;
154
- for (const indexed of notes) {
155
- maxEnd = Math.max(maxEnd, indexed.endBeat);
156
- prefix.push(maxEnd);
157
- }
158
- prefixMaxEnd = prefix;
159
- }
160
- return prefixMaxEnd[index] ?? Number.NEGATIVE_INFINITY;
161
- };
162
- return {
163
- notes,
164
- at(beat) {
165
- let index = upperBound(notes, beat + EPS2) - 1;
166
- let best;
167
- while (index >= 0) {
168
- if (maxEndUpTo(index) <= beat + EPS2) {
169
- break;
170
- }
171
- const indexed = notes[index];
172
- index -= 1;
173
- if (indexed === void 0 || indexed.note.durationBeat <= 0 || indexed.note.startBeat - EPS2 > beat || beat >= indexed.endBeat - EPS2) {
174
- continue;
175
- }
176
- if (best === void 0) {
177
- best = indexed;
178
- if (tieBreak === "last") {
179
- break;
180
- }
181
- continue;
182
- }
183
- if (Math.abs(indexed.note.startBeat - best.note.startBeat) >= EPS2) {
184
- break;
185
- }
186
- const higher = indexed.note.pitch > best.note.pitch;
187
- if (tieBreak === "highest" ? higher : !higher) {
188
- best = indexed;
189
- }
190
- }
191
- return best;
192
- },
193
- attacksAt(beat) {
194
- let index = upperBound(notes, beat + EPS2) - 1;
195
- while (index >= 0) {
196
- const candidate = notes[index];
197
- if (candidate === void 0 || Math.abs(candidate.note.startBeat - beat) >= EPS2) {
198
- return false;
199
- }
200
- if (candidate.note.durationBeat > 0) {
201
- return true;
202
- }
203
- index -= 1;
204
- }
205
- return false;
206
- },
207
- onsetsBetween(startBeat, endBeat) {
208
- const result = [];
209
- let index = upperBound(notes, startBeat + EPS2);
210
- while (index < notes.length) {
211
- const indexed = notes[index];
212
- const onset = indexed?.note.startBeat;
213
- if (indexed === void 0 || onset === void 0 || onset >= endBeat - EPS2) {
214
- break;
215
- }
216
- index += 1;
217
- if (indexed.note.durationBeat <= 0) {
218
- continue;
219
- }
220
- const previous = result[result.length - 1];
221
- if (previous === void 0 || Math.abs(previous - onset) >= EPS2) {
222
- result.push(onset);
223
- }
224
- }
225
- return result;
226
- }
227
- };
228
- }
229
-
230
- export {
231
- parseTimeSignature,
232
- formatTimeSignature,
233
- isCompound,
234
- beatsPerBar,
235
- pulsesPerBar,
236
- beatToBarPosition,
237
- pulseBeats,
238
- barPositionToPulse,
239
- formatBarPosition,
240
- barPositionToBeat,
241
- metricWeight,
242
- isStrongBeat,
243
- tuplet,
244
- createNoteEventIndex
245
- };
246
- //# sourceMappingURL=chunk-YLEMGEFH.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/core/meter/index.ts","../src/core/event-index/index.ts"],"sourcesContent":["/**\n * Meter and time signatures: bar/beat arithmetic, simple vs. compound meters,\n * metric-accent hierarchy, and tuplet subdivision.\n *\n * Positions and durations are measured in quarter-note beats, matching the rest\n * of the library's beat convention (four quarter-note beats per bar in 4/4).\n */\n\nimport { InvalidInputError } from '../errors/index.js';\nimport {\n assertFiniteNumber,\n assertInteger,\n assertPositiveInt,\n assertRange,\n assertTimeSignature,\n} from '../validation/index.js';\nimport {\n barBeatsOf,\n isAdditiveReading,\n isCompoundNumerator,\n pulseBeatsOf,\n pulseCountOf,\n} from './internal.js';\n\n/**\n * A time signature as a numerator over a note-value denominator.\n *\n * @category Rhythm & Meter\n */\nexport type TimeSignature = {\n numerator: number;\n denominator: number;\n /**\n * Optional additive grouping of the bar's main pulses, as the felt-beat\n * lengths in pulses — e.g. `[2, 2, 3]` for a 2+2+3 reading of 7/8, or\n * `[3, 2]` for 5/8. The entries must be positive integers summing to the\n * numerator.\n *\n * A compound numerator accepts both readings. 9/8 groups its three dotted\n * pulses as `[1, 1, 1]`, or reads additively as `[2, 2, 2, 3]` quavers; a\n * grouping that sums to the numerator selects the additive reading, so\n * aksak and other additive metres are expressible. {@link pulsesPerBar}\n * and {@link isCompound} follow whichever reading the grouping selects.\n *\n * When present, the head pulse of each group (other than the downbeat) is\n * treated as a secondary strong pulse by\n * {@link metricWeight}/{@link isStrongBeat}; when absent, all main pulses\n * are weighted equally as flat, evenly divided pulses.\n */\n grouping?: number[];\n};\n\n/**\n * A position expressed as a bar index and a quarter-note offset within the bar.\n *\n * @category Rhythm & Meter\n */\nexport type BarPosition = {\n /** 0-based bar index. {@link formatBarPosition} renders it 1-based. */\n bar: number;\n /**\n * Quarter-note offset from the start of the bar — not the felt beat. In 6/8\n * the second felt beat is at `beat: 1.5`; {@link barPositionToPulse} converts\n * to felt-beat numbering.\n */\n beat: number;\n};\n\nconst EPS = 1e-9;\n\n/** Whether `value` is an integer multiple of `unit` (within a float tolerance). */\nfunction isMultiple(value: number, unit: number): boolean {\n if (unit === 0) {\n return false;\n }\n const ratio = value / unit;\n return Math.abs(ratio - Math.round(ratio)) < EPS;\n}\n\n/**\n * Parse a time signature such as `\"4/4\"` or `\"6/8\"`.\n *\n * @param text The signature text.\n * @returns The parsed time signature.\n * @throws If the text is not `n/d` with positive integers.\n * @example\n * ```ts\n * import { parseTimeSignature } from '@libraz/libcantus';\n * parseTimeSignature('6/8'); // { numerator: 6, denominator: 8 }\n * ```\n * @category Rhythm & Meter\n */\nexport function parseTimeSignature(text: string): TimeSignature {\n const match = /^\\s*(\\d+)\\s*\\/\\s*(\\d+)\\s*$/.exec(text);\n if (!match) {\n throw new InvalidInputError(`Invalid time signature: ${text}`);\n }\n const numerator = Number.parseInt(match[1] ?? '', 10);\n const denominator = Number.parseInt(match[2] ?? '', 10);\n if (\n !Number.isSafeInteger(numerator) ||\n !Number.isSafeInteger(denominator) ||\n numerator <= 0 ||\n denominator <= 0\n ) {\n throw new InvalidInputError(`Invalid time signature: ${text}`);\n }\n // The same signature every consumer validates, validated here too: parsing\n // a signature the rest of the library rejects only moves the failure.\n return assertTimeSignature({ numerator, denominator }, `time signature ${text}`);\n}\n\n/**\n * Render a time signature as `\"n/d\"`, or as `\"a+b+c/d\"` when it carries an\n * additive grouping and `grouping` is requested.\n *\n * The plain form drops the grouping, so a round trip through it reads 7/8 as\n * flat rather than as 2+2+3; ask for the additive form to keep it.\n *\n * @param ts The time signature.\n * @param opts Set `grouping: true` to render an additive grouping.\n * @returns The formatted signature.\n * @example\n * ```ts\n * import { formatTimeSignature } from '@libraz/libcantus';\n * const aksak = { numerator: 7, denominator: 8, grouping: [2, 2, 3] };\n * formatTimeSignature(aksak); // '7/8'\n * formatTimeSignature(aksak, { grouping: true }); // '2+2+3/8'\n * ```\n * @category Rhythm & Meter\n */\nexport function formatTimeSignature(ts: TimeSignature, opts: { grouping?: boolean } = {}): string {\n assertTimeSignature(ts);\n if (opts.grouping === true && ts.grouping !== undefined) {\n return `${ts.grouping.join('+')}/${ts.denominator}`;\n }\n return `${ts.numerator}/${ts.denominator}`;\n}\n\n/**\n * Whether a meter is compound: its main pulses each divide into three, as in\n * 6/8, 9/8, 12/8, or 6/4. Compound meters are those whose numerator is a\n * multiple of three greater than three, independent of the denominator. Meters\n * like 3/8 and 3/4 (simple triples, numerator 3) are not compound, and neither\n * is a signature whose {@link TimeSignature.grouping} selects the additive\n * reading (9/8 as 2+2+2+3).\n *\n * @param ts The time signature.\n * @returns True for compound meters.\n * @category Rhythm & Meter\n */\nexport function isCompound(ts: TimeSignature): boolean {\n assertTimeSignature(ts);\n return isCompoundNumerator(ts.numerator) && !isAdditiveReading(ts);\n}\n\n/**\n * Length of a bar in quarter-note beats.\n *\n * @param ts The time signature.\n * @returns The bar length in quarter notes.\n * @category Rhythm & Meter\n */\nexport function beatsPerBar(ts: TimeSignature): number {\n assertTimeSignature(ts);\n return barBeatsOf(ts);\n}\n\n/**\n * Number of main pulses (felt beats) per bar: the numerator for simple meters,\n * a third of it for compound meters.\n *\n * For additive/irregular meters such as 7/8 or 5/8 this counts the raw pulses\n * (7 and 5 respectively), which are otherwise felt as flat, evenly divided and\n * equally accented pulses. To recover a 2+2+3 or 3+2 felt-beat grouping, set\n * {@link TimeSignature.grouping}; {@link metricWeight} then accents each\n * group's head pulse.\n *\n * @param ts The time signature.\n * @returns The pulse count per bar.\n * @category Rhythm & Meter\n */\nexport function pulsesPerBar(ts: TimeSignature): number {\n assertTimeSignature(ts);\n return pulseCountOf(ts);\n}\n\n/** Whether `pulseIndex` is the head pulse of one of the additive groups. */\nfunction isGroupHead(grouping: number[], pulseIndex: number): boolean {\n let acc = 0;\n for (const g of grouping) {\n if (acc === pulseIndex) {\n return true;\n }\n acc += g;\n }\n return false;\n}\n\n/**\n * Convert an absolute quarter-note position to a bar index and in-bar offset.\n *\n * @param beatInQuarters Absolute position in quarter-note beats.\n * @param ts The time signature.\n * @returns The bar and in-bar quarter-note offset.\n * @example\n * ```ts\n * import { parseTimeSignature, beatToBarPosition } from '@libraz/libcantus';\n * const ts = parseTimeSignature('4/4');\n * beatToBarPosition(5, ts); // { bar: 1, beat: 1 }\n * ```\n * @category Rhythm & Meter\n */\nexport function beatToBarPosition(beatInQuarters: number, ts: TimeSignature): BarPosition {\n assertFiniteNumber(beatInQuarters, 'beat');\n assertTimeSignature(ts);\n const barLen = barBeatsOf(ts);\n const bar = Math.floor(beatInQuarters / barLen);\n return { bar, beat: beatInQuarters - bar * barLen };\n}\n\n/**\n * Length of one felt beat (main pulse) in quarter-note beats.\n *\n * A quarter note in simple meters, a dotted quarter in compound ones, and one\n * denominator unit in an additive metre. Position displays, click tracks, and\n * score export are all written against this length.\n *\n * @param ts The time signature.\n * @returns The pulse length in quarter-note beats.\n * @example\n * ```ts\n * import { parseTimeSignature, pulseBeats } from '@libraz/libcantus';\n * pulseBeats(parseTimeSignature('6/8')); // 1.5\n * ```\n * @category Rhythm & Meter\n */\nexport function pulseBeats(ts: TimeSignature): number {\n assertTimeSignature(ts);\n return pulseBeatsOf(ts);\n}\n\n/**\n * The felt-beat number of a bar position, 1-based.\n *\n * {@link BarPosition.beat} is a quarter-note offset, so in 6/8 the second felt\n * beat reads as 1.5. This is the number a musician says and a position display\n * shows.\n *\n * @param pos The bar position.\n * @param ts The time signature.\n * @returns The 1-based felt-beat number, fractional between pulses.\n * @example\n * ```ts\n * import { barPositionToPulse, parseTimeSignature } from '@libraz/libcantus';\n * barPositionToPulse({ bar: 0, beat: 1.5 }, parseTimeSignature('6/8')); // 2\n * ```\n * @category Rhythm & Meter\n */\nexport function barPositionToPulse(pos: BarPosition, ts: TimeSignature): number {\n assertFiniteNumber(pos.beat, 'position.beat');\n assertTimeSignature(ts);\n return pos.beat / pulseBeatsOf(ts) + 1;\n}\n\n/**\n * Render an absolute position as the `bar.beat` a DAW or a score shows:\n * 1-based bar, 1-based felt beat.\n *\n * @param beatInQuarters Absolute position in quarter-note beats.\n * @param ts The time signature.\n * @param decimals Digits of the fractional beat to keep.\n * @returns The formatted position, e.g. `'3.2'` for bar 3, felt beat 2.\n * @example\n * ```ts\n * import { formatBarPosition, parseTimeSignature } from '@libraz/libcantus';\n * formatBarPosition(7.5, parseTimeSignature('6/8')); // '3.2'\n * ```\n * @category Rhythm & Meter\n */\nexport function formatBarPosition(beatInQuarters: number, ts: TimeSignature, decimals = 2): string {\n const position = beatToBarPosition(beatInQuarters, ts);\n const pulse = barPositionToPulse(position, ts);\n const rounded = Number(pulse.toFixed(decimals));\n return `${position.bar + 1}.${rounded}`;\n}\n\n/**\n * Convert a bar index and in-bar offset back to an absolute quarter-note\n * position.\n *\n * @param pos The bar position.\n * @param ts The time signature.\n * @returns The absolute position in quarter-note beats.\n * @category Rhythm & Meter\n */\nexport function barPositionToBeat(pos: BarPosition, ts: TimeSignature): number {\n assertInteger(pos.bar, 'bar position bar');\n assertFiniteNumber(pos.beat, 'bar position beat');\n assertTimeSignature(ts);\n return pos.bar * barBeatsOf(ts) + pos.beat;\n}\n\n/**\n * Metric weight of a position within its bar, on a 0–3 scale:\n * 3 the downbeat, 2 a secondary strong pulse, 1 any other main pulse, and 0 an\n * off-pulse subdivision.\n *\n * The secondary strong pulse is the bar's midpoint in even simple/compound\n * meters. For an additive/irregular meter with {@link TimeSignature.grouping}\n * set (e.g. `[2, 2, 3]` for 7/8), each group's head pulse other than the\n * downbeat is the secondary strong pulse instead; without a grouping every\n * non-downbeat main pulse of such a meter weighs 1 (flat, evenly divided\n * pulses).\n *\n * @param beatInQuarters Absolute or in-bar quarter-note position.\n * @param ts The time signature.\n * @returns The metric weight (0–3).\n * @throws If the time signature is malformed, including a grouping that is not\n * a positive-integer list summing to the pulse count or the numerator.\n * @example\n * ```ts\n * import { parseTimeSignature, metricWeight } from '@libraz/libcantus';\n * const ts = parseTimeSignature('4/4');\n * metricWeight(0, ts); // 3 (the downbeat)\n * ```\n * @category Rhythm & Meter\n */\nexport function metricWeight(beatInQuarters: number, ts: TimeSignature): number {\n assertFiniteNumber(beatInQuarters, 'beat');\n assertTimeSignature(ts);\n const barLen = barBeatsOf(ts);\n const beat = beatInQuarters - Math.floor(beatInQuarters / barLen) * barLen;\n const pulse = pulseBeatsOf(ts);\n if (!isMultiple(beat, pulse)) {\n return 0;\n }\n const pulses = pulseCountOf(ts);\n const pulseIndex = Math.round(beat / pulse) % pulses;\n if (pulseIndex === 0) {\n return 3;\n }\n const grouping = ts.grouping;\n if (grouping !== undefined) {\n return isGroupHead(grouping, pulseIndex) ? 2 : 1;\n }\n if (pulses % 2 === 0 && pulseIndex === pulses / 2) {\n return 2;\n }\n return 1;\n}\n\n/**\n * Whether a position is metrically accented (weight 2 or more — a downbeat or a\n * secondary strong pulse).\n *\n * @param beatInQuarters Absolute or in-bar quarter-note position.\n * @param ts The time signature.\n * @returns True on strong beats.\n * @category Rhythm & Meter\n */\nexport function isStrongBeat(beatInQuarters: number, ts: TimeSignature): boolean {\n return metricWeight(beatInQuarters, ts) >= 2;\n}\n\n/**\n * Subdivide a span into `count` equal tuplet durations (e.g. an eighth-note\n * triplet is `tuplet(1, 3)`).\n *\n * @param totalBeats Total span in quarter-note beats.\n * @param count Number of equal parts.\n * @returns `count` equal durations summing to `totalBeats`.\n * @throws If `count` is not a positive integer.\n * @category Rhythm & Meter\n */\nexport function tuplet(totalBeats: number, count: number): number[] {\n assertRange(totalBeats, 0, Number.MAX_SAFE_INTEGER, 'tuplet total beats');\n assertPositiveInt(count, 'tuplet count');\n return new Array<number>(count).fill(totalBeats / count);\n}\n","import type { NoteEvent } from '../types.js';\nimport { assertNoteEvents } from '../validation/index.js';\n\n/**\n * A validated note retaining its position in the caller's original array.\n *\n * @category Core\n */\nexport type IndexedNoteEvent = {\n note: NoteEvent;\n originalIndex: number;\n endBeat: number;\n};\n\n/**\n * Which note wins when several sound from the same onset.\n *\n * `'highest'` and `'lowest'` name a voice, so the answer does not depend on the\n * order the caller happened to store the chord in; `'last'` takes the one\n * latest in the input array.\n *\n * @category Core\n */\nexport type OnsetTieBreak = 'highest' | 'lowest' | 'last';\n\n/**\n * Binary-searchable, stable-sorted index over note onsets and active spans.\n *\n * @category Core\n */\nexport type NoteEventIndex = {\n notes: IndexedNoteEvent[];\n /**\n * Latest-onset note sounding at `beat`. Simultaneous onsets are resolved by\n * the index's {@link OnsetTieBreak}.\n */\n at: (beat: number) => IndexedNoteEvent | undefined;\n /** Whether one or more sounding notes attack at `beat`. */\n attacksAt: (beat: number) => boolean;\n /** Unique sounding-note attack beats strictly inside `(startBeat, endBeat)`. */\n onsetsBetween: (startBeat: number, endBeat: number) => number[];\n};\n\nconst EPS = 1e-9;\n\nfunction upperBound(values: IndexedNoteEvent[], beat: number): number {\n let low = 0;\n let high = values.length;\n while (low < high) {\n const middle = Math.floor((low + high) / 2);\n if ((values[middle]?.note.startBeat ?? Number.POSITIVE_INFINITY) <= beat) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n return low;\n}\n\n/**\n * Validate and stable-sort note events once, then expose logarithmic onset and\n * active-note lookups. Non-positive-duration notes may be retained for callers\n * that intentionally filter them later, but never count as sounding.\n *\n * @category Core\n */\nexport function createNoteEventIndex(\n events: readonly NoteEvent[],\n options: {\n allowNonPositiveDuration?: boolean;\n tieBreak?: OnsetTieBreak;\n budget?: number;\n /** What the events are, for the validation error message. */\n name?: string;\n } = {},\n): NoteEventIndex {\n assertNoteEvents(events, options.name ?? 'note events', options);\n const tieBreak = options.tieBreak ?? 'highest';\n const notes = events\n .map((note, originalIndex) => ({\n note,\n originalIndex,\n endBeat: note.startBeat + note.durationBeat,\n }))\n .sort((a, b) => a.note.startBeat - b.note.startBeat || a.originalIndex - b.originalIndex);\n // Built on the first `at` call rather than up front: several callers use this\n // only to validate and sort, and never ask what sounds at a beat.\n let prefixMaxEnd: number[] | undefined;\n const maxEndUpTo = (index: number): number => {\n if (prefixMaxEnd === undefined) {\n const prefix: number[] = [];\n let maxEnd = Number.NEGATIVE_INFINITY;\n for (const indexed of notes) {\n maxEnd = Math.max(maxEnd, indexed.endBeat);\n prefix.push(maxEnd);\n }\n prefixMaxEnd = prefix;\n }\n return prefixMaxEnd[index] ?? Number.NEGATIVE_INFINITY;\n };\n\n return {\n notes,\n at(beat) {\n let index = upperBound(notes, beat + EPS) - 1;\n let best: IndexedNoteEvent | undefined;\n while (index >= 0) {\n if (maxEndUpTo(index) <= beat + EPS) {\n break;\n }\n const indexed = notes[index];\n index -= 1;\n if (\n indexed === undefined ||\n indexed.note.durationBeat <= 0 ||\n indexed.note.startBeat - EPS > beat ||\n beat >= indexed.endBeat - EPS\n ) {\n continue;\n }\n if (best === undefined) {\n best = indexed;\n if (tieBreak === 'last') {\n break;\n }\n continue;\n }\n // Only notes from the same onset compete: a later onset always wins.\n if (Math.abs(indexed.note.startBeat - best.note.startBeat) >= EPS) {\n break;\n }\n const higher = indexed.note.pitch > best.note.pitch;\n if (tieBreak === 'highest' ? higher : !higher) {\n best = indexed;\n }\n }\n return best;\n },\n attacksAt(beat) {\n // Only a sounding note attacks: a zero-length artefact must not make\n // `attacksAt(b)` true on a beat where `at(b)` finds nothing.\n let index = upperBound(notes, beat + EPS) - 1;\n while (index >= 0) {\n const candidate = notes[index];\n if (candidate === undefined || Math.abs(candidate.note.startBeat - beat) >= EPS) {\n return false;\n }\n if (candidate.note.durationBeat > 0) {\n return true;\n }\n index -= 1;\n }\n return false;\n },\n onsetsBetween(startBeat, endBeat) {\n const result: number[] = [];\n let index = upperBound(notes, startBeat + EPS);\n while (index < notes.length) {\n const indexed = notes[index];\n const onset = indexed?.note.startBeat;\n if (indexed === undefined || onset === undefined || onset >= endBeat - EPS) {\n break;\n }\n index += 1;\n if (indexed.note.durationBeat <= 0) {\n continue;\n }\n const previous = result[result.length - 1];\n if (previous === undefined || Math.abs(previous - onset) >= EPS) {\n result.push(onset);\n }\n }\n return result;\n },\n };\n}\n"],"mappings":"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","names":["EPS"]}
@@ -1,173 +0,0 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
2
-
3
-
4
-
5
-
6
-
7
-
8
- var _chunkCAN76NCUcjs = require('./chunk-CAN76NCU.cjs');
9
-
10
-
11
-
12
-
13
-
14
- var _chunkFNDWXRRIcjs = require('./chunk-FNDWXRRI.cjs');
15
-
16
-
17
- var _chunk7MGZ7D62cjs = require('./chunk-7MGZ7D62.cjs');
18
-
19
- // src/generate/reharmony/index.ts
20
- function hasInterval(chord, semitone) {
21
- return chord.intervals.some((iv) => _chunk7MGZ7D62cjs.pitchClassOf.call(void 0, iv) === semitone);
22
- }
23
- function isDominantType(chord) {
24
- return hasInterval(chord, 4) && hasInterval(chord, 10);
25
- }
26
- function isStableTarget(chord) {
27
- const hasFifth = hasInterval(chord, 7);
28
- const hasThird = hasInterval(chord, 3) || hasInterval(chord, 4);
29
- return hasFifth && hasThird && !isDominantType(chord);
30
- }
31
- function commonToneCount(a, b) {
32
- const set = new Set(b);
33
- return a.filter((pc) => set.has(pc)).length;
34
- }
35
- function triadQualityOf(chord) {
36
- const semis = new Set(chord.intervals.map((interval) => _chunk7MGZ7D62cjs.pitchClassOf.call(void 0, interval)));
37
- const hasThird = semis.has(4) ? "maj" : semis.has(3) ? "min" : void 0;
38
- if (hasThird === void 0) {
39
- return chord.quality;
40
- }
41
- if (semis.has(6) && !semis.has(7)) {
42
- return hasThird === "min" ? "dim" : "majb5";
43
- }
44
- if (semis.has(8) && !semis.has(7)) {
45
- return "aug";
46
- }
47
- return hasThird;
48
- }
49
- function diatonicTriadsOf(key) {
50
- const degrees = _chunkFNDWXRRIcjs.scaleTonesInDegreeOrder.call(void 0, key).length;
51
- const triads = [];
52
- for (let degree = 0; degree < degrees; degree += 1) {
53
- triads.push(_chunkFNDWXRRIcjs.diatonicTriad.call(void 0, degree, key));
54
- }
55
- return triads;
56
- }
57
- function sameChord(a, b) {
58
- return _chunk7MGZ7D62cjs.pitchClassOf.call(void 0, a.rootPc) === _chunk7MGZ7D62cjs.pitchClassOf.call(void 0, b.rootPc) && a.quality === b.quality;
59
- }
60
- var THIRD_OFFSETS = [3, 4, 8, 9];
61
- function substituteChord(chord, key, opts) {
62
- const originalTriad = _chunkFNDWXRRIcjs.chordPitchClasses.call(void 0, _chunkFNDWXRRIcjs.makeChord.call(void 0, chord.rootPc, triadQualityOf(chord)));
63
- const candidates = [];
64
- if (isDominantType(chord)) {
65
- candidates.push({ chord: _chunkFNDWXRRIcjs.makeChord.call(void 0, _chunk7MGZ7D62cjs.pitchClassOf.call(void 0, chord.rootPc + 6), "dom7"), type: "tritone" });
66
- }
67
- if (isStableTarget(chord)) {
68
- candidates.push({
69
- chord: _chunkFNDWXRRIcjs.makeChord.call(void 0, _chunk7MGZ7D62cjs.pitchClassOf.call(void 0, chord.rootPc + 7), "dom7"),
70
- type: "secondaryDominant"
71
- });
72
- }
73
- for (const triad of diatonicTriadsOf(key)) {
74
- const offset = _chunk7MGZ7D62cjs.pitchClassOf.call(void 0, triad.rootPc - chord.rootPc);
75
- if (THIRD_OFFSETS.includes(offset) && commonToneCount(originalTriad, _chunkFNDWXRRIcjs.chordPitchClasses.call(void 0, triad)) === 2) {
76
- candidates.push({ chord: triad, type: "relative" });
77
- }
78
- }
79
- const targetFunction = _chunkCAN76NCUcjs.functionOf.call(void 0, chord, key);
80
- for (const triad of diatonicTriadsOf(_chunkCAN76NCUcjs.parallelKey.call(void 0, key))) {
81
- if (_chunkCAN76NCUcjs.functionOf.call(void 0, triad, key) === targetFunction && !_chunkCAN76NCUcjs.isDiatonic.call(void 0, triad, key)) {
82
- candidates.push({ chord: triad, type: "borrowed" });
83
- }
84
- }
85
- for (const offset of THIRD_OFFSETS) {
86
- for (const quality of ["maj", "min"]) {
87
- const mediant = _chunkFNDWXRRIcjs.makeChord.call(void 0, _chunk7MGZ7D62cjs.pitchClassOf.call(void 0, chord.rootPc + offset), quality);
88
- if (commonToneCount(originalTriad, _chunkFNDWXRRIcjs.chordPitchClasses.call(void 0, mediant)) === 1 && !_chunkCAN76NCUcjs.isDiatonic.call(void 0, mediant, key)) {
89
- candidates.push({ chord: mediant, type: "chromaticMediant" });
90
- }
91
- }
92
- }
93
- const melodyPcs = _optionalChain([opts, 'optionalAccess', _ => _.melodyPcs, 'optionalAccess', _2 => _2.map, 'call', _3 => _3(_chunk7MGZ7D62cjs.pitchClassOf)]);
94
- const seen = /* @__PURE__ */ new Set();
95
- const results = [];
96
- for (const candidate of candidates) {
97
- if (sameChord(candidate.chord, chord)) {
98
- continue;
99
- }
100
- const dedupeKey = `${_chunk7MGZ7D62cjs.pitchClassOf.call(void 0, candidate.chord.rootPc)}:${candidate.chord.quality}`;
101
- if (seen.has(dedupeKey)) {
102
- continue;
103
- }
104
- seen.add(dedupeKey);
105
- if (melodyPcs !== void 0) {
106
- const pcs = new Set(_chunkFNDWXRRIcjs.chordPitchClasses.call(void 0, candidate.chord));
107
- if (!melodyPcs.every((pc) => pcs.has(pc))) {
108
- continue;
109
- }
110
- }
111
- results.push({
112
- chord: candidate.chord,
113
- type: candidate.type,
114
- // A secondary dominant is named as the applied chord it is, so the
115
- // numeral re-parses to the chord that was proposed rather than to a
116
- // chromatic chord on the same root.
117
- roman: _chunkCAN76NCUcjs.chordToRoman.call(void 0, candidate.chord, key, {
118
- applied: candidate.type === "secondaryDominant"
119
- }),
120
- function: _chunkCAN76NCUcjs.functionOf.call(void 0, candidate.chord, key)
121
- });
122
- }
123
- return results;
124
- }
125
- function modalInterchangePalette(key) {
126
- const palette = [];
127
- const seen = /* @__PURE__ */ new Set();
128
- const add = (chord) => {
129
- const dedupeKey = `${_chunk7MGZ7D62cjs.pitchClassOf.call(void 0, chord.rootPc)}:${chord.quality}`;
130
- if (seen.has(dedupeKey)) {
131
- return;
132
- }
133
- seen.add(dedupeKey);
134
- palette.push({
135
- chord,
136
- roman: _chunkCAN76NCUcjs.chordToRoman.call(void 0, chord, key),
137
- source: _chunkCAN76NCUcjs.borrowedSource.call(void 0, chord, key)
138
- });
139
- };
140
- for (const triad of diatonicTriadsOf(_chunkCAN76NCUcjs.parallelKey.call(void 0, key))) {
141
- if (!_chunkCAN76NCUcjs.isDiatonic.call(void 0, triad, key)) {
142
- add(triad);
143
- }
144
- }
145
- const neapolitan = _chunkFNDWXRRIcjs.makeChord.call(void 0, _chunk7MGZ7D62cjs.pitchClassOf.call(void 0, key.rootPc + 1), "maj");
146
- if (!_chunkCAN76NCUcjs.isDiatonic.call(void 0, neapolitan, key)) {
147
- add(neapolitan);
148
- }
149
- return palette;
150
- }
151
- function negativeHarmonyMirror(chord, key) {
152
- const tonic = _chunk7MGZ7D62cjs.pitchClassOf.call(void 0, key.rootPc);
153
- const mirror = (p) => _chunk7MGZ7D62cjs.pitchClassOf.call(void 0, 2 * tonic + 7 - p);
154
- const pcs = _chunkFNDWXRRIcjs.chordPitchClasses.call(void 0, chord).map(mirror);
155
- const bassPc = chord.bassPc !== void 0 ? mirror(chord.bassPc) : void 0;
156
- const pitches = bassPc !== void 0 ? [bassPc - 12, ...pcs] : pcs;
157
- const detected = _chunkCAN76NCUcjs.detectChordBest.call(void 0, pitches);
158
- if (detected) {
159
- if (bassPc === void 0 && detected.bassPc !== void 0) {
160
- return _chunkFNDWXRRIcjs.makeChord.call(void 0, detected.rootPc, detected.quality);
161
- }
162
- return detected;
163
- }
164
- const quality = chord.quality;
165
- return _chunkFNDWXRRIcjs.makeChord.call(void 0, mirror(chord.rootPc), quality, bassPc);
166
- }
167
-
168
-
169
-
170
-
171
-
172
- exports.substituteChord = substituteChord; exports.modalInterchangePalette = modalInterchangePalette; exports.negativeHarmonyMirror = negativeHarmonyMirror;
173
- //# sourceMappingURL=chunk-ZEJR4TKL.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["/home/runner/work/libcantus/libcantus/dist/chunk-ZEJR4TKL.cjs","../src/generate/reharmony/index.ts"],"names":[],"mappings":"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","file":"/home/runner/work/libcantus/libcantus/dist/chunk-ZEJR4TKL.cjs","sourcesContent":[null,"/**\n * Reharmonization: chord substitution, modal-interchange palettes, and negative\n * harmony. These transforms take an existing chord (or a key) and propose\n * alternative harmonies that preserve a chosen relationship — a shared function,\n * a common tone, a tonicizing dominant, or a reflection across the key axis.\n *\n * Roots are pitch classes (0..11); no spelled key signature is required.\n */\n\nimport { detectChordBest } from '../../analyze/detect/index.js';\nimport {\n type BorrowedSource,\n borrowedSource,\n chordToRoman,\n functionOf,\n type HarmonicFunction,\n isDiatonic,\n parallelKey,\n} from '../../analyze/functional/index.js';\nimport { pitchClassOf as mod12 } from '../../core/pitch/index.js';\nimport type { KeyScale } from '../../core/types.js';\nimport {\n type Chord,\n type ChordQuality,\n chordPitchClasses,\n diatonicTriad,\n makeChord,\n} from '../../theory/chord/index.js';\nimport { scaleTonesInDegreeOrder } from '../../theory/scale/index.js';\n\n/**\n * The kind of substitution relationship a candidate realizes.\n *\n * @category Reharmonization\n */\nexport type SubstitutionType =\n | 'tritone'\n | 'relative'\n | 'borrowed'\n | 'chromaticMediant'\n | 'secondaryDominant';\n\n/**\n * A proposed chord substitution with its relationship, numeral, and function.\n *\n * @category Reharmonization\n */\nexport type Substitution = {\n chord: Chord;\n type: SubstitutionType;\n roman: string;\n function: HarmonicFunction;\n};\n\n/**\n * Options controlling which substitutions {@link substituteChord} returns.\n *\n * @category Reharmonization\n */\nexport type SubstituteOptions = {\n /**\n * Melody pitch classes that must remain chord tones. When given, only\n * substitutions whose pitch classes contain every listed pitch class are\n * kept, so the melody stays consonant against the new harmony.\n */\n melodyPcs?: number[];\n};\n\n/** Whether the chord's template carries a given interval (mod 12). */\nfunction hasInterval(chord: Chord, semitone: number): boolean {\n return chord.intervals.some((iv) => mod12(iv) === semitone);\n}\n\n/** Whether a chord is a dominant type: a major third with a minor seventh. */\nfunction isDominantType(chord: Chord): boolean {\n return hasInterval(chord, 4) && hasInterval(chord, 10);\n}\n\n/**\n * A chord that can be tonicized: it has a perfect fifth and a third but is not\n * itself a dominant seventh (which is an unstable, tonicizing sonority rather\n * than a stable target).\n */\nfunction isStableTarget(chord: Chord): boolean {\n const hasFifth = hasInterval(chord, 7);\n const hasThird = hasInterval(chord, 3) || hasInterval(chord, 4);\n return hasFifth && hasThird && !isDominantType(chord);\n}\n\n/** Count the pitch classes shared between two pitch-class sets. */\nfunction commonToneCount(a: number[], b: number[]): number {\n const set = new Set(b);\n return a.filter((pc) => set.has(pc)).length;\n}\n\n/** The plain triad quality underlying a chord's own quality. */\nfunction triadQualityOf(chord: Chord): ChordQuality {\n const semis = new Set(chord.intervals.map((interval) => mod12(interval)));\n const hasThird = semis.has(4) ? 'maj' : semis.has(3) ? 'min' : undefined;\n if (hasThird === undefined) {\n return chord.quality; // sus, power, and other non-tertian chords stand alone\n }\n if (semis.has(6) && !semis.has(7)) {\n return hasThird === 'min' ? 'dim' : 'majb5';\n }\n if (semis.has(8) && !semis.has(7)) {\n return 'aug';\n }\n return hasThird;\n}\n\n/** The diatonic triads of a key, one per scale degree. */\nfunction diatonicTriadsOf(key: KeyScale): Chord[] {\n const degrees = scaleTonesInDegreeOrder(key).length;\n const triads: Chord[] = [];\n for (let degree = 0; degree < degrees; degree += 1) {\n triads.push(diatonicTriad(degree, key));\n }\n return triads;\n}\n\n/** Whether two chords share the same root pitch class and quality. */\nfunction sameChord(a: Chord, b: Chord): boolean {\n return mod12(a.rootPc) === mod12(b.rootPc) && a.quality === b.quality;\n}\n\n/** Root offsets (semitones) that form a third above or below a root. */\nconst THIRD_OFFSETS = [3, 4, 8, 9] as const;\n\n/**\n * Propose substitutions for a chord within a key.\n *\n * Candidates are generated by several relationships and deduplicated against the\n * original chord and each other:\n *\n * - `tritone`: for a dominant-type chord, the dominant seventh a tritone away.\n * - `secondaryDominant`: for a stable target, the dominant seventh a fifth\n * above its root (tonicizing it).\n * - `relative`: a diatonic triad a third away that shares two common tones.\n * - `borrowed`: a triad from the parallel mode with the same harmonic function.\n * - `chromaticMediant`: a major/minor triad a third away sharing one common\n * tone.\n *\n * Each result carries its Roman numeral and harmonic function in `key`. When\n * `opts.melodyPcs` is given, only substitutions whose pitch classes contain all\n * of those pitch classes are returned.\n *\n * @param chord The chord to reharmonize.\n * @param key The prevailing key.\n * @param opts Optional melody-preservation constraint.\n * @returns The deduplicated substitution candidates.\n * @example\n * ```ts\n * import { substituteChord, makeChord, majorKey } from '@libraz/libcantus';\n * const subs = substituteChord(makeChord(7, 'dom7'), majorKey(0));\n * // includes the tritone sub Db7 (rootPc 1, quality 'dom7')\n * ```\n * @category Reharmonization\n */\nexport function substituteChord(\n chord: Chord,\n key: KeyScale,\n opts?: SubstituteOptions,\n): Substitution[] {\n // The relative test counts common tones against the chord's triad, not its\n // full pitch-class set: an exact count of two would otherwise be decided by\n // how many tensions the input carries, so C proposes both Em and Am while\n // Cmaj7 proposes only Am — penalising the candidate that overlaps *more*.\n const originalTriad = chordPitchClasses(makeChord(chord.rootPc, triadQualityOf(chord)));\n const candidates: { chord: Chord; type: SubstitutionType }[] = [];\n\n // Tritone substitution: only for dominant-type chords.\n if (isDominantType(chord)) {\n candidates.push({ chord: makeChord(mod12(chord.rootPc + 6), 'dom7'), type: 'tritone' });\n }\n\n // Secondary dominant: the V7 tonicizing a stable target.\n if (isStableTarget(chord)) {\n candidates.push({\n chord: makeChord(mod12(chord.rootPc + 7), 'dom7'),\n type: 'secondaryDominant',\n });\n }\n\n // Relative: a diatonic triad a third away sharing two common tones.\n for (const triad of diatonicTriadsOf(key)) {\n const offset = mod12(triad.rootPc - chord.rootPc);\n if (\n (THIRD_OFFSETS as readonly number[]).includes(offset) &&\n commonToneCount(originalTriad, chordPitchClasses(triad)) === 2\n ) {\n candidates.push({ chord: triad, type: 'relative' });\n }\n }\n\n // Borrowed: a parallel-mode triad sharing the original's harmonic function.\n const targetFunction = functionOf(chord, key);\n for (const triad of diatonicTriadsOf(parallelKey(key))) {\n if (functionOf(triad, key) === targetFunction && !isDiatonic(triad, key)) {\n candidates.push({ chord: triad, type: 'borrowed' });\n }\n }\n\n // Chromatic mediants: major/minor triads a third away sharing one common tone.\n for (const offset of THIRD_OFFSETS) {\n for (const quality of ['maj', 'min'] as const) {\n const mediant = makeChord(mod12(chord.rootPc + offset), quality);\n if (\n commonToneCount(originalTriad, chordPitchClasses(mediant)) === 1 &&\n !isDiatonic(mediant, key)\n ) {\n candidates.push({ chord: mediant, type: 'chromaticMediant' });\n }\n }\n }\n\n const melodyPcs = opts?.melodyPcs?.map(mod12);\n const seen = new Set<string>();\n const results: Substitution[] = [];\n for (const candidate of candidates) {\n if (sameChord(candidate.chord, chord)) {\n continue;\n }\n const dedupeKey = `${mod12(candidate.chord.rootPc)}:${candidate.chord.quality}`;\n if (seen.has(dedupeKey)) {\n continue;\n }\n seen.add(dedupeKey);\n if (melodyPcs !== undefined) {\n const pcs = new Set(chordPitchClasses(candidate.chord));\n if (!melodyPcs.every((pc) => pcs.has(pc))) {\n continue;\n }\n }\n results.push({\n chord: candidate.chord,\n type: candidate.type,\n // A secondary dominant is named as the applied chord it is, so the\n // numeral re-parses to the chord that was proposed rather than to a\n // chromatic chord on the same root.\n roman: chordToRoman(candidate.chord, key, {\n applied: candidate.type === 'secondaryDominant',\n }),\n function: functionOf(candidate.chord, key),\n });\n }\n return results;\n}\n\n/**\n * A borrowed chord in the modal-interchange palette of a key.\n *\n * @category Reharmonization\n */\nexport type BorrowedChord = {\n chord: Chord;\n roman: string;\n source: BorrowedSource;\n};\n\n/**\n * The modal-interchange palette of a key.\n *\n * Lists the diatonic triads of the {@link parallelKey} that are not diatonic to\n * `key` (the classic borrowed chords such as `iv`, `bVI`, and `bVII` in a major\n * key), plus the Neapolitan (a major triad on the flat second degree). Each\n * carries its Roman numeral and {@link borrowedSource} relative to `key`.\n *\n * @param key The prevailing key.\n * @returns The borrowed-chord palette.\n * @category Reharmonization\n */\nexport function modalInterchangePalette(key: KeyScale): BorrowedChord[] {\n const palette: BorrowedChord[] = [];\n const seen = new Set<string>();\n const add = (chord: Chord) => {\n const dedupeKey = `${mod12(chord.rootPc)}:${chord.quality}`;\n if (seen.has(dedupeKey)) {\n return;\n }\n seen.add(dedupeKey);\n palette.push({\n chord,\n roman: chordToRoman(chord, key),\n source: borrowedSource(chord, key),\n });\n };\n\n for (const triad of diatonicTriadsOf(parallelKey(key))) {\n if (!isDiatonic(triad, key)) {\n add(triad);\n }\n }\n // The Neapolitan (major triad on b2) sits outside both parallel modes, so it\n // is added explicitly — unless b2 is already diatonic (e.g. in a Phrygian key,\n // where it is a native chord rather than a borrowing).\n const neapolitan = makeChord(mod12(key.rootPc + 1), 'maj');\n if (!isDiatonic(neapolitan, key)) {\n add(neapolitan);\n }\n return palette;\n}\n\n/**\n * Reflect a chord across the tonic–dominant axis of a key (negative harmony).\n *\n * Every pitch class `p` maps to `(2*T + 7 - p) mod 12`, where `T` is the key\n * tonic — the reflection that swaps the tonic with the dominant. The mirrored\n * pitch classes (including the bass, if set) are reassembled into a chord by\n * {@link detectChordBest}; when no chord is recognized, the mirrored root and\n * the original quality are used as a fallback.\n *\n * @param chord The chord to reflect.\n * @param key The prevailing key.\n * @returns The negative-harmony counterpart of the chord.\n * @example\n * ```ts\n * import { negativeHarmonyMirror, makeChord, majorKey } from '@libraz/libcantus';\n * negativeHarmonyMirror(makeChord(7, 'maj'), majorKey(0));\n * // G major reflected across C's axis => F minor (rootPc 5, quality 'min')\n * ```\n * @category Reharmonization\n */\nexport function negativeHarmonyMirror(chord: Chord, key: KeyScale): Chord {\n const tonic = mod12(key.rootPc);\n const mirror = (p: number) => mod12(2 * tonic + 7 - p);\n const pcs = chordPitchClasses(chord).map(mirror);\n const bassPc = chord.bassPc !== undefined ? mirror(chord.bassPc) : undefined;\n // Place the mirrored bass an octave below so it is recognized as the lowest\n // note and preserved through detection.\n const pitches = bassPc !== undefined ? [bassPc - 12, ...pcs] : pcs;\n const detected = detectChordBest(pitches);\n if (detected) {\n // Without an input bass the mirrored tones are octave-less, so detection may\n // read the lowest pitch class as a slash bass and report a spurious\n // inversion. Restore the recognized chord to root position in that case.\n if (bassPc === undefined && detected.bassPc !== undefined) {\n return makeChord(detected.rootPc, detected.quality);\n }\n return detected;\n }\n const quality: ChordQuality = chord.quality;\n return makeChord(mirror(chord.rootPc), quality, bassPc);\n}\n"]}