@libraz/libcantus 0.9.2 → 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 (200) hide show
  1. package/README.md +6 -5
  2. package/dist/analyze/index.cjs +41 -1676
  3. package/dist/analyze/index.cjs.map +1 -1
  4. package/dist/analyze/index.d.cts +194 -122
  5. package/dist/analyze/index.d.ts +194 -122
  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-R3S2LUBH.js → chunk-7NKDPF74.js} +660 -305
  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-7Y7WF64D.js → chunk-D2FAJBYL.js} +104 -35
  18. package/dist/chunk-D2FAJBYL.js.map +1 -0
  19. package/dist/{chunk-COKTX2G5.js → chunk-EHVEBWM5.js} +1052 -510
  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-GQKN5KDT.js → chunk-GQSGS2N4.js} +43 -29
  26. package/dist/chunk-GQSGS2N4.js.map +1 -0
  27. package/dist/chunk-HKGYCQ34.js +53 -0
  28. package/dist/chunk-HKGYCQ34.js.map +1 -0
  29. package/dist/chunk-I24MV65U.js +106 -0
  30. package/dist/chunk-I24MV65U.js.map +1 -0
  31. package/dist/{chunk-LN7ZZMD2.js → chunk-KMHOICIY.js} +322 -173
  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-N6IHQUOX.js +1 -0
  36. package/dist/chunk-N6IHQUOX.js.map +1 -0
  37. package/dist/chunk-NKIBQ6PT.js +30 -0
  38. package/dist/chunk-NKIBQ6PT.js.map +1 -0
  39. package/dist/chunk-NP4NA2FB.cjs +106 -0
  40. package/dist/chunk-NP4NA2FB.cjs.map +1 -0
  41. package/dist/chunk-NVE34YGZ.cjs +1449 -0
  42. package/dist/chunk-NVE34YGZ.cjs.map +1 -0
  43. package/dist/{chunk-A4532ZFR.js → chunk-NYYWMV2C.js} +115 -47
  44. package/dist/chunk-NYYWMV2C.js.map +1 -0
  45. package/dist/{chunk-5HTNMQW4.js → chunk-Q2XUP227.js} +310 -118
  46. package/dist/chunk-Q2XUP227.js.map +1 -0
  47. package/dist/chunk-Q6OP4U43.cjs +327 -0
  48. package/dist/chunk-Q6OP4U43.cjs.map +1 -0
  49. package/dist/chunk-RC6GBMCD.cjs +845 -0
  50. package/dist/chunk-RC6GBMCD.cjs.map +1 -0
  51. package/dist/{chunk-XTC3X6MS.js → chunk-RK35RIN7.js} +453 -152
  52. package/dist/chunk-RK35RIN7.js.map +1 -0
  53. package/dist/chunk-RUMU3UA6.cjs +53 -0
  54. package/dist/chunk-RUMU3UA6.cjs.map +1 -0
  55. package/dist/chunk-T64C7ZD7.cjs +1 -0
  56. package/dist/chunk-T64C7ZD7.cjs.map +1 -0
  57. package/dist/chunk-XEBG3G53.js +498 -0
  58. package/dist/chunk-XEBG3G53.js.map +1 -0
  59. package/dist/chunk-YLEMGEFH.js +246 -0
  60. package/dist/chunk-YLEMGEFH.js.map +1 -0
  61. package/dist/chunk-ZEJR4TKL.cjs +173 -0
  62. package/dist/chunk-ZEJR4TKL.cjs.map +1 -0
  63. package/dist/core/index.cjs +130 -431
  64. package/dist/core/index.cjs.map +1 -1
  65. package/dist/core/index.d.cts +331 -17
  66. package/dist/core/index.d.ts +331 -17
  67. package/dist/core/index.js +71 -13
  68. package/dist/{function-2bJW1Zp_.d.ts → function-BtiZb9i9.d.ts} +28 -7
  69. package/dist/{function-r2sh-jOO.d.cts → function-DNugAOrt.d.cts} +28 -7
  70. package/dist/generate/index.cjs +30 -3751
  71. package/dist/generate/index.cjs.map +1 -1
  72. package/dist/generate/index.d.cts +373 -59
  73. package/dist/generate/index.d.ts +373 -59
  74. package/dist/generate/index.js +23 -9
  75. package/dist/{index-DZ1M21do.d.cts → index-6VR8-kit.d.ts} +20 -4
  76. package/dist/index-70p168VM.d.ts +192 -0
  77. package/dist/{index-D_ZTUNKp.d.cts → index-Be74mcon.d.cts} +18 -17
  78. package/dist/index-BtXT6UQ0.d.cts +192 -0
  79. package/dist/{index-BMPxwjwV.d.ts → index-C5UBbQ4G.d.ts} +18 -17
  80. package/dist/{index-BOBXBNwP.d.cts → index-C83mZI1P.d.ts} +93 -8
  81. package/dist/{index-BdeQ1r_p.d.cts → index-D6b6na69.d.cts} +14 -11
  82. package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
  83. package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
  84. package/dist/index-OG65ocOr.d.cts +272 -0
  85. package/dist/index-OG65ocOr.d.ts +272 -0
  86. package/dist/{index-BreoJRw0.d.ts → index-tJhkmQgE.d.cts} +20 -4
  87. package/dist/{index-CP3wAfEH.d.ts → index-tSQh-xwY.d.ts} +14 -11
  88. package/dist/{index-CM5W4rcm.d.ts → index-y_ak9_Hi.d.cts} +93 -8
  89. package/dist/index.cjs +419 -6848
  90. package/dist/index.cjs.map +1 -1
  91. package/dist/index.d.cts +14 -13
  92. package/dist/index.d.ts +14 -13
  93. package/dist/index.js +132 -27
  94. package/dist/model/index.cjs +8 -2423
  95. package/dist/model/index.cjs.map +1 -1
  96. package/dist/model/index.d.cts +433 -74
  97. package/dist/model/index.d.ts +433 -74
  98. package/dist/model/index.js +6 -7
  99. package/dist/styled-DIRkk9Uz.d.ts +506 -0
  100. package/dist/styled-DJbkJQfj.d.cts +506 -0
  101. package/dist/theory/index.cjs +172 -1676
  102. package/dist/theory/index.cjs.map +1 -1
  103. package/dist/theory/index.d.cts +151 -233
  104. package/dist/theory/index.d.ts +151 -233
  105. package/dist/theory/index.js +31 -7
  106. package/dist/{types-MGP29r3t.d.cts → types-BN6_Q9Kl.d.cts} +11 -2
  107. package/dist/{types-MGP29r3t.d.ts → types-BN6_Q9Kl.d.ts} +11 -2
  108. package/package.json +73 -24
  109. package/dist/chunk-5HTNMQW4.js.map +0 -1
  110. package/dist/chunk-7Y7WF64D.js.map +0 -1
  111. package/dist/chunk-A4532ZFR.js.map +0 -1
  112. package/dist/chunk-BHUT2S5G.js +0 -22
  113. package/dist/chunk-BHUT2S5G.js.map +0 -1
  114. package/dist/chunk-CCBLVAPK.js +0 -42
  115. package/dist/chunk-CCBLVAPK.js.map +0 -1
  116. package/dist/chunk-COKTX2G5.js.map +0 -1
  117. package/dist/chunk-DYWFNC4A.js +0 -152
  118. package/dist/chunk-DYWFNC4A.js.map +0 -1
  119. package/dist/chunk-GQKN5KDT.js.map +0 -1
  120. package/dist/chunk-LN7ZZMD2.js.map +0 -1
  121. package/dist/chunk-MSWFZF7J.js +0 -118
  122. package/dist/chunk-MSWFZF7J.js.map +0 -1
  123. package/dist/chunk-R3S2LUBH.js.map +0 -1
  124. package/dist/chunk-SDM3VJJ5.js +0 -41
  125. package/dist/chunk-SDM3VJJ5.js.map +0 -1
  126. package/dist/chunk-XTC3X6MS.js.map +0 -1
  127. package/dist/chunk-ZXW5YIWK.js +0 -55
  128. package/dist/chunk-ZXW5YIWK.js.map +0 -1
  129. package/dist/index-CGMZNCx6.d.cts +0 -140
  130. package/dist/index-CGMZNCx6.d.ts +0 -140
  131. package/dist/styled-BjXtOFNk.d.cts +0 -282
  132. package/dist/styled-D7BAx3S3.d.ts +0 -282
  133. package/src/analyze/arrange/index.ts +0 -21
  134. package/src/analyze/arrange/internal.ts +0 -125
  135. package/src/analyze/arrange/tension.ts +0 -187
  136. package/src/analyze/arrange/tracks.ts +0 -279
  137. package/src/analyze/detect/index.ts +0 -235
  138. package/src/analyze/functional/borrowed.ts +0 -84
  139. package/src/analyze/functional/cadence.ts +0 -66
  140. package/src/analyze/functional/function.ts +0 -184
  141. package/src/analyze/functional/index.ts +0 -23
  142. package/src/analyze/functional/internal.ts +0 -29
  143. package/src/analyze/functional/roman.ts +0 -393
  144. package/src/analyze/index.ts +0 -49
  145. package/src/analyze/timeline/index.ts +0 -415
  146. package/src/analyze/voice/index.ts +0 -330
  147. package/src/core/index.ts +0 -47
  148. package/src/core/interval/index.ts +0 -81
  149. package/src/core/meter/index.ts +0 -282
  150. package/src/core/pitch/index.ts +0 -311
  151. package/src/core/random/index.ts +0 -53
  152. package/src/core/tuning/index.ts +0 -156
  153. package/src/core/types.ts +0 -29
  154. package/src/generate/bass/index.ts +0 -350
  155. package/src/generate/countermelody/index.ts +0 -379
  156. package/src/generate/drums/beat.ts +0 -266
  157. package/src/generate/drums/euclid.ts +0 -58
  158. package/src/generate/drums/fills.ts +0 -356
  159. package/src/generate/drums/ghost.ts +0 -119
  160. package/src/generate/drums/hihat.ts +0 -285
  161. package/src/generate/drums/hit.ts +0 -31
  162. package/src/generate/drums/index.ts +0 -329
  163. package/src/generate/drums/internal.ts +0 -257
  164. package/src/generate/drums/kick.ts +0 -160
  165. package/src/generate/drums/percussion.ts +0 -121
  166. package/src/generate/drums/rng.ts +0 -7
  167. package/src/generate/drums/swing.ts +0 -71
  168. package/src/generate/groove/index.ts +0 -326
  169. package/src/generate/harmonize/index.ts +0 -440
  170. package/src/generate/index.ts +0 -59
  171. package/src/generate/motif/index.ts +0 -398
  172. package/src/generate/progression/index.ts +0 -370
  173. package/src/generate/reharmony/index.ts +0 -330
  174. package/src/generate/rhythm/index.ts +0 -165
  175. package/src/index.ts +0 -247
  176. package/src/model/chord.ts +0 -423
  177. package/src/model/index.ts +0 -15
  178. package/src/model/interval.ts +0 -82
  179. package/src/model/key.ts +0 -266
  180. package/src/model/note.ts +0 -176
  181. package/src/model/progression.ts +0 -161
  182. package/src/model/shared.ts +0 -12
  183. package/src/theory/chord/index.ts +0 -373
  184. package/src/theory/chordscale/index.ts +0 -374
  185. package/src/theory/counterpoint/index.ts +0 -230
  186. package/src/theory/harmony/index.ts +0 -92
  187. package/src/theory/index.ts +0 -107
  188. package/src/theory/safety/index.ts +0 -419
  189. package/src/theory/scale/degrees.ts +0 -123
  190. package/src/theory/scale/index.ts +0 -28
  191. package/src/theory/scale/key.ts +0 -56
  192. package/src/theory/scale/masks.ts +0 -153
  193. package/src/theory/spelling/index.ts +0 -182
  194. package/src/theory/symbol/index.ts +0 -268
  195. package/src/theory/voicing/index.ts +0 -5
  196. package/src/theory/voicing/internal.ts +0 -168
  197. package/src/theory/voicing/leading.ts +0 -112
  198. package/src/theory/voicing/satb.ts +0 -195
  199. package/src/theory/voicing/styled.ts +0 -176
  200. package/tsconfig.json +0 -23
