@libraz/libcantus 0.9.0 → 0.9.2

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 (155) hide show
  1. package/README.md +25 -2
  2. package/dist/analyze/index.cjs +1697 -0
  3. package/dist/analyze/index.cjs.map +1 -0
  4. package/dist/analyze/index.d.cts +429 -0
  5. package/dist/analyze/index.d.ts +429 -0
  6. package/dist/analyze/index.js +51 -0
  7. package/dist/analyze/index.js.map +1 -0
  8. package/dist/chunk-5HTNMQW4.js +653 -0
  9. package/dist/chunk-5HTNMQW4.js.map +1 -0
  10. package/dist/chunk-7Y7WF64D.js +410 -0
  11. package/dist/chunk-7Y7WF64D.js.map +1 -0
  12. package/dist/chunk-A4532ZFR.js +259 -0
  13. package/dist/chunk-A4532ZFR.js.map +1 -0
  14. package/dist/chunk-BHUT2S5G.js +22 -0
  15. package/dist/chunk-BHUT2S5G.js.map +1 -0
  16. package/dist/chunk-CCBLVAPK.js +42 -0
  17. package/dist/chunk-CCBLVAPK.js.map +1 -0
  18. package/dist/chunk-COKTX2G5.js +907 -0
  19. package/dist/chunk-COKTX2G5.js.map +1 -0
  20. package/dist/chunk-DYWFNC4A.js +152 -0
  21. package/dist/chunk-DYWFNC4A.js.map +1 -0
  22. package/dist/chunk-GQKN5KDT.js +159 -0
  23. package/dist/chunk-GQKN5KDT.js.map +1 -0
  24. package/dist/chunk-LN7ZZMD2.js +530 -0
  25. package/dist/chunk-LN7ZZMD2.js.map +1 -0
  26. package/dist/chunk-MSWFZF7J.js +118 -0
  27. package/dist/chunk-MSWFZF7J.js.map +1 -0
  28. package/dist/chunk-R3S2LUBH.js +2722 -0
  29. package/dist/chunk-R3S2LUBH.js.map +1 -0
  30. package/dist/chunk-SDM3VJJ5.js +41 -0
  31. package/dist/chunk-SDM3VJJ5.js.map +1 -0
  32. package/dist/chunk-XTC3X6MS.js +816 -0
  33. package/dist/chunk-XTC3X6MS.js.map +1 -0
  34. package/dist/chunk-ZXW5YIWK.js +55 -0
  35. package/dist/chunk-ZXW5YIWK.js.map +1 -0
  36. package/dist/core/index.cjs +432 -0
  37. package/dist/core/index.cjs.map +1 -0
  38. package/dist/core/index.d.cts +210 -0
  39. package/dist/core/index.d.ts +210 -0
  40. package/dist/core/index.js +73 -0
  41. package/dist/core/index.js.map +1 -0
  42. package/dist/function-2bJW1Zp_.d.ts +187 -0
  43. package/dist/function-r2sh-jOO.d.cts +187 -0
  44. package/dist/generate/index.cjs +3787 -0
  45. package/dist/generate/index.cjs.map +1 -0
  46. package/dist/generate/index.d.cts +980 -0
  47. package/dist/generate/index.d.ts +980 -0
  48. package/dist/generate/index.js +52 -0
  49. package/dist/generate/index.js.map +1 -0
  50. package/dist/index-BMPxwjwV.d.ts +151 -0
  51. package/dist/index-BOBXBNwP.d.cts +157 -0
  52. package/dist/index-BdeQ1r_p.d.cts +56 -0
  53. package/dist/index-BreoJRw0.d.ts +127 -0
  54. package/dist/index-CGMZNCx6.d.cts +140 -0
  55. package/dist/index-CGMZNCx6.d.ts +140 -0
  56. package/dist/index-CM5W4rcm.d.ts +157 -0
  57. package/dist/index-CP3wAfEH.d.ts +56 -0
  58. package/dist/index-Cl7mhOFM.d.cts +164 -0
  59. package/dist/index-Cl7mhOFM.d.ts +164 -0
  60. package/dist/index-DZ1M21do.d.cts +127 -0
  61. package/dist/index-D_ZTUNKp.d.cts +151 -0
  62. package/dist/index.cjs +5277 -4893
  63. package/dist/index.cjs.map +1 -1
  64. package/dist/index.d.cts +14 -2621
  65. package/dist/index.d.ts +14 -2621
  66. package/dist/index.js +170 -6152
  67. package/dist/index.js.map +1 -1
  68. package/dist/model/index.cjs +2435 -0
  69. package/dist/model/index.cjs.map +1 -0
  70. package/dist/model/index.d.cts +630 -0
  71. package/dist/model/index.d.ts +630 -0
  72. package/dist/model/index.js +21 -0
  73. package/dist/model/index.js.map +1 -0
  74. package/dist/styled-BjXtOFNk.d.cts +282 -0
  75. package/dist/styled-D7BAx3S3.d.ts +282 -0
  76. package/dist/theory/index.cjs +1677 -0
  77. package/dist/theory/index.cjs.map +1 -0
  78. package/dist/theory/index.d.cts +535 -0
  79. package/dist/theory/index.d.ts +535 -0
  80. package/dist/theory/index.js +149 -0
  81. package/dist/theory/index.js.map +1 -0
  82. package/dist/types-MGP29r3t.d.cts +30 -0
  83. package/dist/types-MGP29r3t.d.ts +30 -0
  84. package/package.json +31 -1
  85. package/src/analyze/arrange/index.ts +21 -0
  86. package/src/analyze/arrange/internal.ts +125 -0
  87. package/src/analyze/arrange/tension.ts +187 -0
  88. package/src/analyze/arrange/tracks.ts +279 -0
  89. package/src/{detect → analyze/detect}/index.ts +82 -23
  90. package/src/analyze/functional/borrowed.ts +84 -0
  91. package/src/analyze/functional/cadence.ts +66 -0
  92. package/src/analyze/functional/function.ts +184 -0
  93. package/src/analyze/functional/index.ts +23 -0
  94. package/src/analyze/functional/internal.ts +29 -0
  95. package/src/analyze/functional/roman.ts +393 -0
  96. package/src/analyze/index.ts +49 -0
  97. package/src/{timeline → analyze/timeline}/index.ts +87 -24
  98. package/src/{analysis → analyze/voice}/index.ts +81 -12
  99. package/src/core/index.ts +47 -0
  100. package/src/{interval → core/interval}/index.ts +15 -1
  101. package/src/{meter → core/meter}/index.ts +108 -5
  102. package/src/{pitch → core/pitch}/index.ts +58 -3
  103. package/src/{random → core/random}/index.ts +13 -1
  104. package/src/{tuning → core/tuning}/index.ts +28 -1
  105. package/src/{bass → generate/bass}/index.ts +66 -17
  106. package/src/{countermelody → generate/countermelody}/index.ts +54 -24
  107. package/src/{drums → generate/drums}/fills.ts +31 -20
  108. package/src/{drums → generate/drums}/index.ts +127 -9
  109. package/src/{drums → generate/drums}/internal.ts +7 -1
  110. package/src/{drums → generate/drums}/rng.ts +2 -2
  111. package/src/{drums → generate/drums}/swing.ts +7 -2
  112. package/src/{groove → generate/groove}/index.ts +118 -22
  113. package/src/{harmonize → generate/harmonize}/index.ts +96 -23
  114. package/src/generate/index.ts +59 -0
  115. package/src/{motif → generate/motif}/index.ts +94 -17
  116. package/src/{progression → generate/progression}/index.ts +61 -27
  117. package/src/{reharmony → generate/reharmony}/index.ts +46 -15
  118. package/src/{rhythm → generate/rhythm}/index.ts +47 -13
  119. package/src/index.ts +130 -116
  120. package/src/model/chord.ts +423 -0
  121. package/src/model/index.ts +5 -947
  122. package/src/model/interval.ts +82 -0
  123. package/src/model/key.ts +266 -0
  124. package/src/model/note.ts +176 -0
  125. package/src/model/progression.ts +161 -0
  126. package/src/model/shared.ts +12 -0
  127. package/src/{chord → theory/chord}/index.ts +85 -4
  128. package/src/{chordscale → theory/chordscale}/index.ts +67 -9
  129. package/src/{counterpoint → theory/counterpoint}/index.ts +23 -2
  130. package/src/{harmony → theory/harmony}/index.ts +19 -2
  131. package/src/theory/index.ts +107 -0
  132. package/src/{safety → theory/safety}/index.ts +59 -12
  133. package/src/theory/scale/degrees.ts +123 -0
  134. package/src/theory/scale/index.ts +28 -0
  135. package/src/theory/scale/key.ts +56 -0
  136. package/src/theory/scale/masks.ts +153 -0
  137. package/src/{spelling → theory/spelling}/index.ts +21 -3
  138. package/src/{symbol → theory/symbol}/index.ts +91 -36
  139. package/src/theory/voicing/index.ts +5 -0
  140. package/src/theory/voicing/internal.ts +168 -0
  141. package/src/theory/voicing/leading.ts +112 -0
  142. package/src/theory/voicing/satb.ts +195 -0
  143. package/src/theory/voicing/styled.ts +176 -0
  144. package/src/arrange/index.ts +0 -409
  145. package/src/functional/index.ts +0 -602
  146. package/src/scale/index.ts +0 -215
  147. package/src/voicing/index.ts +0 -523
  148. /package/src/{types.ts → core/types.ts} +0 -0
  149. /package/src/{drums → generate/drums}/beat.ts +0 -0
  150. /package/src/{drums → generate/drums}/euclid.ts +0 -0
  151. /package/src/{drums → generate/drums}/ghost.ts +0 -0
  152. /package/src/{drums → generate/drums}/hihat.ts +0 -0
  153. /package/src/{drums → generate/drums}/hit.ts +0 -0
  154. /package/src/{drums → generate/drums}/kick.ts +0 -0
  155. /package/src/{drums → generate/drums}/percussion.ts +0 -0
