@libraz/libcantus 0.9.3 → 0.9.4

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 (198) hide show
  1. package/README.md +2 -2
  2. package/dist/analyze/index.cjs +39 -1947
  3. package/dist/analyze/index.cjs.map +1 -1
  4. package/dist/analyze/index.d.cts +197 -37
  5. package/dist/analyze/index.d.ts +197 -37
  6. package/dist/analyze/index.js +19 -8
  7. package/dist/chunk-36N73KZ3.cjs +246 -0
  8. package/dist/chunk-36N73KZ3.cjs.map +1 -0
  9. package/dist/chunk-4HLLZPXT.cjs +3077 -0
  10. package/dist/chunk-4HLLZPXT.cjs.map +1 -0
  11. package/dist/chunk-7MGZ7D62.cjs +498 -0
  12. package/dist/chunk-7MGZ7D62.cjs.map +1 -0
  13. package/dist/{chunk-4MHA432G.js → chunk-7NKDPF74.js} +403 -180
  14. package/dist/chunk-7NKDPF74.js.map +1 -0
  15. package/dist/chunk-CAN76NCU.cjs +679 -0
  16. package/dist/chunk-CAN76NCU.cjs.map +1 -0
  17. package/dist/{chunk-MIWO425M.js → chunk-D2FAJBYL.js} +101 -34
  18. package/dist/chunk-D2FAJBYL.js.map +1 -0
  19. package/dist/{chunk-SGU7SPLK.js → chunk-EHVEBWM5.js} +1056 -531
  20. package/dist/chunk-EHVEBWM5.js.map +1 -0
  21. package/dist/chunk-FCANXBAT.cjs +30 -0
  22. package/dist/chunk-FCANXBAT.cjs.map +1 -0
  23. package/dist/chunk-FNDWXRRI.cjs +479 -0
  24. package/dist/chunk-FNDWXRRI.cjs.map +1 -0
  25. package/dist/{chunk-IT2O4XFE.js → chunk-GQSGS2N4.js} +43 -24
  26. package/dist/chunk-GQSGS2N4.js.map +1 -0
  27. package/dist/{chunk-62QZKCJQ.js → chunk-HKGYCQ34.js} +10 -6
  28. package/dist/chunk-HKGYCQ34.js.map +1 -0
  29. package/dist/{chunk-AKLYPWIN.js → chunk-I24MV65U.js} +29 -6
  30. package/dist/chunk-I24MV65U.js.map +1 -0
  31. package/dist/{chunk-CHMXELDS.js → chunk-KMHOICIY.js} +311 -181
  32. package/dist/chunk-KMHOICIY.js.map +1 -0
  33. package/dist/chunk-LW53AHQU.cjs +1117 -0
  34. package/dist/chunk-LW53AHQU.cjs.map +1 -0
  35. package/dist/{chunk-2ZWMSJH6.js → chunk-NKIBQ6PT.js} +2 -2
  36. package/dist/chunk-NKIBQ6PT.js.map +1 -0
  37. package/dist/chunk-NP4NA2FB.cjs +106 -0
  38. package/dist/chunk-NP4NA2FB.cjs.map +1 -0
  39. package/dist/chunk-NVE34YGZ.cjs +1449 -0
  40. package/dist/chunk-NVE34YGZ.cjs.map +1 -0
  41. package/dist/{chunk-EH6EM37U.js → chunk-NYYWMV2C.js} +61 -49
  42. package/dist/chunk-NYYWMV2C.js.map +1 -0
  43. package/dist/{chunk-GOAW4VYL.js → chunk-Q2XUP227.js} +243 -117
  44. package/dist/chunk-Q2XUP227.js.map +1 -0
  45. package/dist/chunk-Q6OP4U43.cjs +327 -0
  46. package/dist/chunk-Q6OP4U43.cjs.map +1 -0
  47. package/dist/chunk-RC6GBMCD.cjs +845 -0
  48. package/dist/chunk-RC6GBMCD.cjs.map +1 -0
  49. package/dist/{chunk-C5XYKUVG.js → chunk-RK35RIN7.js} +370 -157
  50. package/dist/chunk-RK35RIN7.js.map +1 -0
  51. package/dist/chunk-RUMU3UA6.cjs +53 -0
  52. package/dist/chunk-RUMU3UA6.cjs.map +1 -0
  53. package/dist/chunk-T64C7ZD7.cjs +1 -0
  54. package/dist/chunk-T64C7ZD7.cjs.map +1 -0
  55. package/dist/chunk-XEBG3G53.js +498 -0
  56. package/dist/chunk-XEBG3G53.js.map +1 -0
  57. package/dist/{chunk-I5R7ZBJS.js → chunk-YLEMGEFH.js} +103 -59
  58. package/dist/chunk-YLEMGEFH.js.map +1 -0
  59. package/dist/chunk-ZEJR4TKL.cjs +173 -0
  60. package/dist/chunk-ZEJR4TKL.cjs.map +1 -0
  61. package/dist/core/index.cjs +130 -647
  62. package/dist/core/index.cjs.map +1 -1
  63. package/dist/core/index.d.cts +301 -39
  64. package/dist/core/index.d.ts +301 -39
  65. package/dist/core/index.js +54 -16
  66. package/dist/{function-DYt4iKsn.d.cts → function-BtiZb9i9.d.ts} +28 -7
  67. package/dist/{function-DVH4Eusr.d.ts → function-DNugAOrt.d.cts} +28 -7
  68. package/dist/generate/index.cjs +28 -4085
  69. package/dist/generate/index.cjs.map +1 -1
  70. package/dist/generate/index.d.cts +370 -62
  71. package/dist/generate/index.d.ts +370 -62
  72. package/dist/generate/index.js +23 -9
  73. package/dist/{index-CxqBQtH1.d.cts → index-6VR8-kit.d.ts} +19 -4
  74. package/dist/index-70p168VM.d.ts +192 -0
  75. package/dist/{index-CpOsP1y9.d.cts → index-Be74mcon.d.cts} +18 -17
  76. package/dist/index-BtXT6UQ0.d.cts +192 -0
  77. package/dist/{index-DxvARIP2.d.ts → index-C5UBbQ4G.d.ts} +18 -17
  78. package/dist/{index-Bt4tOUD9.d.ts → index-C83mZI1P.d.ts} +93 -8
  79. package/dist/{index-Bg5dYMmZ.d.ts → index-D6b6na69.d.cts} +14 -11
  80. package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
  81. package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
  82. package/dist/index-OG65ocOr.d.cts +272 -0
  83. package/dist/index-OG65ocOr.d.ts +272 -0
  84. package/dist/{index-Jc0Ryi79.d.ts → index-tJhkmQgE.d.cts} +19 -4
  85. package/dist/{index-Dikst44v.d.cts → index-tSQh-xwY.d.ts} +14 -11
  86. package/dist/{index-BNlGP86A.d.cts → index-y_ak9_Hi.d.cts} +93 -8
  87. package/dist/index.cjs +419 -7383
  88. package/dist/index.cjs.map +1 -1
  89. package/dist/index.d.cts +14 -14
  90. package/dist/index.d.ts +14 -14
  91. package/dist/index.js +111 -27
  92. package/dist/model/index.cjs +8 -2562
  93. package/dist/model/index.cjs.map +1 -1
  94. package/dist/model/index.d.cts +433 -75
  95. package/dist/model/index.d.ts +433 -75
  96. package/dist/model/index.js +6 -7
  97. package/dist/styled-DIRkk9Uz.d.ts +506 -0
  98. package/dist/styled-DJbkJQfj.d.cts +506 -0
  99. package/dist/theory/index.cjs +172 -1822
  100. package/dist/theory/index.cjs.map +1 -1
  101. package/dist/theory/index.d.cts +151 -233
  102. package/dist/theory/index.d.ts +151 -233
  103. package/dist/theory/index.js +30 -7
  104. package/dist/{types-MGP29r3t.d.cts → types-BN6_Q9Kl.d.cts} +11 -2
  105. package/dist/{types-MGP29r3t.d.ts → types-BN6_Q9Kl.d.ts} +11 -2
  106. package/package.json +24 -6
  107. package/dist/chunk-2ZWMSJH6.js.map +0 -1
  108. package/dist/chunk-4MHA432G.js.map +0 -1
  109. package/dist/chunk-62QZKCJQ.js.map +0 -1
  110. package/dist/chunk-AKLYPWIN.js.map +0 -1
  111. package/dist/chunk-C5XYKUVG.js.map +0 -1
  112. package/dist/chunk-CHMXELDS.js.map +0 -1
  113. package/dist/chunk-DYWFNC4A.js +0 -152
  114. package/dist/chunk-DYWFNC4A.js.map +0 -1
  115. package/dist/chunk-EH6EM37U.js.map +0 -1
  116. package/dist/chunk-FBQK3ZQN.js +0 -131
  117. package/dist/chunk-FBQK3ZQN.js.map +0 -1
  118. package/dist/chunk-GOAW4VYL.js.map +0 -1
  119. package/dist/chunk-I5R7ZBJS.js.map +0 -1
  120. package/dist/chunk-IT2O4XFE.js.map +0 -1
  121. package/dist/chunk-MIWO425M.js.map +0 -1
  122. package/dist/chunk-SGU7SPLK.js.map +0 -1
  123. package/dist/index-CGMZNCx6.d.cts +0 -140
  124. package/dist/index-CGMZNCx6.d.ts +0 -140
  125. package/dist/index-CnZCo29u.d.cts +0 -108
  126. package/dist/index-JnF4v9bx.d.ts +0 -108
  127. package/dist/styled-DZ7GjShb.d.ts +0 -282
  128. package/dist/styled-ZTbdaVFM.d.cts +0 -282
  129. package/src/analyze/arrange/index.ts +0 -21
  130. package/src/analyze/arrange/internal.ts +0 -127
  131. package/src/analyze/arrange/tension.ts +0 -199
  132. package/src/analyze/arrange/tracks.ts +0 -312
  133. package/src/analyze/detect/index.ts +0 -272
  134. package/src/analyze/functional/borrowed.ts +0 -84
  135. package/src/analyze/functional/cadence.ts +0 -66
  136. package/src/analyze/functional/function.ts +0 -184
  137. package/src/analyze/functional/index.ts +0 -23
  138. package/src/analyze/functional/internal.ts +0 -29
  139. package/src/analyze/functional/roman.ts +0 -402
  140. package/src/analyze/index.ts +0 -49
  141. package/src/analyze/timeline/index.ts +0 -451
  142. package/src/analyze/voice/index.ts +0 -339
  143. package/src/core/event-index/index.ts +0 -104
  144. package/src/core/index.ts +0 -61
  145. package/src/core/interval/index.ts +0 -81
  146. package/src/core/meter/index.ts +0 -302
  147. package/src/core/pitch/index.ts +0 -311
  148. package/src/core/random/index.ts +0 -76
  149. package/src/core/tuning/index.ts +0 -186
  150. package/src/core/types.ts +0 -29
  151. package/src/core/validation/index.ts +0 -119
  152. package/src/generate/bass/index.ts +0 -373
  153. package/src/generate/countermelody/index.ts +0 -470
  154. package/src/generate/drums/beat.ts +0 -266
  155. package/src/generate/drums/euclid.ts +0 -65
  156. package/src/generate/drums/fills.ts +0 -356
  157. package/src/generate/drums/ghost.ts +0 -119
  158. package/src/generate/drums/hihat.ts +0 -285
  159. package/src/generate/drums/hit.ts +0 -31
  160. package/src/generate/drums/index.ts +0 -361
  161. package/src/generate/drums/internal.ts +0 -257
  162. package/src/generate/drums/kick.ts +0 -160
  163. package/src/generate/drums/percussion.ts +0 -121
  164. package/src/generate/drums/rng.ts +0 -7
  165. package/src/generate/drums/swing.ts +0 -71
  166. package/src/generate/groove/index.ts +0 -351
  167. package/src/generate/harmonize/index.ts +0 -451
  168. package/src/generate/index.ts +0 -58
  169. package/src/generate/motif/index.ts +0 -422
  170. package/src/generate/progression/index.ts +0 -362
  171. package/src/generate/reharmony/index.ts +0 -323
  172. package/src/generate/rhythm/index.ts +0 -175
  173. package/src/index.ts +0 -13
  174. package/src/model/chord.ts +0 -442
  175. package/src/model/index.ts +0 -15
  176. package/src/model/interval.ts +0 -82
  177. package/src/model/key.ts +0 -266
  178. package/src/model/note.ts +0 -176
  179. package/src/model/progression.ts +0 -161
  180. package/src/model/shared.ts +0 -12
  181. package/src/theory/chord/index.ts +0 -375
  182. package/src/theory/chordscale/index.ts +0 -374
  183. package/src/theory/counterpoint/index.ts +0 -230
  184. package/src/theory/harmony/index.ts +0 -77
  185. package/src/theory/index.ts +0 -112
  186. package/src/theory/safety/index.ts +0 -488
  187. package/src/theory/scale/degrees.ts +0 -123
  188. package/src/theory/scale/index.ts +0 -28
  189. package/src/theory/scale/key.ts +0 -56
  190. package/src/theory/scale/masks.ts +0 -153
  191. package/src/theory/spelling/index.ts +0 -249
  192. package/src/theory/symbol/index.ts +0 -268
  193. package/src/theory/voicing/index.ts +0 -5
  194. package/src/theory/voicing/internal.ts +0 -168
  195. package/src/theory/voicing/leading.ts +0 -112
  196. package/src/theory/voicing/satb.ts +0 -216
  197. package/src/theory/voicing/styled.ts +0 -199
  198. package/tsconfig.json +0 -23
