@libraz/libcantus 0.9.5 → 1.0.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.
- package/README.md +61 -270
- package/README_ja.md +61 -214
- package/dist/analyze/index.cjs +54 -7
- package/dist/analyze/index.cjs.map +1 -1
- package/dist/analyze/index.d.cts +200 -353
- package/dist/analyze/index.d.ts +200 -353
- package/dist/analyze/index.js +56 -9
- package/dist/chunk-3JXCW6DX.cjs +3220 -0
- package/dist/chunk-3JXCW6DX.cjs.map +1 -0
- package/dist/chunk-4A2RMUCC.js +7647 -0
- package/dist/chunk-4A2RMUCC.js.map +1 -0
- package/dist/chunk-6XPTBE7Y.cjs +49 -0
- package/dist/chunk-6XPTBE7Y.cjs.map +1 -0
- package/dist/chunk-AFM2ULJH.cjs +5121 -0
- package/dist/chunk-AFM2ULJH.cjs.map +1 -0
- package/dist/chunk-E3F4AHAH.cjs +690 -0
- package/dist/chunk-E3F4AHAH.cjs.map +1 -0
- package/dist/{chunk-2PTFGVD7.js → chunk-ELCTZUJJ.js} +250 -132
- package/dist/chunk-ELCTZUJJ.js.map +1 -0
- package/dist/chunk-FVSI36UV.js +690 -0
- package/dist/chunk-FVSI36UV.js.map +1 -0
- package/dist/chunk-GEDTJHAC.cjs +1 -0
- package/dist/chunk-GEDTJHAC.cjs.map +1 -0
- package/dist/chunk-GOZG57ER.js +3220 -0
- package/dist/chunk-GOZG57ER.js.map +1 -0
- package/dist/chunk-H3VORETI.js +2243 -0
- package/dist/chunk-H3VORETI.js.map +1 -0
- package/dist/chunk-H42UHPYW.js +1577 -0
- package/dist/chunk-H42UHPYW.js.map +1 -0
- package/dist/chunk-HFPS7JFD.js +1 -0
- package/dist/chunk-INHVXP6Z.js +3053 -0
- package/dist/chunk-INHVXP6Z.js.map +1 -0
- package/dist/chunk-K4ZJUGF4.cjs +379 -0
- package/dist/chunk-K4ZJUGF4.cjs.map +1 -0
- package/dist/chunk-KNJEKLKV.js +49 -0
- package/dist/chunk-KNJEKLKV.js.map +1 -0
- package/dist/chunk-M4CWTJWJ.cjs +7647 -0
- package/dist/chunk-M4CWTJWJ.cjs.map +1 -0
- package/dist/chunk-MXO72PLY.cjs +211 -0
- package/dist/chunk-MXO72PLY.cjs.map +1 -0
- package/dist/chunk-NLEU4QSE.cjs +360 -0
- package/dist/chunk-NLEU4QSE.cjs.map +1 -0
- package/dist/chunk-OE6WK2Y2.cjs +1577 -0
- package/dist/chunk-OE6WK2Y2.cjs.map +1 -0
- package/dist/chunk-Q2WGKJNN.cjs +1064 -0
- package/dist/chunk-Q2WGKJNN.cjs.map +1 -0
- package/dist/chunk-QYQQ5J6E.js +1 -0
- package/dist/chunk-QYQQ5J6E.js.map +1 -0
- package/dist/chunk-RHIJ3S4I.cjs +1 -0
- package/dist/chunk-RHIJ3S4I.cjs.map +1 -0
- package/dist/chunk-TTCLJX6D.js +211 -0
- package/dist/chunk-TTCLJX6D.js.map +1 -0
- package/dist/chunk-UADYAYAT.cjs +3053 -0
- package/dist/chunk-UADYAYAT.cjs.map +1 -0
- package/dist/chunk-VOHZB45S.js +5121 -0
- package/dist/chunk-VOHZB45S.js.map +1 -0
- package/dist/chunk-VTI6FOLB.js +1064 -0
- package/dist/chunk-VTI6FOLB.js.map +1 -0
- package/dist/chunk-WWURE4XT.cjs +2243 -0
- package/dist/chunk-WWURE4XT.cjs.map +1 -0
- package/dist/chunk-YH3BIKHH.js +360 -0
- package/dist/chunk-YH3BIKHH.js.map +1 -0
- package/dist/core/index.cjs +93 -6
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +87 -346
- package/dist/core/index.d.ts +87 -346
- package/dist/core/index.js +94 -7
- package/dist/generate/index.cjs +89 -10
- package/dist/generate/index.cjs.map +1 -1
- package/dist/generate/index.d.cts +436 -1094
- package/dist/generate/index.d.ts +436 -1094
- package/dist/generate/index.js +94 -15
- package/dist/index-9wcMhG73.d.ts +1095 -0
- package/dist/{index-Cfx8xgH0.d.ts → index-B4o55NDr.d.cts} +13 -9
- package/dist/index-B9MNZboQ.d.cts +770 -0
- package/dist/index-B9MNZboQ.d.ts +770 -0
- package/dist/index-BGFW-8P2.d.cts +2307 -0
- package/dist/index-BO6xE9YU.d.cts +1804 -0
- package/dist/index-BkGmlyI3.d.cts +470 -0
- package/dist/index-CCNJRG-P.d.cts +1095 -0
- package/dist/index-CGpRv5sl.d.cts +643 -0
- package/dist/index-C_8o--VE.d.cts +1242 -0
- package/dist/index-CgHqxdps.d.ts +2307 -0
- package/dist/index-Chbst3Jx.d.ts +1343 -0
- package/dist/index-Cj49F892.d.ts +470 -0
- package/dist/{index-x2vcsduE.d.cts → index-CunYKLiu.d.ts} +13 -9
- package/dist/index-D57QRbVn.d.ts +1804 -0
- package/dist/index-DInQqT8Q.d.cts +66 -0
- package/dist/index-DInQqT8Q.d.ts +66 -0
- package/dist/index-Dzr13q3d.d.cts +1343 -0
- package/dist/index-G1uwGGWQ.d.ts +643 -0
- package/dist/index-szBv3496.d.ts +1242 -0
- package/dist/index.cjs +348 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -15
- package/dist/index.d.ts +17 -15
- package/dist/index.js +397 -64
- package/dist/masks-DYbE485J.d.cts +445 -0
- package/dist/masks-DYbE485J.d.ts +445 -0
- package/dist/model/index.cjs +39 -7
- package/dist/model/index.cjs.map +1 -1
- package/dist/model/index.d.cts +4659 -249
- package/dist/model/index.d.ts +4659 -249
- package/dist/model/index.js +40 -8
- package/dist/positional-Cptlj-xs.d.ts +287 -0
- package/dist/positional-DvH_sd8u.d.cts +287 -0
- package/dist/theory/index.cjs +101 -7
- package/dist/theory/index.cjs.map +1 -1
- package/dist/theory/index.d.cts +511 -171
- package/dist/theory/index.d.ts +511 -171
- package/dist/theory/index.js +118 -24
- package/package.json +6 -4
- package/dist/chunk-2A2QMA74.cjs +0 -261
- package/dist/chunk-2A2QMA74.cjs.map +0 -1
- package/dist/chunk-2PTFGVD7.js.map +0 -1
- package/dist/chunk-357ZDPVU.cjs +0 -162
- package/dist/chunk-357ZDPVU.cjs.map +0 -1
- package/dist/chunk-3KX76RLG.js +0 -1548
- package/dist/chunk-3KX76RLG.js.map +0 -1
- package/dist/chunk-4VZ5MZAM.cjs +0 -889
- package/dist/chunk-4VZ5MZAM.cjs.map +0 -1
- package/dist/chunk-7EONYVI5.js +0 -1204
- package/dist/chunk-7EONYVI5.js.map +0 -1
- package/dist/chunk-7JEP3MFY.js +0 -162
- package/dist/chunk-7JEP3MFY.js.map +0 -1
- package/dist/chunk-AID75RN3.js +0 -3176
- package/dist/chunk-AID75RN3.js.map +0 -1
- package/dist/chunk-BSLTLGM3.cjs +0 -497
- package/dist/chunk-BSLTLGM3.cjs.map +0 -1
- package/dist/chunk-CU6VG2YM.cjs +0 -109
- package/dist/chunk-CU6VG2YM.cjs.map +0 -1
- package/dist/chunk-DZMHIGI4.js +0 -337
- package/dist/chunk-DZMHIGI4.js.map +0 -1
- package/dist/chunk-EDOTEABK.js +0 -109
- package/dist/chunk-EDOTEABK.js.map +0 -1
- package/dist/chunk-HN2OEUN2.js +0 -33
- package/dist/chunk-HN2OEUN2.js.map +0 -1
- package/dist/chunk-KBTTEX7Q.js +0 -497
- package/dist/chunk-KBTTEX7Q.js.map +0 -1
- package/dist/chunk-KQN2ZYND.cjs +0 -724
- package/dist/chunk-KQN2ZYND.cjs.map +0 -1
- package/dist/chunk-MPTVJIVG.cjs +0 -3176
- package/dist/chunk-MPTVJIVG.cjs.map +0 -1
- package/dist/chunk-MTMZTMWM.cjs +0 -53
- package/dist/chunk-MTMZTMWM.cjs.map +0 -1
- package/dist/chunk-N6IHQUOX.js +0 -1
- package/dist/chunk-NNB67XA2.js +0 -724
- package/dist/chunk-NNB67XA2.js.map +0 -1
- package/dist/chunk-OL3IHHSJ.cjs +0 -1204
- package/dist/chunk-OL3IHHSJ.cjs.map +0 -1
- package/dist/chunk-PI3UXRWK.cjs +0 -528
- package/dist/chunk-PI3UXRWK.cjs.map +0 -1
- package/dist/chunk-R2VVFAIN.js +0 -528
- package/dist/chunk-R2VVFAIN.js.map +0 -1
- package/dist/chunk-T64C7ZD7.cjs +0 -1
- package/dist/chunk-T64C7ZD7.cjs.map +0 -1
- package/dist/chunk-WFDXOLUB.cjs +0 -337
- package/dist/chunk-WFDXOLUB.cjs.map +0 -1
- package/dist/chunk-WTAN4ACG.cjs +0 -1548
- package/dist/chunk-WTAN4ACG.cjs.map +0 -1
- package/dist/chunk-YFJXS3AU.cjs +0 -33
- package/dist/chunk-YFJXS3AU.cjs.map +0 -1
- package/dist/chunk-YUE6TJ4M.js +0 -889
- package/dist/chunk-YUE6TJ4M.js.map +0 -1
- package/dist/chunk-ZGLJM4A4.js +0 -53
- package/dist/chunk-ZGLJM4A4.js.map +0 -1
- package/dist/function-CxixTB02.d.cts +0 -299
- package/dist/function-DTCt675z.d.ts +0 -299
- package/dist/index-B2Ulxxpo.d.cts +0 -144
- package/dist/index-BkYJ0xMq.d.cts +0 -152
- package/dist/index-BuaB-ypt.d.ts +0 -144
- package/dist/index-CAN7v-kw.d.ts +0 -199
- package/dist/index-COw1Ky9g.d.ts +0 -242
- package/dist/index-DRtIhlJj.d.ts +0 -152
- package/dist/index-DXd9JikN.d.cts +0 -246
- package/dist/index-DXd9JikN.d.ts +0 -246
- package/dist/index-alrRninU.d.cts +0 -199
- package/dist/index-oTQbYYIs.d.cts +0 -242
- package/dist/index-tR9jTn26.d.cts +0 -275
- package/dist/index-tR9jTn26.d.ts +0 -275
- package/dist/styled-Bl0ZK28h.d.ts +0 -543
- package/dist/styled-Crv6SA4S.d.cts +0 -543
- package/dist/types-BN6_Q9Kl.d.cts +0 -39
- package/dist/types-BN6_Q9Kl.d.ts +0 -39
- /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
|
|
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
|
[](https://github.com/libraz/libcantus/actions)
|
|
16
6
|
[](https://www.npmjs.com/package/@libraz/libcantus)
|
|
17
7
|
[](https://codecov.io/gh/libraz/libcantus)
|
|
18
8
|
[](https://github.com/libraz/libcantus/blob/main/LICENSE)
|
|
19
9
|
[](https://www.typescriptlang.org/)
|
|
20
|
-
[](https://nodejs.org/)
|
|
21
11
|
[](https://libraz.github.io/libcantus/)
|
|
22
12
|
|
|
23
|
-
## What
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
+
What each layer covers:
|
|
134
37
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
152
|
-
import { generateProgression, majorKey } from '@libraz/libcantus';
|
|
51
|
+
## What it doesn't do
|
|
153
52
|
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
##
|
|
60
|
+
## Requirements
|
|
159
61
|
|
|
160
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
224
|
-
|
|
66
|
+
```sh
|
|
67
|
+
yarn add @libraz/libcantus
|
|
225
68
|
```
|
|
226
69
|
|
|
227
|
-
##
|
|
70
|
+
## Quick start
|
|
228
71
|
|
|
229
|
-
|
|
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
|
|
77
|
+
const key = Key.major('C');
|
|
78
|
+
const dominant = key.chord(5, 'dom7');
|
|
236
79
|
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
// [
|
|
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
|
-
`
|
|
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
|
-
##
|
|
87
|
+
## Import paths
|
|
250
88
|
|
|
251
|
-
|
|
89
|
+
The package root exports everything. To pull in a single layer, import its subpath instead:
|
|
252
90
|
|
|
253
91
|
```ts
|
|
254
|
-
import {
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
115
|
+
[@libraz/libcantus](https://github.com/libraz/libcantus) is released under the [Apache License 2.0](LICENSE).
|