@libraz/libcantus 0.9.5 → 1.0.1

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 (185) hide show
  1. package/README.md +61 -270
  2. package/README_ja.md +61 -214
  3. package/dist/analyze/index.cjs +54 -7
  4. package/dist/analyze/index.cjs.map +1 -1
  5. package/dist/analyze/index.d.cts +200 -353
  6. package/dist/analyze/index.d.ts +200 -353
  7. package/dist/analyze/index.js +56 -9
  8. package/dist/chunk-2LCPM3EG.js +3224 -0
  9. package/dist/chunk-2LCPM3EG.js.map +1 -0
  10. package/dist/chunk-6XH6QZYQ.cjs +7647 -0
  11. package/dist/chunk-6XH6QZYQ.cjs.map +1 -0
  12. package/dist/chunk-6XPTBE7Y.cjs +49 -0
  13. package/dist/chunk-6XPTBE7Y.cjs.map +1 -0
  14. package/dist/chunk-AFM2ULJH.cjs +5121 -0
  15. package/dist/chunk-AFM2ULJH.cjs.map +1 -0
  16. package/dist/chunk-B6XNKWZV.cjs +3224 -0
  17. package/dist/chunk-B6XNKWZV.cjs.map +1 -0
  18. package/dist/chunk-E3F4AHAH.cjs +690 -0
  19. package/dist/chunk-E3F4AHAH.cjs.map +1 -0
  20. package/dist/{chunk-2PTFGVD7.js → chunk-ELCTZUJJ.js} +250 -132
  21. package/dist/chunk-ELCTZUJJ.js.map +1 -0
  22. package/dist/chunk-FVSI36UV.js +690 -0
  23. package/dist/chunk-FVSI36UV.js.map +1 -0
  24. package/dist/chunk-GEDTJHAC.cjs +1 -0
  25. package/dist/chunk-GEDTJHAC.cjs.map +1 -0
  26. package/dist/chunk-H3VORETI.js +2243 -0
  27. package/dist/chunk-H3VORETI.js.map +1 -0
  28. package/dist/chunk-H42UHPYW.js +1577 -0
  29. package/dist/chunk-H42UHPYW.js.map +1 -0
  30. package/dist/chunk-HFPS7JFD.js +1 -0
  31. package/dist/chunk-INHVXP6Z.js +3053 -0
  32. package/dist/chunk-INHVXP6Z.js.map +1 -0
  33. package/dist/chunk-K4ZJUGF4.cjs +379 -0
  34. package/dist/chunk-K4ZJUGF4.cjs.map +1 -0
  35. package/dist/chunk-KNJEKLKV.js +49 -0
  36. package/dist/chunk-KNJEKLKV.js.map +1 -0
  37. package/dist/chunk-MXO72PLY.cjs +211 -0
  38. package/dist/chunk-MXO72PLY.cjs.map +1 -0
  39. package/dist/chunk-NLEU4QSE.cjs +360 -0
  40. package/dist/chunk-NLEU4QSE.cjs.map +1 -0
  41. package/dist/chunk-OE6WK2Y2.cjs +1577 -0
  42. package/dist/chunk-OE6WK2Y2.cjs.map +1 -0
  43. package/dist/chunk-PKFNUHDY.js +7647 -0
  44. package/dist/chunk-PKFNUHDY.js.map +1 -0
  45. package/dist/chunk-Q2WGKJNN.cjs +1064 -0
  46. package/dist/chunk-Q2WGKJNN.cjs.map +1 -0
  47. package/dist/chunk-QYQQ5J6E.js +1 -0
  48. package/dist/chunk-QYQQ5J6E.js.map +1 -0
  49. package/dist/chunk-RHIJ3S4I.cjs +1 -0
  50. package/dist/chunk-RHIJ3S4I.cjs.map +1 -0
  51. package/dist/chunk-TTCLJX6D.js +211 -0
  52. package/dist/chunk-TTCLJX6D.js.map +1 -0
  53. package/dist/chunk-UADYAYAT.cjs +3053 -0
  54. package/dist/chunk-UADYAYAT.cjs.map +1 -0
  55. package/dist/chunk-VOHZB45S.js +5121 -0
  56. package/dist/chunk-VOHZB45S.js.map +1 -0
  57. package/dist/chunk-VTI6FOLB.js +1064 -0
  58. package/dist/chunk-VTI6FOLB.js.map +1 -0
  59. package/dist/chunk-WWURE4XT.cjs +2243 -0
  60. package/dist/chunk-WWURE4XT.cjs.map +1 -0
  61. package/dist/chunk-YH3BIKHH.js +360 -0
  62. package/dist/chunk-YH3BIKHH.js.map +1 -0
  63. package/dist/core/index.cjs +93 -6
  64. package/dist/core/index.cjs.map +1 -1
  65. package/dist/core/index.d.cts +87 -346
  66. package/dist/core/index.d.ts +87 -346
  67. package/dist/core/index.js +94 -7
  68. package/dist/generate/index.cjs +89 -10
  69. package/dist/generate/index.cjs.map +1 -1
  70. package/dist/generate/index.d.cts +436 -1094
  71. package/dist/generate/index.d.ts +436 -1094
  72. package/dist/generate/index.js +94 -15
  73. package/dist/index-9wcMhG73.d.ts +1095 -0
  74. package/dist/{index-Cfx8xgH0.d.ts → index-B4o55NDr.d.cts} +13 -9
  75. package/dist/index-B9MNZboQ.d.cts +770 -0
  76. package/dist/index-B9MNZboQ.d.ts +770 -0
  77. package/dist/index-BGFW-8P2.d.cts +2307 -0
  78. package/dist/index-BO6xE9YU.d.cts +1804 -0
  79. package/dist/index-BkGmlyI3.d.cts +470 -0
  80. package/dist/index-CCNJRG-P.d.cts +1095 -0
  81. package/dist/index-CGpRv5sl.d.cts +643 -0
  82. package/dist/index-C_8o--VE.d.cts +1242 -0
  83. package/dist/index-CgHqxdps.d.ts +2307 -0
  84. package/dist/index-Chbst3Jx.d.ts +1343 -0
  85. package/dist/index-Cj49F892.d.ts +470 -0
  86. package/dist/{index-x2vcsduE.d.cts → index-CunYKLiu.d.ts} +13 -9
  87. package/dist/index-D57QRbVn.d.ts +1804 -0
  88. package/dist/index-DInQqT8Q.d.cts +66 -0
  89. package/dist/index-DInQqT8Q.d.ts +66 -0
  90. package/dist/index-Dzr13q3d.d.cts +1343 -0
  91. package/dist/index-G1uwGGWQ.d.ts +643 -0
  92. package/dist/index-szBv3496.d.ts +1242 -0
  93. package/dist/index.cjs +348 -15
  94. package/dist/index.cjs.map +1 -1
  95. package/dist/index.d.cts +17 -15
  96. package/dist/index.d.ts +17 -15
  97. package/dist/index.js +397 -64
  98. package/dist/masks-DYbE485J.d.cts +445 -0
  99. package/dist/masks-DYbE485J.d.ts +445 -0
  100. package/dist/model/index.cjs +39 -7
  101. package/dist/model/index.cjs.map +1 -1
  102. package/dist/model/index.d.cts +4659 -249
  103. package/dist/model/index.d.ts +4659 -249
  104. package/dist/model/index.js +40 -8
  105. package/dist/positional-Cptlj-xs.d.ts +287 -0
  106. package/dist/positional-DvH_sd8u.d.cts +287 -0
  107. package/dist/theory/index.cjs +101 -7
  108. package/dist/theory/index.cjs.map +1 -1
  109. package/dist/theory/index.d.cts +511 -171
  110. package/dist/theory/index.d.ts +511 -171
  111. package/dist/theory/index.js +118 -24
  112. package/package.json +6 -4
  113. package/dist/chunk-2A2QMA74.cjs +0 -261
  114. package/dist/chunk-2A2QMA74.cjs.map +0 -1
  115. package/dist/chunk-2PTFGVD7.js.map +0 -1
  116. package/dist/chunk-357ZDPVU.cjs +0 -162
  117. package/dist/chunk-357ZDPVU.cjs.map +0 -1
  118. package/dist/chunk-3KX76RLG.js +0 -1548
  119. package/dist/chunk-3KX76RLG.js.map +0 -1
  120. package/dist/chunk-4VZ5MZAM.cjs +0 -889
  121. package/dist/chunk-4VZ5MZAM.cjs.map +0 -1
  122. package/dist/chunk-7EONYVI5.js +0 -1204
  123. package/dist/chunk-7EONYVI5.js.map +0 -1
  124. package/dist/chunk-7JEP3MFY.js +0 -162
  125. package/dist/chunk-7JEP3MFY.js.map +0 -1
  126. package/dist/chunk-AID75RN3.js +0 -3176
  127. package/dist/chunk-AID75RN3.js.map +0 -1
  128. package/dist/chunk-BSLTLGM3.cjs +0 -497
  129. package/dist/chunk-BSLTLGM3.cjs.map +0 -1
  130. package/dist/chunk-CU6VG2YM.cjs +0 -109
  131. package/dist/chunk-CU6VG2YM.cjs.map +0 -1
  132. package/dist/chunk-DZMHIGI4.js +0 -337
  133. package/dist/chunk-DZMHIGI4.js.map +0 -1
  134. package/dist/chunk-EDOTEABK.js +0 -109
  135. package/dist/chunk-EDOTEABK.js.map +0 -1
  136. package/dist/chunk-HN2OEUN2.js +0 -33
  137. package/dist/chunk-HN2OEUN2.js.map +0 -1
  138. package/dist/chunk-KBTTEX7Q.js +0 -497
  139. package/dist/chunk-KBTTEX7Q.js.map +0 -1
  140. package/dist/chunk-KQN2ZYND.cjs +0 -724
  141. package/dist/chunk-KQN2ZYND.cjs.map +0 -1
  142. package/dist/chunk-MPTVJIVG.cjs +0 -3176
  143. package/dist/chunk-MPTVJIVG.cjs.map +0 -1
  144. package/dist/chunk-MTMZTMWM.cjs +0 -53
  145. package/dist/chunk-MTMZTMWM.cjs.map +0 -1
  146. package/dist/chunk-N6IHQUOX.js +0 -1
  147. package/dist/chunk-NNB67XA2.js +0 -724
  148. package/dist/chunk-NNB67XA2.js.map +0 -1
  149. package/dist/chunk-OL3IHHSJ.cjs +0 -1204
  150. package/dist/chunk-OL3IHHSJ.cjs.map +0 -1
  151. package/dist/chunk-PI3UXRWK.cjs +0 -528
  152. package/dist/chunk-PI3UXRWK.cjs.map +0 -1
  153. package/dist/chunk-R2VVFAIN.js +0 -528
  154. package/dist/chunk-R2VVFAIN.js.map +0 -1
  155. package/dist/chunk-T64C7ZD7.cjs +0 -1
  156. package/dist/chunk-T64C7ZD7.cjs.map +0 -1
  157. package/dist/chunk-WFDXOLUB.cjs +0 -337
  158. package/dist/chunk-WFDXOLUB.cjs.map +0 -1
  159. package/dist/chunk-WTAN4ACG.cjs +0 -1548
  160. package/dist/chunk-WTAN4ACG.cjs.map +0 -1
  161. package/dist/chunk-YFJXS3AU.cjs +0 -33
  162. package/dist/chunk-YFJXS3AU.cjs.map +0 -1
  163. package/dist/chunk-YUE6TJ4M.js +0 -889
  164. package/dist/chunk-YUE6TJ4M.js.map +0 -1
  165. package/dist/chunk-ZGLJM4A4.js +0 -53
  166. package/dist/chunk-ZGLJM4A4.js.map +0 -1
  167. package/dist/function-CxixTB02.d.cts +0 -299
  168. package/dist/function-DTCt675z.d.ts +0 -299
  169. package/dist/index-B2Ulxxpo.d.cts +0 -144
  170. package/dist/index-BkYJ0xMq.d.cts +0 -152
  171. package/dist/index-BuaB-ypt.d.ts +0 -144
  172. package/dist/index-CAN7v-kw.d.ts +0 -199
  173. package/dist/index-COw1Ky9g.d.ts +0 -242
  174. package/dist/index-DRtIhlJj.d.ts +0 -152
  175. package/dist/index-DXd9JikN.d.cts +0 -246
  176. package/dist/index-DXd9JikN.d.ts +0 -246
  177. package/dist/index-alrRninU.d.cts +0 -199
  178. package/dist/index-oTQbYYIs.d.cts +0 -242
  179. package/dist/index-tR9jTn26.d.cts +0 -275
  180. package/dist/index-tR9jTn26.d.ts +0 -275
  181. package/dist/styled-Bl0ZK28h.d.ts +0 -543
  182. package/dist/styled-Crv6SA4S.d.cts +0 -543
  183. package/dist/types-BN6_Q9Kl.d.cts +0 -39
  184. package/dist/types-BN6_Q9Kl.d.ts +0 -39
  185. /package/dist/{chunk-N6IHQUOX.js.map → chunk-HFPS7JFD.js.map} +0 -0
