tonus 0.9.0 → 0.9.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.
- package/CHANGELOG.md +62 -0
- package/README.md +3 -3
- package/dist/engines/score/emitters/moderna.js +150 -12
- package/dist/engines/temper/gabc.d.ts +12 -0
- package/dist/engines/temper/gabc.js +51 -18
- package/docs/api/calendar.md +4 -4
- package/docs/api/census.md +20 -20
- package/docs/api/chant.md +21 -21
- package/docs/api/heavens.md +7 -7
- package/docs/api/index.md +5 -5
- package/docs/api/score.md +44 -44
- package/docs/api/tuning.md +14 -14
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,68 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to tonus. Newest first.
|
|
4
4
|
|
|
5
|
+
## 0.9.1 — 2026-08-24
|
|
6
|
+
|
|
7
|
+
Moderna reads the pitch it was given: the whole page had been sitting a sixth
|
|
8
|
+
too high, and a transposed chant said nothing about its own accidentals.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Moderna drew every note a sixth above where it belonged.** `writtenY`
|
|
13
|
+
measured the note against a reference term that had not been normalised the
|
|
14
|
+
same way (`4 * 7` against a note term carrying `+2`), so sounding E4 computed
|
|
15
|
+
as 14 staff steps instead of 0. Every note of every moderna score sat above
|
|
16
|
+
the top line. The port was faithful — the same expression is in the locked
|
|
17
|
+
reference generator, which was fed pitches an octave below today's corpus, so
|
|
18
|
+
two errors cancelled; when the corpus moved to true octaves only one of them
|
|
19
|
+
moved. The clef's octave lift is now stated once, explicitly.
|
|
20
|
+
- **A transposed chromatic rendered as a silent wrong pitch.** `temperamentum`'s
|
|
21
|
+
`transpose` moves what a chant sounds, and moderna reads sounding pitch — but
|
|
22
|
+
the accidental was parsed and discarded, so `Ab3` and `A3` landed on one slot
|
|
23
|
+
with no sign drawn. The page read a semitone off with nothing to say so.
|
|
24
|
+
Spelling now follows `spn`: flats stay flats, sharps stay sharps. Signs
|
|
25
|
+
written in the GABC are unaffected — `computeAccidentals` remains
|
|
26
|
+
authoritative wherever it has an opinion, keyed on the degree a sign alters
|
|
27
|
+
rather than the row that carries it.
|
|
28
|
+
- **Moderna had no ledger lines at all.** A note outside the five rendered as a
|
|
29
|
+
head floating in blank space. One line per staff line the note has passed,
|
|
30
|
+
drawn behind the head: the spaces immediately outside (steps 9 and −1) get
|
|
31
|
+
none, and a line-note and the space above it share one.
|
|
32
|
+
- **`midiToGabc` threw on every B-flat** — the one accidental chant sings, and
|
|
33
|
+
the whole reason `parse.ts` carries a flat state machine. It now spells it
|
|
34
|
+
`ix`, and `gabcToMidi` reads that back, so the two stay inverses. Other
|
|
35
|
+
chromatic pitch classes still throw. Verified over 222 round-trips.
|
|
36
|
+
- **The GABC clef tables disagreed about which clefs exist.** `temper/gabc.ts`
|
|
37
|
+
knew six (`c1`–`c4`, `f3`, `f4`) while `parse.ts` accepted sixteen, so a
|
|
38
|
+
`cb2` the parser reads happily raised "Unknown clef". `CLEFS` is now derived
|
|
39
|
+
rather than hand-listed; all six original entries are reproduced exactly.
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- **A low moderna chant is written an octave up.** One clef throughout — what
|
|
44
|
+
floats is the written octave, the move the hand already makes when it lifts a
|
|
45
|
+
chant onto the gamut's fingers. Chant sounds below this staff (corpus median
|
|
46
|
+
50–62 against a window of 52–65), and transposing down pushes it further: at
|
|
47
|
+
−4 an unlifted score put 43.6% of all notes under the bottom line, now 9.8%.
|
|
48
|
+
Untransposed moderna output moves for most chants as a result, always toward
|
|
49
|
+
the staff (7.8% off-staff to 5.8%). Whole octaves only, and one lift for the
|
|
50
|
+
whole chant — a page that changed register partway would read as two pieces.
|
|
51
|
+
The limit is honest: an ambitus straddling the window cannot be aligned by
|
|
52
|
+
any multiple of 12, and 59 of 200 measured chants are wider than the staff.
|
|
53
|
+
`notatio`'s `spn` remains the authority on what actually sounds.
|
|
54
|
+
- **Quadrata is untouched by all of this**, by construction: it draws from
|
|
55
|
+
staff position, which transposition does not move. Verified byte-identical.
|
|
56
|
+
|
|
57
|
+
### Packaging
|
|
58
|
+
|
|
59
|
+
- `.npmignore` removed — it contradicted the `files` allowlist, and two
|
|
60
|
+
mechanisms deciding what ships is one more than can be reasoned about.
|
|
61
|
+
Measured before and after: the tarball is byte-identical at 216 files.
|
|
62
|
+
- `"sideEffects": false` declared, and verified true — nothing in `dist/` runs
|
|
63
|
+
at module scope and no data file is read at import time.
|
|
64
|
+
- The description no longer claims "and performance"; the voice lives in
|
|
65
|
+
tonus-sonus.
|
|
66
|
+
|
|
5
67
|
## 0.9.0 — 2026-08-19
|
|
6
68
|
|
|
7
69
|
The tracks stop promising a contract they never kept, and the geometry says
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ belong to which day and returns analysis and notation, and calculates the
|
|
|
8
8
|
_music of the spheres_ proposed by Pythagoras, Boethius, and more as real
|
|
9
9
|
intervals.
|
|
10
10
|
|
|
11
|
-
**[
|
|
11
|
+
**[Interactive demo](https://orreliquum.com/)**
|
|
12
12
|
|
|
13
13
|
Everything is computed locally and deterministically: the same question always
|
|
14
14
|
returns the same answer, from data that ships inside the package, with no network
|
|
@@ -72,8 +72,8 @@ its final, dominant, ambitus, and ethos, with its psalm tones.
|
|
|
72
72
|
**The calendar** is Tridentine, resolved against Easter by
|
|
73
73
|
the Julian or the Gregorian computus.
|
|
74
74
|
|
|
75
|
-
**The repertoire** is 2,187 chants across ten books
|
|
76
|
-
the _Nocturnale Romanum_ night office
|
|
75
|
+
**The repertoire** is 2,187 chants across ten books, nine Solesmes editions and
|
|
76
|
+
the _Nocturnale Romanum_ night office in
|
|
77
77
|
[GABC](https://gregorio-project.github.io/) notation: Mass propers, the Kyriale,
|
|
78
78
|
the Office hours, the psalter, and the Matins nocturns.
|
|
79
79
|
|
|
@@ -30,7 +30,53 @@ const G = {
|
|
|
30
30
|
noteheadBlack: "E0A4",
|
|
31
31
|
augmentationDot: "E1E7",
|
|
32
32
|
quilisma: "EA20", // medRenQuilismaCMN
|
|
33
|
+
accidentalFlat: "E260",
|
|
34
|
+
accidentalNatural: "E261",
|
|
35
|
+
accidentalSharp: "E262",
|
|
33
36
|
};
|
|
37
|
+
// The staff holds nine written diatonic slots — bottom line E4 to top line F5
|
|
38
|
+
// — which under the transposing gClef8vb sounds E3–F4, MIDI 52–65.
|
|
39
|
+
const STAFF_LO = 52;
|
|
40
|
+
const STAFF_HI = 65;
|
|
41
|
+
/**
|
|
42
|
+
* How many octaves to lift the written notes so the chant sits on the staff.
|
|
43
|
+
*
|
|
44
|
+
* ONE clef, always: moderna draws a gClef8vb and nothing else. What floats is
|
|
45
|
+
* the written octave, the same move the orreliquum hand already makes when it
|
|
46
|
+
* lifts a chant by whole octaves onto the gamut's fingers.
|
|
47
|
+
*
|
|
48
|
+
* It earns its place going DOWN, which is the direction chant is transposed:
|
|
49
|
+
* the corpus already sounds below this staff (median 50–62 against 52–65), so
|
|
50
|
+
* a transposition of −4 puts 43.6% of all notes below the bottom line, some
|
|
51
|
+
* four ledger lines deep. Choosing the octave takes that to 15.8%. Upward it
|
|
52
|
+
* changes little, because chant does not sit high to begin with.
|
|
53
|
+
*
|
|
54
|
+
* Whole octaves only, and the same lift for the whole chant: shifting by
|
|
55
|
+
* anything else would respell the music, and shifting per-phrase would make
|
|
56
|
+
* one page read in two registers.
|
|
57
|
+
*
|
|
58
|
+
* The honest limit: an 8vb clef already says "sounds an octave lower", so a
|
|
59
|
+
* further lift understates the true pitch with nothing on the page confessing
|
|
60
|
+
* it. That is the accepted cost of keeping one clef; `notatio`'s `spn` remains
|
|
61
|
+
* the authority on what actually sounds.
|
|
62
|
+
*/
|
|
63
|
+
function pickOctaveLift(rows) {
|
|
64
|
+
const midis = rows.map((r) => r.midi).filter((m) => typeof m === "number");
|
|
65
|
+
if (midis.length === 0)
|
|
66
|
+
return 0;
|
|
67
|
+
let best = 0;
|
|
68
|
+
let fewest = Infinity;
|
|
69
|
+
// Nearest first, so a tie keeps the chant where it was written.
|
|
70
|
+
for (const lift of [0, 1, -1]) {
|
|
71
|
+
const shifted = lift * 12;
|
|
72
|
+
const off = midis.filter((m) => m + shifted < STAFF_LO || m + shifted > STAFF_HI).length;
|
|
73
|
+
if (off < fewest) {
|
|
74
|
+
fewest = off;
|
|
75
|
+
best = lift;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return best;
|
|
79
|
+
}
|
|
34
80
|
// ── geometry, as a function of the staff ──────────────────────────────────
|
|
35
81
|
//
|
|
36
82
|
// THE CONTRACT (ruled 2026-08-04): `staffHeight` is the height of the STAFF
|
|
@@ -114,14 +160,42 @@ export function metrics(staffHeight) {
|
|
|
114
160
|
}
|
|
115
161
|
const LETTERS = { C: 0, D: 1, E: 2, F: 3, G: 4, A: 5, B: 6 };
|
|
116
162
|
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
117
|
-
/**
|
|
118
|
-
|
|
119
|
-
|
|
163
|
+
/**
|
|
164
|
+
* Written y for a scientific pitch name on the treble-8 staff (bottom line
|
|
165
|
+
* written E4, sounding E3).
|
|
166
|
+
*
|
|
167
|
+
* `spn` is a SOUNDING pitch; the gClef8vb this species draws transposes, so the
|
|
168
|
+
* note is WRITTEN an octave above where it sounds. Hence the `+ 1` below:
|
|
169
|
+
* sounding G3 is written G4 and sits on the second line, where mode VII's final
|
|
170
|
+
* belongs. The reference generator folded that lift into a short reference term
|
|
171
|
+
* (`4 * 7` against a note term carrying `+2`) because it was fed pitches an
|
|
172
|
+
* octave below today's corpus; the two errors cancelled. Stated once, plainly,
|
|
173
|
+
* both halves now agree — and the chant lands on the staff instead of a sixth
|
|
174
|
+
* above it.
|
|
175
|
+
*
|
|
176
|
+
* `lift` is the further whole-octave shift `pickOctaveLift` chose for this
|
|
177
|
+
* chant, so a low or downward-transposed piece is written where it can be read
|
|
178
|
+
* rather than on a stack of ledger lines.
|
|
179
|
+
*
|
|
180
|
+
* The ACCIDENTAL is read but never changes the slot: a flat and its natural
|
|
181
|
+
* share one line (B-flat sits on the B line), which is why the letter alone
|
|
182
|
+
* fixes `steps`. It is returned separately so the caller can draw the sign.
|
|
183
|
+
* Discarding it was a silent wrong pitch — under transposition `Ab3` and `A3`
|
|
184
|
+
* landed on the same slot with nothing to tell them apart.
|
|
185
|
+
*/
|
|
186
|
+
function writtenY(spn, systemY, gm, lift = 0) {
|
|
187
|
+
const m = /([A-G])([#b]?)(-?\d)/.exec(spn);
|
|
120
188
|
if (!m)
|
|
121
|
-
return { y: systemY + gm.MTOP + 4 * gm.MSP, steps: 0 };
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
|
|
189
|
+
return { y: systemY + gm.MTOP + 4 * gm.MSP, steps: 0, accidental: 0 };
|
|
190
|
+
const accidental = m[2] === "b" ? -1 : m[2] === "#" ? 1 : 0;
|
|
191
|
+
const written = Number(m[3]) + 1 + lift; // the 8vb clef writes an octave up
|
|
192
|
+
const di = (written + 2) * 7 + LETTERS[m[1]];
|
|
193
|
+
const steps = di - ((4 + 2) * 7 + LETTERS["E"]); // relative to bottom line E4
|
|
194
|
+
return {
|
|
195
|
+
y: systemY + gm.MTOP + 4 * gm.MSP - steps * (gm.MSP / 2),
|
|
196
|
+
steps,
|
|
197
|
+
accidental,
|
|
198
|
+
};
|
|
125
199
|
}
|
|
126
200
|
// Moderna's ink, as a CSS custom property with the house default as fallback —
|
|
127
201
|
// the same three properties quadrata emits, so one stylesheet themes both
|
|
@@ -229,6 +303,35 @@ function accidentalWidth(code, gm) {
|
|
|
229
303
|
function accidentalMark(x, y, code, gm) {
|
|
230
304
|
return classedGlyph("accidental", code, x - gm.NH_W / 2 - accidentalWidth(code, gm), y, (gm.SCALE * 0.62));
|
|
231
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* The short lines that carry a notehead outside the five, as quadrata's
|
|
308
|
+
* `ledger` does: one per LINE position the note has passed, not one per step.
|
|
309
|
+
*
|
|
310
|
+
* `steps` counts diatonic slots up from the bottom line, so the staff occupies
|
|
311
|
+
* 0–8 and its lines are the even positions. A note at 10 gets a ledger at 10;
|
|
312
|
+
* at 11 (a space above that) it still gets only the one at 10. Below, the same
|
|
313
|
+
* downward: -1 draws nothing, -2 draws at -2.
|
|
314
|
+
*
|
|
315
|
+
* Rendered wider than the head by a quarter of its width on each side, the
|
|
316
|
+
* proportion quadrata uses, so the line reads as staff rather than as a tick.
|
|
317
|
+
*/
|
|
318
|
+
function ledgerLines(steps, mx, systemY, gm) {
|
|
319
|
+
if (steps >= 0 && steps <= 8)
|
|
320
|
+
return [];
|
|
321
|
+
const half = gm.NH_W / 2 + gm.NH_W * 0.25;
|
|
322
|
+
const out = [];
|
|
323
|
+
const emit = (lp) => {
|
|
324
|
+
const ly = systemY + gm.MTOP + 4 * gm.MSP - lp * (gm.MSP / 2);
|
|
325
|
+
out.push(`<line class="ledger" x1="${(mx - half).toFixed(2)}" y1="${ly.toFixed(2)}" ` +
|
|
326
|
+
`x2="${(mx + half).toFixed(2)}" y2="${ly.toFixed(2)}" ` +
|
|
327
|
+
`stroke="${INK}" stroke-width="0.7"/>`);
|
|
328
|
+
};
|
|
329
|
+
for (let lp = 10; lp <= steps; lp += 2)
|
|
330
|
+
emit(lp);
|
|
331
|
+
for (let lp = -2; lp >= steps; lp -= 2)
|
|
332
|
+
emit(lp);
|
|
333
|
+
return out;
|
|
334
|
+
}
|
|
232
335
|
const DIV_KIND = {
|
|
233
336
|
"`": "tick", ",": "tick", ";": "half", ":": "full", "::": "double",
|
|
234
337
|
};
|
|
@@ -290,6 +393,17 @@ export function toModerna(rows, chant, options = {}) {
|
|
|
290
393
|
const markByRow = new Map();
|
|
291
394
|
rows.forEach((row, i) => { const m = marks[i]; if (m)
|
|
292
395
|
markByRow.set(row, m); });
|
|
396
|
+
// One lift for the whole chant, chosen before any note is placed — a page
|
|
397
|
+
// that changed register partway would read as two pieces.
|
|
398
|
+
const lift = pickOctaveLift(rows);
|
|
399
|
+
// The pitches an engine-computed sign already accounts for. `degreeSpn` is
|
|
400
|
+
// the mark's own statement of which pitch it alters, so this is the engine
|
|
401
|
+
// speaking rather than a second guess at its scoping rules — the spelled
|
|
402
|
+
// path below stays silent wherever a written sign has already spoken.
|
|
403
|
+
const alteredSpns = new Set();
|
|
404
|
+
for (const m of marks)
|
|
405
|
+
if (m?.kind === "glyph" && m.degreeSpn)
|
|
406
|
+
alteredSpns.add(m.degreeSpn);
|
|
293
407
|
// ── Front matter ── The same official display quadrata sets (title centered
|
|
294
408
|
// over the score, the genus/mode mark stacked at the left margin — the
|
|
295
409
|
// `annotation: "auto"` params), honoured here so both species open a piece
|
|
@@ -316,6 +430,10 @@ export function toModerna(rows, chant, options = {}) {
|
|
|
316
430
|
}
|
|
317
431
|
const body = [];
|
|
318
432
|
const slurs = [];
|
|
433
|
+
// Ledger lines, collected as the notes are placed but emitted with the STAFF
|
|
434
|
+
// — they are staff, continuing it for one note, and must lie behind the
|
|
435
|
+
// notehead rather than cross it. `staff` is joined before `body` below.
|
|
436
|
+
const ledgers = [];
|
|
319
437
|
const lyricSvgs = [];
|
|
320
438
|
const lyricRuns = [];
|
|
321
439
|
const placements = [];
|
|
@@ -415,25 +533,45 @@ export function toModerna(rows, chant, options = {}) {
|
|
|
415
533
|
const mk = markByRow.get(r);
|
|
416
534
|
if (mk?.kind === "glyph")
|
|
417
535
|
nx += accidentalWidth(mk.glyph, gm); // room for the accidental
|
|
418
|
-
const { y, steps } = writtenY(r.spn, systemY, gm);
|
|
419
|
-
|
|
536
|
+
const { y, steps, accidental } = writtenY(r.spn, systemY, gm, lift);
|
|
537
|
+
// A sign the SPELLING demands. `computeAccidentals` speaks for the marks
|
|
538
|
+
// written in the GABC (`accidentalSource: "explicit"`); a transposed
|
|
539
|
+
// score carries its accidentals in `spn` alone, where nothing was ever
|
|
540
|
+
// written, so those would otherwise go undrawn and the note would read a
|
|
541
|
+
// semitone off with no sign to say so.
|
|
542
|
+
//
|
|
543
|
+
// The guard is `alteredSpns`, NOT this row's own `mk`. A written sign is
|
|
544
|
+
// carried by the note BEFORE the one it governs — on gregobase:1180 the
|
|
545
|
+
// three signs sit on rows 27/39/62 and their B-flats on 31/40/66, wholly
|
|
546
|
+
// disjoint — so asking "does this row carry a mark?" says no on the very
|
|
547
|
+
// note the sign already covers, and drew a second glyph for each.
|
|
548
|
+
const spelled = accidental !== 0 && !alteredSpns.has(r.spn)
|
|
549
|
+
? (accidental === -1 ? G.accidentalFlat : G.accidentalSharp)
|
|
550
|
+
: null;
|
|
551
|
+
if (spelled)
|
|
552
|
+
nx += accidentalWidth(spelled, gm);
|
|
553
|
+
notePos.push({ mx: nx, my: y, steps, spelled });
|
|
420
554
|
nx += gm.ADV + 4.6 * r.mora;
|
|
421
555
|
}
|
|
422
556
|
const notesW = nx - x - gm.ADV + gm.NH_W / 2 + 2;
|
|
423
557
|
const sylW = Math.max(notesW, textW(lyr));
|
|
424
558
|
// Draw notes.
|
|
425
559
|
srows.forEach((r, i) => {
|
|
426
|
-
const { mx, my, steps } = notePos[i];
|
|
560
|
+
const { mx, my, steps, spelled } = notePos[i];
|
|
427
561
|
const onLine = steps % 2 === 0;
|
|
562
|
+
ledgers.push(...ledgerLines(steps, mx, systemY, gm));
|
|
428
563
|
if (r.quilisma)
|
|
429
564
|
body.push(quilismaMark(mx, my, gm));
|
|
430
565
|
const mk = markByRow.get(r);
|
|
431
566
|
if (mk?.kind === "glyph") {
|
|
432
567
|
// Vertically the sign belongs to the pitch it alters, not to the note
|
|
433
568
|
// it was written before; horizontally it stays at this note's column.
|
|
434
|
-
const ay = mk.degreeSpn ? writtenY(mk.degreeSpn, systemY, gm).y : my;
|
|
569
|
+
const ay = mk.degreeSpn ? writtenY(mk.degreeSpn, systemY, gm, lift).y : my;
|
|
435
570
|
body.push(accidentalMark(mx, ay, mk.glyph, gm));
|
|
436
571
|
}
|
|
572
|
+
// A spelled accidental alters THIS note, so it sits on this note's line.
|
|
573
|
+
else if (spelled)
|
|
574
|
+
body.push(accidentalMark(mx, my, spelled, gm));
|
|
437
575
|
else if (mk?.kind === "cents") {
|
|
438
576
|
// Cents labels float in a band above the staff (not glued to the
|
|
439
577
|
// head) — an analytic overlay, not an engraving mark.
|
|
@@ -599,7 +737,7 @@ export function toModerna(rows, chant, options = {}) {
|
|
|
599
737
|
`width="${W}" height="${height}" class="tonus-chant moderna">${svgTitle}` +
|
|
600
738
|
lyricEmbed +
|
|
601
739
|
header.join("") +
|
|
602
|
-
staff.join("") + clefSvgs.join("") + body.join("") + slurs.join("") + lyricSvgs.join("") +
|
|
740
|
+
staff.join("") + ledgers.join("") + clefSvgs.join("") + body.join("") + slurs.join("") + lyricSvgs.join("") +
|
|
603
741
|
`</svg>`;
|
|
604
742
|
const geometry = placements.map((pl) => ({
|
|
605
743
|
phraseIndex: pl.row.phraseIndex,
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The GABC letter for a MIDI pitch under `clef` — with `x` appended when the
|
|
3
|
+
* pitch is a B-flat (`jx`, the flat sign then the note).
|
|
4
|
+
*
|
|
5
|
+
* B-flat is the ONE accidental chant sings — the b molle of the medieval gamut,
|
|
6
|
+
* the whole reason `parse.ts` carries a flat state machine and the `b` clefs
|
|
7
|
+
* exist. This function used to throw "is not diatonic" on it, which made the
|
|
8
|
+
* apparent inverse of `gabcToMidi` unable to spell a pitch the parser reads on
|
|
9
|
+
* every other page. Every other chromatic pitch class still throws: those are
|
|
10
|
+
* outside the gamut, and inventing a spelling for them would be a worse answer
|
|
11
|
+
* than refusing.
|
|
12
|
+
*/
|
|
1
13
|
export declare function midiToGabc(midi: number, clef?: string): string;
|
|
2
14
|
export declare function gabcToMidi(letter: string, clef?: string): number;
|
|
3
15
|
export declare function pcToGabc(pc: number, clef?: string, oct?: number): string;
|
|
@@ -18,26 +18,52 @@ const LETTERS = "abcdefghijklm";
|
|
|
18
18
|
// A higher c-clef (c4 vs c1) moves "do" up the staff, so the same letter reads a
|
|
19
19
|
// lower pitch — hence doIdx climbs 3→5→7→9 across c1→c4. The f-clefs anchor on
|
|
20
20
|
// fa (MIDI 53) and are used for lower-tessitura chant.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
};
|
|
21
|
+
// The staff-line slots the clefs anchor to, low to high: letters d/f/h/j per
|
|
22
|
+
// the Gregorio spec (2-line staff = a–i, 3-line = a–k, 4-line = a–m).
|
|
23
|
+
const LINE_SLOTS = [3, 5, 7, 9];
|
|
24
|
+
// Every clef GABC can declare, built from the same two rules rather than listed
|
|
25
|
+
// by hand: a c-clef puts "do" (MIDI 60) on its named line, an f-clef puts "fa"
|
|
26
|
+
// (MIDI 53) there. cN/fN for N in 1–4, plus the `b` variants (cbN/fbN) that
|
|
27
|
+
// additionally declare a B-flat key signature.
|
|
28
|
+
//
|
|
29
|
+
// This table used to hold six entries — c1–c4, f3, f4 — while `parse.ts`'s
|
|
30
|
+
// CLEF_OFFSETS held all sixteen. Two tables disagreeing about what a clef is,
|
|
31
|
+
// with the smaller one throwing "Unknown clef" on inputs the parser accepts
|
|
32
|
+
// happily. Deriving them removes the chance of a third disagreement.
|
|
33
|
+
const CLEFS = {};
|
|
34
|
+
for (let n = 1; n <= 4; n++) {
|
|
35
|
+
const doIdx = LINE_SLOTS[n - 1];
|
|
36
|
+
// An f-clef names fa's line, and fa is the 4th diatonic step (index 3), so
|
|
37
|
+
// "do" sits three slots below the named line — which is what doMidi 53
|
|
38
|
+
// (the F below middle C) is measured from.
|
|
39
|
+
CLEFS[`c${n}`] = { doMidi: 60, doIdx };
|
|
40
|
+
CLEFS[`cb${n}`] = { doMidi: 60, doIdx };
|
|
41
|
+
CLEFS[`f${n}`] = { doMidi: 53, doIdx };
|
|
42
|
+
CLEFS[`fb${n}`] = { doMidi: 53, doIdx };
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The GABC letter for a MIDI pitch under `clef` — with `x` appended when the
|
|
46
|
+
* pitch is a B-flat (`jx`, the flat sign then the note).
|
|
47
|
+
*
|
|
48
|
+
* B-flat is the ONE accidental chant sings — the b molle of the medieval gamut,
|
|
49
|
+
* the whole reason `parse.ts` carries a flat state machine and the `b` clefs
|
|
50
|
+
* exist. This function used to throw "is not diatonic" on it, which made the
|
|
51
|
+
* apparent inverse of `gabcToMidi` unable to spell a pitch the parser reads on
|
|
52
|
+
* every other page. Every other chromatic pitch class still throws: those are
|
|
53
|
+
* outside the gamut, and inventing a spelling for them would be a worse answer
|
|
54
|
+
* than refusing.
|
|
55
|
+
*/
|
|
34
56
|
export function midiToGabc(midi, clef = "c4") {
|
|
35
57
|
const def = CLEFS[clef];
|
|
36
58
|
if (!def)
|
|
37
59
|
throw new Error(`Unknown clef: ${clef}`);
|
|
38
|
-
const octave = Math.floor(midi / 12) - 1;
|
|
39
60
|
const pc = midi % 12;
|
|
40
|
-
|
|
61
|
+
// A flat is spelled on the staff slot of the natural ABOVE it: B-flat takes
|
|
62
|
+
// B's line with an `x`. Resolve to that natural, then mark the result.
|
|
63
|
+
const flat = pc === 10;
|
|
64
|
+
const natural = flat ? midi + 1 : midi;
|
|
65
|
+
const octave = Math.floor(natural / 12) - 1;
|
|
66
|
+
const diatIdx = DIATONIC.indexOf(natural % 12);
|
|
41
67
|
if (diatIdx === -1)
|
|
42
68
|
throw new Error(`MIDI note ${midi} (pc ${pc}) is not diatonic`);
|
|
43
69
|
const doOctave = Math.floor(def.doMidi / 12) - 1;
|
|
@@ -45,13 +71,19 @@ export function midiToGabc(midi, clef = "c4") {
|
|
|
45
71
|
const letter = LETTERS[staffPos];
|
|
46
72
|
if (!letter)
|
|
47
73
|
throw new Error(`MIDI ${midi} out of GABC range for clef ${clef}`);
|
|
48
|
-
return letter;
|
|
74
|
+
return flat ? `${letter}x` : letter;
|
|
49
75
|
}
|
|
50
76
|
export function gabcToMidi(letter, clef = "c4") {
|
|
51
77
|
const def = CLEFS[clef];
|
|
52
78
|
if (!def)
|
|
53
79
|
throw new Error(`Unknown clef: ${clef}`);
|
|
54
|
-
|
|
80
|
+
// Accept the `x` that `midiToGabc` emits, so the two stay inverses. A flat
|
|
81
|
+
// lowers the natural it marks by a semitone; only B carries one in the gamut,
|
|
82
|
+
// but the arithmetic is written once for whatever letter arrives.
|
|
83
|
+
const raw = letter.toLowerCase();
|
|
84
|
+
const flat = raw.endsWith("x");
|
|
85
|
+
const bare = flat ? raw.slice(0, -1) : raw;
|
|
86
|
+
const staffPos = LETTERS.indexOf(bare);
|
|
55
87
|
if (staffPos === -1)
|
|
56
88
|
throw new Error(`Unknown GABC letter: ${letter}`);
|
|
57
89
|
// Diatonic steps from "do", split into whole octaves (÷7) and the step within
|
|
@@ -61,7 +93,8 @@ export function gabcToMidi(letter, clef = "c4") {
|
|
|
61
93
|
const octOffset = Math.floor(stepsFromDo / 7);
|
|
62
94
|
const diatStep = ((stepsFromDo % 7) + 7) % 7;
|
|
63
95
|
const doOctave = Math.floor(def.doMidi / 12) - 1;
|
|
64
|
-
|
|
96
|
+
const natural = (doOctave + octOffset + 1) * 12 + DIATONIC[diatStep];
|
|
97
|
+
return flat ? natural - 1 : natural;
|
|
65
98
|
}
|
|
66
99
|
export function pcToGabc(pc, clef = "c4", oct = 0) {
|
|
67
100
|
const def = CLEFS[clef];
|
package/docs/api/calendar.md
CHANGED
|
@@ -108,7 +108,7 @@ The feast returned **carries the view** (`feast.before`), and every chant
|
|
|
108
108
|
verb reads it back: `proprium`, `ordinarium`, and `officium`
|
|
109
109
|
serve only chants attested by the same year, without being told the year
|
|
110
110
|
twice. One `before` at the calendar door views the whole day. The chant side
|
|
111
|
-
|
|
111
|
+
(what "attested" means, and what a slot the view excludes does) is in
|
|
112
112
|
[chant.md](chant.md#the-repertoire-as-of-a-date--the-era-view).
|
|
113
113
|
|
|
114
114
|
```ts
|
|
@@ -142,12 +142,12 @@ interface Feast {
|
|
|
142
142
|
}
|
|
143
143
|
```
|
|
144
144
|
|
|
145
|
-
The `masses` list is derived from the Kyriale's own printed rubric
|
|
145
|
+
The `masses` list is derived from the Kyriale's own printed rubric, one
|
|
146
146
|
category per mass, by RANK: "In Paschal Time", "For feasts of the I class",
|
|
147
147
|
"For Sundays throughout the Year", "For ferias". A day resolves to exactly
|
|
148
148
|
one rubric (a BVM feast is "of the Blessed Virgin" even in Paschaltide),
|
|
149
149
|
and the masses carrying that rubric are the masses it may sing, in the
|
|
150
|
-
book's own numbering
|
|
150
|
+
book's own numbering. Where a rubric names several (II class 1–5), that
|
|
151
151
|
numbering is the book's invitation to choose, and `ordinarium` rotates
|
|
152
152
|
among them by year. The book's per-mass nicknames (_Orbis factor_ for
|
|
153
153
|
Sundays, and so on) record customary use, which disagrees with the rubric for 9
|
|
@@ -211,7 +211,7 @@ derived from the date; overflow entries, such as the Epiphany weeks
|
|
|
211
211
|
resumed before Septuagesima, take the season of the day they fall on.
|
|
212
212
|
|
|
213
213
|
Season drives real liturgy in the ordinary: in the penitential seasons
|
|
214
|
-
(`adv`, `quadp`, `quad`) the Gloria is omitted, and the Ite with it
|
|
214
|
+
(`adv`, `quadp`, `quad`) the Gloria is omitted, and the Ite with it. The
|
|
215
215
|
Benedicamus dismissal appears only where the selected mass carries a
|
|
216
216
|
setting ([chant.md](chant.md#the-ordinary--ordinarium)).
|
|
217
217
|
|
package/docs/api/census.md
CHANGED
|
@@ -20,7 +20,7 @@ is, where it is unusual, and what it is near.
|
|
|
20
20
|
tonus.census({ id: "gregobase:1210" });
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
Everything comes back in one call
|
|
23
|
+
Everything comes back in one call: profile, balance, neighbors.
|
|
24
24
|
|
|
25
25
|
```js
|
|
26
26
|
{
|
|
@@ -56,7 +56,7 @@ interface CensusQuery {
|
|
|
56
56
|
}
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
The census covers the **2,187 chants tonus ships
|
|
59
|
+
The census covers the **2,187 chants tonus ships**, the same population
|
|
60
60
|
`cantus({ id })` addresses, one block per chant. An id with no block throws
|
|
61
61
|
rather than returning an empty answer, because a silent nothing reads as "this
|
|
62
62
|
chant is unlike everything," which is a different claim.
|
|
@@ -78,24 +78,24 @@ what they describe:
|
|
|
78
78
|
| `textual` | 7 | vowel distribution by sung duration, accent rate, melisma mean |
|
|
79
79
|
|
|
80
80
|
Four more fields ride in the block and are **not** similarity dimensions:
|
|
81
|
-
`flags` (a bitfield), `attest` (dating
|
|
81
|
+
`flags` (a bitfield), `attest` (dating, which is what `before` reads),
|
|
82
82
|
`extras`, and `reserve`. `by` will not accept them.
|
|
83
83
|
|
|
84
84
|
## How the measurement works
|
|
85
85
|
|
|
86
|
-
Every number in a block reads off a single `notatio()` parse
|
|
87
|
-
`score` gives you
|
|
86
|
+
Every number in a block reads off a single `notatio()` parse (the same parse
|
|
87
|
+
`score` gives you), so the census can never disagree with the library about
|
|
88
88
|
what a chant is.
|
|
89
89
|
|
|
90
90
|
Each float is a named measurement, not a learned one: time spent on the
|
|
91
91
|
subfinal, how often a rising second follows a falling third. When the census
|
|
92
92
|
calls two chants near, the profile says in what respect.
|
|
93
93
|
|
|
94
|
-
Most groups are normalized to sum to one, so a group holds a distribution
|
|
95
|
-
where the melody's time goes, not how much of it there is
|
|
94
|
+
Most groups are normalized to sum to one, so a group holds a distribution:
|
|
95
|
+
where the melody's time goes, not how much of it there is. Length is not a
|
|
96
96
|
similarity. The trigram and cadence groups count against dictionaries mined
|
|
97
|
-
from the corpus itself
|
|
98
|
-
one bucket for the rest
|
|
97
|
+
from the corpus itself (its commonest motifs, its commonest closing gestures,
|
|
98
|
+
one bucket for the rest), so the corpus supplies the vocabulary and the chant
|
|
99
99
|
supplies the usage.
|
|
100
100
|
|
|
101
101
|
The reference is the mean block over all 2,187 chants, group by group. Because
|
|
@@ -105,15 +105,15 @@ divided by their count, are the season's mean profile in the same 221 slots.
|
|
|
105
105
|
## Distance is cosine per field group
|
|
106
106
|
|
|
107
107
|
**This is a contract, not an implementation note.** The census answers about
|
|
108
|
-
one chant at a time
|
|
109
|
-
manuscript"
|
|
108
|
+
one chant at a time. Grouping ("all Communions," "this season," "this
|
|
109
|
+
manuscript") is yours to do. The moment you pool blocks yourself you are
|
|
110
110
|
computing a distance, and if you compute it differently from the rule below
|
|
111
111
|
your numbers will not agree with `census()`'s. Nothing will error.
|
|
112
112
|
|
|
113
113
|
The rule, in three lines:
|
|
114
114
|
|
|
115
115
|
1. Cosine **per field group**, never over the flat 221.
|
|
116
|
-
2. `by: "all"` is the **equal-weight mean** of the per-group cosines
|
|
116
|
+
2. `by: "all"` is the **equal-weight mean** of the per-group cosines: every
|
|
117
117
|
dimension one vote, no tunable weights.
|
|
118
118
|
3. Ties break to the lower id, so the same question always has the same answer.
|
|
119
119
|
|
|
@@ -122,7 +122,7 @@ sheer magnitude, so a long Tract would neighbor other long chants for being
|
|
|
122
122
|
long. Per-group cosine asks about **shape within each dimension**.
|
|
123
123
|
|
|
124
124
|
[`CENSUS_GROUPS`](index.md#the-appendix) gives you the group names and their
|
|
125
|
-
field counts, and [`CENSUS_ORDER`](index.md#the-appendix) every censused id
|
|
125
|
+
field counts, and [`CENSUS_ORDER`](index.md#the-appendix) every censused id,
|
|
126
126
|
so you can pool a set without guessing at either.
|
|
127
127
|
|
|
128
128
|
### Reading the numbers
|
|
@@ -140,7 +140,7 @@ per-group version spreads from about 0.85 down to 0.65. That compression comes
|
|
|
140
140
|
from one wide block outvoting the other eight.
|
|
141
141
|
|
|
142
142
|
**`before` filters before ranking.** It restricts the candidate pool, then
|
|
143
|
-
ranks
|
|
143
|
+
ranks, so `k` stays satisfiable, and a filtered list is *not* a subset of the
|
|
144
144
|
unfiltered one. Chants that were ranked out by later material rise into it.
|
|
145
145
|
Typicality is unaffected: it is always measured against the whole shipped
|
|
146
146
|
corpus (see [Profile and typicality](#profile-and-typicality)).
|
|
@@ -201,7 +201,7 @@ const ranked = ids
|
|
|
201
201
|
|
|
202
202
|
The per-group breakdown is where the answer becomes legible. _Quinque
|
|
203
203
|
prudentes_ leads on `textual`, `cadenceMedial` and `trigram`, at about 0.99 on
|
|
204
|
-
each
|
|
204
|
+
each (it sets its text and turns its phrases the way Communions do), while its
|
|
205
205
|
`cadenceFinal` is only about 0.82, so the one thing it does unlike a typical
|
|
206
206
|
Communion is end. A chant is typical of its genus in some dimensions and not
|
|
207
207
|
others.
|
|
@@ -213,8 +213,8 @@ Each group's `typicality` is its cosine against the corpus mean for that group:
|
|
|
213
213
|
"unlike the rest."
|
|
214
214
|
|
|
215
215
|
The two numbers above are a fair illustration. _Ab occultis meis_ is a mode-2
|
|
216
|
-
Gradual whose `modal` typicality is about 0.99
|
|
217
|
-
mode-2 chant
|
|
216
|
+
Gradual whose `modal` typicality is about 0.99, so modally it is a typical
|
|
217
|
+
mode-2 chant. Its `melodic` typicality is about 0.70, because its
|
|
218
218
|
interval
|
|
219
219
|
vocabulary is its own. One chant can be conventional in one dimension and
|
|
220
220
|
distinctive in another, which is the reason the groups are kept apart.
|
|
@@ -233,7 +233,7 @@ balance: { distance: 0.091, deviantGroups: ["degreeHist", "melodic"] }
|
|
|
233
233
|
the corpus mean, 1 has nothing in common with it.
|
|
234
234
|
|
|
235
235
|
`deviantGroups` names where a chant is unusual **relative to its own mean**,
|
|
236
|
-
most deviant first
|
|
236
|
+
most deviant first, not against an absolute threshold. The question it answers
|
|
237
237
|
is "given how typical this chant is overall, where does it depart from
|
|
238
238
|
itself?", which is what makes the answer legible for a chant that is unusual
|
|
239
239
|
everywhere or nowhere.
|
|
@@ -273,12 +273,12 @@ tonus.census({ id: "gregobase:1210", before: 1100 });
|
|
|
273
273
|
```
|
|
274
274
|
|
|
275
275
|
Restricts neighbors to chants a manuscript of the 11th century or earlier
|
|
276
|
-
already holds
|
|
276
|
+
already holds, 1,790 of the 2,186 candidates. This is the same rule as
|
|
277
277
|
[`cantus({ before })`](chant.md#the-repertoire-as-of-a-date--the-era-view),
|
|
278
278
|
through the same admissibility door: **evidence, not existence**, so a chant
|
|
279
279
|
with no dated witness is excluded rather than assumed old.
|
|
280
280
|
|
|
281
|
-
The seed chant itself is never filtered
|
|
281
|
+
The seed chant itself is never filtered, because you asked about it by name.
|
|
282
282
|
|
|
283
283
|
## What the census is not
|
|
284
284
|
|
package/docs/api/chant.md
CHANGED
|
@@ -69,7 +69,7 @@ those marks too.
|
|
|
69
69
|
The corpus is **assignment-driven**: a chant ships when some day of the
|
|
70
70
|
liturgical year calls for it. The calendar is walked year by year until it stops
|
|
71
71
|
finding new assignments (39 years, in the event), and what it never reaches is
|
|
72
|
-
not shipped
|
|
72
|
+
not shipped: 10,156 book chants become 2,187.
|
|
73
73
|
|
|
74
74
|
Everything here answers "what was sung on this day". A query for a chant the
|
|
75
75
|
calendar never calls for returns nothing.
|
|
@@ -77,11 +77,11 @@ calendar never calls for returns nothing.
|
|
|
77
77
|
## The books — `corpus`
|
|
78
78
|
|
|
79
79
|
`corpus(code)` returns one book's bibliographic identity and a breakdown of what
|
|
80
|
-
it holds
|
|
80
|
+
it holds: how many chants, in what genres, in what modes. `corpus({ book })` is
|
|
81
81
|
the same question in the query form every other verb uses; both spellings return
|
|
82
82
|
one answer.
|
|
83
83
|
|
|
84
|
-
`corpus()` with no argument returns **the whole shelf
|
|
84
|
+
`corpus()` with no argument returns **the whole shelf**, the rollup plus every
|
|
85
85
|
book's ledger:
|
|
86
86
|
|
|
87
87
|
```js
|
|
@@ -94,7 +94,7 @@ tonus.corpus();
|
|
|
94
94
|
// books: [ …10 Corpus entries, in registry order ] }
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
-
**`count` is the number of chants
|
|
97
|
+
**`count` is the number of chants**, the one to quote. `listings` is how long
|
|
98
98
|
the shelf is: a melody printed in several books is stored once and listed under
|
|
99
99
|
each, so the shelf runs longer than the repertory. The breakdowns describe the
|
|
100
100
|
same population `count` does, so `genera` and `modes` sum to it.
|
|
@@ -141,7 +141,7 @@ am.genera[0]; // { office: "an", genus: "Antiphona", count: 458 }
|
|
|
141
141
|
am.full.genera[0]; // { office: "an", genus: "Antiphona", count: 1049 }
|
|
142
142
|
```
|
|
143
143
|
|
|
144
|
-
Reading the two tallies side by side names what was left out
|
|
144
|
+
Reading the two tallies side by side names what was left out: 1,049 antiphons
|
|
145
145
|
in the book, 458 sung.
|
|
146
146
|
|
|
147
147
|
Only the extractor can measure this. By the time tonus loads, the keep set has
|
|
@@ -163,7 +163,7 @@ is largely the Graduale and the Antiphonarius bound together (it shares hundreds
|
|
|
163
163
|
of chants with each), while the Antiphonale Monasticum is almost entirely its own.
|
|
164
164
|
|
|
165
165
|
The Nocturnale (`nr`) is compared differently, because it has no GregoBase
|
|
166
|
-
catalogue: its counts come from its own extract, and it shares **nothing
|
|
166
|
+
catalogue: its counts come from its own extract, and it shares **nothing**, so
|
|
167
167
|
`unique` is all 1,564 chants it holds. That is a measurement, not a gap. The
|
|
168
168
|
nocturnale–GregoBase crosswalk is a route to metadata, not a claim that the two
|
|
169
169
|
books print the same chant, so it does not count as sharing.
|
|
@@ -295,18 +295,18 @@ A plain search does not sweep it in: `{ mode: 5 }` returns the shelf. Ask for a
|
|
|
295
295
|
Kyrie and you get Kyries.
|
|
296
296
|
|
|
297
297
|
For the setting a given DAY calls for, [`ordinarium`](#the-ordinary--ordinarium)
|
|
298
|
-
is the verb
|
|
298
|
+
is the verb, and it applies the Kyriale's own rubrics. This is flat retrieval.
|
|
299
299
|
|
|
300
300
|
### On chant ids
|
|
301
301
|
|
|
302
|
-
An id's prefix names **the catalogue the identifier came from
|
|
302
|
+
An id's prefix names **the catalogue the identifier came from**, not the book
|
|
303
303
|
the chant is printed in, and not a claim about who the melody belongs to. A
|
|
304
304
|
chant carrying `gregobase:1210` is a Solesmes book chant that GregoBase happens
|
|
305
305
|
to have catalogued; the corpus is assembled from ten books, and GregoBase is
|
|
306
306
|
one source among several.
|
|
307
307
|
|
|
308
308
|
The prefix is therefore **not a namespace you can query against**. GregoBase
|
|
309
|
-
holds 18,148 chants; tonus ships 1,717 of them
|
|
309
|
+
holds 18,148 chants; tonus ships 1,717 of them (9.5%), because the corpus is
|
|
310
310
|
assignment-driven, so an id copied from the GregoBase site will usually return
|
|
311
311
|
`[]` here. That is not a lookup failure; it means no day of the calendar calls
|
|
312
312
|
for that chant. The two prefixes in the shipped corpus are `gregobase:` (1,717)
|
|
@@ -322,7 +322,7 @@ to a chant rather than to a printing.
|
|
|
322
322
|
`before: 1098` keeps only chants a manuscript of the 10th century or earlier
|
|
323
323
|
already holds. This is **evidence, not existence**: the dates come from
|
|
324
324
|
CANTUS's manuscript index, a terminus ante quem, so the filter answers "what
|
|
325
|
-
is attested by then," never "what existed then"
|
|
325
|
+
is attested by then," never "what existed then." A chant with no dated
|
|
326
326
|
witness is excluded rather than assumed old. CANTUS dates only to the century,
|
|
327
327
|
so a year admits the centuries that have CLOSED before it (`before: 1098` →
|
|
328
328
|
through the 900s).
|
|
@@ -339,7 +339,7 @@ tonus.ordinarium({ feast: easter }); // the ordinary the view attests
|
|
|
339
339
|
```
|
|
340
340
|
|
|
341
341
|
What happens to a slot the view excludes differs by verb, on the rubric's
|
|
342
|
-
own logic: `ordinarium` **re-picks
|
|
342
|
+
own logic: `ordinarium` **re-picks**, because the Kyriale offers ranked
|
|
343
343
|
alternatives by design, so the rotation runs over the admissible pool and
|
|
344
344
|
the day still sings. `proprium` and `officium` have no pool
|
|
345
345
|
of alternatives, so an excluded chant **falls silent**. A `before` given to a
|
|
@@ -371,15 +371,15 @@ interface PropriumQuery extends CantusQuery {
|
|
|
371
371
|
## The ordinary — `ordinarium`
|
|
372
372
|
|
|
373
373
|
`ordinarium(query?)` retrieves the fixed chants of the Mass from the
|
|
374
|
-
Kyriale. A feast drives mass selection through its `masses` list
|
|
374
|
+
Kyriale. A feast drives mass selection through its `masses` list, the
|
|
375
375
|
masses the day's Kyriale RUBRIC appoints, derived as described in
|
|
376
376
|
[calendar.md](calendar.md#the-days-feasts--festum); `mass` pins a kyriale
|
|
377
377
|
number directly. Where the rubric names several masses, the year rotates
|
|
378
378
|
through them (same feast, same year → same answer, every time), and sibling
|
|
379
379
|
printings under one number (Mass I prints two dismissals; Mass XVII prints
|
|
380
380
|
Kyrie A/B/C) rotate with it. Slots resolve independently, which the book
|
|
381
|
-
licenses outright
|
|
382
|
-
from others"
|
|
381
|
+
licenses outright ("chants from one Mass may be used together with those
|
|
382
|
+
from others") with one exception, the book's own: **"the Ferial Masses
|
|
383
383
|
excepted."** Under a ferial rubric the sung ordinary is not gathered from
|
|
384
384
|
several masses; only the dismissal travels.
|
|
385
385
|
|
|
@@ -397,10 +397,10 @@ tonus.ordinarium({ feast: easter });
|
|
|
397
397
|
|
|
398
398
|
The **Gloria follows the day's rank rubric, not its season**: the ferial
|
|
399
399
|
masses print none (XVI, XVIII) and the penitential-Sunday mass none (XVII),
|
|
400
|
-
while a I-class feast inside Advent or Lent
|
|
400
|
+
while a I-class feast inside Advent or Lent (the Immaculate Conception)
|
|
401
401
|
keeps its Gloria. At a Gloria-less Mass the dismissal is the Benedicamus
|
|
402
402
|
Domino, and a mass with no dismissal of its own borrows one exactly as the
|
|
403
|
-
book directs: "Benedicamus Domino **as in Mass II
|
|
403
|
+
book directs: "Benedicamus Domino **as in Mass II**," so a green feria
|
|
404
404
|
sings Mass XVI whole with the Mass II Benedicamus. The ad libitum appendix
|
|
405
405
|
is a **solemnity boost**, reachable only under the festal rubrics (it takes
|
|
406
406
|
its turn in the rotation once every _n + 1_ years); it never reaches a
|
|
@@ -475,7 +475,7 @@ seasonal ordo and returned in liturgical order. With no feast, each resolves
|
|
|
475
475
|
for the [default epoch](index.md#dates).
|
|
476
476
|
|
|
477
477
|
**Matins is returned flat.** The night office answers like any other hour,
|
|
478
|
-
its responsories drawn from the Nocturnale Romanum (`nr`)
|
|
478
|
+
its responsories drawn from the Nocturnale Romanum (`nr`). But the
|
|
479
479
|
three-nocturn, twelve-psalm division is not modelled: the chants are right,
|
|
480
480
|
their grouping into nocturns is not expressed.
|
|
481
481
|
|
|
@@ -492,7 +492,7 @@ interface OfficiumQuery extends CantusQuery {
|
|
|
492
492
|
}
|
|
493
493
|
```
|
|
494
494
|
|
|
495
|
-
The eight hours ship as [`HORAE`](index.md#the-appendix), Matins first
|
|
495
|
+
The eight hours ship as [`HORAE`](index.md#the-appendix), Matins first. Read
|
|
496
496
|
them from there rather than transcribing them, and an unrecognised `hora`
|
|
497
497
|
throws rather than matching nothing, so a misspelling cannot read as an empty
|
|
498
498
|
hour.
|
|
@@ -507,9 +507,9 @@ tonus.officium({ hora: "vespers" }); // throws: unknown hora "vespers"
|
|
|
507
507
|
|
|
508
508
|
### One cursus, the Benedictine
|
|
509
509
|
|
|
510
|
-
tonus assembles a single office
|
|
510
|
+
tonus assembles a single office (the monastic cursus) with no option to
|
|
511
511
|
choose another. The chants come from the Antiphonale Monasticum (`am`) and its
|
|
512
|
-
companions; the psalmody follows the Benedictine distribution
|
|
512
|
+
companions; the psalmody follows the Benedictine distribution, the little
|
|
513
513
|
hours take the gradual psalms (Terce 119–121, Sext 122–124, None 125–127),
|
|
514
514
|
with Sunday and Monday walking their portions of Ps 118 instead; Prime walks
|
|
515
515
|
Pss 1–19 across the week (Sunday opens Ps 118); and Compline is the fixed
|
|
@@ -530,7 +530,7 @@ the opening formula is included, as it is for a psalm's first verse.
|
|
|
530
530
|
mediant, as a psalm sung without an antiphon; `solemn` uses a tone's
|
|
531
531
|
ornamented mediant where it has one. Canticles are addressed by name:
|
|
532
532
|
`benedictus`, `magnificat`, `nunc dimittis`, `benedicite`. (The Te Deum
|
|
533
|
-
is not psalmody
|
|
533
|
+
is not psalmody: it carries its own melody and is not addressable here.)
|
|
534
534
|
|
|
535
535
|
```js
|
|
536
536
|
tonus.psalmus({ psalm: 109, verse: "1a", mode: 1 });
|
package/docs/api/heavens.md
CHANGED
|
@@ -168,12 +168,12 @@ and moves with precession; a table carrying "March 21" would be wrong for most
|
|
|
168
168
|
of the period this library models, and wrong differently every century.
|
|
169
169
|
|
|
170
170
|
**What is omitted, and why.** The exaltation degrees Ptolemy gives (the Sun at
|
|
171
|
-
19° Arietis and the rest) are not carried
|
|
172
|
-
here reads. Nor are the lunar nodes' exaltations, because the nodes are not
|
|
171
|
+
19° Arietis and the rest) are not carried, because the sign is the resolution
|
|
172
|
+
anything here reads. Nor are the lunar nodes' exaltations, because the nodes are not
|
|
173
173
|
tonus bodies. Five signs exalt nobody: that silence is the tradition's, not a
|
|
174
174
|
gap in the table.
|
|
175
175
|
|
|
176
|
-
The `melothesia` is the *homo signorum* of medieval calendars
|
|
176
|
+
The `melothesia` is the *homo signorum* of medieval calendars, Aries at the
|
|
177
177
|
head down to Pisces at the feet. It was practice, not decoration: phlebotomy
|
|
178
178
|
was timed against it, and while the Moon stood in a sign its member was not to
|
|
179
179
|
be touched. Sourced from Ptolemy's *Tetrabiblos* I.17 and I.19
|
|
@@ -216,7 +216,7 @@ The doctrinae:
|
|
|
216
216
|
Sphere pitches are computed directly from the doctrina's pure ratios,
|
|
217
217
|
anchored at the temperamentum's A4, so historical coherence holds:
|
|
218
218
|
`temperamentum("ptolemy-intense")` with `harmonia({ doctrina: "ptolemy" })`
|
|
219
|
-
gives pure Ptolemaic intervals throughout
|
|
219
|
+
gives pure Ptolemaic intervals throughout: Sun→Jupiter a pure 3/2,
|
|
220
220
|
Sun→Saturn a pure 2/1. The temperamentum's scale governs pitch naming and
|
|
221
221
|
the imprint.
|
|
222
222
|
|
|
@@ -322,7 +322,7 @@ h.tabula.find((r) => r.name === "Jupiter");
|
|
|
322
322
|
```
|
|
323
323
|
|
|
324
324
|
`ratio` is the doctrina's own fraction against the mese, the primary datum of
|
|
325
|
-
the whole scheme
|
|
325
|
+
the whole scheme. `spn` and `hz` are that ratio sounded against A4, not
|
|
326
326
|
independent claims. Comparing doctrinae means comparing these: the table
|
|
327
327
|
under [Theory & Context](#theory--context) is what the field returns.
|
|
328
328
|
|
|
@@ -364,7 +364,7 @@ The doctrina ratios are reconstructed from the primary texts through
|
|
|
364
364
|
Joscelyn Godwin's syntheses, mapping each body to a Greek tone-name and
|
|
365
365
|
deriving its ratio by Pythagorean interval arithmetic normalized to the
|
|
366
366
|
mese (Sun = 1/1). The full method, the taxonomy, and the decisions taken
|
|
367
|
-
along the way are documented at the data
|
|
367
|
+
along the way are documented at the data. See `DOCTRINAE` in
|
|
368
368
|
[`harmonia/data/doctrines.ts`](https://github.com/jeffreypierce/tonus/blob/main/src/engines/harmonia/data/doctrines.ts).
|
|
369
369
|
The same arithmetic is laid out from the tuning side in
|
|
370
370
|
[tuning.md](tuning.md#theory--context).
|
|
@@ -385,7 +385,7 @@ The resulting ratios, by sphere from the outermost:
|
|
|
385
385
|
|
|
386
386
|
The single pitch separating Pythagoras from Boethius is Venus: a whole
|
|
387
387
|
tone above the Sun in the disjunct system (9/8, B durum), a semitone in
|
|
388
|
-
the conjunct (256/243, B molle)
|
|
388
|
+
the conjunct (256/243, B molle). This is the origin of the durum/molle
|
|
389
389
|
distinction that runs through all of medieval music theory.
|
|
390
390
|
|
|
391
391
|
## Sources
|
package/docs/api/index.md
CHANGED
|
@@ -4,7 +4,7 @@ The technical center of tonus: the full public API, the conventions every method
|
|
|
4
4
|
obeys, and the error contract. The API is **fourteen methods on the `tonus`
|
|
5
5
|
namespace**, no sub-namespaces.
|
|
6
6
|
|
|
7
|
-
**[
|
|
7
|
+
**[Interactive demo](https://orreliquum.com/)**
|
|
8
8
|
|
|
9
9
|
```js
|
|
10
10
|
import tonus from "tonus";
|
|
@@ -123,7 +123,7 @@ is for.
|
|
|
123
123
|
| `CENSUS_GROUPS` | the field groups → `{ offset, count }`; the keys are the valid `by:` values **and** the `profile` keys |
|
|
124
124
|
| `CENSUS_ORDER` | every censused chant id, in block order — so membership is a lookup, not a `try/catch` |
|
|
125
125
|
|
|
126
|
-
Use these to pool blocks without reproducing the distance rule
|
|
126
|
+
Use these to pool blocks without reproducing the distance rule. See [the census
|
|
127
127
|
contract](census.md#distance-is-cosine-per-field-group).
|
|
128
128
|
|
|
129
129
|
## Full contents
|
|
@@ -218,7 +218,7 @@ Other fields carry only one register. Latin-only, _e.g._ `genus`, `ordinarium`,
|
|
|
218
218
|
`incipit`, `differentia`, `accentus`. English-only, _e.g._ `date`, `velocity`, `hz`.
|
|
219
219
|
|
|
220
220
|
Display strings live in exported maps (_e.g._ `SEASON_LABEL`),
|
|
221
|
-
never as label fields on objects
|
|
221
|
+
never as label fields on objects. The maps are [the appendix](#the-appendix).
|
|
222
222
|
|
|
223
223
|
### Dates
|
|
224
224
|
|
|
@@ -238,14 +238,14 @@ an ensemble) it is seeded, so the same seed yields byte-identical output.
|
|
|
238
238
|
|
|
239
239
|
### Error contract
|
|
240
240
|
|
|
241
|
-
- Query functions return `[]` on no match, never throw
|
|
241
|
+
- Query functions return `[]` on no match, never throw. But an **empty or
|
|
242
242
|
unknown-key query** throws (a mistyped filter is a bug, not an empty result):
|
|
243
243
|
`festum({ month: 12 })` and `cantus({})` throw rather than silently resolving a
|
|
244
244
|
plausible-looking answer.
|
|
245
245
|
- Builder functions throw `Error` with a descriptive message on invalid input.
|
|
246
246
|
- `notatio` throws on invalid `Chant` input.
|
|
247
247
|
- `inscriptio` throws on a non-`Score` argument or an unknown notation species.
|
|
248
|
-
- `temperamentum.tonus()` throws if `mode` is `"auto"
|
|
248
|
+
- `temperamentum.tonus()` throws if `mode` is `"auto"`. Mode must be set
|
|
249
249
|
explicitly.
|
|
250
250
|
- Malformed `comma`, ratio, or Scala input throws `RangeError`; custom scales
|
|
251
251
|
must supply 7 or 12 steps, beginning at `1/1` (a degree list) or ending at
|
package/docs/api/score.md
CHANGED
|
@@ -42,8 +42,8 @@ const score = tonus.notatio(introit, { temperamentum: t });
|
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
The structured view is `score.phrases`; the flat view, one row per note,
|
|
45
|
-
is `score.tabula`. Phrases split at every divisio
|
|
46
|
-
notation, signs of punctuation rather than measure:
|
|
45
|
+
is `score.tabula`. Phrases split at every divisio, the bars of chant
|
|
46
|
+
notation, which are signs of punctuation rather than measure:
|
|
47
47
|
|
|
48
48
|
| divisio | name |
|
|
49
49
|
| ------- | ---------------------------- |
|
|
@@ -103,7 +103,7 @@ text: the `<sp>` shortcuts arrive as real characters (`<sp>V/</sp>` → ℣,
|
|
|
103
103
|
`\greheightstar` verbatim → the raised *), centering braces and layout tags
|
|
104
104
|
(`<clear>`, `<nlba>`) vanish, above-lines text (`<alt>`) is not lyric text,
|
|
105
105
|
and page cross-references (`\pageref`) to the paper books are dropped. Style
|
|
106
|
-
tags
|
|
106
|
+
tags (`<i>`, `<b>`, `<sc>`, `<c>` for rubric color, `<e>` for elision) survive
|
|
107
107
|
as `runs`, styled spans that concatenate to `lyric`; a style opened in one
|
|
108
108
|
syllable and closed several later (the common `<i>ij.</i>` and euouae
|
|
109
109
|
patterns) styles every syllable it crosses. Both notation species draw the
|
|
@@ -289,7 +289,7 @@ analysis, visualization, or emission.
|
|
|
289
289
|
|
|
290
290
|
`Harmony` exposes the same surface for voiced bodies
|
|
291
291
|
([heavens.md](heavens.md#the-tabula)). The tabula is also the rendering
|
|
292
|
-
surface
|
|
292
|
+
surface. The SVG renderer ([below](#rendering)) consumes it directly, which is
|
|
293
293
|
why `hz`, `velocity`, `bend`, and the ornament flags live on each row.
|
|
294
294
|
|
|
295
295
|
```js
|
|
@@ -364,7 +364,7 @@ interface ChantTabulaRow {
|
|
|
364
364
|
|
|
365
365
|
## Rendering
|
|
366
366
|
|
|
367
|
-
The score is drawn as **SVG
|
|
367
|
+
The score is drawn as **SVG**: a self-contained, square-note chant staff with
|
|
368
368
|
SMuFL glyphs baked as inline paths (no external font). It consumes `score.tabula`,
|
|
369
369
|
so the interpretation applied through `pondus` and `accentus` is already in the
|
|
370
370
|
geometry. Microtuning lives on each tabula row's `bend`, `hz`, and `offset` for
|
|
@@ -373,7 +373,7 @@ a Web-Audio player to read directly.
|
|
|
373
373
|
### inscriptio — the standalone renderer
|
|
374
374
|
|
|
375
375
|
`tonus.inscriptio(score, opts?)` draws a `Score` and returns `{ svg, geometry }`.
|
|
376
|
-
Rendering is a standalone function that _takes_ a score, not a method on one
|
|
376
|
+
Rendering is a standalone function that _takes_ a score, not a method on one. The
|
|
377
377
|
score analyzes, `inscriptio` inks. It throws on a non-Score or an unknown
|
|
378
378
|
notation species (the builder-function contract).
|
|
379
379
|
|
|
@@ -391,14 +391,14 @@ Two notation species, each with its own spacing pass:
|
|
|
391
391
|
|
|
392
392
|
**Layout is deterministic, and lyric widths are computed rather than measured.**
|
|
393
393
|
The same score and options give byte-identical SVG on every machine, with no
|
|
394
|
-
DOM, no canvas, and no font file
|
|
394
|
+
DOM, no canvas, and no font file, so `inscriptio` runs anywhere Node does. Note
|
|
395
395
|
glyphs carry exact SMuFL advance widths; lyric text is computed from character
|
|
396
396
|
classes, since measuring it would require the font's own metrics. Line breaks,
|
|
397
397
|
system fill, and the width of the returned canvas all rest on that figure. It is
|
|
398
398
|
close, not exact: a lyric set in a face far from the assumed proportions will
|
|
399
399
|
break slightly early or late.
|
|
400
400
|
|
|
401
|
-
Two consequences worth planning around. `width` is a **request, not a promise
|
|
401
|
+
Two consequences worth planning around. `width` is a **request, not a promise**:
|
|
402
402
|
the canvas returned is `max(width, content)`, so a chant whose content cannot fit
|
|
403
403
|
comes back wider rather than clipped. And a caller who needs typographic
|
|
404
404
|
precision should render at a generous `width` and scale the result, rather than
|
|
@@ -406,22 +406,22 @@ relying on the estimate to land a tight column exactly.
|
|
|
406
406
|
|
|
407
407
|
Options, by group (all optional):
|
|
408
408
|
|
|
409
|
-
- **layout
|
|
409
|
+
- **layout**: `width` wraps systems to fit (absent = a single line); `scale`
|
|
410
410
|
sets how big the chant is drawn: `"small"`, `"normal"` (default), `"large"`,
|
|
411
411
|
or a staff height in px for fitting a known column. Everything scales from it
|
|
412
|
-
|
|
412
|
+
(notes, lyrics, the air between systems), and it reflows the music, so a
|
|
413
413
|
larger scale means fewer notes per line. The page margin does not scale: it
|
|
414
414
|
belongs to the page rather than the notation, and scaling it gave a large
|
|
415
415
|
chant *less* usable width than a small one.
|
|
416
|
-
- **front matter
|
|
416
|
+
- **front matter**: set as the Solesmes books open a piece: `title` centers
|
|
417
417
|
over the score; `rubric` (or `annotation: "auto"` to derive the genus/mode
|
|
418
418
|
mark, e.g. _Introitus. 8._) sits upright at the left margin; `dropcap` draws
|
|
419
419
|
the initial the printed books open with, taking the first letter out of the
|
|
420
420
|
lyric and indenting the first system to hold it. Both species take the
|
|
421
|
-
title; the margin mark and the initial are **quadrata's alone
|
|
421
|
+
title; the margin mark and the initial are **quadrata's alone**, because moderna is
|
|
422
422
|
a transcription read as an edition, and carries the analysis tracks a
|
|
423
423
|
reserved cap column would fight. It ignores them rather than refusing.
|
|
424
|
-
- **theme
|
|
424
|
+
- **theme**: the dress, `fonts` and `colors`.
|
|
425
425
|
|
|
426
426
|
### theme — faces and ink
|
|
427
427
|
|
|
@@ -441,14 +441,14 @@ tonus.inscriptio(score, {
|
|
|
441
441
|
```
|
|
442
442
|
|
|
443
443
|
**`fonts`** carries four roles. A book's dropcap is very often *not* its lyric
|
|
444
|
-
face
|
|
444
|
+
face, but a Lombardic or uncial initial against a text hand, which is the pairing
|
|
445
445
|
the printed books use. Each role takes a font-family string or
|
|
446
446
|
`{ family, weight?, scale? }` (`scale` adjusts that role's size, for a face
|
|
447
447
|
whose apparent size differs from the house serif).
|
|
448
448
|
|
|
449
449
|
The SVG carries font-family *references* by default, and the page hosting it
|
|
450
450
|
supplies the face (`@font-face`). A slot may instead carry
|
|
451
|
-
`embed: { base64, format? }`
|
|
451
|
+
`embed: { base64, format? }` (the caller's own bytes), and the face then rides
|
|
452
452
|
inside the SVG's `<style>`, making the file self-contained (at the cost of its
|
|
453
453
|
size; one `@font-face` per family + weight, deduped). tonus bundles no font
|
|
454
454
|
files: with `embed` it is a conduit for data the consumer supplies, so the
|
|
@@ -489,7 +489,7 @@ is 196 KB base64'd, which triples a typical chant and repeats in every file,
|
|
|
489
489
|
where a reference is cached once.
|
|
490
490
|
|
|
491
491
|
**`colors`** reach the SVG as CSS custom properties with the theme's own value
|
|
492
|
-
as the fallback
|
|
492
|
+
as the fallback, `fill="var(--tonus-note, #111)"`. A rendered chant therefore
|
|
493
493
|
carries the ink it was drawn with *and* stays themable: a host stylesheet that
|
|
494
494
|
sets the property rethemes the score without re-rendering it.
|
|
495
495
|
|
|
@@ -502,8 +502,8 @@ sets the property rethemes the score without re-rendering it.
|
|
|
502
502
|
}
|
|
503
503
|
```
|
|
504
504
|
|
|
505
|
-
The emitter's semantic classes
|
|
506
|
-
`episema`, `divisio`, `clef`, `mora`, `ictus`
|
|
505
|
+
The emitter's semantic classes (`note`, `lyric`, `dropcap`, `custos`,
|
|
506
|
+
`episema`, `divisio`, `clef`, `mora`, `ictus`) are stylable from the host page.
|
|
507
507
|
|
|
508
508
|
**`scale` is not part of the theme**: line breaking consumes it, so a scale
|
|
509
509
|
change re-renders while a colour change does not.
|
|
@@ -537,11 +537,11 @@ is in, not an offset to add.
|
|
|
537
537
|
### The analysis tracks
|
|
538
538
|
|
|
539
539
|
`tracks` draws an analysis band beneath every system. Any track rides either
|
|
540
|
-
species, and all may ride one score
|
|
540
|
+
species, and all may ride one score: the selection is independent of the
|
|
541
541
|
notation, as `notation` itself is. One governing ink system runs through them:
|
|
542
542
|
every mark draws in the score's black, strata graded by opacity alone (the
|
|
543
|
-
liturgical red belongs to the claims
|
|
544
|
-
prosodia's accent dots), and every pressure-bearing line shares one nib law
|
|
543
|
+
liturgical red belongs to the claims, the tonarium's mode line and the
|
|
544
|
+
prosodia's accent dots), and every pressure-bearing line shares one nib law:
|
|
545
545
|
velocity as stroke width.
|
|
546
546
|
|
|
547
547
|
```js
|
|
@@ -679,7 +679,7 @@ interface ModalAffinity {
|
|
|
679
679
|
|
|
680
680
|
## Metrics
|
|
681
681
|
|
|
682
|
-
`score.metrics` measures the chant's shape
|
|
682
|
+
`score.metrics` measures the chant's shape: counts, range, melisma, melodic
|
|
683
683
|
motion, contour, tessitura, rhythm, cadence. It is chant-specific; `Harmony`
|
|
684
684
|
has no metrics.
|
|
685
685
|
|
|
@@ -715,7 +715,7 @@ interface Metrics {
|
|
|
715
715
|
The five composite fields each answer one question about the chant.
|
|
716
716
|
|
|
717
717
|
**Where the melody sits, and the shape it traces.** `noteRange` is the plain
|
|
718
|
-
compass; `arcus` reads the classic chant arch
|
|
718
|
+
compass; `arcus` reads the classic chant arch, a rise to a peak and a return to the
|
|
719
719
|
final.
|
|
720
720
|
|
|
721
721
|
```ts
|
|
@@ -734,7 +734,7 @@ interface Arcus {
|
|
|
734
734
|
```
|
|
735
735
|
|
|
736
736
|
**How the melody moves.** Chant is overwhelmingly stepwise, so `motus` sorts
|
|
737
|
-
every adjacent within-phrase motion by size
|
|
737
|
+
every adjacent within-phrase motion by size. The three names are the tradition's
|
|
738
738
|
own, and a chant whose skips and leaps outnumber its steps is unusual enough to
|
|
739
739
|
question.
|
|
740
740
|
|
|
@@ -778,12 +778,12 @@ broken by breaths.
|
|
|
778
778
|
| `colon` | `:` | divisio maior | 1.0 |
|
|
779
779
|
| `doubleBar` | `::` | divisio finalis | 1.5 |
|
|
780
780
|
|
|
781
|
-
This table is canonical
|
|
781
|
+
This table is canonical: `metrics.ts` cites it rather than restating the
|
|
782
782
|
weights.
|
|
783
783
|
|
|
784
784
|
## Cadences
|
|
785
785
|
|
|
786
|
-
`score.cadences` names the melodic close of each phrase
|
|
786
|
+
`score.cadences` names the melodic close of each phrase. Where metrics
|
|
787
787
|
only counts the divisio bars, this identifies the figure. One `Cadence` per
|
|
788
788
|
phrase-ending divisio: its resolution `target`, the melodic `approach`, and the
|
|
789
789
|
`divisio` that tells medial from final (the double bar `::` is the final
|
|
@@ -795,21 +795,21 @@ the tabula.
|
|
|
795
795
|
Two catalogues describe a cadence, and they answer different questions. Read
|
|
796
796
|
this before deciding which field to use:
|
|
797
797
|
|
|
798
|
-
> Every cadence carries a **`signature
|
|
798
|
+
> Every cadence carries a **`signature`**, always. Some are **catalogued** by
|
|
799
799
|
> the corpus (`finality`, and everything in
|
|
800
800
|
> [`CADENTIAE`](index.md#the-appendix)). Some, on the final, are **named** by
|
|
801
801
|
> received theory (`formula`).
|
|
802
802
|
|
|
803
803
|
- **`formula`** is _tradita_: the mode's cadence figures as the treatises give
|
|
804
804
|
them ([tuning.md](tuning.md#cadence-figures)), matched in solmization
|
|
805
|
-
relative to the final
|
|
805
|
+
relative to the final (`"la-sol"`, `"mi-re"`). It fires **only on the
|
|
806
806
|
finalis**, because the received catalogue holds only final figures.
|
|
807
807
|
- **`signature`** is _inventa_: the tail's interval shape and where it lands,
|
|
808
808
|
keyed as `"2,0,-2 @0"` and mined from the corpus. It fires on **any** target,
|
|
809
809
|
so it is the one of the two that speaks about **medial** cadences.
|
|
810
810
|
|
|
811
|
-
Measured over the cadences `notatio` reports across the shipped corpus
|
|
812
|
-
26,800 of them
|
|
811
|
+
Measured over the cadences `notatio` reports across the shipped corpus (about
|
|
812
|
+
26,800 of them), roughly 43% carry a formula, 57% join the catalogue, and 31%
|
|
813
813
|
carry both, so about a third carry neither. Neither is derivable from the
|
|
814
814
|
other, because the signature is mode-blind and the formula is mode-relative.
|
|
815
815
|
|
|
@@ -822,7 +822,7 @@ the final, 31 do not close, and finality across the catalogue runs the whole
|
|
|
822
822
|
range from 0 to 1. So
|
|
823
823
|
`arrival === 0` implies nothing about whether a close is final.
|
|
824
824
|
|
|
825
|
-
It is `null` when the signature falls below the catalogue's floor
|
|
825
|
+
It is `null` when the signature falls below the catalogue's floor: an
|
|
826
826
|
uncatalogued close, not a close that never closes.
|
|
827
827
|
|
|
828
828
|
```ts
|
|
@@ -843,7 +843,7 @@ interface Cadence {
|
|
|
843
843
|
}
|
|
844
844
|
```
|
|
845
845
|
|
|
846
|
-
A one-note phrase is a cadence
|
|
846
|
+
A one-note phrase is a cadence (a landing with no gesture) and keys with an
|
|
847
847
|
empty shape (`" @0"`), which is why `signature` is that key rather than null.
|
|
848
848
|
|
|
849
849
|
`arrival` is signed and not octave-reduced: `@-5`, a fourth below the final,
|
|
@@ -852,7 +852,7 @@ and `@+7`, a fifth above, are distinct families.
|
|
|
852
852
|
## Modulations
|
|
853
853
|
|
|
854
854
|
`score.modulations` marks where the tonal centre leans away from the home
|
|
855
|
-
mode
|
|
855
|
+
mode, the local counterpart to the imprint's global modal
|
|
856
856
|
affinity. Each phrase is scored against all eight modes (the imprint's
|
|
857
857
|
affinity math); a run of phrases that favours a foreign mode, by a margin,
|
|
858
858
|
becomes one `Modulation` span. The margin is calibrated against Suñol's
|
|
@@ -861,13 +861,13 @@ distribution-based: it finds where a passage leans, not a functional analysis.
|
|
|
861
861
|
|
|
862
862
|
`kind` says what the span is evidence OF, which matters because the three are
|
|
863
863
|
not the same phenomenon. **`inflection`** is a single phrase leaning away and
|
|
864
|
-
back
|
|
864
|
+
back: passing colour, not a shift. **`modulation`** is a sustained internal
|
|
865
865
|
excursion, two phrases or more, that returns. **`transposition`** is the whole
|
|
866
866
|
chant sitting in a foreign mode's frame: it does not close on its labelled
|
|
867
867
|
final and one foreign mode dominates most of its phrases, meaning the melody is
|
|
868
868
|
notated at a transposed position (the affinal) or the label disagrees with the
|
|
869
|
-
notation. A transposed chant is not modulating
|
|
870
|
-
so a caller displaying "modulations" should treat those spans as a re-reading of
|
|
869
|
+
notation. A transposed chant is not modulating, because the displacement is
|
|
870
|
+
global, so a caller displaying "modulations" should treat those spans as a re-reading of
|
|
871
871
|
the whole chant rather than an event inside it.
|
|
872
872
|
|
|
873
873
|
```ts
|
|
@@ -913,35 +913,35 @@ The classifier applies Carroll's three melodic rules in priority order
|
|
|
913
913
|
|
|
914
914
|
The first compound beat of an incise is always arsic. When every rule is inconclusive, the
|
|
915
915
|
shape alternates from the previous group. Two conventional overrides
|
|
916
|
-
precede the rules
|
|
917
|
-
summit is the arsic gesture
|
|
916
|
+
precede the rules. The **salicus** is always arsic, because the tension toward
|
|
917
|
+
its summit is the arsic gesture, and the **doubly-dotted clivis** is always
|
|
918
918
|
thetic, as a cadential figure.
|
|
919
919
|
|
|
920
920
|
A salicus here is Cardine's: an ascent of at least three notes whose
|
|
921
921
|
**next-to-last note is an oriscus** [biblio: cardine-semiology, ch. 16]. The
|
|
922
922
|
oriscus is what makes one. An ascending group carrying only the editorial
|
|
923
|
-
Solesmes ictus is a **scandicus** that was marked for rhythm
|
|
924
|
-
worth stating because conflating the two is, in Bevenot's word, a trap: over
|
|
923
|
+
Solesmes ictus is a **scandicus** that was marked for rhythm. The distinction
|
|
924
|
+
is worth stating because conflating the two is, in Bevenot's word, a trap: over
|
|
925
925
|
the sung corpus tonus finds about 260 salici against about 1,900 scandici, so
|
|
926
926
|
only about an eighth of that wider set carries an oriscus at all.
|
|
927
927
|
|
|
928
928
|
Cardine's correction also decides WHICH note is principal. The printed
|
|
929
929
|
editions lengthen the oriscus itself; the manuscripts show the principal note
|
|
930
|
-
is the one **immediately following** it
|
|
930
|
+
is the one **immediately following** it (the summit), so tonus prolongs that
|
|
931
931
|
note and takes the oriscus lightly. This is the one point where the rhythmic
|
|
932
932
|
layer departs from Mocquereau and Suñol, and it does so deliberately.
|
|
933
933
|
|
|
934
934
|
### Rhythmic types
|
|
935
935
|
|
|
936
|
-
Above the per-beat arsis/thesis, each phrase carries a `rhythmicType
|
|
936
|
+
Above the per-beat arsis/thesis, each phrase carries a `rhythmicType`, Le
|
|
937
937
|
Guennant's taxonomy (via Carroll) of how the incise's compound beats chain, and
|
|
938
938
|
the `beats` sequence it reads. The observable types are modeled: **IV** (a single
|
|
939
939
|
arsis to a single thesis), **V** (several arses to one thesis), **VI** (one arsis
|
|
940
940
|
to several theses), **VII** (regular A–T alternation), and **VIII** (a
|
|
941
|
-
contraction
|
|
941
|
+
contraction of two simple rhythms overlapping at a shared ictus, after Suñol).
|
|
942
942
|
Types I–III use sub-beat cells that never surface in isolation and are not
|
|
943
943
|
labeled; an incise that fits no type is `null`. The classification rules live at
|
|
944
|
-
the data
|
|
944
|
+
the data. See `classifyRhythmicType` in
|
|
945
945
|
[`score/ir.ts`](https://github.com/jeffreypierce/tonus/blob/main/src/engines/score/ir.ts).
|
|
946
946
|
|
|
947
947
|
### Modeled and not
|
package/docs/api/tuning.md
CHANGED
|
@@ -343,11 +343,11 @@ interface GamutOptions {
|
|
|
343
343
|
`modus` returns the full profile of one of the eight modes, as the
|
|
344
344
|
medieval tonaries describe them:
|
|
345
345
|
|
|
346
|
-
- its identity
|
|
346
|
+
- its identity: Latin name, modern alias, _maneria_ (the family: Protus,
|
|
347
347
|
Deuterus, Tritus, Tetrardus), and authentic or plagal type;
|
|
348
|
-
- its structure
|
|
348
|
+
- its structure: finalis, tenor, scale degrees, ambitus, and the species
|
|
349
349
|
of fifth and fourth that build it;
|
|
350
|
-
- its practice
|
|
350
|
+
- its practice: hexachords in rank order, melodic profile, its cadence
|
|
351
351
|
figures, and permitted modulations.
|
|
352
352
|
|
|
353
353
|
Each mode also carries its traditional **ethos** (the character medieval theory
|
|
@@ -365,7 +365,7 @@ data, in [`temper/data/modes.ts`](https://github.com/jeffreypierce/tonus/blob/ma
|
|
|
365
365
|
temperamentum: the **finalis** and **reciting** tone as tuned notes (pitch +
|
|
366
366
|
Guidonian step), and **ambitusNotes**, the mode's diatonic range walked out
|
|
367
367
|
note by note. The raw `ModeData` fields (`final`, `tenor`, `cadences`, …)
|
|
368
|
-
remain alongside
|
|
368
|
+
remain alongside, because `Modus` extends `ModeData`.
|
|
369
369
|
|
|
370
370
|
```js
|
|
371
371
|
t.modus(1);
|
|
@@ -436,7 +436,7 @@ interface Modus extends ModeData {
|
|
|
436
436
|
### Cadence figures
|
|
437
437
|
|
|
438
438
|
Each mode carries the melodic figures its phrases characteristically close
|
|
439
|
-
on, in `modus(n).cadences
|
|
439
|
+
on, in `modus(n).cadences`, the shapes by which a chant comes to rest, stored
|
|
440
440
|
as diatonic steps relative to the final. The score engine reads them to name a
|
|
441
441
|
phrase's cadence ([score.md](score.md#cadences)).
|
|
442
442
|
|
|
@@ -449,10 +449,10 @@ phrase's cadence ([score.md](score.md#cadences)).
|
|
|
449
449
|
|
|
450
450
|
The catalogue is an editorial synthesis covering the final cadences; its step
|
|
451
451
|
encoding, sources, and known gaps (medial cadences are not yet included) are
|
|
452
|
-
documented at the data
|
|
452
|
+
documented at the data. See `CadenceFigure` in
|
|
453
453
|
[`temper/data/modes.ts`](https://github.com/jeffreypierce/tonus/blob/main/src/engines/temper/data/modes.ts).
|
|
454
454
|
|
|
455
|
-
This is the **tradita** half
|
|
455
|
+
This is the **tradita** half, what the treatises say. Its counterpart is the
|
|
456
456
|
corpus tally below, and they are not interchangeable: see [one spine, two
|
|
457
457
|
annotations](score.md#one-spine-two-annotations).
|
|
458
458
|
|
|
@@ -460,8 +460,8 @@ annotations](score.md#one-spine-two-annotations).
|
|
|
460
460
|
|
|
461
461
|
Where the figures above are received, [`CADENTIAE`](index.md#the-appendix) is
|
|
462
462
|
**mined**: every phrase-end in the sung corpus, grouped into families by what
|
|
463
|
-
the melody actually did. A family is a **shape**
|
|
464
|
-
successive semitone intervals
|
|
463
|
+
the melody actually did. A family is a **shape** (the closing tail's
|
|
464
|
+
successive semitone intervals) and an **arrival**, where it landed relative
|
|
465
465
|
to the chant's own closing note. Together they are the key, `"2,0,-2 @0"`, and
|
|
466
466
|
the key is the family's whole name.
|
|
467
467
|
|
|
@@ -505,14 +505,14 @@ lift(fam, 6); // 2.15
|
|
|
505
505
|
lift(fam, 4); // 0.07
|
|
506
506
|
```
|
|
507
507
|
|
|
508
|
-
The commonest family in the corpus
|
|
509
|
-
phrase-ends, closing a little under half the time
|
|
508
|
+
The commonest family in the corpus (about 1,100 occurrences, about 4% of all
|
|
509
|
+
phrase-ends, closing a little under half the time) is not reached for evenly.
|
|
510
510
|
Mode 6 reaches for this close more than twice as often as the corpus does;
|
|
511
511
|
mode 4 almost never. That is the figure the tonarium prints under a cadence
|
|
512
512
|
([score.md](score.md#the-analysis-tracks)).
|
|
513
513
|
|
|
514
|
-
**The ratio is not baked.** The table exports the vocabulary
|
|
515
|
-
their denominators
|
|
514
|
+
**The ratio is not baked.** The table exports the vocabulary (counts and
|
|
515
|
+
their denominators) and leaves the arithmetic to the caller. Two cautions when
|
|
516
516
|
you take it: below roughly ten in-mode occurrences the ratio is one or two
|
|
517
517
|
chants deciding a number that reads like a measurement, and a mode-less chant
|
|
518
518
|
(`"?"`) has no denominator at all. In both cases fall back to `share`.
|
|
@@ -524,7 +524,7 @@ pitches: the **intonatio** (the opening rise), the **mediatio** (the
|
|
|
524
524
|
cadence at the verse's colon), and the **terminatio** (the closing
|
|
525
525
|
cadence). The termination varies by **differentia**, the cadential variant
|
|
526
526
|
chosen to lead back into the antiphon; the mode's default differentia is
|
|
527
|
-
used when none is given. The mode must be set explicitly
|
|
527
|
+
used when none is given. The mode must be set explicitly, because with mode
|
|
528
528
|
`"auto"`, `tonus()` throws.
|
|
529
529
|
|
|
530
530
|
```js
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tonus",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Medieval music analysis
|
|
5
|
+
"description": "Medieval music analysis: GABC plainchant exports, liturgical calendar, tuning systems, ephemeris, and the harmony of the spheres",
|
|
6
6
|
"author": "Jeffrey Pierce <jeffrey@jeffreypierce.net>",
|
|
7
7
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
10
|
"url": "git+https://github.com/jeffreypierce/tonus.git"
|
|
11
11
|
},
|
|
12
|
-
"homepage": "https://
|
|
12
|
+
"homepage": "https://orreliquum.com/",
|
|
13
13
|
"bugs": "https://github.com/jeffreypierce/tonus/issues",
|
|
14
14
|
"keywords": [
|
|
15
15
|
"gregorian-chant",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
},
|
|
37
37
|
"main": "./dist/index.js",
|
|
38
38
|
"types": "./dist/index.d.ts",
|
|
39
|
+
"sideEffects": false,
|
|
39
40
|
"files": [
|
|
40
41
|
"dist",
|
|
41
42
|
"docs/api",
|