@libraz/libcantus 0.9.0

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 (52) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +291 -0
  3. package/dist/index.cjs +6465 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +2621 -0
  6. package/dist/index.d.ts +2621 -0
  7. package/dist/index.js +6297 -0
  8. package/dist/index.js.map +1 -0
  9. package/package.json +71 -0
  10. package/src/analysis/index.ts +261 -0
  11. package/src/arrange/index.ts +409 -0
  12. package/src/bass/index.ts +301 -0
  13. package/src/chord/index.ts +292 -0
  14. package/src/chordscale/index.ts +316 -0
  15. package/src/countermelody/index.ts +349 -0
  16. package/src/counterpoint/index.ts +209 -0
  17. package/src/detect/index.ts +176 -0
  18. package/src/drums/beat.ts +266 -0
  19. package/src/drums/euclid.ts +58 -0
  20. package/src/drums/fills.ts +345 -0
  21. package/src/drums/ghost.ts +119 -0
  22. package/src/drums/hihat.ts +285 -0
  23. package/src/drums/hit.ts +31 -0
  24. package/src/drums/index.ts +211 -0
  25. package/src/drums/internal.ts +251 -0
  26. package/src/drums/kick.ts +160 -0
  27. package/src/drums/percussion.ts +121 -0
  28. package/src/drums/rng.ts +7 -0
  29. package/src/drums/swing.ts +66 -0
  30. package/src/functional/index.ts +602 -0
  31. package/src/groove/index.ts +230 -0
  32. package/src/harmonize/index.ts +367 -0
  33. package/src/harmony/index.ts +75 -0
  34. package/src/index.ts +233 -0
  35. package/src/interval/index.ts +67 -0
  36. package/src/meter/index.ts +179 -0
  37. package/src/model/index.ts +957 -0
  38. package/src/motif/index.ts +321 -0
  39. package/src/pitch/index.ts +256 -0
  40. package/src/progression/index.ts +336 -0
  41. package/src/random/index.ts +41 -0
  42. package/src/reharmony/index.ts +299 -0
  43. package/src/rhythm/index.ts +131 -0
  44. package/src/safety/index.ts +372 -0
  45. package/src/scale/index.ts +215 -0
  46. package/src/spelling/index.ts +164 -0
  47. package/src/symbol/index.ts +213 -0
  48. package/src/timeline/index.ts +352 -0
  49. package/src/tuning/index.ts +129 -0
  50. package/src/types.ts +29 -0
  51. package/src/voicing/index.ts +523 -0
  52. package/tsconfig.json +23 -0