package/README.md CHANGED
@@ -1,324 +1,115 @@
1
1
  # @libraz/libcantus
2
2
 
3
- Pure-TypeScript music theory engine. It takes MIDI note numbers and chord
4
- symbols and gives back harmonic meaning (Roman numerals, function, cadence,
5
- key), then generates new parts against that meaning. Built for the tools that
6
- sit next to a DAW: analyzers, generators, and composition assistants.
7
-
8
- It is a theory layer, not an I/O or audio layer. There is no MIDI file reader or
9
- writer, no audio analysis, and no playback — bring your own parser and hand it
10
- note events. No runtime dependencies. If you need that other layer as well,
11
- [libsonare](https://github.com/libraz/libsonare) covers audio analysis,
12
- mastering, synthesis, and SMF I/O, and its npm binding sits alongside this one;
13
- the two share no code and neither requires the other.
3
+ Pure-TypeScript music theory for MIDI note events. Build and spell chords, inspect harmony, analyze keys and form, and generate parts from the same data model. The package has no runtime dependencies.
14
4
 
15
5
  [![CI](https://img.shields.io/github/actions/workflow/status/libraz/libcantus/ci.yml?branch=main&label=CI)](https://github.com/libraz/libcantus/actions)
16
6
  [![npm](https://img.shields.io/npm/v/@libraz/libcantus)](https://www.npmjs.com/package/@libraz/libcantus)
17
7
  [![codecov](https://codecov.io/gh/libraz/libcantus/branch/main/graph/badge.svg)](https://codecov.io/gh/libraz/libcantus)
18
8
  [![License](https://img.shields.io/badge/license-Apache--2.0-blue)](https://github.com/libraz/libcantus/blob/main/LICENSE)
19
9
  [![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
20
- [![Node.js](https://img.shields.io/badge/Node.js-22.x-339933?logo=node.js&logoColor=white)](https://nodejs.org/)
10
+ [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D22-339933?logo=node.js&logoColor=white)](https://nodejs.org/)
21
11
  [![docs](https://img.shields.io/badge/docs-API%20reference-b5892e)](https://libraz.github.io/libcantus/)
22
12
 
23
- ## What can you do with it?
24
-
25
- - **Name and move notes** — parse `C4`, transpose it, convert to/from MIDI, spell
26
- intervals with correct enharmonics (`F#` vs `Gb`).
27
- - **Build chords and progressions** — from scale degrees, Roman numerals
28
- (`V7/V`), or lead-sheet symbols (`Cmaj7`, `F#m7b5`, `C/G`).
29
- - **Analyze harmony** — Roman-numeral analysis, harmonic function, cadence
30
- detection, and borrowed / modal-interchange chords.
31
- - **Recognize chords and keys** — notes in, chord or key out (the inverse of the
32
- builders).
33
- - **Reharmonize** — tritone subs, chromatic mediants, negative harmony, modal
34
- interchange palettes.
35
- - **Voice chords** — smooth 4-voice (SATB) leading, plus drop-2/3, shell, and
36
- rootless comping styles.
37
- - **Generate parts** — bass lines, countermelodies, drums, rhythms, motifs, and
38
- whole progressions; deterministic for a given seed.
39
- - **Analyze a whole arrangement** — feed raw multi-track note events, recover the
40
- harmony, get a whole-piece analysis (with per-note conflicts), and generate the
41
- missing parts against it.
42
- - **Work with pitch as sound** — frequencies, cents, EDO, and just intonation.
43
-
44
- The theory itself lives in **tree-shakeable pure functions**. On top of them sits
45
- a **fluent, immutable class API** (`Note`, `Interval`, `Chord`, `Key`,
46
- `Progression`) that reads like music theory. Which one you get follows a single
47
- rule: the musical **values** — notes, chords, keys, progressions — have both
48
- forms and interoperate freely, while operations over **collections and
49
- timelines** (part generation, whole-arrangement analysis, key ranking) take
50
- arrays of note events and are exposed as functions only.
13
+ ## What it does
51
14
 
52
- ## Install
53
-
54
- ```sh
55
- yarn add @libraz/libcantus
56
- ```
57
-
58
- ## Subpath imports
59
-
60
- The package root exports everything. To pull in a single layer, import its
61
- subpath instead:
62
-
63
- ```ts
64
- import { Chord, Key, Note } from '@libraz/libcantus/model'; // class API
65
- import { majorKey, makeChord } from '@libraz/libcantus/theory'; // scales, chords
66
- import { generateDrums, generateProgression } from '@libraz/libcantus/generate';
67
- import { analyzeArrangement, detectKey } from '@libraz/libcantus/analyze';
68
- import { parseNote, edo } from '@libraz/libcantus/core'; // pitch, meter, tuning
69
- ```
70
-
71
- The layers are `core`, `theory`, `analyze`, `generate`, and `model`.
72
-
73
- ## Quick start
74
-
75
- The class API chains immutably and carries key context, so analysis needs no
76
- repetition:
77
-
78
- ```ts
79
- import { Chord, Key, Note } from '@libraz/libcantus';
80
-
81
- const c = Key.major('C');
82
-
83
- c.chord(4, 'dom7').pitchClasses(); // [2, 5, 7, 11] (G7)
84
- c.roman('V7/V').voice(); // [ ...SATB MIDI ] (secondary dominant, voiced)
85
- Note.of('C4').transpose(7).name; // 'G4'
86
-
87
- // A ii–V–I, built and analyzed in one line:
88
- c.chord(1, 'min').progressionTo(c.chord(4, 'dom7'), c.chord(0, 'maj')).analyze();
89
- // { chords: [...functional analysis...], cadence: 'authentic' }
90
-
91
- Chord.detect([60, 64, 67])[0].quality; // 'maj'
92
- ```
93
-
94
- Every class wraps a plain object (`Chord.data`, `Note.data`) and delegates to the
95
- pure functions, so the two styles interoperate freely. The same task with the
96
- functional API:
15
+ It sits between software that already holds notes or chord symbols — a DAW project, a MIDI parser, a practice tool — and what those notes mean harmonically. Give it note events and it recovers the harmony; give it that harmony back and it writes parts against it:
97
16
 
98
17
  ```ts
99
- import { chordFromDegree, chordPitchClasses, classifyInterval, ConsonanceClass, majorKey } from '@libraz/libcantus';
100
-
101
- const cMajor = majorKey(0);
102
-
103
- classifyInterval(7); // ConsonanceClass.PerfectConsonance
104
- chordPitchClasses(chordFromDegree(4, 'dom7', cMajor)); // [2, 5, 7, 11] (G7)
105
- ```
18
+ import { Composer, Score } from '@libraz/libcantus';
106
19
 
107
- ## Name and move notes
20
+ // Four bars of block chords, as a DAW would hand them over:
21
+ const harmony = [[48, 60, 64, 67], [41, 60, 65, 69], [43, 59, 62, 65], [48, 60, 64, 67]].flatMap(
22
+ (pitches, bar) => pitches.map((pitch) => ({ pitch, startBeat: bar * 4, durationBeat: 4 })),
23
+ );
108
24
 
109
- Parse and format notes, convert to MIDI, and spell intervals so enharmonics the
110
- pitch-class layer can't tell apart come out right:
25
+ const chords = Score.of(harmony).timeline();
26
+ chords.roman().map((entry) => entry.roman); // ['I', 'IV', 'V7', 'I']
27
+ chords.cadences().at(-1)?.cadence.type; // 'authentic'
111
28
 
112
- ```ts
113
- import { Interval, Note } from '@libraz/libcantus';
114
-
115
- Note.of('C4').transpose(7).name; // 'G4'
116
- Note.of('C4').midi; // 60
117
- Interval.between(Note.of('C4'), Note.of('F#4')).name; // 'A4' (augmented fourth)
118
- Interval.between(Note.of('C4'), Note.of('Gb4')).name; // 'd5' (diminished fifth)
29
+ // A walking bass over the harmony that was just recovered:
30
+ Composer.of({ key: chords.key, bpm: 120, seed: 1 }).bass(chords, { style: 'walking' }).notes;
31
+ // 16 notes: [{ pitch: 36, startBeat: 0, durationBeat: 1, velocity: 100 }, ...]
119
32
  ```
120
33
 
121
- ## Keys and scales
122
-
123
- A `KeyScale` is a root pitch class plus a 12-bit `modeMask12` (bit `n` set means
124
- pitch class `(rootPc + n) % 12` is in the scale). `majorKey`, `minorKey`, and
125
- `scaleByName` cover the modes, pentatonics, blues, whole-tone, and octatonic
126
- scales; `MAJOR_MASK` / `NATURAL_MINOR_MASK` let you define custom keys.
127
-
128
- `nearestScaleTone` snaps a pitch to the closest in-scale MIDI pitch (preferring
129
- the lower on a tie) — handy when constraining generated notes to a key.
130
-
131
- ## Build chords and progressions
34
+ Chord boundaries are searched for rather than assumed, and so is the key, so a piece that modulates is not read against the key it started in.
132
35
 
133
- From scale degrees, Roman numerals, or lead-sheet symbols — and back again:
36
+ What each layer covers:
134
37
 
135
- ```ts
136
- import { Chord, Key } from '@libraz/libcantus';
137
-
138
- const c = Key.major('C');
38
+ - **[Pitch and notation](docs/en/pitch-and-notation.md)** — spelled notes and intervals, MIDI conversion, note names read and written in English, German, Japanese, Italian or fixed-do, and line spelling solved as one path instead of note by note. Every text parser has a non-throwing sibling (`tryParseNote`, `tryParseInterval`, `tryParseChordSymbol`, `tryParseKeyName`, `tryParseTimeSignature`, and `tryParse` on `Note`, `Interval`, `Key`, `Chord` and `Meter`), so a text field needs no `try`/`catch` per keystroke.
39
+ - **[Harmony](docs/en/harmony.md)** — chords as structured values, scales including modes, pentatonics and the `WORLD_SCALES` set, Roman numerals and function, voicing, figured bass, and the part-writing and species-counterpoint checkers that report a violation with the voice it happened in and the reason.
40
+ - **[Analysis](docs/en/analysis.md)** — chord and key detection over note events, timelines and cadences, prolongational reduction, phrases, sections, hypermeter, motifs, and arrangement reports.
41
+ - **[Generation](docs/en/generation.md)** — progressions, motifs, rhythms, drums, bass and counter-melody, all drawing on one `GenerationContext`: a project seed, three additive complexity dials (rhythmic, harmonic, ornament), a separate difficulty ceiling, the tempo, and the instrument each part is written for. Same context, same output.
42
+ - **[Time and arrangement](docs/en/time-and-arrangement.md)** — meter, tempo, positions in bars and beats, and analysis across several tracks at once.
43
+ - **[Class API](docs/en/api-reference.md)** — the same theory as one immutable class per thing the library works with, `Score`, `Timeline` and `Composer` included, each a thin skin over the functions above. Reach for a function or a class as it suits the call site; the answers are the same.
139
44
 
140
- c.roman('V7/V').symbol(); // 'D7' (a secondary dominant)
141
- c.chord(4, 'dom7').roman(); // 'V7'
142
- Chord.parse('F#m7b5').pitchClasses(); // [0, 4, 6, 9]
143
- ```
45
+ Answers say why they were reached. `analyzeChord`, `detectCadence` and `explainRoman` carry a `rationale`, `detectKey` attaches one on request, and each can report the readings it turned down as `alternatives`. Where the input cannot settle a question, the field comes back `null` rather than a plausible guess — grading a cadence perfect or imperfect needs a voicing, because only a voicing says what is in the soprano.
144
46
 
145
- Chord vocabulary spans triads through thirteenths — `dim7`, `m7b5`, `minMaj7`,
146
- `aug7`, sixths, and altered dominants included.
47
+ Spelling is kept wherever it decides the answer. A diminished fifth and an augmented fourth are the same distance in pitch classes but not the same interval, so the counterpoint and part-writing checkers take spelled notes rather than numbers.
147
48
 
148
- Or generate a progression from a style preset (a collection-level operation, so
149
- it stays functional):
49
+ Seven worked guides start from the data an application already has: [DAW workflow](docs/en/use-cases/daw-workflow.md), [piece analysis](docs/en/use-cases/piece-analysis.md), [modulation report](docs/en/use-cases/modulation-report.md), [harmony exercise checker](docs/en/use-cases/harmony-exercise-checker.md), [generative arrangement](docs/en/use-cases/generative-arrangement.md), [chord chart import](docs/en/use-cases/chord-chart-import.md), and [part preparation](docs/en/use-cases/part-preparation.md).
150
50
 
151
- ```ts
152
- import { generateProgression, majorKey } from '@libraz/libcantus';
51
+ ## What it doesn't do
153
52
 
154
- // One chord per bar, secondary dominants inserted where they fit:
155
- generateProgression({ key: majorKey(0), style: 'idol', bars: 8, reharmonize: true, seed: 1 });
156
- ```
53
+ - **No I/O, notation or audio.** No MIDI file reader or writer, no score rendering, no audio analysis, no playback. Bring your own parser and hand it note events. If that layer is what you need, [libsonare](https://github.com/libraz/libsonare) covers audio analysis, mastering, synthesis and SMF I/O; the two share no code and neither requires the other.
54
+ - **No microtonal analysis.** Frequencies, cents, equal divisions of the octave and just-intonation ratios live in `core`, but everything above the pitch layer runs on twelve pitch classes. Music organised in smaller steps than a semitone is out of reach, which is also why the maqāmāt built on half-flat degrees are left out of `WORLD_SCALES` rather than rounded to a twelve-tone neighbour.
55
+ - **No modal systems.** `WORLD_SCALES` records the pitch material a tradition names — a thāt, a maqām's set, a Japanese pentatonic — and not the system built on it: ascent and descent forms, the notes a phrase leans on, and the phrase grammar have no place in a mask.
56
+ - **No assumption that the material is tonal.** Roman numerals and functional harmony presuppose the common practice, and `supportsFunctionalHarmony` says whether a named scale admits that reading at all — `'dorian'` does, `'miyakoBushi'` does not.
57
+ - **No corpus.** Nothing is bundled to run statistics over.
58
+ - **Known gaps in functional harmony.** `functionOf` answers from a chord and a key alone, so a cadential six-four reads there as an inverted tonic; the dominant reading of it belongs to the layers that see time, and `detectCadence` gives it when the chord before the dominant is passed as `approach`.
157
59
 
158
- ## Analyze harmony
60
+ ## Requirements
159
61
 
160
- Turn chords into Roman-numeral analysis with function, cadences, and modal
161
- interchange — in major and minor, respecting inversions:
162
-
163
- ```ts
164
- import { Chord, Key } from '@libraz/libcantus';
165
-
166
- // A minor iv in a major key reads as a borrowed subdominant:
167
- Chord.of('F', 'min').analyze(Key.major('C'));
168
- // { function: 'subdominant', borrowed: true, source: 'parallelMinor', roman: 'iv' }
169
- ```
62
+ Node.js >= 22.
170
63
 
171
- ## Recognize chords and keys
172
-
173
- Notes in, chord or key out — the inverse of the builders:
174
-
175
- ```ts
176
- import { Chord, detectKey } from '@libraz/libcantus';
177
-
178
- Chord.detect([60, 64, 67])[0].symbol(); // 'C'
179
- Chord.detectBest([60, 63, 67, 70])?.symbol(); // 'Cm7'
180
- detectKey([0, 0, 0, 4, 7])[0]; // C major, best fit (key ranking stays functional)
181
- ```
182
-
183
- ## Spell chords and scales
184
-
185
- A `Key` carries a spelled tonic, so the pitch-class core produces letter names,
186
- correct in major and minor:
187
-
188
- ```ts
189
- import { Chord, Key } from '@libraz/libcantus';
190
-
191
- Key.named('harmonicMinor', 'A').noteNames(); // ['A', 'B', 'C', 'D', 'E', 'F', 'G#']
192
- Key.minor('E').noteNames(); // ['E', 'F#', 'G', 'A', 'B', 'C', 'D']
193
-
194
- // Chords spell in a key's context too:
195
- Key.major('C').chord(4, 'dom7').spell().map((n) => n.name); // ['G', 'B', 'D', 'F']
196
- ```
197
-
198
- ## Reharmonize
199
-
200
- Reflect a chord through negative harmony on the class, or list substitution
201
- candidates — tritone, relative, borrowed, and chromatic-mediant subs, plus
202
- modal-interchange palettes — with the functions:
203
-
204
- ```ts
205
- import { Chord, Key, majorKey, parseChordSymbol, substituteChord } from '@libraz/libcantus';
206
-
207
- Key.major('C').chord(4, 'dom7').negativeHarmony().symbol(); // 'Dm7b5'
208
-
209
- // Substitution/palette search returns lists, so it stays functional:
210
- substituteChord(parseChordSymbol('G7'), majorKey(0));
211
- // [{ chord: Db7, type: 'tritone', ... }, ...]
212
- ```
213
-
214
- ## Choose scales and tensions
215
-
216
- Find scales compatible with a chord and its available tensions and avoid notes
217
- on the class; get a continuity-optimized scale choice across a whole set of
218
- changes with `scalesForChanges`:
219
-
220
- ```ts
221
- import { Chord } from '@libraz/libcantus';
64
+ ## Install
222
65
 
223
- Chord.of('C', 'dom7').scales()[0]; // { name: 'mixolydian', rootPc: 0 }
224
- Chord.of('C', 'maj7').tensions('ionian'); // [2, 9] (9 and 13; the 11 is an avoid note)
66
+ ```sh
67
+ yarn add @libraz/libcantus
225
68
  ```
226
69
 
227
- ## Voice chords
70
+ ## Quick start
228
71
 
229
- Realize a progression into smooth 4-voice (SATB) MIDI voicings with minimal
230
- motion, or voice a single chord in a comping style (drop-2/3, shell, rootless):
72
+ The theory lives in tree-shakeable pure functions. On top of them sits an immutable class API — one class per thing the library works with, from a single `Note`, `Interval` or `Chord` up to the `Score`, `Timeline`, `Arrangement` and `Composer` that carry a whole piece — that reads the way theory is spoken:
231
73
 
232
74
  ```ts
233
75
  import { Chord, Key } from '@libraz/libcantus';
234
76
 
235
- const c = Key.major('C');
77
+ const key = Key.major('C');
78
+ const dominant = key.chord(5, 'dom7');
236
79
 
237
- // A whole progression, voiced with minimal motion:
238
- c.chord(0, 'maj').progressionTo(c.chord(5, 'maj'), c.chord(4, 'dom7'), c.chord(0, 'maj')).voice();
239
- // [[...], [...], [...], [...]] (one ascending pitch per voice)
240
-
241
- // A single chord in a shell comping voicing:
242
- Chord.of('C', 'maj7').styledVoicing({ style: 'shell' }); // root, third, seventh
80
+ dominant.symbol(); // 'G7'
81
+ dominant.analyze(key).roman; // 'V7'
82
+ Chord.parse('C7(b9,#11)').pitchClasses(); // [0, 1, 4, 6, 7, 10]
243
83
  ```
244
84
 
245
- `voiceLeadingCost` and `nextVoicing` let you steer the leading, and the
246
- `counterpoint` predicates (parallel/hidden perfects, spacing, voice crossing,
247
- leading-tone resolution, …) let you validate it.
85
+ The functions are the library; every class is a thin skin over them. A class wraps a plain object it exposes as `.data`, and the functions return data a class can be built from, so neither side is a walled garden and the two give the same answers. What a class adds is holding a value together with its context, so a chain of calls does not restate the same facts: a `Score` carries its notes with their meter, tempo, and key, and a `Composer` carries the key, tempo, and seed one piece is written under.
248
86
 
249
- ## Generate parts
87
+ ## Import paths
250
88
 
251
- Seeded, deterministic generators for melody and accompaniment:
89
+ The package root exports everything. To pull in a single layer, import its subpath instead:
252
90
 
253
91
  ```ts
254
- import {
255
- generateBassLine, generateCounterMelody, generateDrums, generateRhythm,
256
- generateMotif, majorKey, parseTimeSignature,
257
- } from '@libraz/libcantus';
258
-
259
- generateRhythm(parseTimeSignature('4/4'), { seed: 1, density: 0.5 }); // strong-beat-weighted onsets
260
- const motif = generateMotif({ key: majorKey(0), bars: 2, contour: 'arch', seed: 1 });
261
- motif.notes.length > 0; // true — a deterministic short melodic cell
262
- ```
263
-
264
- `humanize`, `extractGrooveTemplate`, and `applyGrooveTemplate` add a meter-aware
265
- feel — or capture the feel of one performance and transplant it onto another.
266
-
267
- ## Analyze and extend an arrangement
268
-
269
- The arrangement layer takes raw multi-track `NoteEvent`s — whatever your DAW or
270
- MIDI-file parser produced — recovers the harmony, then analyses and generates
271
- against it:
272
-
273
- ```ts
274
- import {
275
- analyzeArrangement, chordTimelineFromNotes, generateBassLine, generateCounterMelody,
276
- } from '@libraz/libcantus';
277
-
278
- // Infer a chord progression (and the key) from played notes:
279
- const { timeline, key } = chordTimelineFromNotes(melodyAndChordNotes);
280
-
281
- // Whole-piece analysis: inferred chords, cadences, per-note theory labels, and
282
- // notes that clash with the sounding harmony (with reasons and suggestions):
283
- const report = analyzeArrangement([
284
- { role: 'melody', notes: melodyNotes },
285
- { role: 'harmony', notes: chordNotes },
286
- ]);
287
- report.conflicts; // [{ beat, trackName, pitch, safety, reasons, rationale }, ...]
288
-
289
- // Generate the missing parts from the recovered timeline:
290
- generateBassLine({ segments: timeline.segments, key, style: 'walking', seed: 1 });
291
- generateCounterMelody({ melody: melodyNotes, timeline, key, register: 'below' });
92
+ import { parseNote, edo } from '@libraz/libcantus/core'; // pitch, meter, tempo, tuning
93
+ import { majorKey, makeChord } from '@libraz/libcantus/theory'; // scales, chords, voicing, rules
94
+ import { analyzeArrangement, detectKey } from '@libraz/libcantus/analyze';
95
+ import { generateDrums, generateProgression } from '@libraz/libcantus/generate';
96
+ import { Chord, Key, Note } from '@libraz/libcantus/model'; // class API
292
97
  ```
293
98
 
294
- `harmonizeMelody` goes the other way — given a bare melody, it searches for the
295
- best key, transpose, and chord path (with optional reharmonization) to harmonize
296
- it.
99
+ Both ESM and CommonJS builds ship for every path.
297
100
 
298
- Note events shared across the arrangement and generation modules use one type,
299
- `NoteEvent` (`{ pitch, startBeat, durationBeat, velocity? }`, MIDI pitch in
300
- quarter-note beats). Seeded generators (`bass`, `groove`, `countermelody`,
301
- `rhythm`, `drums`, `motif`, `progression`) are deterministic for a given seed.
101
+ ## Documentation
302
102
 
303
- ## Work with pitch as sound
103
+ Start here: [Introduction](docs/en/introduction.md), [Getting started](docs/en/getting-started.md), [Use cases](docs/en/use-cases/index.md).
304
104
 
305
- Frequencies, cents, EDO, and just intonation — for tuning, microtonality, and
306
- analysis:
105
+ Domain guides: [Pitch and notation](docs/en/pitch-and-notation.md), [Scales and modes](docs/en/scales-and-modes.md), [Harmony](docs/en/harmony.md), [Key relations and modulation](docs/en/key-relations-and-modulation.md), [Voicing](docs/en/voicing.md), [Counterpoint and part-writing](docs/en/counterpoint-and-part-writing.md), [Time and arrangement](docs/en/time-and-arrangement.md), [Analysis](docs/en/analysis.md), [Melody and motifs](docs/en/melody-and-motifs.md), [Rhythm and groove](docs/en/rhythm-and-groove.md), [Generation](docs/en/generation.md), [Reharmonization](docs/en/reharmonization.md), [Instruments and playability](docs/en/instruments-and-playability.md), [Tuning and frequency](docs/en/tuning-and-frequency.md).
307
106
 
308
- ```ts
309
- import { frequencyOf, edo, justDeviationCents } from '@libraz/libcantus';
310
- ```
107
+ Cross-cutting: [Determinism and seeding](docs/en/determinism-and-seeding.md), [Errors and validation](docs/en/errors-and-validation.md), [Performance](docs/en/performance.md), [Interoperability](docs/en/interoperability.md).
311
108
 
312
- Meter helpers (`TimeSignature`, `parseTimeSignature`, `beatsPerBar`,
313
- `metricWeight`, `isStrongBeat`, `tuplet`) cover simple and compound meters and
314
- back the accent-aware generators.
315
-
316
- ## Documentation
109
+ Reference: [API reference](docs/en/api-reference.md), [Glossary](docs/en/glossary.md), [Questions and limitations](docs/en/faq.md).
317
110
 
318
- Full API reference — every export with signatures, categorized by domain and
319
- with runnable examples — is generated from the source and published at
320
- **[libraz.github.io/libcantus](https://libraz.github.io/libcantus/)**.
111
+ Every `ts` example in these guides is executed by the test suite, and an expected value written as a trailing comment is checked against what the call returns. The generated TypeDoc reference is written to `docs/api` and is separate from the hand-authored guides.
321
112
 
322
113
  ## License
323
114
 
324
- Apache-2.0
115
+ [@libraz/libcantus](https://github.com/libraz/libcantus) is released under the [Apache License 2.0](LICENSE).