@@ -1,339 +0,0 @@
1
- import type { KeyScale, NoteEvent } from '../../core/types.js';
2
- import { assertNoteEvents } from '../../core/validation/index.js';
3
- import type { Chord } from '../../theory/chord/index.js';
4
- import { chordPitchClasses, chordToneRole } from '../../theory/chord/index.js';
5
- import {
6
- createsVerticalDissonance,
7
- isLeadingToneResolution,
8
- } from '../../theory/counterpoint/index.js';
9
- import type { VoiceSnapshot } from '../../theory/safety/index.js';
10
-
11
- /**
12
- * A theory annotation attached to a note.
13
- *
14
- * @category Arrangement & Analysis
15
- */
16
- export type TheoryLabel =
17
- | { kind: 'chordTone'; role: 'root' | 'third' | 'fifth' | 'sixth' | 'seventh' }
18
- | { kind: 'tension'; degree: 9 | 11 | 13 }
19
- | { kind: 'avoid' }
20
- | { kind: 'passing' }
21
- | { kind: 'neighbor' }
22
- | { kind: 'suspension'; type: 'sus4-3' | 'sus7-6' | 'sus9-8' | 'sus2-3'; resolveTo: number }
23
- | { kind: 'anticipation' }
24
- | { kind: 'escape' }
25
- | { kind: 'needsResolution'; resolveTo: number }
26
- | { kind: 'leadingTone'; resolveTo: number };
27
-
28
- /**
29
- * A note with its theory labels and a short rationale.
30
- *
31
- * @category Arrangement & Analysis
32
- */
33
- export type AnalyzedNote = {
34
- noteId: number;
35
- labels: TheoryLabel[];
36
- rationale?: string;
37
- };
38
-
39
- /**
40
- * A single note in a monophonic voice: a {@link NoteEvent} with a stable id.
41
- *
42
- * @category Arrangement & Analysis
43
- */
44
- export type VoiceNote = NoteEvent & { id: number };
45
-
46
- /** Float tolerance for beat comparisons. */
47
- const EPS = 1e-9;
48
-
49
- function pitchClass(pitch: number): number {
50
- return ((Math.trunc(pitch) % 12) + 12) % 12;
51
- }
52
-
53
- function intervalAboveRoot(pitch: number, chord: Chord): number {
54
- return (((pitchClass(pitch) - pitchClass(chord.rootPc)) % 12) + 12) % 12;
55
- }
56
-
57
- /**
58
- * Interval class of a pitch above the actual sounding bass.
59
- *
60
- * Suspension figures (4-3, 7-6, 9-8) are named for the interval above the bass,
61
- * not the chord root, so this prefers the lowest pitch among the other sounding
62
- * voices when one lies below the note. Without a lower sounding voice it falls
63
- * back to the chord's bass pitch class (or root in root position).
64
- */
65
- function intervalAboveBass(pitch: number, others: VoiceSnapshot[], chord: Chord): number {
66
- let bass = Number.POSITIVE_INFINITY;
67
- for (const ov of others) {
68
- bass = Math.min(bass, ov.pitch);
69
- }
70
- if (Number.isFinite(bass) && bass < pitch) {
71
- return (((pitch - bass) % 12) + 12) % 12;
72
- }
73
- const ref = chord.bassPc ?? chord.rootPc;
74
- return (((pitchClass(pitch) - pitchClass(ref)) % 12) + 12) % 12;
75
- }
76
-
77
- /**
78
- * Map an interval class above the root to its extended-tension scale degree:
79
- * a flat/natural ninth (1 or 2) to 9, an eleventh (5 or 6) to 11, and a
80
- * thirteenth (8 or 9) to 13.
81
- */
82
- function tensionDegree(ic: number): 9 | 11 | 13 {
83
- if (ic === 1 || ic === 2) {
84
- return 9;
85
- }
86
- if (ic === 5 || ic === 6) {
87
- return 11;
88
- }
89
- return 13;
90
- }
91
-
92
- function isChordMember(pitch: number, chord: Chord | null): boolean {
93
- return chord ? chordPitchClasses(chord).includes(pitchClass(pitch)) : false;
94
- }
95
-
96
- function isStep(a: number, b: number): boolean {
97
- const d = Math.abs(a - b);
98
- return d === 1 || d === 2;
99
- }
100
-
101
- /**
102
- * Classify a suspension figure from the interval class above the sounding bass
103
- * (`ic`) and the resolution direction (`delta`, positive when resolving upward).
104
- */
105
- function suspensionType(ic: number, delta: number): TheoryLabel & { kind: 'suspension' } {
106
- let type: 'sus4-3' | 'sus7-6' | 'sus9-8' | 'sus2-3';
107
- if (delta > 0) {
108
- type = 'sus2-3';
109
- } else if (ic === 2) {
110
- type = 'sus9-8';
111
- } else if (ic === 10 || ic === 11) {
112
- type = 'sus7-6';
113
- } else {
114
- type = 'sus4-3';
115
- }
116
- return { kind: 'suspension', type, resolveTo: 0 };
117
- }
118
-
119
- function stepResolution(pitch: number, chord: Chord): number | undefined {
120
- for (let delta = 1; delta <= 2; delta += 1) {
121
- if (isChordMember(pitch - delta, chord)) {
122
- return pitch - delta;
123
- }
124
- }
125
- return undefined;
126
- }
127
-
128
- /**
129
- * Label every note of a voice with its theory roles.
130
- *
131
- * Each note is classified against the chord sounding at its beat. Chord tones
132
- * get a role label; non-chord tones are matched, in order, as suspensions
133
- * (prepared by an identical consonant pitch and resolving by step), passing
134
- * tones, neighbors, anticipations, and escape tones, then fall back to tension,
135
- * avoid, or an unresolved-dissonance label. Leading-tone resolutions are noted
136
- * additionally.
137
- *
138
- * The voice is expected to be monophonic (one note at a time). Notes sharing an
139
- * onset are treated as simultaneous cluster members, not melodic neighbors, so
140
- * they receive no melodic labels (suspension, passing, neighbor, anticipation,
141
- * escape) — only harmonic ones. Callers with truly polyphonic material should
142
- * split it into monophonic sub-voices first, as `analyzeArrangement` does.
143
- *
144
- * @param voice The monophonic voice, in time order.
145
- * @param chordAtBeat Chord sounding at a given beat, or null.
146
- * @param key Key context for leading-tone detection.
147
- * @param otherVoicesAtBeat Other sounding voices at a given beat.
148
- * @returns One annotation per input note.
149
- * @example
150
- * ```ts
151
- * import { analyzeVoice, makeChord, majorKey } from '@libraz/libcantus';
152
- * const voice = [
153
- * { id: 0, pitch: 60, startBeat: 0, durationBeat: 1 },
154
- * { id: 1, pitch: 62, startBeat: 1, durationBeat: 1 },
155
- * { id: 2, pitch: 64, startBeat: 2, durationBeat: 1 },
156
- * ];
157
- * const cMajor = makeChord(0, 'maj');
158
- * const labels = analyzeVoice(voice, () => cMajor, majorKey(0), () => []);
159
- * labels; // one AnalyzedNote per input note, in the same order
160
- * ```
161
- * @category Arrangement & Analysis
162
- */
163
- export function analyzeVoice(
164
- voice: VoiceNote[],
165
- chordAtBeat: (beat: number) => Chord | null,
166
- key: KeyScale,
167
- otherVoicesAtBeat: (beat: number) => VoiceSnapshot[],
168
- ): AnalyzedNote[] {
169
- assertNoteEvents(voice, 'voice notes', { allowNonPositiveDuration: true });
170
- const result: AnalyzedNote[] = [];
171
-
172
- for (let i = 0; i < voice.length; i += 1) {
173
- const note = voice[i];
174
- if (!note) {
175
- continue;
176
- }
177
- const prevNote = i > 0 ? voice[i - 1] : undefined;
178
- const nextNote = i + 1 < voice.length ? voice[i + 1] : undefined;
179
- // A neighboring note sharing this note's onset is a simultaneous cluster
180
- // member, not a melodic predecessor/successor; treating it as one would
181
- // fabricate passing/suspension figures, so it is dropped here.
182
- const prevEnd =
183
- prevNote === undefined
184
- ? Number.NEGATIVE_INFINITY
185
- : prevNote.startBeat + prevNote.durationBeat;
186
- const noteEnd = note.startBeat + note.durationBeat;
187
- const prev =
188
- prevNote !== undefined && Math.abs(prevEnd - note.startBeat) <= EPS ? prevNote : undefined;
189
- const next =
190
- nextNote !== undefined && Math.abs(noteEnd - nextNote.startBeat) <= EPS
191
- ? nextNote
192
- : undefined;
193
- const chord = chordAtBeat(note.startBeat);
194
- const labels: TheoryLabel[] = [];
195
- const member = isChordMember(note.pitch, chord);
196
- let handled = false;
197
-
198
- if (chord && member) {
199
- const role = chordToneRole(note.pitch, chord);
200
- if (role) {
201
- labels.push({ kind: 'chordTone', role });
202
- } else {
203
- const ic = intervalAboveRoot(note.pitch, chord);
204
- labels.push({ kind: 'tension', degree: tensionDegree(ic) });
205
- }
206
- handled = true;
207
- }
208
-
209
- if (!handled && chord && prev && next) {
210
- const prevChord = chordAtBeat(prev.startBeat);
211
- const prepared = prev.pitch === note.pitch && isChordMember(prev.pitch, prevChord);
212
- const resolves = isStep(next.pitch, note.pitch);
213
- const others = otherVoicesAtBeat(note.startBeat);
214
- const verticallyDissonant = others.some((ov) =>
215
- createsVerticalDissonance(note.pitch, ov.pitch, true),
216
- );
217
- if (prepared && verticallyDissonant && resolves) {
218
- const ic = intervalAboveBass(note.pitch, others, chord);
219
- const sus = suspensionType(ic, next.pitch - note.pitch);
220
- sus.resolveTo = next.pitch;
221
- labels.push(sus);
222
- handled = true;
223
- }
224
- }
225
-
226
- if (!handled && chord && prev && next) {
227
- const prevMember = isChordMember(prev.pitch, chordAtBeat(prev.startBeat));
228
- const nextMember = isChordMember(next.pitch, chordAtBeat(next.startBeat));
229
- const up = note.pitch - prev.pitch > 0;
230
- const contInto = next.pitch - note.pitch > 0;
231
- const sameDir = note.pitch !== prev.pitch && next.pitch !== note.pitch && up === contInto;
232
- if (
233
- prevMember &&
234
- nextMember &&
235
- !member &&
236
- isStep(note.pitch, prev.pitch) &&
237
- isStep(next.pitch, note.pitch) &&
238
- sameDir
239
- ) {
240
- labels.push({ kind: 'passing' });
241
- handled = true;
242
- }
243
- }
244
-
245
- if (
246
- !handled &&
247
- chord &&
248
- prev &&
249
- next &&
250
- !member &&
251
- prev.pitch === next.pitch &&
252
- isChordMember(prev.pitch, chordAtBeat(prev.startBeat)) &&
253
- isStep(note.pitch, prev.pitch)
254
- ) {
255
- labels.push({ kind: 'neighbor' });
256
- handled = true;
257
- }
258
-
259
- if (!handled && chord && next && !member && note.pitch === next.pitch) {
260
- const nextChord = chordAtBeat(next.startBeat);
261
- if (isChordMember(next.pitch, nextChord)) {
262
- labels.push({ kind: 'anticipation' });
263
- handled = true;
264
- }
265
- }
266
-
267
- if (!handled && chord && prev && next && !member) {
268
- const prevMember = isChordMember(prev.pitch, chordAtBeat(prev.startBeat));
269
- const nextMember = isChordMember(next.pitch, chordAtBeat(next.startBeat));
270
- const stepFromPrev = isStep(note.pitch, prev.pitch);
271
- const leapToNext = Math.abs(next.pitch - note.pitch) >= 3;
272
- const opposite = note.pitch - prev.pitch > 0 !== next.pitch - note.pitch > 0;
273
- if (prevMember && nextMember && stepFromPrev && leapToNext && opposite) {
274
- labels.push({ kind: 'escape' });
275
- handled = true;
276
- }
277
- }
278
-
279
- if (!handled && chord && !member) {
280
- const ic = intervalAboveRoot(note.pitch, chord);
281
- const isTension = ic === 2 || ic === 5 || ic === 9;
282
- const avoid =
283
- (ic === 5 && chord.intervals.includes(4) && !chord.intervals.includes(5)) ||
284
- (ic === 11 && chord.intervals.includes(4) && chord.intervals.includes(10));
285
- if (avoid) {
286
- labels.push({ kind: 'avoid' });
287
- const resolveTo = stepResolution(note.pitch, chord);
288
- if (resolveTo !== undefined) {
289
- labels.push({ kind: 'needsResolution', resolveTo });
290
- }
291
- } else if (isTension) {
292
- labels.push({ kind: 'tension', degree: tensionDegree(ic) });
293
- } else {
294
- const resolveTo = stepResolution(note.pitch, chord);
295
- if (resolveTo !== undefined) {
296
- labels.push({ kind: 'needsResolution', resolveTo });
297
- }
298
- }
299
- }
300
-
301
- if (next && isLeadingToneResolution(note.pitch, next.pitch, key)) {
302
- labels.push({ kind: 'leadingTone', resolveTo: next.pitch });
303
- }
304
-
305
- result.push({ noteId: note.id, labels, rationale: describe(labels) });
306
- }
307
-
308
- return result;
309
- }
310
-
311
- /** Build a short rationale from a note's primary label. */
312
- function describe(labels: TheoryLabel[]): string {
313
- const primary = labels[0];
314
- if (!primary) {
315
- return 'Unclassified note';
316
- }
317
- switch (primary.kind) {
318
- case 'chordTone':
319
- return `Chord ${primary.role}`;
320
- case 'tension':
321
- return `${primary.degree}th tension`;
322
- case 'avoid':
323
- return 'Avoid note';
324
- case 'passing':
325
- return 'Passing tone between chord tones';
326
- case 'neighbor':
327
- return 'Neighbor tone';
328
- case 'suspension':
329
- return `Suspension (${primary.type})`;
330
- case 'anticipation':
331
- return 'Anticipation of the next chord';
332
- case 'escape':
333
- return 'Escape tone';
334
- case 'needsResolution':
335
- return 'Unresolved dissonance';
336
- case 'leadingTone':
337
- return 'Leading tone resolving to the tonic';
338
- }
339
- }
@@ -1,104 +0,0 @@
1
- import type { NoteEvent } from '../types.js';
2
- import { assertNoteEvents } from '../validation/index.js';
3
-
4
- /** A validated note retaining its position in the caller's original array. */
5
- export type IndexedNoteEvent = {
6
- note: NoteEvent;
7
- originalIndex: number;
8
- endBeat: number;
9
- };
10
-
11
- /** Binary-searchable, stable-sorted index over note onsets and active spans. */
12
- export type NoteEventIndex = {
13
- notes: IndexedNoteEvent[];
14
- /** Latest-onset note sounding at `beat`, with later input order winning ties. */
15
- at: (beat: number) => IndexedNoteEvent | undefined;
16
- /** Whether one or more notes attack at `beat`. */
17
- attacksAt: (beat: number) => boolean;
18
- /** Unique attack beats strictly inside `(startBeat, endBeat)`. */
19
- onsetsBetween: (startBeat: number, endBeat: number) => number[];
20
- };
21
-
22
- const EPS = 1e-9;
23
-
24
- function upperBound(values: IndexedNoteEvent[], beat: number): number {
25
- let low = 0;
26
- let high = values.length;
27
- while (low < high) {
28
- const middle = Math.floor((low + high) / 2);
29
- if ((values[middle]?.note.startBeat ?? Number.POSITIVE_INFINITY) <= beat) {
30
- low = middle + 1;
31
- } else {
32
- high = middle;
33
- }
34
- }
35
- return low;
36
- }
37
-
38
- /**
39
- * Validate and stable-sort note events once, then expose logarithmic onset and
40
- * active-note lookups. Non-positive-duration notes may be retained for callers
41
- * that intentionally filter them later, but never count as sounding.
42
- */
43
- export function createNoteEventIndex(
44
- events: NoteEvent[],
45
- options: { allowNonPositiveDuration?: boolean } = {},
46
- ): NoteEventIndex {
47
- assertNoteEvents(events, 'note events', options);
48
- const notes = events
49
- .map((note, originalIndex) => ({
50
- note,
51
- originalIndex,
52
- endBeat: note.startBeat + note.durationBeat,
53
- }))
54
- .sort((a, b) => a.note.startBeat - b.note.startBeat || a.originalIndex - b.originalIndex);
55
- const prefixMaxEnd: number[] = [];
56
- let maxEnd = Number.NEGATIVE_INFINITY;
57
- for (const indexed of notes) {
58
- maxEnd = Math.max(maxEnd, indexed.endBeat);
59
- prefixMaxEnd.push(maxEnd);
60
- }
61
-
62
- return {
63
- notes,
64
- at(beat) {
65
- let index = upperBound(notes, beat + EPS) - 1;
66
- while (index >= 0) {
67
- if ((prefixMaxEnd[index] ?? Number.NEGATIVE_INFINITY) <= beat + EPS) {
68
- return undefined;
69
- }
70
- const indexed = notes[index];
71
- if (
72
- indexed &&
73
- indexed.note.durationBeat > 0 &&
74
- indexed.note.startBeat - EPS <= beat &&
75
- beat < indexed.endBeat - EPS
76
- ) {
77
- return indexed;
78
- }
79
- index -= 1;
80
- }
81
- return undefined;
82
- },
83
- attacksAt(beat) {
84
- const firstAfter = upperBound(notes, beat + EPS);
85
- const candidate = notes[firstAfter - 1];
86
- return candidate !== undefined && Math.abs(candidate.note.startBeat - beat) < EPS;
87
- },
88
- onsetsBetween(startBeat, endBeat) {
89
- const result: number[] = [];
90
- let index = upperBound(notes, startBeat + EPS);
91
- while (index < notes.length) {
92
- const onset = notes[index]?.note.startBeat;
93
- if (onset === undefined || onset >= endBeat - EPS) {
94
- break;
95
- }
96
- if (result[result.length - 1] !== onset) {
97
- result.push(onset);
98
- }
99
- index += 1;
100
- }
101
- return result;
102
- },
103
- };
104
- }
package/src/core/index.ts DELETED
@@ -1,61 +0,0 @@
1
- /**
2
- * Public API of the core layer. Re-exports every core-layer module;
3
- * also available from the package root.
4
- */
5
-
6
- export type { IndexedNoteEvent, NoteEventIndex } from './event-index/index.js';
7
- export { createNoteEventIndex } from './event-index/index.js';
8
- export {
9
- classifyInterval,
10
- IntervalQuality,
11
- isConsonantInterval,
12
- isPerfectInterval,
13
- } from './interval/index.js';
14
- export type { BarPosition, TimeSignature } from './meter/index.js';
15
- export {
16
- barPositionToBeat,
17
- beatsPerBar,
18
- beatToBarPosition,
19
- formatTimeSignature,
20
- isCompound,
21
- isStrongBeat,
22
- metricWeight,
23
- parseTimeSignature,
24
- pulsesPerBar,
25
- tuplet,
26
- } from './meter/index.js';
27
- export type { Note as NoteData, SpelledInterval } from './pitch/index.js';
28
- export {
29
- formatNote,
30
- midiToNote,
31
- noteToMidi,
32
- noteToPitchClass,
33
- parseNote,
34
- spelledInterval,
35
- } from './pitch/index.js';
36
- export type { Rng } from './random/index.js';
37
- export { createRng } from './random/index.js';
38
- export type { Tuning } from './tuning/index.js';
39
- export {
40
- centsBetweenFreq,
41
- centsOfSteps,
42
- edo,
43
- frequencyOf,
44
- JUST_RATIOS,
45
- justDeviationCents,
46
- nearestStep,
47
- ratioToCents,
48
- TWELVE_TET,
49
- } from './tuning/index.js';
50
- export type { KeyScale, NoteEvent } from './types.js';
51
- export {
52
- assertFiniteNumber,
53
- assertGenerationBudget,
54
- assertInteger,
55
- assertNoteEvent,
56
- assertNoteEvents,
57
- assertPositiveInt,
58
- assertRange,
59
- assertTimeSignature,
60
- DEFAULT_GENERATION_BUDGET,
61
- } from './validation/index.js';
@@ -1,81 +0,0 @@
1
- /**
2
- * Counterpoint classification of a harmonic interval.
3
- *
4
- * @category Pitch & Intervals
5
- */
6
- export enum IntervalQuality {
7
- PerfectConsonance = 0,
8
- ImperfectConsonance = 1,
9
- Dissonance = 2,
10
- }
11
-
12
- /** Reduce an interval to a simple interval class in the range [0, 11]. */
13
- function simpleInterval(semitones: number): number {
14
- return Math.abs(semitones) % 12;
15
- }
16
-
17
- /**
18
- * Classify an interval for counterpoint evaluation.
19
- *
20
- * The interval is reduced modulo 12, so compound intervals classify as their
21
- * simple equivalents. The perfect fourth (5) is context-dependent: with
22
- * `twoVoice` (the default) it classifies as a dissonance, matching
23
- * {@link isConsonantInterval}; otherwise it is an imperfect consonance.
24
- *
25
- * @param semitones Interval size in semitones (may be negative or compound).
26
- * @param twoVoice When true, the perfect fourth is treated as dissonant.
27
- * @returns The counterpoint quality of the interval.
28
- * @example
29
- * ```ts
30
- * import { classifyInterval, IntervalQuality } from '@libraz/libcantus';
31
- * classifyInterval(7); // IntervalQuality.PerfectConsonance
32
- * classifyInterval(5); // IntervalQuality.Dissonance (two-voice)
33
- * classifyInterval(5, false); // IntervalQuality.ImperfectConsonance
34
- * ```
35
- * @category Pitch & Intervals
36
- */
37
- export function classifyInterval(semitones: number, twoVoice = true): IntervalQuality {
38
- const pc = simpleInterval(semitones);
39
- if (pc === 0 || pc === 7) {
40
- return IntervalQuality.PerfectConsonance;
41
- }
42
- if (pc === 3 || pc === 4 || pc === 8 || pc === 9) {
43
- return IntervalQuality.ImperfectConsonance;
44
- }
45
- if (pc === 5) {
46
- return twoVoice ? IntervalQuality.Dissonance : IntervalQuality.ImperfectConsonance;
47
- }
48
- return IntervalQuality.Dissonance;
49
- }
50
-
51
- /**
52
- * Test whether an interval is a perfect interval (unison/octave or fifth).
53
- *
54
- * @param semitones Interval size in semitones (may be negative or compound).
55
- * @returns True for the unison/octave (0) and the perfect fifth (7) mod 12.
56
- * @category Pitch & Intervals
57
- */
58
- export function isPerfectInterval(semitones: number): boolean {
59
- const pc = simpleInterval(semitones);
60
- return pc === 0 || pc === 7;
61
- }
62
-
63
- /**
64
- * Test whether an interval is consonant.
65
- *
66
- * The consonant set is {0, 3, 4, 5, 7, 8, 9} mod 12. In two-voice counterpoint
67
- * the perfect fourth (5) is treated as a dissonance, so `twoVoice` set to true
68
- * excludes it.
69
- *
70
- * @param semitones Interval size in semitones (may be negative or compound).
71
- * @param twoVoice When true, the perfect fourth is treated as dissonant.
72
- * @returns True if the interval is consonant in the given context.
73
- * @category Pitch & Intervals
74
- */
75
- export function isConsonantInterval(semitones: number, twoVoice = true): boolean {
76
- const pc = simpleInterval(semitones);
77
- if (pc === 5) {
78
- return !twoVoice;
79
- }
80
- return pc === 0 || pc === 3 || pc === 4 || pc === 7 || pc === 8 || pc === 9;
81
- }