@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.
Files changed (201) hide show
  1. package/README.md +29 -20
  2. package/README_ja.md +268 -0
  3. package/dist/analyze/index.cjs +39 -1947
  4. package/dist/analyze/index.cjs.map +1 -1
  5. package/dist/analyze/index.d.cts +161 -91
  6. package/dist/analyze/index.d.ts +161 -91
  7. package/dist/analyze/index.js +19 -8
  8. package/dist/chunk-2A2QMA74.cjs +261 -0
  9. package/dist/chunk-2A2QMA74.cjs.map +1 -0
  10. package/dist/chunk-2PTFGVD7.js +261 -0
  11. package/dist/chunk-2PTFGVD7.js.map +1 -0
  12. package/dist/chunk-357ZDPVU.cjs +162 -0
  13. package/dist/chunk-357ZDPVU.cjs.map +1 -0
  14. package/dist/chunk-3KX76RLG.js +1548 -0
  15. package/dist/chunk-3KX76RLG.js.map +1 -0
  16. package/dist/chunk-4VZ5MZAM.cjs +889 -0
  17. package/dist/chunk-4VZ5MZAM.cjs.map +1 -0
  18. package/dist/{chunk-C5XYKUVG.js → chunk-7EONYVI5.js} +471 -171
  19. package/dist/chunk-7EONYVI5.js.map +1 -0
  20. package/dist/{chunk-IT2O4XFE.js → chunk-7JEP3MFY.js} +42 -34
  21. package/dist/chunk-7JEP3MFY.js.map +1 -0
  22. package/dist/{chunk-4MHA432G.js → chunk-AID75RN3.js} +572 -250
  23. package/dist/chunk-AID75RN3.js.map +1 -0
  24. package/dist/chunk-BSLTLGM3.cjs +497 -0
  25. package/dist/chunk-BSLTLGM3.cjs.map +1 -0
  26. package/dist/chunk-CU6VG2YM.cjs +109 -0
  27. package/dist/chunk-CU6VG2YM.cjs.map +1 -0
  28. package/dist/{chunk-EH6EM37U.js → chunk-DZMHIGI4.js} +81 -59
  29. package/dist/chunk-DZMHIGI4.js.map +1 -0
  30. package/dist/{chunk-AKLYPWIN.js → chunk-EDOTEABK.js} +45 -19
  31. package/dist/chunk-EDOTEABK.js.map +1 -0
  32. package/dist/{chunk-2ZWMSJH6.js → chunk-HN2OEUN2.js} +7 -4
  33. package/dist/chunk-HN2OEUN2.js.map +1 -0
  34. package/dist/{chunk-MIWO425M.js → chunk-KBTTEX7Q.js} +128 -43
  35. package/dist/chunk-KBTTEX7Q.js.map +1 -0
  36. package/dist/chunk-KQN2ZYND.cjs +724 -0
  37. package/dist/chunk-KQN2ZYND.cjs.map +1 -0
  38. package/dist/chunk-MPTVJIVG.cjs +3176 -0
  39. package/dist/chunk-MPTVJIVG.cjs.map +1 -0
  40. package/dist/chunk-MTMZTMWM.cjs +53 -0
  41. package/dist/chunk-MTMZTMWM.cjs.map +1 -0
  42. package/dist/{chunk-CHMXELDS.js → chunk-NNB67XA2.js} +372 -197
  43. package/dist/chunk-NNB67XA2.js.map +1 -0
  44. package/dist/chunk-OL3IHHSJ.cjs +1204 -0
  45. package/dist/chunk-OL3IHHSJ.cjs.map +1 -0
  46. package/dist/chunk-PI3UXRWK.cjs +528 -0
  47. package/dist/chunk-PI3UXRWK.cjs.map +1 -0
  48. package/dist/chunk-R2VVFAIN.js +528 -0
  49. package/dist/chunk-R2VVFAIN.js.map +1 -0
  50. package/dist/chunk-T64C7ZD7.cjs +1 -0
  51. package/dist/chunk-T64C7ZD7.cjs.map +1 -0
  52. package/dist/chunk-WFDXOLUB.cjs +337 -0
  53. package/dist/chunk-WFDXOLUB.cjs.map +1 -0
  54. package/dist/chunk-WTAN4ACG.cjs +1548 -0
  55. package/dist/chunk-WTAN4ACG.cjs.map +1 -0
  56. package/dist/chunk-YFJXS3AU.cjs +33 -0
  57. package/dist/chunk-YFJXS3AU.cjs.map +1 -0
  58. package/dist/{chunk-GOAW4VYL.js → chunk-YUE6TJ4M.js} +315 -145
  59. package/dist/chunk-YUE6TJ4M.js.map +1 -0
  60. package/dist/{chunk-62QZKCJQ.js → chunk-ZGLJM4A4.js} +10 -6
  61. package/dist/chunk-ZGLJM4A4.js.map +1 -0
  62. package/dist/core/index.cjs +140 -647
  63. package/dist/core/index.cjs.map +1 -1
  64. package/dist/core/index.d.cts +313 -41
  65. package/dist/core/index.d.ts +313 -41
  66. package/dist/core/index.js +64 -16
  67. package/dist/{function-DVH4Eusr.d.ts → function-CxixTB02.d.cts} +122 -10
  68. package/dist/{function-DYt4iKsn.d.cts → function-DTCt675z.d.ts} +122 -10
  69. package/dist/generate/index.cjs +28 -4085
  70. package/dist/generate/index.cjs.map +1 -1
  71. package/dist/generate/index.d.cts +374 -66
  72. package/dist/generate/index.d.ts +374 -66
  73. package/dist/generate/index.js +23 -9
  74. package/dist/{index-Jc0Ryi79.d.ts → index-B2Ulxxpo.d.cts} +21 -5
  75. package/dist/{index-CpOsP1y9.d.cts → index-BkYJ0xMq.d.cts} +18 -17
  76. package/dist/{index-CxqBQtH1.d.cts → index-BuaB-ypt.d.ts} +21 -5
  77. package/dist/index-CAN7v-kw.d.ts +199 -0
  78. package/dist/{index-Bt4tOUD9.d.ts → index-COw1Ky9g.d.ts} +93 -8
  79. package/dist/{index-Dikst44v.d.cts → index-Cfx8xgH0.d.ts} +14 -11
  80. package/dist/{index-DxvARIP2.d.ts → index-DRtIhlJj.d.ts} +18 -17
  81. package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
  82. package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
  83. package/dist/index-alrRninU.d.cts +199 -0
  84. package/dist/{index-BNlGP86A.d.cts → index-oTQbYYIs.d.cts} +93 -8
  85. package/dist/index-tR9jTn26.d.cts +275 -0
  86. package/dist/index-tR9jTn26.d.ts +275 -0
  87. package/dist/{index-Bg5dYMmZ.d.ts → index-x2vcsduE.d.cts} +14 -11
  88. package/dist/index.cjs +429 -7383
  89. package/dist/index.cjs.map +1 -1
  90. package/dist/index.d.cts +15 -15
  91. package/dist/index.d.ts +15 -15
  92. package/dist/index.js +121 -27
  93. package/dist/model/index.cjs +8 -2562
  94. package/dist/model/index.cjs.map +1 -1
  95. package/dist/model/index.d.cts +471 -81
  96. package/dist/model/index.d.ts +471 -81
  97. package/dist/model/index.js +6 -7
  98. package/dist/styled-Bl0ZK28h.d.ts +543 -0
  99. package/dist/styled-Crv6SA4S.d.cts +543 -0
  100. package/dist/theory/index.cjs +172 -1822
  101. package/dist/theory/index.cjs.map +1 -1
  102. package/dist/theory/index.d.cts +151 -233
  103. package/dist/theory/index.d.ts +151 -233
  104. package/dist/theory/index.js +30 -7
  105. package/dist/{types-MGP29r3t.d.cts → types-BN6_Q9Kl.d.cts} +11 -2
  106. package/dist/{types-MGP29r3t.d.ts → types-BN6_Q9Kl.d.ts} +11 -2
  107. package/package.json +34 -13
  108. package/dist/chunk-2ZWMSJH6.js.map +0 -1
  109. package/dist/chunk-4MHA432G.js.map +0 -1
  110. package/dist/chunk-62QZKCJQ.js.map +0 -1
  111. package/dist/chunk-AKLYPWIN.js.map +0 -1
  112. package/dist/chunk-C5XYKUVG.js.map +0 -1
  113. package/dist/chunk-CHMXELDS.js.map +0 -1
  114. package/dist/chunk-DYWFNC4A.js +0 -152
  115. package/dist/chunk-DYWFNC4A.js.map +0 -1
  116. package/dist/chunk-EH6EM37U.js.map +0 -1
  117. package/dist/chunk-FBQK3ZQN.js +0 -131
  118. package/dist/chunk-FBQK3ZQN.js.map +0 -1
  119. package/dist/chunk-GOAW4VYL.js.map +0 -1
  120. package/dist/chunk-I5R7ZBJS.js +0 -202
  121. package/dist/chunk-I5R7ZBJS.js.map +0 -1
  122. package/dist/chunk-IT2O4XFE.js.map +0 -1
  123. package/dist/chunk-MIWO425M.js.map +0 -1
  124. package/dist/chunk-SGU7SPLK.js +0 -924
  125. package/dist/chunk-SGU7SPLK.js.map +0 -1
  126. package/dist/index-CGMZNCx6.d.cts +0 -140
  127. package/dist/index-CGMZNCx6.d.ts +0 -140
  128. package/dist/index-CnZCo29u.d.cts +0 -108
  129. package/dist/index-JnF4v9bx.d.ts +0 -108
  130. package/dist/styled-DZ7GjShb.d.ts +0 -282
  131. package/dist/styled-ZTbdaVFM.d.cts +0 -282
  132. package/src/analyze/arrange/index.ts +0 -21
  133. package/src/analyze/arrange/internal.ts +0 -127
  134. package/src/analyze/arrange/tension.ts +0 -199
  135. package/src/analyze/arrange/tracks.ts +0 -312
  136. package/src/analyze/detect/index.ts +0 -272
  137. package/src/analyze/functional/borrowed.ts +0 -84
  138. package/src/analyze/functional/cadence.ts +0 -66
  139. package/src/analyze/functional/function.ts +0 -184
  140. package/src/analyze/functional/index.ts +0 -23
  141. package/src/analyze/functional/internal.ts +0 -29
  142. package/src/analyze/functional/roman.ts +0 -402
  143. package/src/analyze/index.ts +0 -49
  144. package/src/analyze/timeline/index.ts +0 -451
  145. package/src/analyze/voice/index.ts +0 -339
  146. package/src/core/event-index/index.ts +0 -104
  147. package/src/core/index.ts +0 -61
  148. package/src/core/interval/index.ts +0 -81
  149. package/src/core/meter/index.ts +0 -302
  150. package/src/core/pitch/index.ts +0 -311
  151. package/src/core/random/index.ts +0 -76
  152. package/src/core/tuning/index.ts +0 -186
  153. package/src/core/types.ts +0 -29
  154. package/src/core/validation/index.ts +0 -119
  155. package/src/generate/bass/index.ts +0 -373
  156. package/src/generate/countermelody/index.ts +0 -470
  157. package/src/generate/drums/beat.ts +0 -266
  158. package/src/generate/drums/euclid.ts +0 -65
  159. package/src/generate/drums/fills.ts +0 -356
  160. package/src/generate/drums/ghost.ts +0 -119
  161. package/src/generate/drums/hihat.ts +0 -285
  162. package/src/generate/drums/hit.ts +0 -31
  163. package/src/generate/drums/index.ts +0 -361
  164. package/src/generate/drums/internal.ts +0 -257
  165. package/src/generate/drums/kick.ts +0 -160
  166. package/src/generate/drums/percussion.ts +0 -121
  167. package/src/generate/drums/rng.ts +0 -7
  168. package/src/generate/drums/swing.ts +0 -71
  169. package/src/generate/groove/index.ts +0 -351
  170. package/src/generate/harmonize/index.ts +0 -451
  171. package/src/generate/index.ts +0 -58
  172. package/src/generate/motif/index.ts +0 -422
  173. package/src/generate/progression/index.ts +0 -362
  174. package/src/generate/reharmony/index.ts +0 -323
  175. package/src/generate/rhythm/index.ts +0 -175
  176. package/src/index.ts +0 -13
  177. package/src/model/chord.ts +0 -442
  178. package/src/model/index.ts +0 -15
  179. package/src/model/interval.ts +0 -82
  180. package/src/model/key.ts +0 -266
  181. package/src/model/note.ts +0 -176
  182. package/src/model/progression.ts +0 -161
  183. package/src/model/shared.ts +0 -12
  184. package/src/theory/chord/index.ts +0 -375
  185. package/src/theory/chordscale/index.ts +0 -374
  186. package/src/theory/counterpoint/index.ts +0 -230
  187. package/src/theory/harmony/index.ts +0 -77
  188. package/src/theory/index.ts +0 -112
  189. package/src/theory/safety/index.ts +0 -488
  190. package/src/theory/scale/degrees.ts +0 -123
  191. package/src/theory/scale/index.ts +0 -28
  192. package/src/theory/scale/key.ts +0 -56
  193. package/src/theory/scale/masks.ts +0 -153
  194. package/src/theory/spelling/index.ts +0 -249
  195. package/src/theory/symbol/index.ts +0 -268
  196. package/src/theory/voicing/index.ts +0 -5
  197. package/src/theory/voicing/internal.ts +0 -168
  198. package/src/theory/voicing/leading.ts +0 -112
  199. package/src/theory/voicing/satb.ts +0 -216
  200. package/src/theory/voicing/styled.ts +0 -199
  201. 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
- }