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 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
- **[Orreliquum — the library at work →](https://jeffreypierce.github.io/orreliquum/)**
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 nine Solesmes editions and
76
- the _Nocturnale Romanum_ night office in
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
117
- /** Written y for a scientific pitch name on the treble-8 staff (bottom line E4). */
118
- function writtenY(spn, systemY, gm) {
119
- const m = /([A-G])[#b]?(-?\d)/.exec(spn);
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 di = (Number(m[2]) + 2) * 7 + LETTERS[m[1]];
123
- const steps = di - (4 * 7 + LETTERS["E"]); // relative to bottom line E4
124
- return { y: systemY + gm.MTOP + 4 * gm.MSP - steps * (gm.MSP / 2), steps };
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
- notePos.push({ mx: nx, my: y, steps });
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
- const CLEFS = {
22
- c1: { doMidi: 60, doIdx: 3 },
23
- c2: { doMidi: 60, doIdx: 5 },
24
- c3: { doMidi: 60, doIdx: 7 },
25
- c4: { doMidi: 60, doIdx: 9 },
26
- // f-clefs anchor fa on the named line. Staff lines (bottom→top) sit at
27
- // letters d/f/h/j (per the Gregorio spec: 2-line staff = a–i, 3-line = a–k,
28
- // 4-line = a–m, pinning the lines at slots 3/5/7/9), so f3 puts fa at 'h'
29
- // (7) and f4 at 'j' (9). Previous values (5 and 3) were off by a third and
30
- // read every f-clef chant at the wrong staff position.
31
- f3: { doMidi: 53, doIdx: 7 },
32
- f4: { doMidi: 53, doIdx: 9 },
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
- const diatIdx = DIATONIC.indexOf(pc);
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
- const staffPos = LETTERS.indexOf(letter.toLowerCase());
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
- return (doOctave + octOffset + 1) * 12 + DIATONIC[diatStep];
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];
@@ -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
- what "attested" means, and what a slot the view excludes does is in
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 one
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 where a rubric names several (II class 1–5), that
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 — the
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
 
@@ -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 profile, balance, neighbors:
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** the same population
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 that is what `before` reads),
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 the same parse
87
- `score` gives you so the census can never disagree with the library about
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; length is not a
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 its commonest motifs, its commonest closing gestures,
98
- one bucket for the rest so the corpus supplies the vocabulary and the chant
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; grouping "all Communions," "this season," "this
109
- manuscript" is yours to do. The moment you pool blocks yourself you are
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 every
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 so `k` stays satisfiable, and a filtered list is *not* a subset of the
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 it sets its text and turns its phrases the way Communions do while its
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 modally it is a typical
217
- mode-2 chant while its `melodic` typicality is about 0.70, because its
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 not against an absolute threshold. The question it answers
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 1,790 of the 2,186 candidates. This is the same rule as
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 you asked about it by name.
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 10,156 book chants become 2,187.
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 how many chants, in what genres, in what modes. `corpus({ book })` is
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** the rollup plus every
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** the one to quote. `listings` is how long
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 1,049 antiphons
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 it applies the Kyriale's own rubrics. This is flat retrieval.
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** not the book
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 9.5% because the corpus is
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" and a chant with no dated
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** the Kyriale offers ranked
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 the
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 "chants from one Mass may be used together with those
382
- from others" with one exception, the book's own: **"the Ferial Masses
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 the Immaculate Conception
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**" so a green feria
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`) but the
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 — read
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 the monastic cursus with no option to
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 the little
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 it carries its own melody and is not addressable here.)
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 });
@@ -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 the sign is the resolution anything
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 Aries at the
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 Sun→Jupiter a pure 3/2,
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 `spn` and `hz` are that ratio sounded against A4, not
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 see `DOCTRINAE` in
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) the origin of the durum/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
- **[Orreliquum — the library at work →](https://jeffreypierce.github.io/orreliquum/)**
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 see [the census
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 the maps are [the appendix](#the-appendix).
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 but an **empty or
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"` mode must be set
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 the bars of chant
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 `<i>`, `<b>`, `<sc>`, `<c>` (rubric color), `<e>` (elision) survive
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 the SVG renderer ([below](#rendering)) consumes it directly, which is
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** a self-contained, square-note chant staff with
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 — the
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 `inscriptio` runs anywhere Node does. Note
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** `width` wraps systems to fit (absent = a single line); `scale`
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
- notes, lyrics, the air between systems and it reflows the music, so a
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** set as the Solesmes books open a piece: `title` centers
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** moderna is
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** the dress: `fonts` and `colors`.
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 a Lombardic or uncial initial against a text hand, which is the pairing
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? }` the caller's own bytes and the face then rides
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 `fill="var(--tonus-note, #111)"`. A rendered chant therefore
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 `note`, `lyric`, `dropcap`, `custos`,
506
- `episema`, `divisio`, `clef`, `mora`, `ictus` are stylable from the host page.
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 the selection is independent of the
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 the tonarium's mode line and the
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 counts, range, melisma, melodic
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 rise to a peak, return to the
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 the three names are the tradition's
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 `metrics.ts` cites it rather than restating the
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 where metrics
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`** always. Some are **catalogued** by
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 `"la-sol"`, `"mi-re"`. It fires **only on the
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 about
812
- 26,800 of them roughly 43% carry a formula, 57% join the catalogue, and 31%
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 an
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 a landing with no gesture and keys with an
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 the local, temporal counterpart to the imprint's global modal
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 passing colour, not a shift. **`modulation`** is a sustained internal
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 the displacement is global —
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: the **salicus** is always arsic the tension toward its
917
- summit is the arsic gesture and the **doubly-dotted clivis** is always
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 a distinction
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 the summit so tonus prolongs that
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` Le
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 two simple rhythms overlapping at a shared ictus, after Suñol).
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 see `classifyRhythmicType` in
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
@@ -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 Latin name, modern alias, _maneria_ (the family: Protus,
346
+ - its identity: Latin name, modern alias, _maneria_ (the family: Protus,
347
347
  Deuterus, Tritus, Tetrardus), and authentic or plagal type;
348
- - its structure finalis, tenor, scale degrees, ambitus, and the species
348
+ - its structure: finalis, tenor, scale degrees, ambitus, and the species
349
349
  of fifth and fourth that build it;
350
- - its practice hexachords in rank order, melodic profile, its cadence
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 `Modus` extends `ModeData`.
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` the shapes by which a chant comes to rest, stored
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 see `CadenceFigure` in
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 what the treatises say. Its counterpart is the
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** the closing tail's
464
- successive semitone intervals and an **arrival**, where it landed relative
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 about 1,100 occurrences, about 4% of all
509
- phrase-ends, closing a little under half the time is not reached for evenly.
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 counts and
515
- their denominators and leaves the arithmetic to the caller. Two cautions when
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 with mode
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.0",
3
+ "version": "0.9.1",
4
4
  "type": "module",
5
- "description": "Medieval music analysis and performance: GABC plainchant exports, liturgical calendar, tuning systems, ephemeris, and the harmony of the spheres",
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://jeffreypierce.github.io/orreliquum/",
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",