@libraz/libcantus 0.9.3 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/README.md +2 -2
  2. package/dist/analyze/index.cjs +39 -1947
  3. package/dist/analyze/index.cjs.map +1 -1
  4. package/dist/analyze/index.d.cts +197 -37
  5. package/dist/analyze/index.d.ts +197 -37
  6. package/dist/analyze/index.js +19 -8
  7. package/dist/chunk-36N73KZ3.cjs +246 -0
  8. package/dist/chunk-36N73KZ3.cjs.map +1 -0
  9. package/dist/chunk-4HLLZPXT.cjs +3077 -0
  10. package/dist/chunk-4HLLZPXT.cjs.map +1 -0
  11. package/dist/chunk-7MGZ7D62.cjs +498 -0
  12. package/dist/chunk-7MGZ7D62.cjs.map +1 -0
  13. package/dist/{chunk-4MHA432G.js → chunk-7NKDPF74.js} +403 -180
  14. package/dist/chunk-7NKDPF74.js.map +1 -0
  15. package/dist/chunk-CAN76NCU.cjs +679 -0
  16. package/dist/chunk-CAN76NCU.cjs.map +1 -0
  17. package/dist/{chunk-MIWO425M.js → chunk-D2FAJBYL.js} +101 -34
  18. package/dist/chunk-D2FAJBYL.js.map +1 -0
  19. package/dist/{chunk-SGU7SPLK.js → chunk-EHVEBWM5.js} +1056 -531
  20. package/dist/chunk-EHVEBWM5.js.map +1 -0
  21. package/dist/chunk-FCANXBAT.cjs +30 -0
  22. package/dist/chunk-FCANXBAT.cjs.map +1 -0
  23. package/dist/chunk-FNDWXRRI.cjs +479 -0
  24. package/dist/chunk-FNDWXRRI.cjs.map +1 -0
  25. package/dist/{chunk-IT2O4XFE.js → chunk-GQSGS2N4.js} +43 -24
  26. package/dist/chunk-GQSGS2N4.js.map +1 -0
  27. package/dist/{chunk-62QZKCJQ.js → chunk-HKGYCQ34.js} +10 -6
  28. package/dist/chunk-HKGYCQ34.js.map +1 -0
  29. package/dist/{chunk-AKLYPWIN.js → chunk-I24MV65U.js} +29 -6
  30. package/dist/chunk-I24MV65U.js.map +1 -0
  31. package/dist/{chunk-CHMXELDS.js → chunk-KMHOICIY.js} +311 -181
  32. package/dist/chunk-KMHOICIY.js.map +1 -0
  33. package/dist/chunk-LW53AHQU.cjs +1117 -0
  34. package/dist/chunk-LW53AHQU.cjs.map +1 -0
  35. package/dist/{chunk-2ZWMSJH6.js → chunk-NKIBQ6PT.js} +2 -2
  36. package/dist/chunk-NKIBQ6PT.js.map +1 -0
  37. package/dist/chunk-NP4NA2FB.cjs +106 -0
  38. package/dist/chunk-NP4NA2FB.cjs.map +1 -0
  39. package/dist/chunk-NVE34YGZ.cjs +1449 -0
  40. package/dist/chunk-NVE34YGZ.cjs.map +1 -0
  41. package/dist/{chunk-EH6EM37U.js → chunk-NYYWMV2C.js} +61 -49
  42. package/dist/chunk-NYYWMV2C.js.map +1 -0
  43. package/dist/{chunk-GOAW4VYL.js → chunk-Q2XUP227.js} +243 -117
  44. package/dist/chunk-Q2XUP227.js.map +1 -0
  45. package/dist/chunk-Q6OP4U43.cjs +327 -0
  46. package/dist/chunk-Q6OP4U43.cjs.map +1 -0
  47. package/dist/chunk-RC6GBMCD.cjs +845 -0
  48. package/dist/chunk-RC6GBMCD.cjs.map +1 -0
  49. package/dist/{chunk-C5XYKUVG.js → chunk-RK35RIN7.js} +370 -157
  50. package/dist/chunk-RK35RIN7.js.map +1 -0
  51. package/dist/chunk-RUMU3UA6.cjs +53 -0
  52. package/dist/chunk-RUMU3UA6.cjs.map +1 -0
  53. package/dist/chunk-T64C7ZD7.cjs +1 -0
  54. package/dist/chunk-T64C7ZD7.cjs.map +1 -0
  55. package/dist/chunk-XEBG3G53.js +498 -0
  56. package/dist/chunk-XEBG3G53.js.map +1 -0
  57. package/dist/{chunk-I5R7ZBJS.js → chunk-YLEMGEFH.js} +103 -59
  58. package/dist/chunk-YLEMGEFH.js.map +1 -0
  59. package/dist/chunk-ZEJR4TKL.cjs +173 -0
  60. package/dist/chunk-ZEJR4TKL.cjs.map +1 -0
  61. package/dist/core/index.cjs +130 -647
  62. package/dist/core/index.cjs.map +1 -1
  63. package/dist/core/index.d.cts +301 -39
  64. package/dist/core/index.d.ts +301 -39
  65. package/dist/core/index.js +54 -16
  66. package/dist/{function-DYt4iKsn.d.cts → function-BtiZb9i9.d.ts} +28 -7
  67. package/dist/{function-DVH4Eusr.d.ts → function-DNugAOrt.d.cts} +28 -7
  68. package/dist/generate/index.cjs +28 -4085
  69. package/dist/generate/index.cjs.map +1 -1
  70. package/dist/generate/index.d.cts +370 -62
  71. package/dist/generate/index.d.ts +370 -62
  72. package/dist/generate/index.js +23 -9
  73. package/dist/{index-CxqBQtH1.d.cts → index-6VR8-kit.d.ts} +19 -4
  74. package/dist/index-70p168VM.d.ts +192 -0
  75. package/dist/{index-CpOsP1y9.d.cts → index-Be74mcon.d.cts} +18 -17
  76. package/dist/index-BtXT6UQ0.d.cts +192 -0
  77. package/dist/{index-DxvARIP2.d.ts → index-C5UBbQ4G.d.ts} +18 -17
  78. package/dist/{index-Bt4tOUD9.d.ts → index-C83mZI1P.d.ts} +93 -8
  79. package/dist/{index-Bg5dYMmZ.d.ts → index-D6b6na69.d.cts} +14 -11
  80. package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
  81. package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
  82. package/dist/index-OG65ocOr.d.cts +272 -0
  83. package/dist/index-OG65ocOr.d.ts +272 -0
  84. package/dist/{index-Jc0Ryi79.d.ts → index-tJhkmQgE.d.cts} +19 -4
  85. package/dist/{index-Dikst44v.d.cts → index-tSQh-xwY.d.ts} +14 -11
  86. package/dist/{index-BNlGP86A.d.cts → index-y_ak9_Hi.d.cts} +93 -8
  87. package/dist/index.cjs +419 -7383
  88. package/dist/index.cjs.map +1 -1
  89. package/dist/index.d.cts +14 -14
  90. package/dist/index.d.ts +14 -14
  91. package/dist/index.js +111 -27
  92. package/dist/model/index.cjs +8 -2562
  93. package/dist/model/index.cjs.map +1 -1
  94. package/dist/model/index.d.cts +433 -75
  95. package/dist/model/index.d.ts +433 -75
  96. package/dist/model/index.js +6 -7
  97. package/dist/styled-DIRkk9Uz.d.ts +506 -0
  98. package/dist/styled-DJbkJQfj.d.cts +506 -0
  99. package/dist/theory/index.cjs +172 -1822
  100. package/dist/theory/index.cjs.map +1 -1
  101. package/dist/theory/index.d.cts +151 -233
  102. package/dist/theory/index.d.ts +151 -233
  103. package/dist/theory/index.js +30 -7
  104. package/dist/{types-MGP29r3t.d.cts → types-BN6_Q9Kl.d.cts} +11 -2
  105. package/dist/{types-MGP29r3t.d.ts → types-BN6_Q9Kl.d.ts} +11 -2
  106. package/package.json +24 -6
  107. package/dist/chunk-2ZWMSJH6.js.map +0 -1
  108. package/dist/chunk-4MHA432G.js.map +0 -1
  109. package/dist/chunk-62QZKCJQ.js.map +0 -1
  110. package/dist/chunk-AKLYPWIN.js.map +0 -1
  111. package/dist/chunk-C5XYKUVG.js.map +0 -1
  112. package/dist/chunk-CHMXELDS.js.map +0 -1
  113. package/dist/chunk-DYWFNC4A.js +0 -152
  114. package/dist/chunk-DYWFNC4A.js.map +0 -1
  115. package/dist/chunk-EH6EM37U.js.map +0 -1
  116. package/dist/chunk-FBQK3ZQN.js +0 -131
  117. package/dist/chunk-FBQK3ZQN.js.map +0 -1
  118. package/dist/chunk-GOAW4VYL.js.map +0 -1
  119. package/dist/chunk-I5R7ZBJS.js.map +0 -1
  120. package/dist/chunk-IT2O4XFE.js.map +0 -1
  121. package/dist/chunk-MIWO425M.js.map +0 -1
  122. package/dist/chunk-SGU7SPLK.js.map +0 -1
  123. package/dist/index-CGMZNCx6.d.cts +0 -140
  124. package/dist/index-CGMZNCx6.d.ts +0 -140
  125. package/dist/index-CnZCo29u.d.cts +0 -108
  126. package/dist/index-JnF4v9bx.d.ts +0 -108
  127. package/dist/styled-DZ7GjShb.d.ts +0 -282
  128. package/dist/styled-ZTbdaVFM.d.cts +0 -282
  129. package/src/analyze/arrange/index.ts +0 -21
  130. package/src/analyze/arrange/internal.ts +0 -127
  131. package/src/analyze/arrange/tension.ts +0 -199
  132. package/src/analyze/arrange/tracks.ts +0 -312
  133. package/src/analyze/detect/index.ts +0 -272
  134. package/src/analyze/functional/borrowed.ts +0 -84
  135. package/src/analyze/functional/cadence.ts +0 -66
  136. package/src/analyze/functional/function.ts +0 -184
  137. package/src/analyze/functional/index.ts +0 -23
  138. package/src/analyze/functional/internal.ts +0 -29
  139. package/src/analyze/functional/roman.ts +0 -402
  140. package/src/analyze/index.ts +0 -49
  141. package/src/analyze/timeline/index.ts +0 -451
  142. package/src/analyze/voice/index.ts +0 -339
  143. package/src/core/event-index/index.ts +0 -104
  144. package/src/core/index.ts +0 -61
  145. package/src/core/interval/index.ts +0 -81
  146. package/src/core/meter/index.ts +0 -302
  147. package/src/core/pitch/index.ts +0 -311
  148. package/src/core/random/index.ts +0 -76
  149. package/src/core/tuning/index.ts +0 -186
  150. package/src/core/types.ts +0 -29
  151. package/src/core/validation/index.ts +0 -119
  152. package/src/generate/bass/index.ts +0 -373
  153. package/src/generate/countermelody/index.ts +0 -470
  154. package/src/generate/drums/beat.ts +0 -266
  155. package/src/generate/drums/euclid.ts +0 -65
  156. package/src/generate/drums/fills.ts +0 -356
  157. package/src/generate/drums/ghost.ts +0 -119
  158. package/src/generate/drums/hihat.ts +0 -285
  159. package/src/generate/drums/hit.ts +0 -31
  160. package/src/generate/drums/index.ts +0 -361
  161. package/src/generate/drums/internal.ts +0 -257
  162. package/src/generate/drums/kick.ts +0 -160
  163. package/src/generate/drums/percussion.ts +0 -121
  164. package/src/generate/drums/rng.ts +0 -7
  165. package/src/generate/drums/swing.ts +0 -71
  166. package/src/generate/groove/index.ts +0 -351
  167. package/src/generate/harmonize/index.ts +0 -451
  168. package/src/generate/index.ts +0 -58
  169. package/src/generate/motif/index.ts +0 -422
  170. package/src/generate/progression/index.ts +0 -362
  171. package/src/generate/reharmony/index.ts +0 -323
  172. package/src/generate/rhythm/index.ts +0 -175
  173. package/src/index.ts +0 -13
  174. package/src/model/chord.ts +0 -442
  175. package/src/model/index.ts +0 -15
  176. package/src/model/interval.ts +0 -82
  177. package/src/model/key.ts +0 -266
  178. package/src/model/note.ts +0 -176
  179. package/src/model/progression.ts +0 -161
  180. package/src/model/shared.ts +0 -12
  181. package/src/theory/chord/index.ts +0 -375
  182. package/src/theory/chordscale/index.ts +0 -374
  183. package/src/theory/counterpoint/index.ts +0 -230
  184. package/src/theory/harmony/index.ts +0 -77
  185. package/src/theory/index.ts +0 -112
  186. package/src/theory/safety/index.ts +0 -488
  187. package/src/theory/scale/degrees.ts +0 -123
  188. package/src/theory/scale/index.ts +0 -28
  189. package/src/theory/scale/key.ts +0 -56
  190. package/src/theory/scale/masks.ts +0 -153
  191. package/src/theory/spelling/index.ts +0 -249
  192. package/src/theory/symbol/index.ts +0 -268
  193. package/src/theory/voicing/index.ts +0 -5
  194. package/src/theory/voicing/internal.ts +0 -168
  195. package/src/theory/voicing/leading.ts +0 -112
  196. package/src/theory/voicing/satb.ts +0 -216
  197. package/src/theory/voicing/styled.ts +0 -199
  198. package/tsconfig.json +0 -23
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Pitch spelling: letter-name notes, enharmonic-aware conversion, and spelled
3
+ * intervals.
4
+ *
5
+ * The rest of the library works in pitch classes (0..11), which cannot tell a
6
+ * G# from an Ab or an augmented fourth from a diminished fifth. This module adds
7
+ * an explicit spelling layer — a note is a diatonic letter plus a chromatic
8
+ * alteration — so interval quality (P/M/m/A/d) and enharmonic identity are
9
+ * preserved.
10
+ */
11
+ /**
12
+ * A spelled note: a diatonic letter, a chromatic alteration, and an optional
13
+ * octave.
14
+ *
15
+ * - `letter` is 0..6 for C..B.
16
+ * - `alter` is the number of semitones of alteration: -2 double-flat, -1 flat,
17
+ * 0 natural, +1 sharp, +2 double-sharp.
18
+ * - `octave` follows scientific pitch notation (middle C = C4 = MIDI 60). When
19
+ * absent, the note denotes a bare pitch class.
20
+ *
21
+ * @category Pitch & Intervals
22
+ */
23
+ type Note = {
24
+ letter: number;
25
+ alter: number;
26
+ octave?: number;
27
+ };
28
+ /**
29
+ * The quality label of a spelled interval: perfect, major, minor, or one or
30
+ * more augmentations or diminutions.
31
+ *
32
+ * This names how an interval is *spelled*. It is a different notion from
33
+ * {@link ConsonanceClass}, which classifies how an interval *sounds* against
34
+ * another voice.
35
+ *
36
+ * @see {@link ConsonanceClass}
37
+ * @category Pitch & Intervals
38
+ */
39
+ type IntervalQualityLabel = 'P' | 'M' | 'm' | `A${string}` | `d${string}`;
40
+ /**
41
+ * A spelled interval: a diatonic number, a quality label, and its semitone span.
42
+ *
43
+ * @category Pitch & Intervals
44
+ */
45
+ type SpelledInterval = {
46
+ /** Diatonic size: 1 = unison, 2 = second, ... 8 = octave, and beyond. */
47
+ number: number;
48
+ /** Quality label: 'P', 'M', 'm', or repeated 'A'/'d' for (multiply) aug/dim. */
49
+ quality: IntervalQualityLabel;
50
+ /** Signed semitone distance from the first note to the second. */
51
+ semitones: number;
52
+ };
53
+ /**
54
+ * Reduce a MIDI pitch, a bare pitch class, or any signed offset to a pitch
55
+ * class in [0, 11].
56
+ *
57
+ * This is the library's single definition of pitch-class arithmetic: every
58
+ * layer routes through it so a fractional pitch reduces the same way
59
+ * everywhere. A non-integral input is rounded to the nearest semitone first, so
60
+ * a microtonal 60.6 reads as C# rather than C.
61
+ *
62
+ * @param value A MIDI pitch, pitch class, or signed semitone offset.
63
+ * @returns The pitch class in [0, 11].
64
+ * @example
65
+ * ```ts
66
+ * import { pitchClassOf } from '@libraz/libcantus';
67
+ * pitchClassOf(61); // 1
68
+ * pitchClassOf(-1); // 11
69
+ * pitchClassOf(60.6); // 1
70
+ * ```
71
+ * @category Pitch & Intervals
72
+ */
73
+ declare function pitchClassOf(value: number): number;
74
+ /**
75
+ * Reduce a letter number or letter offset to a diatonic letter in [0, 6]
76
+ * (0 = C .. 6 = B).
77
+ *
78
+ * @param value A letter number or signed letter offset.
79
+ * @returns The diatonic letter in [0, 6].
80
+ * @category Pitch & Intervals
81
+ */
82
+ declare function diatonicLetterOf(value: number): number;
83
+ /**
84
+ * The pitch class of a diatonic letter with no accidental (C = 0, D = 2, ...).
85
+ *
86
+ * @param letter A letter number or signed letter offset; reduced to [0, 6].
87
+ * @returns The natural pitch class of that letter.
88
+ * @category Pitch & Intervals
89
+ */
90
+ declare function naturalPitchClassOf(letter: number): number;
91
+ /**
92
+ * Parse scientific pitch notation into a {@link Note}.
93
+ *
94
+ * Accepts a letter (case-insensitive) followed by any number of same-direction
95
+ * accidentals and an optional octave, e.g. `"C"`, `"C#4"`, `"Bb"`, `"F##3"`,
96
+ * `"Ebb2"`. Both `#` and `x` (double-sharp) are accepted for sharps; `b` for
97
+ * flats. Mixing sharps and flats (e.g. `"C#b"`) is rejected.
98
+ *
99
+ * @param text The note text.
100
+ * @returns The parsed note.
101
+ * @throws If the text is not a valid note.
102
+ * @example
103
+ * ```ts
104
+ * import { parseNote, noteToMidi } from '@libraz/libcantus';
105
+ * const n = parseNote('C#4');
106
+ * noteToMidi(n); // 61
107
+ * ```
108
+ * @category Pitch & Intervals
109
+ */
110
+ declare function parseNote(text: string): Note;
111
+ /**
112
+ * Render a {@link Note} as scientific pitch notation.
113
+ *
114
+ * @param note The note to format.
115
+ * @returns The note text, including the octave when present.
116
+ * @example
117
+ * ```ts
118
+ * import { formatNote, parseNote } from '@libraz/libcantus';
119
+ * formatNote(parseNote('C#4')); // 'C#4'
120
+ * ```
121
+ * @category Pitch & Intervals
122
+ */
123
+ declare function formatNote(note: Note): string;
124
+ /**
125
+ * Pitch class (0..11) of a note, ignoring octave.
126
+ *
127
+ * @param note The note.
128
+ * @returns The pitch class.
129
+ * @example
130
+ * ```ts
131
+ * import { noteToPitchClass, parseNote } from '@libraz/libcantus';
132
+ * noteToPitchClass(parseNote('Db')); // 1
133
+ * ```
134
+ * @category Pitch & Intervals
135
+ */
136
+ declare function noteToPitchClass(note: Note): number;
137
+ /**
138
+ * MIDI number of a note (middle C = C4 = 60).
139
+ *
140
+ * @param note The note; must carry an octave.
141
+ * @returns The MIDI number.
142
+ * @throws If the note has no octave.
143
+ * @example
144
+ * ```ts
145
+ * import { noteToMidi, parseNote } from '@libraz/libcantus';
146
+ * noteToMidi(parseNote('A4')); // 69
147
+ * ```
148
+ * @category Pitch & Intervals
149
+ */
150
+ declare function noteToMidi(note: Note): number;
151
+ /**
152
+ * Name a MIDI number as a {@link Note}, choosing sharp or flat spelling.
153
+ *
154
+ * The input is rounded to the nearest integer but is not clamped to the MIDI
155
+ * range 0..127: out-of-range values extrapolate linearly (e.g. `-1` -> `B-2`,
156
+ * `128` -> `G#9`) and remain an exact inverse of {@link noteToMidi}.
157
+ *
158
+ * @param midi The MIDI number.
159
+ * @param spelling Whether to prefer sharps or flats for black keys.
160
+ * @returns The spelled note, with octave.
161
+ * @example
162
+ * ```ts
163
+ * import { midiToNote, formatNote } from '@libraz/libcantus';
164
+ * formatNote(midiToNote(61)); // 'C#4'
165
+ * formatNote(midiToNote(61, 'flat')); // 'Db4'
166
+ * ```
167
+ * @category Pitch & Intervals
168
+ */
169
+ declare function midiToNote(midi: number, spelling?: 'sharp' | 'flat'): Note;
170
+ /**
171
+ * Transpose a note by a signed number of semitones, keeping its spelling.
172
+ *
173
+ * The letter moves by the diatonic distance of the conventional interval for
174
+ * that many semitones and the accidental is recomputed for the new letter, so
175
+ * enharmonic identity survives: `Ab4` up a major second is `Bb4`, not `A#4`, and
176
+ * the operation is an exact inverse of itself. An octave-less note stays
177
+ * octave-less.
178
+ *
179
+ * A bare semitone count cannot distinguish an augmented fourth from a diminished
180
+ * fifth; pass `spelling` to override the letter distance when the enharmonic
181
+ * choice matters.
182
+ *
183
+ * @param note The note to transpose.
184
+ * @param semitones The signed semitone offset.
185
+ * @param opts `spelling` forces the result onto the sharp or flat side instead
186
+ * of following the source note's letter.
187
+ * @returns The transposed note.
188
+ * @example
189
+ * ```ts
190
+ * import { transposeNote, formatNote, parseNote } from '@libraz/libcantus';
191
+ * formatNote(transposeNote(parseNote('Ab4'), 2)); // 'Bb4'
192
+ * formatNote(transposeNote(parseNote('Ab4'), 2, { spelling: 'sharp' })); // 'A#4'
193
+ * ```
194
+ * @category Pitch & Intervals
195
+ */
196
+ declare function transposeNote(note: Note, semitones: number, opts?: {
197
+ spelling?: 'sharp' | 'flat';
198
+ }): Note;
199
+ /**
200
+ * The semitone span of the interval named by a diatonic number and a quality.
201
+ *
202
+ * The inverse of the quality derivation in {@link spelledInterval}: it turns a
203
+ * name back into a distance, which is what transposing by a named interval or
204
+ * parsing one from text needs.
205
+ *
206
+ * @param numberValue Diatonic size: 1 = unison, 2 = second, ... 8 = octave.
207
+ * @param quality Quality label: `'P'`, `'M'`, `'m'`, or repeated `'A'`/`'d'`.
208
+ * @returns The unsigned semitone span.
209
+ * @throws If the quality cannot apply to the number, such as a major fifth.
210
+ * @example
211
+ * ```ts
212
+ * import { intervalSemitones } from '@libraz/libcantus';
213
+ * intervalSemitones(5, 'P'); // 7
214
+ * ```
215
+ * @category Pitch & Intervals
216
+ */
217
+ declare function intervalSemitones(numberValue: number, quality: IntervalQualityLabel): number;
218
+ /**
219
+ * Parse an interval name such as `'P5'`, `'m3'`, or `'AA4'`.
220
+ *
221
+ * @param name The interval name: a quality label followed by a diatonic number.
222
+ * @returns The spelled interval, with an ascending (positive) span.
223
+ * @throws If the name is not a quality label followed by a number, or the two
224
+ * cannot describe the same interval.
225
+ * @example
226
+ * ```ts
227
+ * import { parseInterval } from '@libraz/libcantus';
228
+ * parseInterval('m3'); // { number: 3, quality: 'm', semitones: 3 }
229
+ * ```
230
+ * @category Pitch & Intervals
231
+ */
232
+ declare function parseInterval(name: string): SpelledInterval;
233
+ /**
234
+ * Transpose a note by a spelled interval, keeping the spelling the interval
235
+ * names.
236
+ *
237
+ * Unlike {@link transposeNote}, which picks a letter from the semitone count,
238
+ * the diatonic number decides the letter here: transposing C by an augmented
239
+ * second gives D#, not Eb.
240
+ *
241
+ * @param note The note to transpose.
242
+ * @param interval The interval to apply; a negative span transposes downward.
243
+ * @returns The transposed note, octave-less if the source was.
244
+ * @example
245
+ * ```ts
246
+ * import { formatNote, parseInterval, parseNote, transposeByInterval } from '@libraz/libcantus';
247
+ * formatNote(transposeByInterval(parseNote('C4'), parseInterval('A2'))); // 'D#4'
248
+ * ```
249
+ * @category Pitch & Intervals
250
+ */
251
+ declare function transposeByInterval(note: Note, interval: SpelledInterval): Note;
252
+ /**
253
+ * The spelled interval from one note to another.
254
+ *
255
+ * When both notes carry octaves, the interval reflects their full signed
256
+ * distance (so `C4 -> C5` is a perfect octave). When either lacks an octave the
257
+ * interval is measured within a single ascending octave from `a` to `b`.
258
+ *
259
+ * @param a The lower/first note.
260
+ * @param b The upper/second note.
261
+ * @returns The diatonic number, quality, and semitone span.
262
+ * @example
263
+ * ```ts
264
+ * import { spelledInterval, parseNote } from '@libraz/libcantus';
265
+ * spelledInterval(parseNote('C4'), parseNote('G4'));
266
+ * // { number: 5, quality: 'P', semitones: 7 }
267
+ * ```
268
+ * @category Pitch & Intervals
269
+ */
270
+ declare function spelledInterval(a: Note, b: Note): SpelledInterval;
271
+
272
+ export { type IntervalQualityLabel as I, type Note as N, type SpelledInterval as S, noteToMidi as a, noteToPitchClass as b, parseNote as c, diatonicLetterOf as d, pitchClassOf as e, formatNote as f, transposeNote as g, intervalSemitones as i, midiToNote as m, naturalPitchClassOf as n, parseInterval as p, spelledInterval as s, transposeByInterval as t };
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Pitch spelling: letter-name notes, enharmonic-aware conversion, and spelled
3
+ * intervals.
4
+ *
5
+ * The rest of the library works in pitch classes (0..11), which cannot tell a
6
+ * G# from an Ab or an augmented fourth from a diminished fifth. This module adds
7
+ * an explicit spelling layer — a note is a diatonic letter plus a chromatic
8
+ * alteration — so interval quality (P/M/m/A/d) and enharmonic identity are
9
+ * preserved.
10
+ */
11
+ /**
12
+ * A spelled note: a diatonic letter, a chromatic alteration, and an optional
13
+ * octave.
14
+ *
15
+ * - `letter` is 0..6 for C..B.
16
+ * - `alter` is the number of semitones of alteration: -2 double-flat, -1 flat,
17
+ * 0 natural, +1 sharp, +2 double-sharp.
18
+ * - `octave` follows scientific pitch notation (middle C = C4 = MIDI 60). When
19
+ * absent, the note denotes a bare pitch class.
20
+ *
21
+ * @category Pitch & Intervals
22
+ */
23
+ type Note = {
24
+ letter: number;
25
+ alter: number;
26
+ octave?: number;
27
+ };
28
+ /**
29
+ * The quality label of a spelled interval: perfect, major, minor, or one or
30
+ * more augmentations or diminutions.
31
+ *
32
+ * This names how an interval is *spelled*. It is a different notion from
33
+ * {@link ConsonanceClass}, which classifies how an interval *sounds* against
34
+ * another voice.
35
+ *
36
+ * @see {@link ConsonanceClass}
37
+ * @category Pitch & Intervals
38
+ */
39
+ type IntervalQualityLabel = 'P' | 'M' | 'm' | `A${string}` | `d${string}`;
40
+ /**
41
+ * A spelled interval: a diatonic number, a quality label, and its semitone span.
42
+ *
43
+ * @category Pitch & Intervals
44
+ */
45
+ type SpelledInterval = {
46
+ /** Diatonic size: 1 = unison, 2 = second, ... 8 = octave, and beyond. */
47
+ number: number;
48
+ /** Quality label: 'P', 'M', 'm', or repeated 'A'/'d' for (multiply) aug/dim. */
49
+ quality: IntervalQualityLabel;
50
+ /** Signed semitone distance from the first note to the second. */
51
+ semitones: number;
52
+ };
53
+ /**
54
+ * Reduce a MIDI pitch, a bare pitch class, or any signed offset to a pitch
55
+ * class in [0, 11].
56
+ *
57
+ * This is the library's single definition of pitch-class arithmetic: every
58
+ * layer routes through it so a fractional pitch reduces the same way
59
+ * everywhere. A non-integral input is rounded to the nearest semitone first, so
60
+ * a microtonal 60.6 reads as C# rather than C.
61
+ *
62
+ * @param value A MIDI pitch, pitch class, or signed semitone offset.
63
+ * @returns The pitch class in [0, 11].
64
+ * @example
65
+ * ```ts
66
+ * import { pitchClassOf } from '@libraz/libcantus';
67
+ * pitchClassOf(61); // 1
68
+ * pitchClassOf(-1); // 11
69
+ * pitchClassOf(60.6); // 1
70
+ * ```
71
+ * @category Pitch & Intervals
72
+ */
73
+ declare function pitchClassOf(value: number): number;
74
+ /**
75
+ * Reduce a letter number or letter offset to a diatonic letter in [0, 6]
76
+ * (0 = C .. 6 = B).
77
+ *
78
+ * @param value A letter number or signed letter offset.
79
+ * @returns The diatonic letter in [0, 6].
80
+ * @category Pitch & Intervals
81
+ */
82
+ declare function diatonicLetterOf(value: number): number;
83
+ /**
84
+ * The pitch class of a diatonic letter with no accidental (C = 0, D = 2, ...).
85
+ *
86
+ * @param letter A letter number or signed letter offset; reduced to [0, 6].
87
+ * @returns The natural pitch class of that letter.
88
+ * @category Pitch & Intervals
89
+ */
90
+ declare function naturalPitchClassOf(letter: number): number;
91
+ /**
92
+ * Parse scientific pitch notation into a {@link Note}.
93
+ *
94
+ * Accepts a letter (case-insensitive) followed by any number of same-direction
95
+ * accidentals and an optional octave, e.g. `"C"`, `"C#4"`, `"Bb"`, `"F##3"`,
96
+ * `"Ebb2"`. Both `#` and `x` (double-sharp) are accepted for sharps; `b` for
97
+ * flats. Mixing sharps and flats (e.g. `"C#b"`) is rejected.
98
+ *
99
+ * @param text The note text.
100
+ * @returns The parsed note.
101
+ * @throws If the text is not a valid note.
102
+ * @example
103
+ * ```ts
104
+ * import { parseNote, noteToMidi } from '@libraz/libcantus';
105
+ * const n = parseNote('C#4');
106
+ * noteToMidi(n); // 61
107
+ * ```
108
+ * @category Pitch & Intervals
109
+ */
110
+ declare function parseNote(text: string): Note;
111
+ /**
112
+ * Render a {@link Note} as scientific pitch notation.
113
+ *
114
+ * @param note The note to format.
115
+ * @returns The note text, including the octave when present.
116
+ * @example
117
+ * ```ts
118
+ * import { formatNote, parseNote } from '@libraz/libcantus';
119
+ * formatNote(parseNote('C#4')); // 'C#4'
120
+ * ```
121
+ * @category Pitch & Intervals
122
+ */
123
+ declare function formatNote(note: Note): string;
124
+ /**
125
+ * Pitch class (0..11) of a note, ignoring octave.
126
+ *
127
+ * @param note The note.
128
+ * @returns The pitch class.
129
+ * @example
130
+ * ```ts
131
+ * import { noteToPitchClass, parseNote } from '@libraz/libcantus';
132
+ * noteToPitchClass(parseNote('Db')); // 1
133
+ * ```
134
+ * @category Pitch & Intervals
135
+ */
136
+ declare function noteToPitchClass(note: Note): number;
137
+ /**
138
+ * MIDI number of a note (middle C = C4 = 60).
139
+ *
140
+ * @param note The note; must carry an octave.
141
+ * @returns The MIDI number.
142
+ * @throws If the note has no octave.
143
+ * @example
144
+ * ```ts
145
+ * import { noteToMidi, parseNote } from '@libraz/libcantus';
146
+ * noteToMidi(parseNote('A4')); // 69
147
+ * ```
148
+ * @category Pitch & Intervals
149
+ */
150
+ declare function noteToMidi(note: Note): number;
151
+ /**
152
+ * Name a MIDI number as a {@link Note}, choosing sharp or flat spelling.
153
+ *
154
+ * The input is rounded to the nearest integer but is not clamped to the MIDI
155
+ * range 0..127: out-of-range values extrapolate linearly (e.g. `-1` -> `B-2`,
156
+ * `128` -> `G#9`) and remain an exact inverse of {@link noteToMidi}.
157
+ *
158
+ * @param midi The MIDI number.
159
+ * @param spelling Whether to prefer sharps or flats for black keys.
160
+ * @returns The spelled note, with octave.
161
+ * @example
162
+ * ```ts
163
+ * import { midiToNote, formatNote } from '@libraz/libcantus';
164
+ * formatNote(midiToNote(61)); // 'C#4'
165
+ * formatNote(midiToNote(61, 'flat')); // 'Db4'
166
+ * ```
167
+ * @category Pitch & Intervals
168
+ */
169
+ declare function midiToNote(midi: number, spelling?: 'sharp' | 'flat'): Note;
170
+ /**
171
+ * Transpose a note by a signed number of semitones, keeping its spelling.
172
+ *
173
+ * The letter moves by the diatonic distance of the conventional interval for
174
+ * that many semitones and the accidental is recomputed for the new letter, so
175
+ * enharmonic identity survives: `Ab4` up a major second is `Bb4`, not `A#4`, and
176
+ * the operation is an exact inverse of itself. An octave-less note stays
177
+ * octave-less.
178
+ *
179
+ * A bare semitone count cannot distinguish an augmented fourth from a diminished
180
+ * fifth; pass `spelling` to override the letter distance when the enharmonic
181
+ * choice matters.
182
+ *
183
+ * @param note The note to transpose.
184
+ * @param semitones The signed semitone offset.
185
+ * @param opts `spelling` forces the result onto the sharp or flat side instead
186
+ * of following the source note's letter.
187
+ * @returns The transposed note.
188
+ * @example
189
+ * ```ts
190
+ * import { transposeNote, formatNote, parseNote } from '@libraz/libcantus';
191
+ * formatNote(transposeNote(parseNote('Ab4'), 2)); // 'Bb4'
192
+ * formatNote(transposeNote(parseNote('Ab4'), 2, { spelling: 'sharp' })); // 'A#4'
193
+ * ```
194
+ * @category Pitch & Intervals
195
+ */
196
+ declare function transposeNote(note: Note, semitones: number, opts?: {
197
+ spelling?: 'sharp' | 'flat';
198
+ }): Note;
199
+ /**
200
+ * The semitone span of the interval named by a diatonic number and a quality.
201
+ *
202
+ * The inverse of the quality derivation in {@link spelledInterval}: it turns a
203
+ * name back into a distance, which is what transposing by a named interval or
204
+ * parsing one from text needs.
205
+ *
206
+ * @param numberValue Diatonic size: 1 = unison, 2 = second, ... 8 = octave.
207
+ * @param quality Quality label: `'P'`, `'M'`, `'m'`, or repeated `'A'`/`'d'`.
208
+ * @returns The unsigned semitone span.
209
+ * @throws If the quality cannot apply to the number, such as a major fifth.
210
+ * @example
211
+ * ```ts
212
+ * import { intervalSemitones } from '@libraz/libcantus';
213
+ * intervalSemitones(5, 'P'); // 7
214
+ * ```
215
+ * @category Pitch & Intervals
216
+ */
217
+ declare function intervalSemitones(numberValue: number, quality: IntervalQualityLabel): number;
218
+ /**
219
+ * Parse an interval name such as `'P5'`, `'m3'`, or `'AA4'`.
220
+ *
221
+ * @param name The interval name: a quality label followed by a diatonic number.
222
+ * @returns The spelled interval, with an ascending (positive) span.
223
+ * @throws If the name is not a quality label followed by a number, or the two
224
+ * cannot describe the same interval.
225
+ * @example
226
+ * ```ts
227
+ * import { parseInterval } from '@libraz/libcantus';
228
+ * parseInterval('m3'); // { number: 3, quality: 'm', semitones: 3 }
229
+ * ```
230
+ * @category Pitch & Intervals
231
+ */
232
+ declare function parseInterval(name: string): SpelledInterval;
233
+ /**
234
+ * Transpose a note by a spelled interval, keeping the spelling the interval
235
+ * names.
236
+ *
237
+ * Unlike {@link transposeNote}, which picks a letter from the semitone count,
238
+ * the diatonic number decides the letter here: transposing C by an augmented
239
+ * second gives D#, not Eb.
240
+ *
241
+ * @param note The note to transpose.
242
+ * @param interval The interval to apply; a negative span transposes downward.
243
+ * @returns The transposed note, octave-less if the source was.
244
+ * @example
245
+ * ```ts
246
+ * import { formatNote, parseInterval, parseNote, transposeByInterval } from '@libraz/libcantus';
247
+ * formatNote(transposeByInterval(parseNote('C4'), parseInterval('A2'))); // 'D#4'
248
+ * ```
249
+ * @category Pitch & Intervals
250
+ */
251
+ declare function transposeByInterval(note: Note, interval: SpelledInterval): Note;
252
+ /**
253
+ * The spelled interval from one note to another.
254
+ *
255
+ * When both notes carry octaves, the interval reflects their full signed
256
+ * distance (so `C4 -> C5` is a perfect octave). When either lacks an octave the
257
+ * interval is measured within a single ascending octave from `a` to `b`.
258
+ *
259
+ * @param a The lower/first note.
260
+ * @param b The upper/second note.
261
+ * @returns The diatonic number, quality, and semitone span.
262
+ * @example
263
+ * ```ts
264
+ * import { spelledInterval, parseNote } from '@libraz/libcantus';
265
+ * spelledInterval(parseNote('C4'), parseNote('G4'));
266
+ * // { number: 5, quality: 'P', semitones: 7 }
267
+ * ```
268
+ * @category Pitch & Intervals
269
+ */
270
+ declare function spelledInterval(a: Note, b: Note): SpelledInterval;
271
+
272
+ export { type IntervalQualityLabel as I, type Note as N, type SpelledInterval as S, noteToMidi as a, noteToPitchClass as b, parseNote as c, diatonicLetterOf as d, pitchClassOf as e, formatNote as f, transposeNote as g, intervalSemitones as i, midiToNote as m, naturalPitchClassOf as n, parseInterval as p, spelledInterval as s, transposeByInterval as t };
@@ -1,5 +1,5 @@
1
- import { K as KeyScale } from './types-MGP29r3t.js';
2
- import { C as Chord } from './index-Bt4tOUD9.js';
1
+ import { K as KeyScale } from './types-BN6_Q9Kl.cjs';
2
+ import { C as Chord } from './index-y_ak9_Hi.cjs';
3
3
 