@@ -1,330 +0,0 @@
1
- import type { KeyScale, NoteEvent } from '../../core/types.js';
2
- import type { Chord } from '../../theory/chord/index.js';
3
- import { chordPitchClasses, chordToneRole } from '../../theory/chord/index.js';
4
- import {
5
- createsVerticalDissonance,
6
- isLeadingToneResolution,
7
- } from '../../theory/counterpoint/index.js';
8
- import type { VoiceSnapshot } from '../../theory/safety/index.js';
9
-
10
- /**
11
- * A theory annotation attached to a note.
12
- *
13
- * @category Arrangement & Analysis
14
- */
15
- export type TheoryLabel =
16
- | { kind: 'chordTone'; role: 'root' | 'third' | 'fifth' | 'sixth' | 'seventh' }
17
- | { kind: 'tension'; degree: 9 | 11 | 13 }
18
- | { kind: 'avoid' }
19
- | { kind: 'passing' }
20
- | { kind: 'neighbor' }
21
- | { kind: 'suspension'; type: 'sus4-3' | 'sus7-6' | 'sus9-8' | 'sus2-3'; resolveTo: number }
22
- | { kind: 'anticipation' }
23
- | { kind: 'escape' }
24
- | { kind: 'needsResolution'; resolveTo: number }
25
- | { kind: 'leadingTone'; resolveTo: number };
26
-
27
- /**
28
- * A note with its theory labels and a short rationale.
29
- *
30
- * @category Arrangement & Analysis
31
- */
32
- export type AnalyzedNote = {
33
- noteId: number;
34
- labels: TheoryLabel[];
35
- rationale?: string;
36
- };
37
-
38
- /**
39
- * A single note in a monophonic voice: a {@link NoteEvent} with a stable id.
40
- *
41
- * @category Arrangement & Analysis
42
- */
43
- export type VoiceNote = NoteEvent & { id: number };
44
-
45
- /** Float tolerance for beat comparisons. */
46
- const EPS = 1e-9;
47
-
48
- function pitchClass(pitch: number): number {
49
- return ((Math.trunc(pitch) % 12) + 12) % 12;
50
- }
51
-
52
- function intervalAboveRoot(pitch: number, chord: Chord): number {
53
- return (((pitchClass(pitch) - pitchClass(chord.rootPc)) % 12) + 12) % 12;
54
- }
55
-
56
- /**
57
- * Interval class of a pitch above the actual sounding bass.
58
- *
59
- * Suspension figures (4-3, 7-6, 9-8) are named for the interval above the bass,
60
- * not the chord root, so this prefers the lowest pitch among the other sounding
61
- * voices when one lies below the note. Without a lower sounding voice it falls
62
- * back to the chord's bass pitch class (or root in root position).
63
- */
64
- function intervalAboveBass(pitch: number, others: VoiceSnapshot[], chord: Chord): number {
65
- let bass = Number.POSITIVE_INFINITY;
66
- for (const ov of others) {
67
- bass = Math.min(bass, ov.pitch);
68
- }
69
- if (Number.isFinite(bass) && bass < pitch) {
70
- return (((pitch - bass) % 12) + 12) % 12;
71
- }
72
- const ref = chord.bassPc ?? chord.rootPc;
73
- return (((pitchClass(pitch) - pitchClass(ref)) % 12) + 12) % 12;
74
- }
75
-
76
- /**
77
- * Map an interval class above the root to its extended-tension scale degree:
78
- * a flat/natural ninth (1 or 2) to 9, an eleventh (5 or 6) to 11, and a
79
- * thirteenth (8 or 9) to 13.
80
- */
81
- function tensionDegree(ic: number): 9 | 11 | 13 {
82
- if (ic === 1 || ic === 2) {
83
- return 9;
84
- }
85
- if (ic === 5 || ic === 6) {
86
- return 11;
87
- }
88
- return 13;
89
- }
90
-
91
- function isChordMember(pitch: number, chord: Chord | null): boolean {
92
- return chord ? chordPitchClasses(chord).includes(pitchClass(pitch)) : false;
93
- }
94
-
95
- function isStep(a: number, b: number): boolean {
96
- const d = Math.abs(a - b);
97
- return d === 1 || d === 2;
98
- }
99
-
100
- /**
101
- * Classify a suspension figure from the interval class above the sounding bass
102
- * (`ic`) and the resolution direction (`delta`, positive when resolving upward).
103
- */
104
- function suspensionType(ic: number, delta: number): TheoryLabel & { kind: 'suspension' } {
105
- let type: 'sus4-3' | 'sus7-6' | 'sus9-8' | 'sus2-3';
106
- if (delta > 0) {
107
- type = 'sus2-3';
108
- } else if (ic === 2) {
109
- type = 'sus9-8';
110
- } else if (ic === 10 || ic === 11) {
111
- type = 'sus7-6';
112
- } else {
113
- type = 'sus4-3';
114
- }
115
- return { kind: 'suspension', type, resolveTo: 0 };
116
- }
117
-
118
- function stepResolution(pitch: number, chord: Chord): number | undefined {
119
- for (let delta = 1; delta <= 2; delta += 1) {
120
- if (isChordMember(pitch - delta, chord)) {
121
- return pitch - delta;
122
- }
123
- }
124
- return undefined;
125
- }
126
-
127
- /**
128
- * Label every note of a voice with its theory roles.
129
- *
130
- * Each note is classified against the chord sounding at its beat. Chord tones
131
- * get a role label; non-chord tones are matched, in order, as suspensions
132
- * (prepared by an identical consonant pitch and resolving by step), passing
133
- * tones, neighbors, anticipations, and escape tones, then fall back to tension,
134
- * avoid, or an unresolved-dissonance label. Leading-tone resolutions are noted
135
- * additionally.
136
- *
137
- * The voice is expected to be monophonic (one note at a time). Notes sharing an
138
- * onset are treated as simultaneous cluster members, not melodic neighbors, so
139
- * they receive no melodic labels (suspension, passing, neighbor, anticipation,
140
- * escape) — only harmonic ones. Callers with truly polyphonic material should
141
- * split it into monophonic sub-voices first, as `analyzeArrangement` does.
142
- *
143
- * @param voice The monophonic voice, in time order.
144
- * @param chordAtBeat Chord sounding at a given beat, or null.
145
- * @param key Key context for leading-tone detection.
146
- * @param otherVoicesAtBeat Other sounding voices at a given beat.
147
- * @returns One annotation per input note.
148
- * @example
149
- * ```ts
150
- * import { analyzeVoice, makeChord, majorKey } from '@libraz/libcantus';
151
- * const voice = [
152
- * { id: 0, pitch: 60, startBeat: 0, durationBeat: 1 },
153
- * { id: 1, pitch: 62, startBeat: 1, durationBeat: 1 },
154
- * { id: 2, pitch: 64, startBeat: 2, durationBeat: 1 },
155
- * ];
156
- * const cMajor = makeChord(0, 'maj');
157
- * const labels = analyzeVoice(voice, () => cMajor, majorKey(0), () => []);
158
- * labels; // one AnalyzedNote per input note, in the same order
159
- * ```
160
- * @category Arrangement & Analysis
161
- */
162
- export function analyzeVoice(
163
- voice: VoiceNote[],
164
- chordAtBeat: (beat: number) => Chord | null,
165
- key: KeyScale,
166
- otherVoicesAtBeat: (beat: number) => VoiceSnapshot[],
167
- ): AnalyzedNote[] {
168
- const result: AnalyzedNote[] = [];
169
-
170
- for (let i = 0; i < voice.length; i += 1) {
171
- const note = voice[i];
172
- if (!note) {
173
- continue;
174
- }
175
- const prevNote = i > 0 ? voice[i - 1] : undefined;
176
- const nextNote = i + 1 < voice.length ? voice[i + 1] : undefined;
177
- // A neighboring note sharing this note's onset is a simultaneous cluster
178
- // member, not a melodic predecessor/successor; treating it as one would
179
- // fabricate passing/suspension figures, so it is dropped here.
180
- const prev =
181
- prevNote !== undefined && prevNote.startBeat < note.startBeat - EPS ? prevNote : undefined;
182
- const next =
183
- nextNote !== undefined && nextNote.startBeat > note.startBeat + EPS ? nextNote : undefined;
184
- const chord = chordAtBeat(note.startBeat);
185
- const labels: TheoryLabel[] = [];
186
- const member = isChordMember(note.pitch, chord);
187
- let handled = false;
188
-
189
- if (chord && member) {
190
- const role = chordToneRole(note.pitch, chord);
191
- if (role) {
192
- labels.push({ kind: 'chordTone', role });
193
- } else {
194
- const ic = intervalAboveRoot(note.pitch, chord);
195
- labels.push({ kind: 'tension', degree: tensionDegree(ic) });
196
- }
197
- handled = true;
198
- }
199
-
200
- if (!handled && chord && prev && next) {
201
- const prevChord = chordAtBeat(prev.startBeat);
202
- const prepared = prev.pitch === note.pitch && isChordMember(prev.pitch, prevChord);
203
- const resolves = isStep(next.pitch, note.pitch);
204
- const others = otherVoicesAtBeat(note.startBeat);
205
- const verticallyDissonant = others.some((ov) =>
206
- createsVerticalDissonance(note.pitch, ov.pitch, true),
207
- );
208
- if (prepared && verticallyDissonant && resolves) {
209
- const ic = intervalAboveBass(note.pitch, others, chord);
210
- const sus = suspensionType(ic, next.pitch - note.pitch);
211
- sus.resolveTo = next.pitch;
212
- labels.push(sus);
213
- handled = true;
214
- }
215
- }
216
-
217
- if (!handled && chord && prev && next) {
218
- const prevMember = isChordMember(prev.pitch, chordAtBeat(prev.startBeat));
219
- const nextMember = isChordMember(next.pitch, chordAtBeat(next.startBeat));
220
- const up = note.pitch - prev.pitch > 0;
221
- const contInto = next.pitch - note.pitch > 0;
222
- const sameDir = note.pitch !== prev.pitch && next.pitch !== note.pitch && up === contInto;
223
- if (
224
- prevMember &&
225
- nextMember &&
226
- !member &&
227
- isStep(note.pitch, prev.pitch) &&
228
- isStep(next.pitch, note.pitch) &&
229
- sameDir
230
- ) {
231
- labels.push({ kind: 'passing' });
232
- handled = true;
233
- }
234
- }
235
-
236
- if (
237
- !handled &&
238
- chord &&
239
- prev &&
240
- next &&
241
- !member &&
242
- prev.pitch === next.pitch &&
243
- isChordMember(prev.pitch, chordAtBeat(prev.startBeat)) &&
244
- isStep(note.pitch, prev.pitch)
245
- ) {
246
- labels.push({ kind: 'neighbor' });
247
- handled = true;
248
- }
249
-
250
- if (!handled && chord && next && !member && note.pitch === next.pitch) {
251
- const nextChord = chordAtBeat(next.startBeat);
252
- if (isChordMember(next.pitch, nextChord)) {
253
- labels.push({ kind: 'anticipation' });
254
- handled = true;
255
- }
256
- }
257
-
258
- if (!handled && chord && prev && next && !member) {
259
- const prevMember = isChordMember(prev.pitch, chordAtBeat(prev.startBeat));
260
- const nextMember = isChordMember(next.pitch, chordAtBeat(next.startBeat));
261
- const stepFromPrev = isStep(note.pitch, prev.pitch);
262
- const leapToNext = Math.abs(next.pitch - note.pitch) >= 3;
263
- const opposite = note.pitch - prev.pitch > 0 !== next.pitch - note.pitch > 0;
264
- if (prevMember && nextMember && stepFromPrev && leapToNext && opposite) {
265
- labels.push({ kind: 'escape' });
266
- handled = true;
267
- }
268
- }
269
-
270
- if (!handled && chord && !member) {
271
- const ic = intervalAboveRoot(note.pitch, chord);
272
- const isTension = ic === 2 || ic === 5 || ic === 9;
273
- const avoid =
274
- (ic === 5 && chord.intervals.includes(4) && !chord.intervals.includes(5)) ||
275
- (ic === 11 && chord.intervals.includes(4) && chord.intervals.includes(10));
276
- if (avoid) {
277
- labels.push({ kind: 'avoid' });
278
- const resolveTo = stepResolution(note.pitch, chord);
279
- if (resolveTo !== undefined) {
280
- labels.push({ kind: 'needsResolution', resolveTo });
281
- }
282
- } else if (isTension) {
283
- labels.push({ kind: 'tension', degree: tensionDegree(ic) });
284
- } else {
285
- const resolveTo = stepResolution(note.pitch, chord);
286
- if (resolveTo !== undefined) {
287
- labels.push({ kind: 'needsResolution', resolveTo });
288
- }
289
- }
290
- }
291
-
292
- if (next && isLeadingToneResolution(note.pitch, next.pitch, key)) {
293
- labels.push({ kind: 'leadingTone', resolveTo: next.pitch });
294
- }
295
-
296
- result.push({ noteId: note.id, labels, rationale: describe(labels) });
297
- }
298
-
299
- return result;
300
- }
301
-
302
- /** Build a short rationale from a note's primary label. */
303
- function describe(labels: TheoryLabel[]): string {
304
- const primary = labels[0];
305
- if (!primary) {
306
- return 'Unclassified note';
307
- }
308
- switch (primary.kind) {
309
- case 'chordTone':
310
- return `Chord ${primary.role}`;
311
- case 'tension':
312
- return `${primary.degree}th tension`;
313
- case 'avoid':
314
- return 'Avoid note';
315
- case 'passing':
316
- return 'Passing tone between chord tones';
317
- case 'neighbor':
318
- return 'Neighbor tone';
319
- case 'suspension':
320
- return `Suspension (${primary.type})`;
321
- case 'anticipation':
322
- return 'Anticipation of the next chord';
323
- case 'escape':
324
- return 'Escape tone';
325
- case 'needsResolution':
326
- return 'Unresolved dissonance';
327
- case 'leadingTone':
328
- return 'Leading tone resolving to the tonic';
329
- }
330
- }
package/src/core/index.ts DELETED
@@ -1,47 +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
- export {
6
- classifyInterval,
7
- IntervalQuality,
8
- isConsonantInterval,
9
- isPerfectInterval,
10
- } from './interval/index.js';
11
- export type { BarPosition, TimeSignature } from './meter/index.js';
12
- export {
13
- barPositionToBeat,
14
- beatsPerBar,
15
- beatToBarPosition,
16
- formatTimeSignature,
17
- isCompound,
18
- isStrongBeat,
19
- metricWeight,
20
- parseTimeSignature,
21
- pulsesPerBar,
22
- tuplet,
23
- } from './meter/index.js';
24
- export type { Note as NoteData, SpelledInterval } from './pitch/index.js';
25
- export {
26
- formatNote,
27
- midiToNote,
28
- noteToMidi,
29
- noteToPitchClass,
30
- parseNote,
31
- spelledInterval,
32
- } from './pitch/index.js';
33
- export type { Rng } from './random/index.js';
34
- export { createRng } from './random/index.js';
35
- export type { Tuning } from './tuning/index.js';
36
- export {
37
- centsBetweenFreq,
38
- centsOfSteps,
39
- edo,
40
- frequencyOf,
41
- JUST_RATIOS,
42
- justDeviationCents,
43
- nearestStep,
44
- ratioToCents,
45
- TWELVE_TET,
46
- } from './tuning/index.js';
47
- export type { KeyScale, NoteEvent } from './types.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
- }