package/src/index.ts ADDED
@@ -0,0 +1,233 @@
1
+ export type { AnalyzedNote, TheoryLabel, VoiceNote } from './analysis/index.js';
2
+ export { analyzeVoice } from './analysis/index.js';
3
+ export type {
4
+ ArrangementAnalysis,
5
+ ArrangementOptions,
6
+ ArrangementTrack,
7
+ Conflict,
8
+ TensionPoint,
9
+ TrackAnalysis,
10
+ TrackRole,
11
+ } from './arrange/index.js';
12
+ export { analyzeArrangement, tensionCurve } from './arrange/index.js';
13
+ export type { BassLineOptions, BassSegment, BassStyle } from './bass/index.js';
14
+ export { generateBassLine } from './bass/index.js';
15
+ export type { Chord as ChordData, ChordQuality } from './chord/index.js';
16
+ export {
17
+ chordFromDegree,
18
+ chordPitchClasses,
19
+ chordQualities,
20
+ chordToneRole,
21
+ diatonicSeventh,
22
+ diatonicTriad,
23
+ makeChord,
24
+ } from './chord/index.js';
25
+ export type { ChordScaleMatch, ChordScaleReportEntry, ScaleChoice } from './chordscale/index.js';
26
+ export {
27
+ availableTensions,
28
+ avoidNotes,
29
+ chordScaleReport,
30
+ chordScales,
31
+ scaleMatchesChord,
32
+ scalesForChanges,
33
+ } from './chordscale/index.js';
34
+ export type { CounterMelodyOptions } from './countermelody/index.js';
35
+ export { generateCounterMelody } from './countermelody/index.js';
36
+ export {
37
+ createsHiddenParallelPerfect,
38
+ createsParallelOctave,
39
+ createsParallelPerfect,
40
+ createsParallelUnison,
41
+ createsVerticalDissonance,
42
+ createsVoiceCrossing,
43
+ createsVoiceOverlap,
44
+ exceedsSpacing,
45
+ isForbiddenMelodicLeap,
46
+ isLeadingToneResolution,
47
+ } from './counterpoint/index.js';
48
+ export type { ChordMatch, KeyMatch } from './detect/index.js';
49
+ export { detectChord, detectChordBest, detectKey } from './detect/index.js';
50
+ export type {
51
+ DrumGenOptions,
52
+ DrumHit,
53
+ DrumRole,
54
+ GrooveFeel,
55
+ GrooveStyle,
56
+ Section,
57
+ } from './drums/index.js';
58
+ export { generateDrums } from './drums/index.js';
59
+ export type {
60
+ BorrowedSource,
61
+ Cadence,
62
+ ChordAnalysis,
63
+ HarmonicFunction,
64
+ } from './functional/index.js';
65
+ export {
66
+ analyzeChord,
67
+ borrowedSource,
68
+ chordToRoman,
69
+ detectCadence,
70
+ functionOf,
71
+ isBorrowedChord,
72
+ isDiatonic,
73
+ isMinorKey,
74
+ parallelKey,
75
+ romanToChord,
76
+ secondaryDominant,
77
+ } from './functional/index.js';
78
+ export type { GrooveSlot, GrooveTemplate, HumanizeOptions } from './groove/index.js';
79
+ export { applyGrooveTemplate, extractGrooveTemplate, humanize } from './groove/index.js';
80
+ export type {
81
+ HarmonizeOptions,
82
+ HarmonizeResult,
83
+ MelodyNote,
84
+ } from './harmonize/index.js';
85
+ export { harmonizeMelody } from './harmonize/index.js';
86
+ export type { HarmonyRole, LockLevel, VoicedRole } from './harmony/index.js';
87
+ export { roleOf } from './harmony/index.js';
88
+ export {
89
+ classifyInterval,
90
+ IntervalQuality,
91
+ isConsonantInterval,
92
+ isPerfectInterval,
93
+ } from './interval/index.js';
94
+ export type { BarPosition, TimeSignature } from './meter/index.js';
95
+ export {
96
+ barPositionToBeat,
97
+ beatsPerBar,
98
+ beatToBarPosition,
99
+ formatTimeSignature,
100
+ isCompound,
101
+ isStrongBeat,
102
+ metricWeight,
103
+ parseTimeSignature,
104
+ pulsesPerBar,
105
+ tuplet,
106
+ } from './meter/index.js';
107
+ export { Chord, Interval, Key, Note, Progression } from './model/index.js';
108
+ export type {
109
+ GenerateMotifOptions,
110
+ MotifCell,
111
+ MotifContour,
112
+ MotifNote,
113
+ MotifTransform,
114
+ } from './motif/index.js';
115
+ export { developMotif, generateMotif, transformMotif } from './motif/index.js';
116
+ export type { Note as NoteData, SpelledInterval } from './pitch/index.js';
117
+ export {
118
+ formatNote,
119
+ midiToNote,
120
+ noteToMidi,
121
+ noteToPitchClass,
122
+ parseNote,
123
+ spelledInterval,
124
+ } from './pitch/index.js';
125
+ export type {
126
+ GeneratedChord,
127
+ GenerateProgressionOptions,
128
+ ProgFunction,
129
+ ProgressionPreset,
130
+ ProgStyle,
131
+ } from './progression/index.js';
132
+ export {
133
+ generateProgression,
134
+ progressions,
135
+ progressionsByStyle,
136
+ } from './progression/index.js';
137
+ export type { Rng } from './random/index.js';
138
+ export { createRng } from './random/index.js';
139
+ export type {
140
+ BorrowedChord,
141
+ SubstituteOptions,
142
+ Substitution,
143
+ SubstitutionType,
144
+ } from './reharmony/index.js';
145
+ export {
146
+ modalInterchangePalette,
147
+ negativeHarmonyMirror,
148
+ substituteChord,
149
+ } from './reharmony/index.js';
150
+ export type { RhythmEvent, RhythmOptions } from './rhythm/index.js';
151
+ export { generateRhythm, onsetWeightCurve, rhythmDensity } from './rhythm/index.js';
152
+ export type {
153
+ SafetyProfile,
154
+ SafetyQuery,
155
+ SafetyResult,
156
+ VoiceSnapshot,
157
+ } from './safety/index.js';
158
+ export { enumerateSafePitches, evaluateSafety, NoteSafety, ReasonFlag } from './safety/index.js';
159
+ export {
160
+ BLUES_MASK,
161
+ CHROMATIC_MASK,
162
+ DORIAN_MASK,
163
+ diatonicPitchClasses,
164
+ HARMONIC_MINOR_MASK,
165
+ isScaleTone,
166
+ LOCRIAN_MASK,
167
+ LYDIAN_MASK,
168
+ MAJOR_MASK,
169
+ MAJOR_PENTATONIC_MASK,
170
+ MELODIC_MINOR_MASK,
171
+ MINOR_PENTATONIC_MASK,
172
+ MIXOLYDIAN_MASK,
173
+ majorKey,
174
+ maskFromOffsets,
175
+ minorKey,
176
+ NAMED_SCALES,
177
+ NATURAL_MINOR_MASK,
178
+ nearestScaleTone,
179
+ OCTATONIC_HALF_WHOLE_MASK,
180
+ OCTATONIC_WHOLE_HALF_MASK,
181
+ PHRYGIAN_MASK,
182
+ pitchToScaleDegree,
183
+ scaleByName,
184
+ scaleTonesInDegreeOrder,
185
+ WHOLE_TONE_MASK,
186
+ } from './scale/index.js';
187
+ export {
188
+ noteNames,
189
+ spellChord,
190
+ spellPitchClass,
191
+ spellPitchClasses,
192
+ spellScale,
193
+ } from './spelling/index.js';
194
+ export { formatChordSymbol, parseChordSymbol, transposeChordSymbol } from './symbol/index.js';
195
+ export type {
196
+ CadenceHit,
197
+ ChordSegment,
198
+ ChordTimeline,
199
+ ChordTimelineOptions,
200
+ ChordTimelineResult,
201
+ } from './timeline/index.js';
202
+ export {
203
+ chordTimelineFromChords,
204
+ chordTimelineFromNotes,
205
+ detectCadences,
206
+ } from './timeline/index.js';
207
+ export type { Tuning } from './tuning/index.js';
208
+ export {
209
+ centsBetweenFreq,
210
+ centsOfSteps,
211
+ edo,
212
+ frequencyOf,
213
+ JUST_RATIOS,
214
+ justDeviationCents,
215
+ nearestStep,
216
+ ratioToCents,
217
+ TWELVE_TET,
218
+ } from './tuning/index.js';
219
+ export type { KeyScale, NoteEvent } from './types.js';
220
+ export type {
221
+ StyledVoicingOptions,
222
+ VoiceRange,
223
+ VoicingOptions,
224
+ VoicingStyle,
225
+ } from './voicing/index.js';
226
+ export {
227
+ nextVoicing,
228
+ SATB_RANGES,
229
+ voiceChord,
230
+ voiceChordStyled,
231
+ voiceLeadingCost,
232
+ voiceProgression,
233
+ } from './voicing/index.js';
@@ -0,0 +1,67 @@
1
+ /** Counterpoint classification of a harmonic interval. */
2
+ export enum IntervalQuality {
3
+ PerfectConsonance = 0,
4
+ ImperfectConsonance = 1,
5
+ Dissonance = 2,
6
+ }
7
+
8
+ /** Reduce an interval to a simple interval class in the range [0, 11]. */
9
+ function simpleInterval(semitones: number): number {
10
+ return Math.abs(semitones) % 12;
11
+ }
12
+
13
+ /**
14
+ * Classify an interval for counterpoint evaluation.
15
+ *
16
+ * The interval is reduced modulo 12, so compound intervals classify as their
17
+ * simple equivalents. The perfect fourth (5) is context-dependent: with
18
+ * `twoVoice` (the default) it classifies as a dissonance, matching
19
+ * {@link isConsonantInterval}; otherwise it is an imperfect consonance.
20
+ *
21
+ * @param semitones Interval size in semitones (may be negative or compound).
22
+ * @param twoVoice When true, the perfect fourth is treated as dissonant.
23
+ * @returns The counterpoint quality of the interval.
24
+ */
25
+ export function classifyInterval(semitones: number, twoVoice = true): IntervalQuality {
26
+ const pc = simpleInterval(semitones);
27
+ if (pc === 0 || pc === 7) {
28
+ return IntervalQuality.PerfectConsonance;
29
+ }
30
+ if (pc === 3 || pc === 4 || pc === 8 || pc === 9) {
31
+ return IntervalQuality.ImperfectConsonance;
32
+ }
33
+ if (pc === 5) {
34
+ return twoVoice ? IntervalQuality.Dissonance : IntervalQuality.ImperfectConsonance;
35
+ }
36
+ return IntervalQuality.Dissonance;
37
+ }
38
+
39
+ /**
40
+ * Test whether an interval is a perfect interval (unison/octave or fifth).
41
+ *
42
+ * @param semitones Interval size in semitones (may be negative or compound).
43
+ * @returns True for the unison/octave (0) and the perfect fifth (7) mod 12.
44
+ */
45
+ export function isPerfectInterval(semitones: number): boolean {
46
+ const pc = simpleInterval(semitones);
47
+ return pc === 0 || pc === 7;
48
+ }
49
+
50
+ /**
51
+ * Test whether an interval is consonant.
52
+ *
53
+ * The consonant set is {0, 3, 4, 5, 7, 8, 9} mod 12. In two-voice counterpoint
54
+ * the perfect fourth (5) is treated as a dissonance, so `twoVoice` set to true
55
+ * excludes it.
56
+ *
57
+ * @param semitones Interval size in semitones (may be negative or compound).
58
+ * @param twoVoice When true, the perfect fourth is treated as dissonant.
59
+ * @returns True if the interval is consonant in the given context.
60
+ */
61
+ export function isConsonantInterval(semitones: number, twoVoice = true): boolean {
62
+ const pc = simpleInterval(semitones);
63
+ if (pc === 5) {
64
+ return !twoVoice;
65
+ }
66
+ return pc === 0 || pc === 3 || pc === 4 || pc === 7 || pc === 8 || pc === 9;
67
+ }
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Meter and time signatures: bar/beat arithmetic, simple vs. compound meters,
3
+ * metric-accent hierarchy, and tuplet subdivision.
4
+ *
5
+ * Positions and durations are measured in quarter-note beats, matching the rest
6
+ * of the library's beat convention (four quarter-note beats per bar in 4/4).
7
+ */
8
+
9
+ /** A time signature as a numerator over a note-value denominator. */
10
+ export type TimeSignature = {
11
+ numerator: number;
12
+ denominator: number;
13
+ };
14
+
15
+ /** A position expressed as a bar index and a quarter-note offset within the bar. */
16
+ export type BarPosition = {
17
+ bar: number;
18
+ /** Quarter-note offset from the start of the bar. */
19
+ beat: number;
20
+ };
21
+
22
+ const EPS = 1e-9;
23
+
24
+ /** Whether `value` is an integer multiple of `unit` (within a float tolerance). */
25
+ function isMultiple(value: number, unit: number): boolean {
26
+ if (unit === 0) {
27
+ return false;
28
+ }
29
+ const ratio = value / unit;
30
+ return Math.abs(ratio - Math.round(ratio)) < EPS;
31
+ }
32
+
33
+ /**
34
+ * Parse a time signature such as `"4/4"` or `"6/8"`.
35
+ *
36
+ * @param text The signature text.
37
+ * @returns The parsed time signature.
38
+ * @throws If the text is not `n/d` with positive integers.
39
+ */
40
+ export function parseTimeSignature(text: string): TimeSignature {
41
+ const match = /^\s*(\d+)\s*\/\s*(\d+)\s*$/.exec(text);
42
+ if (!match) {
43
+ throw new Error(`Invalid time signature: ${text}`);
44
+ }
45
+ const numerator = Number.parseInt(match[1] ?? '', 10);
46
+ const denominator = Number.parseInt(match[2] ?? '', 10);
47
+ if (numerator <= 0 || denominator <= 0) {
48
+ throw new Error(`Invalid time signature: ${text}`);
49
+ }
50
+ return { numerator, denominator };
51
+ }
52
+
53
+ /** Render a time signature as `"n/d"`. */
54
+ export function formatTimeSignature(ts: TimeSignature): string {
55
+ return `${ts.numerator}/${ts.denominator}`;
56
+ }
57
+
58
+ /**
59
+ * Whether a meter is compound: its main pulses each divide into three, as in
60
+ * 6/8, 9/8, 12/8, or 6/4. Compound meters are those whose numerator is a
61
+ * multiple of three greater than three, independent of the denominator. Meters
62
+ * like 3/8 and 3/4 (simple triples, numerator 3) are not compound.
63
+ *
64
+ * @param ts The time signature.
65
+ * @returns True for compound meters.
66
+ */
67
+ export function isCompound(ts: TimeSignature): boolean {
68
+ return ts.numerator % 3 === 0 && ts.numerator > 3;
69
+ }
70
+
71
+ /** Length of one denominator unit in quarter-note beats. */
72
+ function unitBeats(ts: TimeSignature): number {
73
+ return 4 / ts.denominator;
74
+ }
75
+
76
+ /**
77
+ * Length of a bar in quarter-note beats.
78
+ *
79
+ * @param ts The time signature.
80
+ * @returns The bar length in quarter notes.
81
+ */
82
+ export function beatsPerBar(ts: TimeSignature): number {
83
+ return ts.numerator * unitBeats(ts);
84
+ }
85
+
86
+ /** Length of one main pulse (a dotted value in compound meters) in quarter notes. */
87
+ function pulseBeats(ts: TimeSignature): number {
88
+ return isCompound(ts) ? 3 * unitBeats(ts) : unitBeats(ts);
89
+ }
90
+
91
+ /**
92
+ * Number of main pulses (felt beats) per bar: the numerator for simple meters,
93
+ * a third of it for compound meters.
94
+ *
95
+ * @param ts The time signature.
96
+ * @returns The pulse count per bar.
97
+ */
98
+ export function pulsesPerBar(ts: TimeSignature): number {
99
+ return beatsPerBar(ts) / pulseBeats(ts);
100
+ }
101
+
102
+ /**
103
+ * Convert an absolute quarter-note position to a bar index and in-bar offset.
104
+ *
105
+ * @param beatInQuarters Absolute position in quarter-note beats.
106
+ * @param ts The time signature.
107
+ * @returns The bar and in-bar quarter-note offset.
108
+ */
109
+ export function beatToBarPosition(beatInQuarters: number, ts: TimeSignature): BarPosition {
110
+ const barLen = beatsPerBar(ts);
111
+ const bar = Math.floor(beatInQuarters / barLen);
112
+ return { bar, beat: beatInQuarters - bar * barLen };
113
+ }
114
+
115
+ /**
116
+ * Convert a bar index and in-bar offset back to an absolute quarter-note
117
+ * position.
118
+ *
119
+ * @param pos The bar position.
120
+ * @param ts The time signature.
121
+ * @returns The absolute position in quarter-note beats.
122
+ */
123
+ export function barPositionToBeat(pos: BarPosition, ts: TimeSignature): number {
124
+ return pos.bar * beatsPerBar(ts) + pos.beat;
125
+ }
126
+
127
+ /**
128
+ * Metric weight of a position within its bar, on a 0–3 scale:
129
+ * 3 the downbeat, 2 a secondary strong pulse (the bar's midpoint in even
130
+ * meters), 1 any other main pulse, and 0 an off-pulse subdivision.
131
+ *
132
+ * @param beatInQuarters Absolute or in-bar quarter-note position.
133
+ * @param ts The time signature.
134
+ * @returns The metric weight (0–3).
135
+ */
136
+ export function metricWeight(beatInQuarters: number, ts: TimeSignature): number {
137
+ const { beat } = beatToBarPosition(beatInQuarters, ts);
138
+ const pulse = pulseBeats(ts);
139
+ if (!isMultiple(beat, pulse)) {
140
+ return 0;
141
+ }
142
+ const pulses = pulsesPerBar(ts);
143
+ const pulseIndex = Math.round(beat / pulse) % pulses;
144
+ if (pulseIndex === 0) {
145
+ return 3;
146
+ }
147
+ if (pulses % 2 === 0 && pulseIndex === pulses / 2) {
148
+ return 2;
149
+ }
150
+ return 1;
151
+ }
152
+
153
+ /**
154
+ * Whether a position is metrically accented (weight 2 or more — a downbeat or a
155
+ * secondary strong pulse).
156
+ *
157
+ * @param beatInQuarters Absolute or in-bar quarter-note position.
158
+ * @param ts The time signature.
159
+ * @returns True on strong beats.
160
+ */
161
+ export function isStrongBeat(beatInQuarters: number, ts: TimeSignature): boolean {
162
+ return metricWeight(beatInQuarters, ts) >= 2;
163
+ }
164
+
165
+ /**
166
+ * Subdivide a span into `count` equal tuplet durations (e.g. an eighth-note
167
+ * triplet is `tuplet(1, 3)`).
168
+ *
169
+ * @param totalBeats Total span in quarter-note beats.
170
+ * @param count Number of equal parts.
171
+ * @returns `count` equal durations summing to `totalBeats`.
172
+ * @throws If `count` is not a positive integer.
173
+ */
174
+ export function tuplet(totalBeats: number, count: number): number[] {
175
+ if (!Number.isInteger(count) || count <= 0) {
176
+ throw new Error(`Invalid tuplet count: ${count}`);
177
+ }
178
+ return new Array<number>(count).fill(totalBeats / count);
179
+ }