4
4
  /**
5
5
  * Severity policy: `strict` counterpoint vs. lenient `pop` voice-leading.
@@ -102,7 +102,22 @@ type SafetyResult = {
102
102
  * ```
103
103
  * @category Arrangement & Analysis
104
104
  */
105
- declare function evaluateSafety(q: SafetyQuery): SafetyResult;
105
+ declare function evaluateSafety(q: SafetyQuery, opts?: EvaluateSafetyOptions): SafetyResult;
106
+ /**
107
+ * Options for {@link evaluateSafety}.
108
+ *
109
+ * @category Arrangement & Analysis
110
+ */
111
+ type EvaluateSafetyOptions = {
112
+ /**
113
+ * Whether to search for safe replacement pitches when the verdict is not
114
+ * safe. Turning it off skips up to two dozen extra evaluations per rejected
115
+ * pitch, which is worth doing when only the verdict is wanted.
116
+ *
117
+ * @defaultValue true
118
+ */
119
+ suggestions?: boolean;
120
+ };
106
121
  /**
107
122
  * Enumerate placeable pitches in a range, chord tones first, descending.
108
123
  *
@@ -125,4 +140,4 @@ declare function evaluateSafety(q: SafetyQuery): SafetyResult;
125
140
  */
126
141
  declare function enumerateSafePitches(q: Omit<SafetyQuery, 'candidatePitch'>, pitchLow: number, pitchHigh: number): number[];