@@ -1,7 +1,11 @@
1
+ import type { KeyScale } from '../../core/types.js';
1
2
  import { scaleTonesInDegreeOrder } from '../scale/index.js';
2
- import type { KeyScale } from '../types.js';
3
3
 
4
- /** Chord quality identifiers understood by the chord builder. */
4
+ /**
5
+ * Chord quality identifiers understood by the chord builder.
6
+ *
7
+ * @category Chords
8
+ */
5
9
  export type ChordQuality =
6
10
  | 'maj'
7
11
  | 'min'
@@ -34,12 +38,54 @@ export type ChordQuality =
34
38
  | '13'
35
39
  | '5';
36
40
 
37
- /** A chord expressed as a root pitch class plus semitone offsets. */
41
+ /**
42
+ * A bare spelled pitch used as an enharmonic hint: a diatonic letter
43
+ * (0..6 = C..B) plus a chromatic alteration (-2 double-flat .. +2 double-sharp).
44
+ * Mirrors the letter/alter half of the pitch module's `Note` without an octave.
45
+ *
46
+ * @category Chords
47
+ */
48
+ export type PitchSpelling = {
49
+ letter: number;
50
+ alter: number;
51
+ };
52
+
53
+ /**
54
+ * A chord expressed as a root pitch class plus semitone offsets.
55
+ *
56
+ * `rootSpelling`/`bassSpelling` are optional enharmonic hints recorded by
57
+ * parsers (e.g. `parseChordSymbol('Bb7')`) so formatters can reproduce the
58
+ * original spelling instead of defaulting to sharps. Consumers may ignore
59
+ * them; a hint is only trusted when its pitch class still matches the
60
+ * corresponding `rootPc`/`bassPc`.
61
+ *
62
+ * @category Chords
63
+ */
38
64
  export type Chord = {
39
65
  rootPc: number;
40
66
  quality: ChordQuality;
41
67
  intervals: number[];
42
68
  bassPc?: number;
69
+ rootSpelling?: PitchSpelling;
70
+ bassSpelling?: PitchSpelling;
71
+ };
72
+
73
+ /**
74
+ * A chord positioned in time: a root pitch class and quality placed at a beat,
75
+ * with optional harmonic-analysis hints. Serves as the neutral exchange record
76
+ * between progression generators and timeline analysers.
77
+ *
78
+ * @category Composition
79
+ */
80
+ export type ChordSpan = {
81
+ rootPc: number;
82
+ quality: ChordQuality;
83
+ startBeat: number;
84
+ bassPc?: number;
85
+ /** 0-based scale degree of the chord root, when known. */
86
+ degree?: number;
87
+ /** True when the chord is a secondary dominant tonicizing another degree. */
88
+ secondaryDominant?: boolean;
43
89
  };
