tonus 0.8.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.
Files changed (34) hide show
  1. package/CHANGELOG.md +275 -1
  2. package/README.md +3 -3
  3. package/dist/engines/chant/attest.js +8 -8
  4. package/dist/engines/chant/hour.js +3 -3
  5. package/dist/engines/chant/intone.js +7 -1
  6. package/dist/engines/score/cadence.d.ts +10 -4
  7. package/dist/engines/score/cadence.js +10 -4
  8. package/dist/engines/score/emitters/accidentals.d.ts +9 -0
  9. package/dist/engines/score/emitters/accidentals.js +76 -13
  10. package/dist/engines/score/emitters/atramentum.js +1 -1
  11. package/dist/engines/score/emitters/breaking.d.ts +2 -2
  12. package/dist/engines/score/emitters/breaking.js +2 -2
  13. package/dist/engines/score/emitters/moderna.js +165 -17
  14. package/dist/engines/score/emitters/svg.d.ts +16 -0
  15. package/dist/engines/score/emitters/svg.js +19 -12
  16. package/dist/engines/score/emitters/tracks.js +29 -10
  17. package/dist/engines/score/ir.js +24 -4
  18. package/dist/engines/score/neume.d.ts +19 -0
  19. package/dist/engines/score/neume.js +35 -0
  20. package/dist/engines/score/parse.js +4 -1
  21. package/dist/engines/score/types.d.ts +6 -0
  22. package/dist/engines/temper/data/guido.js +4 -2
  23. package/dist/engines/temper/gabc.d.ts +12 -0
  24. package/dist/engines/temper/gabc.js +51 -18
  25. package/dist/engines/temper/neume.d.ts +1 -1
  26. package/dist/engines/temper/neume.js +24 -3
  27. package/docs/api/calendar.md +4 -4
  28. package/docs/api/census.md +20 -20
  29. package/docs/api/chant.md +27 -27
  30. package/docs/api/heavens.md +7 -7
  31. package/docs/api/index.md +5 -5
  32. package/docs/api/score.md +191 -138
  33. package/docs/api/tuning.md +31 -23
  34. package/package.json +4 -3
@@ -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];
@@ -2,7 +2,7 @@ import type { Interval } from "./interval.js";
2
2
  import type { Pitch, PitchInput } from "./pitch.js";
3
3
  import type { Scale } from "./scale.js";
4
4
  export type { Interval };
5
- export type NeumeShape = "punctum" | "pes" | "clivis" | "torculus" | "porrectus" | "scandicus" | "salicus" | "climacus" | "torculus resupinus" | "porrectus flexus" | "scandicus flexus" | "climacus resupinus" | "pes subpunctis" | "compound";
5
+ export type NeumeShape = "punctum" | "pes" | "clivis" | "torculus" | "porrectus" | "scandicus" | "salicus" | "climacus" | "torculus resupinus" | "porrectus flexus" | "scandicus flexus" | "climacus resupinus" | "pes subpunctis" | "distropha" | "tristropha" | "tristropha flexa" | "pressus" | "pressus maior" | "scandicus subpunctis" | "compound";
6
6
  export interface Neume {
7
7
  pitches: Pitch[];
8
8
  intervals: Interval[];
@@ -9,9 +9,12 @@ export function classifyShape(dirs) {
9
9
  return "punctum";
10
10
  const up = (d) => d === "up";
11
11
  const dn = (d) => d === "down";
12
+ const un = (d) => d === "unison";
12
13
  switch (n) {
13
14
  case 1:
14
- return up(dirs[0]) ? "pes" : dn(dirs[0]) ? "clivis" : "punctum";
15
+ // A unison here is two notes on one pitch — a distropha, not a punctum.
16
+ // Reporting "punctum" said a two-note figure was one note.
17
+ return up(dirs[0]) ? "pes" : dn(dirs[0]) ? "clivis" : "distropha";
15
18
  case 2: {
16
19
  const [d0, d1] = dirs;
17
20
  if (up(d0) && dn(d1))
@@ -22,6 +25,10 @@ export function classifyShape(dirs) {
22
25
  return "scandicus";
23
26
  if (dn(d0) && dn(d1))
24
27
  return "climacus";
28
+ if (un(d0) && un(d1))
29
+ return "tristropha";
30
+ if (un(d0) && dn(d1))
31
+ return "pressus";
25
32
  return "compound";
26
33
  }
27
34
  case 3: {
@@ -36,12 +43,26 @@ export function classifyShape(dirs) {
36
43
  return "climacus resupinus";
37
44
  if (up(d0) && dn(d1) && dn(d2))
38
45
  return "pes subpunctis";
46
+ if (un(d0) && un(d1) && dn(d2))
47
+ return "tristropha flexa";
48
+ if (dn(d0) && un(d1) && dn(d2))
49
+ return "pressus maior";
39
50
  return "compound";
40
51
  }
41
- default:
42
- if (up(dirs[0]) && dirs.slice(1).every((d) => dn(d)))
52
+ default: {
53
+ // The long forms: a head, then an unbroken descent. Only the head
54
+ // distinguishes them, and the descent must be total — a figure that
55
+ // turns again is a compound melisma, which at this length most are.
56
+ if (up(dirs[0]) && dirs.slice(1).every(dn))
43
57
  return "pes subpunctis";
58
+ if (up(dirs[0]) && up(dirs[1]) && dirs.slice(2).every(dn)) {
59
+ return "scandicus subpunctis";
60
+ }
61
+ if (dirs.slice(0, -1).every(un) && dn(dirs[dirs.length - 1])) {
62
+ return "tristropha flexa";
63
+ }
44
64
  return "compound";
65
+ }
45
66
  }
46
67
  }
47
68
  export function buildNeume(inputs, scala) {
@@ -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,10 +94,10 @@ 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
98
- the shelf is, and `listings - count` is 580 extra rows, over the 683 chants
99
- printed in more than one book. The breakdowns describe the same population
100
- `count` does, so `genera` and `modes` sum to it.
97
+ **`count` is the number of chants**, the one to quote. `listings` is how long
98
+ the shelf is: a melody printed in several books is stored once and listed under
99
+ each, so the shelf runs longer than the repertory. The breakdowns describe the
100
+ same population `count` does, so `genera` and `modes` sum to it.
101
101
 
102
102
  ```js
103
103
  tonus.corpus("am");
@@ -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,34 +295,34 @@ 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)
313
313
  and `nocturnale:` (470), the latter carrying the Nocturnale's own alphanumeric
314
314
  keys rather than numbers.
315
315
 
316
- Within tonus an id is exactly one chant. A melody printed in several books —
317
- 683 of them are — is stored once, under the record `cantus({ id })` returns, so
318
- `id` is a stable key to a chant rather than to a printing.
316
+ Within tonus an id is exactly one chant. A melody printed in several books is
317
+ stored once, under the record `cantus({ id })` returns, so `id` is a stable key
318
+ to a chant rather than to a printing.
319
319
 
320
320
  ## The repertoire as of a date — the era view
321
321
 
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
- **[Interactive demo →](https://jeffreypierce.github.io/tonus/)**
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