127
142
 
128
- export { NoteSafety as N, ReasonFlag as R, type SafetyProfile as S, type VoiceSnapshot as V, type SafetyQuery as a, type SafetyResult as b, evaluateSafety as c, enumerateSafePitches as e };
143
+ export { type EvaluateSafetyOptions as E, NoteSafety as N, ReasonFlag as R, type SafetyProfile as S, type VoiceSnapshot as V, type SafetyQuery as a, type SafetyResult as b, evaluateSafety as c, enumerateSafePitches as e };
@@ -1,11 +1,11 @@
1
- import { C as Chord } from './index-BNlGP86A.cjs';
1
+ import { d as ChordToneRole, C as Chord } from './index-C83mZI1P.js';
2
2
 
3
3
  /**
4
4
  * Harmonic role a pitch plays within a chord.
5
5
  *
6
6
  * @category Functional Harmony
7
7
  */
8
- type HarmonyRole = 'root' | 'third' | 'fifth' | 'sixth' | 'seventh' | 'tension' | 'doubling';
8
+ type HarmonyRole = ChordToneRole | 'tension' | 'doubling';
9
9
  /**
10
10
  * How firmly a pitch is locked to the chord's identity.
11
11
  *
@@ -29,15 +29,18 @@ type VoicedRole = {
29
29
  /**
30
30
  * Classify a pitch's harmonic role and lock level within a chord.
31
31
  *
32
- * The role comes from the pitch's interval class above the chord root: root (0),
33
- * third (3/4), fifth (6/7/8), seventh (10/11), and 9/11/13 tensions (2/5/9, and
34
- * the flat ninth 1). The root locks the chord identity, the third locks its
35
- * quality, and everything else is free voicing. In a suspended chord (a chord
36
- * whose intervals include a 4th (5) or 2nd (2) but no third), the suspended tone
37
- * takes the third's place as the quality-defining tone and is locked to
38
- * `quality`, since moving it changes the chord. Detecting an octave doubling
39
- * requires the surrounding voicing, which this single-pitch query does not carry,
40
- * so `doubling` is part of the type but not returned here.
32
+ * The role comes from the pitch's interval class above the chord root, read
33
+ * against the chord's own template: root (0), third (3/4), fifth (6/7/8),
34
+ * sixth or seventh (9/10/11). Every pitch the template does not claim is
35
+ * `'tension'` — including one that is simply foreign to the chord, which this
36
+ * single-pitch query cannot tell from a colour tone. The root locks the chord
37
+ * identity, the third locks its quality, and everything else is free voicing.
38
+ * In a suspended chord (a chord whose intervals include a 4th (5) or 2nd (2)
39
+ * but no third), the suspended tone takes the third's place as the
40
+ * quality-defining tone and is locked to `quality`, since moving it changes the
41
+ * chord. Detecting an octave doubling requires the surrounding voicing, which
42
+ * this query does not carry, so `'doubling'` is part of the type — the shared
43
+ * vocabulary of harmonic roles — but is never returned here.
41
44
  *
42
45
  * @param pitch MIDI pitch or bare pitch class.
43
46
  * @param chord The chord providing the root reference.