44
90
 
45
91
  /** Semitone offsets from the root for each supported chord quality. */
@@ -81,7 +127,11 @@ function pitchClass(value: number): number {
81
127
  return ((Math.trunc(value) % 12) + 12) % 12;
82
128
  }
83
129
 
84
- /** All chord qualities the builder understands, longest templates last. */
130
+ /**
131
+ * All chord qualities the builder understands, longest templates last.
132
+ *
133
+ * @category Chords
134
+ */
85
135
  export function chordQualities(): ChordQuality[] {
86
136
  return Object.keys(QUALITY_INTERVALS) as ChordQuality[];
87
137
  }
@@ -97,6 +147,8 @@ export function chordQualities(): ChordQuality[] {
97
147
  * @param ext Chord quality to apply.
98
148
  * @param key Key context supplying the diatonic root.
99
149
  * @returns The constructed chord.
150
+ *
151
+ * @category Chords
100
152
  */
101
153
  export function chordFromDegree(degree: number, ext: ChordQuality, key: KeyScale): Chord {
102
154
  const tones = scaleTonesInDegreeOrder(key);
@@ -113,6 +165,15 @@ export function chordFromDegree(degree: number, ext: ChordQuality, key: KeyScale
113
165
  * @param quality Chord quality supplying the interval template.
114
166
  * @param bassPc Optional slash-chord bass pitch class.
115
167
  * @returns The constructed chord.
168
+ *
169
+ * @example
170
+ * ```ts
171
+ * import { makeChord } from '@libraz/libcantus';
172
+ * const cmaj7 = makeChord(0, 'maj7');
173
+ * // { rootPc: 0, quality: 'maj7', intervals: [0, 4, 7, 11] }
174
+ * ```
175
+ *
176
+ * @category Chords
116
177
  */
117
178
  export function makeChord(rootPc: number, quality: ChordQuality, bassPc?: number): Chord {
118
179
  const chord: Chord = {
@@ -131,6 +192,14 @@ export function makeChord(rootPc: number, quality: ChordQuality, bassPc?: number
131
192
  *
132
193
  * @param chord The chord to enumerate.
133
194
  * @returns The chord's pitch classes, sorted ascending in [0, 11].
195
+ *
196
+ * @example
197
+ * ```ts
198
+ * import { makeChord, chordPitchClasses } from '@libraz/libcantus';
199
+ * chordPitchClasses(makeChord(0, 'maj7')); // [0, 4, 7, 11]
200
+ * ```
201
+ *
202
+ * @category Chords
134
203
  */
135
204
  export function chordPitchClasses(chord: Chord): number[] {
136
205
  const set = new Set<number>();
@@ -156,6 +225,8 @@ function chordToneOffsets(chord: Chord): Set<number> {
156
225
  * @param pitch MIDI pitch or bare pitch class.
157
226
  * @param chord The chord providing the root reference.
158
227
  * @returns The chord-tone role, or null if the pitch has no basic role.
228
+ *
229
+ * @category Chords
159
230
  */
160
231
  export function chordToneRole(
161
232
  pitch: number,
@@ -275,6 +346,14 @@ function stackThirds(degree: number, key: KeyScale, size: 3 | 4): Chord {
275
346
  * @param degree 0-based scale degree of the chord root.
276
347
  * @param key Key/scale context.
277
348
  * @returns The diatonic triad.
349
+ *
350
+ * @example
351
+ * ```ts
352
+ * import { majorKey, diatonicTriad } from '@libraz/libcantus';
353
+ * const tonic = diatonicTriad(0, majorKey(0)); // C major triad (degree 0 of C major)
354
+ * ```
355
+ *
356
+ * @category Chords
278
357
  */
279
358
  export function diatonicTriad(degree: number, key: KeyScale): Chord {
280
359
  return stackThirds(degree, key, 3);
@@ -286,6 +365,8 @@ export function diatonicTriad(degree: number, key: KeyScale): Chord {
286
365
  * @param degree 0-based scale degree of the chord root.
287
366
  * @param key Key/scale context.
288
367
  * @returns The diatonic seventh chord.
368
+ *
369
+ * @category Chords
289
370
  */
290
371
  export function diatonicSeventh(degree: number, key: KeyScale): Chord {
291
372
  return stackThirds(degree, key, 4);
@@ -29,6 +29,8 @@ function maskHasPitchClass(mask: number, scaleRootPc: number, pc: number): boole
29
29
  * @param scaleMask The scale's 12-bit mode mask.
30
30
  * @param scaleRootPc The pitch class the mask is rooted on.
31
31
  * @returns True if the scale is a superset of the chord.
32
+ *
33
+ * @category Scales
32
34
  */
33
35
  export function scaleMatchesChord(
34
36
  chordPcs: number[],
@@ -43,37 +45,67 @@ export function scaleMatchesChord(
43
45
  return true;
44
46
  }
45
47
 
46
- /** A named scale rooted on a pitch class that fits over a chord. */
48
+ /**
49
+ * A named scale rooted on a pitch class that fits over a chord.
50
+ *
51
+ * @category Scales
52
+ */
47
53
  export type ChordScaleMatch = {
48
54
  name: string;
49
55
  rootPc: number;
50
56
  };
51
57
 
58
+ /**
59
+ * Scale names whose mask duplicates another {@link NAMED_SCALES} entry
60
+ * (major/ionian, naturalMinor/aeolian). Chord-scale analysis conventionally
61
+ * speaks in mode names, so these aliases are skipped and only the modal name
62
+ * is reported.
63
+ */
64
+ const ALIASED_SCALE_NAMES = new Set(['major', 'naturalMinor']);
65
+
66
+ /** Number of tones in a heptatonic (seven-note) scale. */
67
+ const HEPTATONIC_SIZE = 7;
68
+
52
69
  /**
53
70
  * List the scales that fit over a chord, best fit first.
54
71
  *
55
72
  * Only the chord root is considered as the scale root, matching the
56
73
  * conventional chord-scale relationship. Every entry of {@link NAMED_SCALES}
57
- * whose pitch-class set is a superset of the chord's is returned, ranked by
58
- * fewest extra scale tones beyond the chord, then by scale size (heptatonic
59
- * before larger scales), then by scale name. The chromatic scale is only
60
- * returned as a fallback when no other scale contains the chord.
74
+ * whose pitch-class set is a superset of the chord's is returned once per
75
+ * distinct pitch-class set (aliased masks such as major/ionian report only the
76
+ * modal name), ranked by fewest extra scale tones beyond the chord, then by
77
+ * scale size (heptatonic before larger scales), then by scale name. For bare
78
+ * triads and smaller chords, heptatonic scales rank before pentatonics and
79
+ * other sizes: a pentatonic adds no modal color over a triad, so the seven-note
80
+ * modes are the more useful answer. The chromatic scale is only returned as a
81
+ * fallback when no other scale contains the chord.
61
82
  *
62
83
  * @param chord The chord to fit scales over.
63
84
  * @returns The matching scales rooted on the chord root, best fit first.
85
+ *
86
+ * @example
87
+ * ```ts
88
+ * import { makeChord, chordScales } from '@libraz/libcantus';
89
+ * const scales = chordScales(makeChord(0, 'maj7'));
90
+ * // scales for Cmaj7, best fit first; each is { name, rootPc: 0 }
91
+ * ```
92
+ *
93
+ * @category Scales
64
94
  */
65
95
  export function chordScales(chord: Chord): ChordScaleMatch[] {
66
96
  const chordPcs = chordPitchClasses(chord);
67
97
  const rootPc = pitchClass(chord.rootPc);
68
98
  const ranked: { name: string; extra: number; size: number }[] = [];
99
+ const seenMasks = new Set<number>();
69
100
  for (const name of Object.keys(NAMED_SCALES)) {
70
- if (name === 'chromatic') {
101
+ if (name === 'chromatic' || ALIASED_SCALE_NAMES.has(name)) {
71
102
  continue;
72
103
  }
73
104
  const mask = NAMED_SCALES[name];
74
- if (mask === undefined) {
105
+ if (mask === undefined || seenMasks.has(mask)) {
75
106
  continue;
76
107
  }
108
+ seenMasks.add(mask);
77
109
  if (scaleMatchesChord(chordPcs, mask, rootPc)) {
78
110
  const size = popcount12(mask);
79
111
  ranked.push({ name, extra: size - chordPcs.length, size });
@@ -87,7 +119,11 @@ export function chordScales(chord: Chord): ChordScaleMatch[] {
87
119
  }
88
120
  return [];
89
121
  }
122
+ const preferHeptatonic = chordPcs.length <= 3;
90
123
  ranked.sort((a, b) => {
124
+ if (preferHeptatonic && (a.size === HEPTATONIC_SIZE) !== (b.size === HEPTATONIC_SIZE)) {
125
+ return a.size === HEPTATONIC_SIZE ? -1 : 1;
126
+ }
91
127
  if (a.extra !== b.extra) {
92
128
  return a.extra - b.extra;
93
129
  }
@@ -109,6 +145,8 @@ export function chordScales(chord: Chord): ChordScaleMatch[] {
109
145
  * @param chord The chord providing the chord tones.
110
146
  * @param scaleName A key of {@link NAMED_SCALES}.
111
147
  * @returns The avoid-note pitch classes, sorted ascending in [0, 11].
148
+ *
149
+ * @category Scales
112
150
  */
113
151
  export function avoidNotes(chord: Chord, scaleName: string): number[] {
114
152
  const mask = NAMED_SCALES[scaleName];
@@ -144,6 +182,14 @@ export function avoidNotes(chord: Chord, scaleName: string): number[] {
144
182
  * @param chord The chord providing the chord tones.
145
183
  * @param scaleName A key of {@link NAMED_SCALES}.
146
184
  * @returns The available-tension pitch classes, sorted ascending in [0, 11].
185
+ *
186
+ * @example
187
+ * ```ts
188
+ * import { makeChord, availableTensions } from '@libraz/libcantus';
189
+ * availableTensions(makeChord(0, 'maj7'), 'ionian'); // [2, 9] — color tones over Cmaj7
190
+ * ```
191
+ *
192
+ * @category Scales
147
193
  */
148
194
  export function availableTensions(chord: Chord, scaleName: string): number[] {
149
195
  const mask = NAMED_SCALES[scaleName];
@@ -167,7 +213,11 @@ export function availableTensions(chord: Chord, scaleName: string): number[] {
167
213
  return tensions.sort((a, b) => a - b);
168
214
  }
169
215
 
170
- /** A scale fit over a chord together with its avoid notes and tensions. */
216
+ /**
217
+ * A scale fit over a chord together with its avoid notes and tensions.
218
+ *
219
+ * @category Scales
220
+ */
171
221
  export type ChordScaleReportEntry = {
172
222
  name: string;
173
223
  rootPc: number;
@@ -184,6 +234,8 @@ export type ChordScaleReportEntry = {
184
234
  * @param chord The chord to analyze.
185
235
  * @param limit Optional maximum number of scales to report; all by default.
186
236
  * @returns One entry per reported scale, best fit first.
237
+ *
238
+ * @category Scales
187
239
  */
188
240
  export function chordScaleReport(chord: Chord, limit?: number): ChordScaleReportEntry[] {
189
241
  const matches = chordScales(chord);
@@ -196,7 +248,11 @@ export function chordScaleReport(chord: Chord, limit?: number): ChordScaleReport
196
248
  }));
197
249
  }
198
250
 
199
- /** A chord paired with the scale chosen for it by {@link scalesForChanges}. */
251
+ /**
252
+ * A chord paired with the scale chosen for it by {@link scalesForChanges}.
253
+ *
254
+ * @category Scales
255
+ */
200
256
  export type ScaleChoice = {
201
257
  chord: Chord;
202
258
  scale: ChordScaleMatch;
@@ -251,6 +307,8 @@ function symmetricDifferenceSize(a: Set<number>, b: Set<number>): number {
251
307
  *
252
308
  * @param chords The chord sequence to choose scales for.
253
309
  * @returns One scale choice per chord, in input order.
310
+ *
311
+ * @category Scales
254
312
  */
255
313
  export function scalesForChanges(chords: Chord[]): ScaleChoice[] {
256
314
  if (chords.length === 0) {
@@ -1,5 +1,5 @@
1
- import { isConsonantInterval } from '../interval/index.js';
2
- import type { KeyScale } from '../types.js';
1
+ import { isConsonantInterval } from '../../core/interval/index.js';
2
+ import type { KeyScale } from '../../core/types.js';
3
3
 
4
4
  /** Reduce a MIDI pitch (or pitch class) to a pitch class in [0, 11]. */
5
5
  function pitchClass(pitch: number): number {
@@ -12,6 +12,7 @@ function pitchClass(pitch: number): number {
12
12
  * @param upperPitch Pitch of the nominally higher voice.
13
13
  * @param lowerPitch Pitch of the nominally lower voice.
14
14
  * @returns True when the upper voice sits below the lower voice.
15
+ * @category Voicing & Counterpoint
15
16
  */
16
17
  export function createsVoiceCrossing(upperPitch: number, lowerPitch: number): boolean {
17
18
  return upperPitch < lowerPitch;
@@ -24,6 +25,7 @@ export function createsVoiceCrossing(upperPitch: number, lowerPitch: number): bo
24
25
  * @param b Second pitch.
25
26
  * @param twoVoice When true, the perfect fourth counts as dissonant.
26
27
  * @returns True when the vertical interval is dissonant.
28
+ * @category Voicing & Counterpoint
27
29
  */
28
30
  export function createsVerticalDissonance(a: number, b: number, twoVoice: boolean): boolean {
29
31
  return !isConsonantInterval(a - b, twoVoice);
@@ -39,6 +41,7 @@ export function createsVerticalDissonance(a: number, b: number, twoVoice: boolea
39
41
  * @param prev Starting pitch.
40
42
  * @param cur Ending pitch.
41
43
  * @returns True when the leap is forbidden in strict counterpoint.
44
+ * @category Voicing & Counterpoint
42
45
  */
43
46
  export function isForbiddenMelodicLeap(prev: number, cur: number): boolean {
44
47
  const semis = Math.abs(cur - prev);
@@ -78,6 +81,8 @@ function bothVoicesMove(aMove: number, bMove: number): boolean {
78
81
  * A fifth expanding to a twelfth counts (same perfect class); a fifth moving to
79
82
  * an octave does not (different perfect kinds — the direct/hidden case owned by
80
83
  * {@link createsHiddenParallelPerfect}).
84
+ *
85
+ * @category Voicing & Counterpoint
81
86
  */
82
87
  export function createsParallelPerfect(
83
88
  aPrev: number,
@@ -96,6 +101,15 @@ export function createsParallelPerfect(
96
101
  /**
97
102
  * Whether two voices move in consecutive parallel octaves (or unisons) by
98
103
  * similar motion.
104
+ *
105
+ * This is a strict subset of {@link createsParallelPerfect}: a similar-motion
106
+ * octave-to-octave is the perfect-class-zero case that predicate already flags
107
+ * (and it additionally catches the contrary-motion anti-parallel). Callers that
108
+ * tally parallel violations should therefore use {@link createsParallelPerfect}
109
+ * alone to avoid double counting; this predicate remains for callers wanting a
110
+ * dedicated similar-motion octave test.
111
+ *
112
+ * @category Voicing & Counterpoint
99
113
  */
100
114
  export function createsParallelOctave(
101
115
  aPrev: number,
@@ -112,6 +126,8 @@ export function createsParallelOctave(
112
126
  /**
113
127
  * Whether two voices move in consecutive parallel unisons — both landing on the
114
128
  * same pitch, having shared a pitch on the previous move.
129
+ *
130
+ * @category Voicing & Counterpoint
115
131
  */
116
132
  export function createsParallelUnison(
117
133
  aPrev: number,
@@ -132,6 +148,8 @@ export function createsParallelUnison(
132
148
  * The traditional step exception is applied: the approach is allowed when the
133
149
  * upper of the two voices moves by step, so only leaps into the perfect
134
150
  * interval are flagged.
151
+ *
152
+ * @category Voicing & Counterpoint
135
153
  */
136
154
  export function createsHiddenParallelPerfect(
137
155
  aPrev: number,
@@ -168,6 +186,7 @@ export function createsHiddenParallelPerfect(
168
186
  * @param upperCur Current pitch of the upper voice.
169
187
  * @param lowerPrev Previous pitch of the lower voice.
170
188
  * @param lowerCur Current pitch of the lower voice.
189
+ * @category Voicing & Counterpoint
171
190
  */
172
191
  export function createsVoiceOverlap(
173
192
  upperPrev: number,
@@ -186,6 +205,7 @@ export function createsVoiceOverlap(
186
205
  * @param upperPitch Pitch of the higher voice.
187
206
  * @param lowerPitch Pitch of the lower voice.
188
207
  * @param maxSemitones Maximum allowed spacing in semitones (default an octave).
208
+ * @category Voicing & Counterpoint
189
209
  */
190
210
  export function exceedsSpacing(upperPitch: number, lowerPitch: number, maxSemitones = 12): boolean {
191
211
  return Math.abs(upperPitch - lowerPitch) > maxSemitones;
@@ -198,6 +218,7 @@ export function exceedsSpacing(upperPitch: number, lowerPitch: number, maxSemito
198
218
  * @param cur The following pitch.
199
219
  * @param key Key context supplying the tonic.
200
220
  * @returns True when `prev` is the leading tone and `cur` is the tonic a step above.
221
+ * @category Voicing & Counterpoint
201
222
  */
202
223
  export function isLeadingToneResolution(prev: number, cur: number, key: KeyScale): boolean {
203
224
  const tonic = pitchClass(key.rootPc);
@@ -1,6 +1,10 @@
1
1
  import type { Chord } from '../chord/index.js';
2
2
 
3
- /** Harmonic role a pitch plays within a chord. */
3
+ /**
4
+ * Harmonic role a pitch plays within a chord.
5
+ *
6
+ * @category Functional Harmony
7
+ */
4
8
  export type HarmonyRole = 'root' | 'third' | 'fifth' | 'sixth' | 'seventh' | 'tension' | 'doubling';
5
9
 
6
10
  /**
@@ -9,10 +13,16 @@ export type HarmonyRole = 'root' | 'third' | 'fifth' | 'sixth' | 'seventh' | 'te
9
13
  * - `identity`: moving it produces a different chord (the root).
10
14
  * - `quality`: moving it flips the chord quality (the third).
11
15
  * - `voicing`: it can move freely without changing chord identity or quality.
16
+ *
17
+ * @category Functional Harmony
12
18
  */
13
19
  export type LockLevel = 'identity' | 'quality' | 'voicing';
14
20
 
15
- /** A pitch's role, lock level, and owning chord. */
21
+ /**
22
+ * A pitch's role, lock level, and owning chord.
23
+ *
24
+ * @category Functional Harmony
25
+ */
16
26
  export type VoicedRole = {
17
27
  role: HarmonyRole;
18
28
  lock: LockLevel;
@@ -36,6 +46,13 @@ export type VoicedRole = {
36
46
  * @param chord The chord providing the root reference.
37
47
  * @param chordId Identifier stored on the result (defaults to 0).
38
48
  * @returns The pitch's role, lock level, and owning chord id.
49
+ * @example
50
+ * ```ts
51
+ * import { roleOf, makeChord } from '@libraz/libcantus';
52
+ * roleOf(64, makeChord(0, 'maj')); // E over C major
53
+ * // { role: 'third', lock: 'quality', belongsToChordId: 0 }
54
+ * ```
55
+ * @category Functional Harmony
39
56
  */
40
57
  export function roleOf(pitch: number, chord: Chord, chordId = 0): VoicedRole {
41
58
  const interval = (((Math.trunc(pitch) - chord.rootPc) % 12) + 12) % 12;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Public API of the theory layer. Re-exports every theory-layer module;
3
+ * also available from the package root.
4
+ */
5
+ export type { Chord as ChordData, ChordQuality } from './chord/index.js';
6
+ export {
7
+ chordFromDegree,
8
+ chordPitchClasses,
9
+ chordQualities,
10
+ chordToneRole,
11
+ diatonicSeventh,
12
+ diatonicTriad,
13
+ makeChord,
14
+ } from './chord/index.js';
15
+ export type {
16
+ ChordScaleMatch,
17
+ ChordScaleReportEntry,
18
+ ScaleChoice,
19
+ } from './chordscale/index.js';
20
+ export {
21
+ availableTensions,
22
+ avoidNotes,
23
+ chordScaleReport,
24
+ chordScales,
25
+ scaleMatchesChord,
26
+ scalesForChanges,
27
+ } from './chordscale/index.js';
28
+ export {
29
+ createsHiddenParallelPerfect,
30
+ createsParallelOctave,
31
+ createsParallelPerfect,
32
+ createsParallelUnison,
33
+ createsVerticalDissonance,
34
+ createsVoiceCrossing,
35
+ createsVoiceOverlap,
36
+ exceedsSpacing,
37
+ isForbiddenMelodicLeap,
38
+ isLeadingToneResolution,
39
+ } from './counterpoint/index.js';
40
+ export type { HarmonyRole, LockLevel, VoicedRole } from './harmony/index.js';
41
+ export { roleOf } from './harmony/index.js';
42
+ export type {
43
+ SafetyProfile,
44
+ SafetyQuery,
45
+ SafetyResult,
46
+ VoiceSnapshot,
47
+ } from './safety/index.js';
48
+ export {
49
+ enumerateSafePitches,
50
+ evaluateSafety,
51
+ NoteSafety,
52
+ ReasonFlag,
53
+ } from './safety/index.js';
54
+ export {
55
+ BLUES_MASK,
56
+ CHROMATIC_MASK,
57
+ DORIAN_MASK,
58
+ diatonicPitchClasses,
59
+ HARMONIC_MINOR_MASK,
60
+ isScaleTone,
61
+ LOCRIAN_MASK,
62
+ LYDIAN_MASK,
63
+ MAJOR_MASK,
64
+ MAJOR_PENTATONIC_MASK,
65
+ MELODIC_MINOR_MASK,
66
+ MINOR_PENTATONIC_MASK,
67
+ MIXOLYDIAN_MASK,
68
+ majorKey,
69
+ maskFromOffsets,
70
+ minorKey,
71
+ NAMED_SCALES,
72
+ NATURAL_MINOR_MASK,
73
+ nearestScaleTone,
74
+ OCTATONIC_HALF_WHOLE_MASK,
75
+ OCTATONIC_WHOLE_HALF_MASK,
76
+ PHRYGIAN_MASK,
77
+ pitchToScaleDegree,
78
+ scaleByName,
79
+ scaleTonesInDegreeOrder,
80
+ WHOLE_TONE_MASK,
81
+ } from './scale/index.js';
82
+ export {
83
+ noteNames,
84
+ spellChord,
85
+ spellPitchClass,
86
+ spellPitchClasses,
87
+ spellScale,
88
+ } from './spelling/index.js';
89
+ export {
90
+ formatChordSymbol,
91
+ parseChordSymbol,
92
+ transposeChordSymbol,
93
+ } from './symbol/index.js';
94
+ export type {
95
+ StyledVoicingOptions,
96
+ VoiceRange,
97
+ VoicingOptions,
98
+ VoicingStyle,
99
+ } from './voicing/index.js';
100
+ export {
101
+ nextVoicing,
102
+ SATB_RANGES,
103
+ voiceChord,
104
+ voiceChordStyled,
105
+ voiceLeadingCost,
106
+ voiceProgression,
107
+ } from './voicing/index.js';
@@ -1,26 +1,37 @@
1
+ import type { KeyScale } from '../../core/types.js';
1
2
  import type { Chord } from '../chord/index.js';
2
3
  import { chordPitchClasses } from '../chord/index.js';
3
4
  import {
4
5
  createsHiddenParallelPerfect,
5
- createsParallelOctave,
6
6
  createsParallelPerfect,
7
7
  createsVerticalDissonance,
8
8
  isForbiddenMelodicLeap,
9
9
  } from '../counterpoint/index.js';
10
10
  import { isScaleTone } from '../scale/index.js';
11
- import type { KeyScale } from '../types.js';
12
11
 
13
- /** Severity policy: `strict` counterpoint vs. lenient `pop` voice-leading. */
12
+ /**
13
+ * Severity policy: `strict` counterpoint vs. lenient `pop` voice-leading.
14
+ *
15
+ * @category Arrangement & Analysis
16
+ */
14
17
  export type SafetyProfile = 'strict' | 'pop';
15
18
 
16
- /** Overall placeability verdict for a candidate pitch. */
19
+ /**
20
+ * Overall placeability verdict for a candidate pitch.
21
+ *
22
+ * @category Arrangement & Analysis
23
+ */
17
24
  export enum NoteSafety {
18
25
  Safe = 0,
19
26
  Warning = 1,
20
27
  Dissonant = 2,
21
28
  }
22
29
 
23
- /** Bit flags describing why a pitch received its verdict. */
30
+ /**
31
+ * Bit flags describing why a pitch received its verdict.
32
+ *
33
+ * @category Arrangement & Analysis
34
+ */
24
35
  export enum ReasonFlag {
25
36
  ChordTone = 1 << 0,
26
37
  Tension = 1 << 1,
@@ -40,13 +51,21 @@ export enum ReasonFlag {
40
51
  NeedsResolution = 1 << 15,
41
52
  }
42
53
 
43
- /** A neighbouring voice's current and previous pitch. */
54
+ /**
55
+ * A neighbouring voice's current and previous pitch.
56
+ *
57
+ * @category Arrangement & Analysis
58
+ */
44
59
  export type VoiceSnapshot = {
45
60
  pitch: number;
46
61
  prevPitch?: number;
47
62
  };
48
63
 
49
- /** Everything needed to evaluate one candidate pitch. */
64
+ /**
65
+ * Everything needed to evaluate one candidate pitch.
66
+ *
67
+ * @category Arrangement & Analysis
68
+ */
50
69
  export type SafetyQuery = {
51
70
  profile: SafetyProfile;
52
71
  candidatePitch: number;
@@ -59,7 +78,11 @@ export type SafetyQuery = {
59
78
  vocalHigh?: number;
60
79
  };
61
80
 
62
- /** The verdict, reason flags, and optional resolution guidance. */
81
+ /**
82
+ * The verdict, reason flags, and optional resolution guidance.
83
+ *
84
+ * @category Arrangement & Analysis
85
+ */
63
86
  export type SafetyResult = {
64
87
  safety: NoteSafety;
65
88
  reasons: number;
@@ -115,6 +138,20 @@ function stepResolution(pitch: number, chord: Chord): number | undefined {
115
138
  *
116
139
  * @param q The candidate and its harmonic/voice-leading context.
117
140
  * @returns The verdict, reason bitmask, and optional resolution guidance.
141
+ * @example
142
+ * ```ts
143
+ * import { evaluateSafety, makeChord, majorKey, NoteSafety } from '@libraz/libcantus';
144
+ * const result = evaluateSafety({
145
+ * profile: 'pop',
146
+ * candidatePitch: 60, // C over a C major chord
147
+ * chord: makeChord(0, 'maj'),
148
+ * key: majorKey(0),
149
+ * otherVoices: [],
150
+ * strongBeat: true,
151
+ * });
152
+ * result.safety === NoteSafety.Safe; // true — C is a chord tone
153
+ * ```
154
+ * @category Arrangement & Analysis
118
155
  */
119
156
  export function evaluateSafety(q: SafetyQuery): SafetyResult {
120
157
  return evaluateInternal(q, true);
@@ -242,10 +279,9 @@ function evaluateInternal(q: SafetyQuery, collectSuggestions: boolean): SafetyRe
242
279
  if (ov.prevPitch === undefined) {
243
280
  continue;
244
281
  }
245
- if (
246
- createsParallelPerfect(prev, pitch, ov.prevPitch, ov.pitch) ||
247
- createsParallelOctave(prev, pitch, ov.prevPitch, ov.pitch)
248
- ) {
282
+ // createsParallelPerfect covers every perfect class (unison/octave and
283
+ // fifth), so no separate octave check is needed.
284
+ if (createsParallelPerfect(prev, pitch, ov.prevPitch, ov.pitch)) {
249
285
  reasons |= ReasonFlag.ParallelPerfect;
250
286
  raise(parallelSeverity);
251
287
  }
@@ -346,6 +382,17 @@ function describe(reasons: number, q: SafetyQuery): string {
346
382
  * @param pitchLow Lowest MIDI pitch to consider (inclusive).
347
383
  * @param pitchHigh Highest MIDI pitch to consider (inclusive).
348
384
  * @returns Placeable pitches (non-dissonant), chord tones before others, each group descending.
385
+ * @example
386
+ * ```ts
387
+ * import { enumerateSafePitches, makeChord, majorKey } from '@libraz/libcantus';
388
+ * const pitches = enumerateSafePitches(
389
+ * { profile: 'pop', chord: makeChord(0, 'maj'), key: majorKey(0), otherVoices: [], strongBeat: true },
390
+ * 60,
391
+ * 72,
392
+ * );
393
+ * pitches; // placeable pitches in [60, 72], chord tones first, each group descending
394
+ * ```
395
+ * @category Arrangement & Analysis
349
396
  */
350
397
  export function enumerateSafePitches(
351
398
  q: Omit<SafetyQuery, 'candidatePitch'>,