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.
- package/CHANGELOG.md +275 -1
- package/README.md +3 -3
- package/dist/engines/chant/attest.js +8 -8
- package/dist/engines/chant/hour.js +3 -3
- package/dist/engines/chant/intone.js +7 -1
- package/dist/engines/score/cadence.d.ts +10 -4
- package/dist/engines/score/cadence.js +10 -4
- package/dist/engines/score/emitters/accidentals.d.ts +9 -0
- package/dist/engines/score/emitters/accidentals.js +76 -13
- package/dist/engines/score/emitters/atramentum.js +1 -1
- package/dist/engines/score/emitters/breaking.d.ts +2 -2
- package/dist/engines/score/emitters/breaking.js +2 -2
- package/dist/engines/score/emitters/moderna.js +165 -17
- package/dist/engines/score/emitters/svg.d.ts +16 -0
- package/dist/engines/score/emitters/svg.js +19 -12
- package/dist/engines/score/emitters/tracks.js +29 -10
- package/dist/engines/score/ir.js +24 -4
- package/dist/engines/score/neume.d.ts +19 -0
- package/dist/engines/score/neume.js +35 -0
- package/dist/engines/score/parse.js +4 -1
- package/dist/engines/score/types.d.ts +6 -0
- package/dist/engines/temper/data/guido.js +4 -2
- package/dist/engines/temper/gabc.d.ts +12 -0
- package/dist/engines/temper/gabc.js +51 -18
- package/dist/engines/temper/neume.d.ts +1 -1
- package/dist/engines/temper/neume.js +24 -3
- package/docs/api/calendar.md +4 -4
- package/docs/api/census.md +20 -20
- package/docs/api/chant.md +27 -27
- package/docs/api/heavens.md +7 -7
- package/docs/api/index.md +5 -5
- package/docs/api/score.md +191 -138
- package/docs/api/tuning.md +31 -23
- 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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
};
|
|
21
|
+
// The staff-line slots the clefs anchor to, low to high: letters d/f/h/j per
|
|
22
|
+
// the Gregorio spec (2-line staff = a–i, 3-line = a–k, 4-line = a–m).
|
|
23
|
+
const LINE_SLOTS = [3, 5, 7, 9];
|
|
24
|
+
// Every clef GABC can declare, built from the same two rules rather than listed
|
|
25
|
+
// by hand: a c-clef puts "do" (MIDI 60) on its named line, an f-clef puts "fa"
|
|
26
|
+
// (MIDI 53) there. cN/fN for N in 1–4, plus the `b` variants (cbN/fbN) that
|
|
27
|
+
// additionally declare a B-flat key signature.
|
|
28
|
+
//
|
|
29
|
+
// This table used to hold six entries — c1–c4, f3, f4 — while `parse.ts`'s
|
|
30
|
+
// CLEF_OFFSETS held all sixteen. Two tables disagreeing about what a clef is,
|
|
31
|
+
// with the smaller one throwing "Unknown clef" on inputs the parser accepts
|
|
32
|
+
// happily. Deriving them removes the chance of a third disagreement.
|
|
33
|
+
const CLEFS = {};
|
|
34
|
+
for (let n = 1; n <= 4; n++) {
|
|
35
|
+
const doIdx = LINE_SLOTS[n - 1];
|
|
36
|
+
// An f-clef names fa's line, and fa is the 4th diatonic step (index 3), so
|
|
37
|
+
// "do" sits three slots below the named line — which is what doMidi 53
|
|
38
|
+
// (the F below middle C) is measured from.
|
|
39
|
+
CLEFS[`c${n}`] = { doMidi: 60, doIdx };
|
|
40
|
+
CLEFS[`cb${n}`] = { doMidi: 60, doIdx };
|
|
41
|
+
CLEFS[`f${n}`] = { doMidi: 53, doIdx };
|
|
42
|
+
CLEFS[`fb${n}`] = { doMidi: 53, doIdx };
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The GABC letter for a MIDI pitch under `clef` — with `x` appended when the
|
|
46
|
+
* pitch is a B-flat (`jx`, the flat sign then the note).
|
|
47
|
+
*
|
|
48
|
+
* B-flat is the ONE accidental chant sings — the b molle of the medieval gamut,
|
|
49
|
+
* the whole reason `parse.ts` carries a flat state machine and the `b` clefs
|
|
50
|
+
* exist. This function used to throw "is not diatonic" on it, which made the
|
|
51
|
+
* apparent inverse of `gabcToMidi` unable to spell a pitch the parser reads on
|
|
52
|
+
* every other page. Every other chromatic pitch class still throws: those are
|
|
53
|
+
* outside the gamut, and inventing a spelling for them would be a worse answer
|
|
54
|
+
* than refusing.
|
|
55
|
+
*/
|
|
34
56
|
export function midiToGabc(midi, clef = "c4") {
|
|
35
57
|
const def = CLEFS[clef];
|
|
36
58
|
if (!def)
|
|
37
59
|
throw new Error(`Unknown clef: ${clef}`);
|
|
38
|
-
const octave = Math.floor(midi / 12) - 1;
|
|
39
60
|
const pc = midi % 12;
|
|
40
|
-
|
|
61
|
+
// A flat is spelled on the staff slot of the natural ABOVE it: B-flat takes
|
|
62
|
+
// B's line with an `x`. Resolve to that natural, then mark the result.
|
|
63
|
+
const flat = pc === 10;
|
|
64
|
+
const natural = flat ? midi + 1 : midi;
|
|
65
|
+
const octave = Math.floor(natural / 12) - 1;
|
|
66
|
+
const diatIdx = DIATONIC.indexOf(natural % 12);
|
|
41
67
|
if (diatIdx === -1)
|
|
42
68
|
throw new Error(`MIDI note ${midi} (pc ${pc}) is not diatonic`);
|
|
43
69
|
const doOctave = Math.floor(def.doMidi / 12) - 1;
|
|
@@ -45,13 +71,19 @@ export function midiToGabc(midi, clef = "c4") {
|
|
|
45
71
|
const letter = LETTERS[staffPos];
|
|
46
72
|
if (!letter)
|
|
47
73
|
throw new Error(`MIDI ${midi} out of GABC range for clef ${clef}`);
|
|
48
|
-
return letter;
|
|
74
|
+
return flat ? `${letter}x` : letter;
|
|
49
75
|
}
|
|
50
76
|
export function gabcToMidi(letter, clef = "c4") {
|
|
51
77
|
const def = CLEFS[clef];
|
|
52
78
|
if (!def)
|
|
53
79
|
throw new Error(`Unknown clef: ${clef}`);
|
|
54
|
-
|
|
80
|
+
// Accept the `x` that `midiToGabc` emits, so the two stay inverses. A flat
|
|
81
|
+
// lowers the natural it marks by a semitone; only B carries one in the gamut,
|
|
82
|
+
// but the arithmetic is written once for whatever letter arrives.
|
|
83
|
+
const raw = letter.toLowerCase();
|
|
84
|
+
const flat = raw.endsWith("x");
|
|
85
|
+
const bare = flat ? raw.slice(0, -1) : raw;
|
|
86
|
+
const staffPos = LETTERS.indexOf(bare);
|
|
55
87
|
if (staffPos === -1)
|
|
56
88
|
throw new Error(`Unknown GABC letter: ${letter}`);
|
|
57
89
|
// Diatonic steps from "do", split into whole octaves (÷7) and the step within
|
|
@@ -61,7 +93,8 @@ export function gabcToMidi(letter, clef = "c4") {
|
|
|
61
93
|
const octOffset = Math.floor(stepsFromDo / 7);
|
|
62
94
|
const diatStep = ((stepsFromDo % 7) + 7) % 7;
|
|
63
95
|
const doOctave = Math.floor(def.doMidi / 12) - 1;
|
|
64
|
-
|
|
96
|
+
const natural = (doOctave + octOffset + 1) * 12 + DIATONIC[diatStep];
|
|
97
|
+
return flat ? natural - 1 : natural;
|
|
65
98
|
}
|
|
66
99
|
export function pcToGabc(pc, clef = "c4", oct = 0) {
|
|
67
100
|
const def = CLEFS[clef];
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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) {
|
package/docs/api/calendar.md
CHANGED
|
@@ -108,7 +108,7 @@ The feast returned **carries the view** (`feast.before`), and every chant
|
|
|
108
108
|
verb reads it back: `proprium`, `ordinarium`, and `officium`
|
|
109
109
|
serve only chants attested by the same year, without being told the year
|
|
110
110
|
twice. One `before` at the calendar door views the whole day. The chant side
|
|
111
|
-
|
|
111
|
+
(what "attested" means, and what a slot the view excludes does) is in
|
|
112
112
|
[chant.md](chant.md#the-repertoire-as-of-a-date--the-era-view).
|
|
113
113
|
|
|
114
114
|
```ts
|
|
@@ -142,12 +142,12 @@ interface Feast {
|
|
|
142
142
|
}
|
|
143
143
|
```
|
|
144
144
|
|
|
145
|
-
The `masses` list is derived from the Kyriale's own printed rubric
|
|
145
|
+
The `masses` list is derived from the Kyriale's own printed rubric, one
|
|
146
146
|
category per mass, by RANK: "In Paschal Time", "For feasts of the I class",
|
|
147
147
|
"For Sundays throughout the Year", "For ferias". A day resolves to exactly
|
|
148
148
|
one rubric (a BVM feast is "of the Blessed Virgin" even in Paschaltide),
|
|
149
149
|
and the masses carrying that rubric are the masses it may sing, in the
|
|
150
|
-
book's own numbering
|
|
150
|
+
book's own numbering. Where a rubric names several (II class 1–5), that
|
|
151
151
|
numbering is the book's invitation to choose, and `ordinarium` rotates
|
|
152
152
|
among them by year. The book's per-mass nicknames (_Orbis factor_ for
|
|
153
153
|
Sundays, and so on) record customary use, which disagrees with the rubric for 9
|
|
@@ -211,7 +211,7 @@ derived from the date; overflow entries, such as the Epiphany weeks
|
|
|
211
211
|
resumed before Septuagesima, take the season of the day they fall on.
|
|
212
212
|
|
|
213
213
|
Season drives real liturgy in the ordinary: in the penitential seasons
|
|
214
|
-
(`adv`, `quadp`, `quad`) the Gloria is omitted, and the Ite with it
|
|
214
|
+
(`adv`, `quadp`, `quad`) the Gloria is omitted, and the Ite with it. The
|
|
215
215
|
Benedicamus dismissal appears only where the selected mass carries a
|
|
216
216
|
setting ([chant.md](chant.md#the-ordinary--ordinarium)).
|
|
217
217
|
|
package/docs/api/census.md
CHANGED
|
@@ -20,7 +20,7 @@ is, where it is unusual, and what it is near.
|
|
|
20
20
|
tonus.census({ id: "gregobase:1210" });
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
Everything comes back in one call
|
|
23
|
+
Everything comes back in one call: profile, balance, neighbors.
|
|
24
24
|
|
|
25
25
|
```js
|
|
26
26
|
{
|
|
@@ -56,7 +56,7 @@ interface CensusQuery {
|
|
|
56
56
|
}
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
The census covers the **2,187 chants tonus ships
|
|
59
|
+
The census covers the **2,187 chants tonus ships**, the same population
|
|
60
60
|
`cantus({ id })` addresses, one block per chant. An id with no block throws
|
|
61
61
|
rather than returning an empty answer, because a silent nothing reads as "this
|
|
62
62
|
chant is unlike everything," which is a different claim.
|
|
@@ -78,24 +78,24 @@ what they describe:
|
|
|
78
78
|
| `textual` | 7 | vowel distribution by sung duration, accent rate, melisma mean |
|
|
79
79
|
|
|
80
80
|
Four more fields ride in the block and are **not** similarity dimensions:
|
|
81
|
-
`flags` (a bitfield), `attest` (dating
|
|
81
|
+
`flags` (a bitfield), `attest` (dating, which is what `before` reads),
|
|
82
82
|
`extras`, and `reserve`. `by` will not accept them.
|
|
83
83
|
|
|
84
84
|
## How the measurement works
|
|
85
85
|
|
|
86
|
-
Every number in a block reads off a single `notatio()` parse
|
|
87
|
-
`score` gives you
|
|
86
|
+
Every number in a block reads off a single `notatio()` parse (the same parse
|
|
87
|
+
`score` gives you), so the census can never disagree with the library about
|
|
88
88
|
what a chant is.
|
|
89
89
|
|
|
90
90
|
Each float is a named measurement, not a learned one: time spent on the
|
|
91
91
|
subfinal, how often a rising second follows a falling third. When the census
|
|
92
92
|
calls two chants near, the profile says in what respect.
|
|
93
93
|
|
|
94
|
-
Most groups are normalized to sum to one, so a group holds a distribution
|
|
95
|
-
where the melody's time goes, not how much of it there is
|
|
94
|
+
Most groups are normalized to sum to one, so a group holds a distribution:
|
|
95
|
+
where the melody's time goes, not how much of it there is. Length is not a
|
|
96
96
|
similarity. The trigram and cadence groups count against dictionaries mined
|
|
97
|
-
from the corpus itself
|
|
98
|
-
one bucket for the rest
|
|
97
|
+
from the corpus itself (its commonest motifs, its commonest closing gestures,
|
|
98
|
+
one bucket for the rest), so the corpus supplies the vocabulary and the chant
|
|
99
99
|
supplies the usage.
|
|
100
100
|
|
|
101
101
|
The reference is the mean block over all 2,187 chants, group by group. Because
|
|
@@ -105,15 +105,15 @@ divided by their count, are the season's mean profile in the same 221 slots.
|
|
|
105
105
|
## Distance is cosine per field group
|
|
106
106
|
|
|
107
107
|
**This is a contract, not an implementation note.** The census answers about
|
|
108
|
-
one chant at a time
|
|
109
|
-
manuscript"
|
|
108
|
+
one chant at a time. Grouping ("all Communions," "this season," "this
|
|
109
|
+
manuscript") is yours to do. The moment you pool blocks yourself you are
|
|
110
110
|
computing a distance, and if you compute it differently from the rule below
|
|
111
111
|
your numbers will not agree with `census()`'s. Nothing will error.
|
|
112
112
|
|
|
113
113
|
The rule, in three lines:
|
|
114
114
|
|
|
115
115
|
1. Cosine **per field group**, never over the flat 221.
|
|
116
|
-
2. `by: "all"` is the **equal-weight mean** of the per-group cosines
|
|
116
|
+
2. `by: "all"` is the **equal-weight mean** of the per-group cosines: every
|
|
117
117
|
dimension one vote, no tunable weights.
|
|
118
118
|
3. Ties break to the lower id, so the same question always has the same answer.
|
|
119
119
|
|
|
@@ -122,7 +122,7 @@ sheer magnitude, so a long Tract would neighbor other long chants for being
|
|
|
122
122
|
long. Per-group cosine asks about **shape within each dimension**.
|
|
123
123
|
|
|
124
124
|
[`CENSUS_GROUPS`](index.md#the-appendix) gives you the group names and their
|
|
125
|
-
field counts, and [`CENSUS_ORDER`](index.md#the-appendix) every censused id
|
|
125
|
+
field counts, and [`CENSUS_ORDER`](index.md#the-appendix) every censused id,
|
|
126
126
|
so you can pool a set without guessing at either.
|
|
127
127
|
|
|
128
128
|
### Reading the numbers
|
|
@@ -140,7 +140,7 @@ per-group version spreads from about 0.85 down to 0.65. That compression comes
|
|
|
140
140
|
from one wide block outvoting the other eight.
|
|
141
141
|
|
|
142
142
|
**`before` filters before ranking.** It restricts the candidate pool, then
|
|
143
|
-
ranks
|
|
143
|
+
ranks, so `k` stays satisfiable, and a filtered list is *not* a subset of the
|
|
144
144
|
unfiltered one. Chants that were ranked out by later material rise into it.
|
|
145
145
|
Typicality is unaffected: it is always measured against the whole shipped
|
|
146
146
|
corpus (see [Profile and typicality](#profile-and-typicality)).
|
|
@@ -201,7 +201,7 @@ const ranked = ids
|
|
|
201
201
|
|
|
202
202
|
The per-group breakdown is where the answer becomes legible. _Quinque
|
|
203
203
|
prudentes_ leads on `textual`, `cadenceMedial` and `trigram`, at about 0.99 on
|
|
204
|
-
each
|
|
204
|
+
each (it sets its text and turns its phrases the way Communions do), while its
|
|
205
205
|
`cadenceFinal` is only about 0.82, so the one thing it does unlike a typical
|
|
206
206
|
Communion is end. A chant is typical of its genus in some dimensions and not
|
|
207
207
|
others.
|
|
@@ -213,8 +213,8 @@ Each group's `typicality` is its cosine against the corpus mean for that group:
|
|
|
213
213
|
"unlike the rest."
|
|
214
214
|
|
|
215
215
|
The two numbers above are a fair illustration. _Ab occultis meis_ is a mode-2
|
|
216
|
-
Gradual whose `modal` typicality is about 0.99
|
|
217
|
-
mode-2 chant
|
|
216
|
+
Gradual whose `modal` typicality is about 0.99, so modally it is a typical
|
|
217
|
+
mode-2 chant. Its `melodic` typicality is about 0.70, because its
|
|
218
218
|
interval
|
|
219
219
|
vocabulary is its own. One chant can be conventional in one dimension and
|
|
220
220
|
distinctive in another, which is the reason the groups are kept apart.
|
|
@@ -233,7 +233,7 @@ balance: { distance: 0.091, deviantGroups: ["degreeHist", "melodic"] }
|
|
|
233
233
|
the corpus mean, 1 has nothing in common with it.
|
|
234
234
|
|
|
235
235
|
`deviantGroups` names where a chant is unusual **relative to its own mean**,
|
|
236
|
-
most deviant first
|
|
236
|
+
most deviant first, not against an absolute threshold. The question it answers
|
|
237
237
|
is "given how typical this chant is overall, where does it depart from
|
|
238
238
|
itself?", which is what makes the answer legible for a chant that is unusual
|
|
239
239
|
everywhere or nowhere.
|
|
@@ -273,12 +273,12 @@ tonus.census({ id: "gregobase:1210", before: 1100 });
|
|
|
273
273
|
```
|
|
274
274
|
|
|
275
275
|
Restricts neighbors to chants a manuscript of the 11th century or earlier
|
|
276
|
-
already holds
|
|
276
|
+
already holds, 1,790 of the 2,186 candidates. This is the same rule as
|
|
277
277
|
[`cantus({ before })`](chant.md#the-repertoire-as-of-a-date--the-era-view),
|
|
278
278
|
through the same admissibility door: **evidence, not existence**, so a chant
|
|
279
279
|
with no dated witness is excluded rather than assumed old.
|
|
280
280
|
|
|
281
|
-
The seed chant itself is never filtered
|
|
281
|
+
The seed chant itself is never filtered, because you asked about it by name.
|
|
282
282
|
|
|
283
283
|
## What the census is not
|
|
284
284
|
|
package/docs/api/chant.md
CHANGED
|
@@ -69,7 +69,7 @@ those marks too.
|
|
|
69
69
|
The corpus is **assignment-driven**: a chant ships when some day of the
|
|
70
70
|
liturgical year calls for it. The calendar is walked year by year until it stops
|
|
71
71
|
finding new assignments (39 years, in the event), and what it never reaches is
|
|
72
|
-
not shipped
|
|
72
|
+
not shipped: 10,156 book chants become 2,187.
|
|
73
73
|
|
|
74
74
|
Everything here answers "what was sung on this day". A query for a chant the
|
|
75
75
|
calendar never calls for returns nothing.
|
|
@@ -77,11 +77,11 @@ calendar never calls for returns nothing.
|
|
|
77
77
|
## The books — `corpus`
|
|
78
78
|
|
|
79
79
|
`corpus(code)` returns one book's bibliographic identity and a breakdown of what
|
|
80
|
-
it holds
|
|
80
|
+
it holds: how many chants, in what genres, in what modes. `corpus({ book })` is
|
|
81
81
|
the same question in the query form every other verb uses; both spellings return
|
|
82
82
|
one answer.
|
|
83
83
|
|
|
84
|
-
`corpus()` with no argument returns **the whole shelf
|
|
84
|
+
`corpus()` with no argument returns **the whole shelf**, the rollup plus every
|
|
85
85
|
book's ledger:
|
|
86
86
|
|
|
87
87
|
```js
|
|
@@ -94,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
|
|
98
|
-
the shelf is
|
|
99
|
-
|
|
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
|
|
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
|
|
298
|
+
is the verb, and it applies the Kyriale's own rubrics. This is flat retrieval.
|
|
299
299
|
|
|
300
300
|
### On chant ids
|
|
301
301
|
|
|
302
|
-
An id's prefix names **the catalogue the identifier came from
|
|
302
|
+
An id's prefix names **the catalogue the identifier came from**, not the book
|
|
303
303
|
the chant is printed in, and not a claim about who the melody belongs to. A
|
|
304
304
|
chant carrying `gregobase:1210` is a Solesmes book chant that GregoBase happens
|
|
305
305
|
to have catalogued; the corpus is assembled from ten books, and GregoBase is
|
|
306
306
|
one source among several.
|
|
307
307
|
|
|
308
308
|
The prefix is therefore **not a namespace you can query against**. GregoBase
|
|
309
|
-
holds 18,148 chants; tonus ships 1,717 of them
|
|
309
|
+
holds 18,148 chants; tonus ships 1,717 of them (9.5%), because the corpus is
|
|
310
310
|
assignment-driven, so an id copied from the GregoBase site will usually return
|
|
311
311
|
`[]` here. That is not a lookup failure; it means no day of the calendar calls
|
|
312
312
|
for that chant. The two prefixes in the shipped corpus are `gregobase:` (1,717)
|
|
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
|
-
|
|
318
|
-
|
|
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"
|
|
325
|
+
is attested by then," never "what existed then." A chant with no dated
|
|
326
326
|
witness is excluded rather than assumed old. CANTUS dates only to the century,
|
|
327
327
|
so a year admits the centuries that have CLOSED before it (`before: 1098` →
|
|
328
328
|
through the 900s).
|
|
@@ -339,7 +339,7 @@ tonus.ordinarium({ feast: easter }); // the ordinary the view attests
|
|
|
339
339
|
```
|
|
340
340
|
|
|
341
341
|
What happens to a slot the view excludes differs by verb, on the rubric's
|
|
342
|
-
own logic: `ordinarium` **re-picks
|
|
342
|
+
own logic: `ordinarium` **re-picks**, because the Kyriale offers ranked
|
|
343
343
|
alternatives by design, so the rotation runs over the admissible pool and
|
|
344
344
|
the day still sings. `proprium` and `officium` have no pool
|
|
345
345
|
of alternatives, so an excluded chant **falls silent**. A `before` given to a
|
|
@@ -371,15 +371,15 @@ interface PropriumQuery extends CantusQuery {
|
|
|
371
371
|
## The ordinary — `ordinarium`
|
|
372
372
|
|
|
373
373
|
`ordinarium(query?)` retrieves the fixed chants of the Mass from the
|
|
374
|
-
Kyriale. A feast drives mass selection through its `masses` list
|
|
374
|
+
Kyriale. A feast drives mass selection through its `masses` list, the
|
|
375
375
|
masses the day's Kyriale RUBRIC appoints, derived as described in
|
|
376
376
|
[calendar.md](calendar.md#the-days-feasts--festum); `mass` pins a kyriale
|
|
377
377
|
number directly. Where the rubric names several masses, the year rotates
|
|
378
378
|
through them (same feast, same year → same answer, every time), and sibling
|
|
379
379
|
printings under one number (Mass I prints two dismissals; Mass XVII prints
|
|
380
380
|
Kyrie A/B/C) rotate with it. Slots resolve independently, which the book
|
|
381
|
-
licenses outright
|
|
382
|
-
from others"
|
|
381
|
+
licenses outright ("chants from one Mass may be used together with those
|
|
382
|
+
from others") with one exception, the book's own: **"the Ferial Masses
|
|
383
383
|
excepted."** Under a ferial rubric the sung ordinary is not gathered from
|
|
384
384
|
several masses; only the dismissal travels.
|
|
385
385
|
|
|
@@ -397,10 +397,10 @@ tonus.ordinarium({ feast: easter });
|
|
|
397
397
|
|
|
398
398
|
The **Gloria follows the day's rank rubric, not its season**: the ferial
|
|
399
399
|
masses print none (XVI, XVIII) and the penitential-Sunday mass none (XVII),
|
|
400
|
-
while a I-class feast inside Advent or Lent
|
|
400
|
+
while a I-class feast inside Advent or Lent (the Immaculate Conception)
|
|
401
401
|
keeps its Gloria. At a Gloria-less Mass the dismissal is the Benedicamus
|
|
402
402
|
Domino, and a mass with no dismissal of its own borrows one exactly as the
|
|
403
|
-
book directs: "Benedicamus Domino **as in Mass II
|
|
403
|
+
book directs: "Benedicamus Domino **as in Mass II**," so a green feria
|
|
404
404
|
sings Mass XVI whole with the Mass II Benedicamus. The ad libitum appendix
|
|
405
405
|
is a **solemnity boost**, reachable only under the festal rubrics (it takes
|
|
406
406
|
its turn in the rotation once every _n + 1_ years); it never reaches a
|
|
@@ -475,7 +475,7 @@ seasonal ordo and returned in liturgical order. With no feast, each resolves
|
|
|
475
475
|
for the [default epoch](index.md#dates).
|
|
476
476
|
|
|
477
477
|
**Matins is returned flat.** The night office answers like any other hour,
|
|
478
|
-
its responsories drawn from the Nocturnale Romanum (`nr`)
|
|
478
|
+
its responsories drawn from the Nocturnale Romanum (`nr`). But the
|
|
479
479
|
three-nocturn, twelve-psalm division is not modelled: the chants are right,
|
|
480
480
|
their grouping into nocturns is not expressed.
|
|
481
481
|
|
|
@@ -492,7 +492,7 @@ interface OfficiumQuery extends CantusQuery {
|
|
|
492
492
|
}
|
|
493
493
|
```
|
|
494
494
|
|
|
495
|
-
The eight hours ship as [`HORAE`](index.md#the-appendix), Matins first
|
|
495
|
+
The eight hours ship as [`HORAE`](index.md#the-appendix), Matins first. Read
|
|
496
496
|
them from there rather than transcribing them, and an unrecognised `hora`
|
|
497
497
|
throws rather than matching nothing, so a misspelling cannot read as an empty
|
|
498
498
|
hour.
|
|
@@ -507,9 +507,9 @@ tonus.officium({ hora: "vespers" }); // throws: unknown hora "vespers"
|
|
|
507
507
|
|
|
508
508
|
### One cursus, the Benedictine
|
|
509
509
|
|
|
510
|
-
tonus assembles a single office
|
|
510
|
+
tonus assembles a single office (the monastic cursus) with no option to
|
|
511
511
|
choose another. The chants come from the Antiphonale Monasticum (`am`) and its
|
|
512
|
-
companions; the psalmody follows the Benedictine distribution
|
|
512
|
+
companions; the psalmody follows the Benedictine distribution, the little
|
|
513
513
|
hours take the gradual psalms (Terce 119–121, Sext 122–124, None 125–127),
|
|
514
514
|
with Sunday and Monday walking their portions of Ps 118 instead; Prime walks
|
|
515
515
|
Pss 1–19 across the week (Sunday opens Ps 118); and Compline is the fixed
|
|
@@ -530,7 +530,7 @@ the opening formula is included, as it is for a psalm's first verse.
|
|
|
530
530
|
mediant, as a psalm sung without an antiphon; `solemn` uses a tone's
|
|
531
531
|
ornamented mediant where it has one. Canticles are addressed by name:
|
|
532
532
|
`benedictus`, `magnificat`, `nunc dimittis`, `benedicite`. (The Te Deum
|
|
533
|
-
is not psalmody
|
|
533
|
+
is not psalmody: it carries its own melody and is not addressable here.)
|
|
534
534
|
|
|
535
535
|
```js
|
|
536
536
|
tonus.psalmus({ psalm: 109, verse: "1a", mode: 1 });
|
package/docs/api/heavens.md
CHANGED
|
@@ -168,12 +168,12 @@ and moves with precession; a table carrying "March 21" would be wrong for most
|
|
|
168
168
|
of the period this library models, and wrong differently every century.
|
|
169
169
|
|
|
170
170
|
**What is omitted, and why.** The exaltation degrees Ptolemy gives (the Sun at
|
|
171
|
-
19° Arietis and the rest) are not carried
|
|
172
|
-
here reads. Nor are the lunar nodes' exaltations, because the nodes are not
|
|
171
|
+
19° Arietis and the rest) are not carried, because the sign is the resolution
|
|
172
|
+
anything here reads. Nor are the lunar nodes' exaltations, because the nodes are not
|
|
173
173
|
tonus bodies. Five signs exalt nobody: that silence is the tradition's, not a
|
|
174
174
|
gap in the table.
|
|
175
175
|
|
|
176
|
-
The `melothesia` is the *homo signorum* of medieval calendars
|
|
176
|
+
The `melothesia` is the *homo signorum* of medieval calendars, Aries at the
|
|
177
177
|
head down to Pisces at the feet. It was practice, not decoration: phlebotomy
|
|
178
178
|
was timed against it, and while the Moon stood in a sign its member was not to
|
|
179
179
|
be touched. Sourced from Ptolemy's *Tetrabiblos* I.17 and I.19
|
|
@@ -216,7 +216,7 @@ The doctrinae:
|
|
|
216
216
|
Sphere pitches are computed directly from the doctrina's pure ratios,
|
|
217
217
|
anchored at the temperamentum's A4, so historical coherence holds:
|
|
218
218
|
`temperamentum("ptolemy-intense")` with `harmonia({ doctrina: "ptolemy" })`
|
|
219
|
-
gives pure Ptolemaic intervals throughout
|
|
219
|
+
gives pure Ptolemaic intervals throughout: Sun→Jupiter a pure 3/2,
|
|
220
220
|
Sun→Saturn a pure 2/1. The temperamentum's scale governs pitch naming and
|
|
221
221
|
the imprint.
|
|
222
222
|
|
|
@@ -322,7 +322,7 @@ h.tabula.find((r) => r.name === "Jupiter");
|
|
|
322
322
|
```
|
|
323
323
|
|
|
324
324
|
`ratio` is the doctrina's own fraction against the mese, the primary datum of
|
|
325
|
-
the whole scheme
|
|
325
|
+
the whole scheme. `spn` and `hz` are that ratio sounded against A4, not
|
|
326
326
|
independent claims. Comparing doctrinae means comparing these: the table
|
|
327
327
|
under [Theory & Context](#theory--context) is what the field returns.
|
|
328
328
|
|
|
@@ -364,7 +364,7 @@ The doctrina ratios are reconstructed from the primary texts through
|
|
|
364
364
|
Joscelyn Godwin's syntheses, mapping each body to a Greek tone-name and
|
|
365
365
|
deriving its ratio by Pythagorean interval arithmetic normalized to the
|
|
366
366
|
mese (Sun = 1/1). The full method, the taxonomy, and the decisions taken
|
|
367
|
-
along the way are documented at the data
|
|
367
|
+
along the way are documented at the data. See `DOCTRINAE` in
|
|
368
368
|
[`harmonia/data/doctrines.ts`](https://github.com/jeffreypierce/tonus/blob/main/src/engines/harmonia/data/doctrines.ts).
|
|
369
369
|
The same arithmetic is laid out from the tuning side in
|
|
370
370
|
[tuning.md](tuning.md#theory--context).
|
|
@@ -385,7 +385,7 @@ The resulting ratios, by sphere from the outermost:
|
|
|
385
385
|
|
|
386
386
|
The single pitch separating Pythagoras from Boethius is Venus: a whole
|
|
387
387
|
tone above the Sun in the disjunct system (9/8, B durum), a semitone in
|
|
388
|
-
the conjunct (256/243, B molle)
|
|
388
|
+
the conjunct (256/243, B molle). This is the origin of the durum/molle
|
|
389
389
|
distinction that runs through all of medieval music theory.
|
|
390
390
|
|
|
391
391
|
## Sources
|
package/docs/api/index.md
CHANGED
|
@@ -4,7 +4,7 @@ The technical center of tonus: the full public API, the conventions every method
|
|
|
4
4
|
obeys, and the error contract. The API is **fourteen methods on the `tonus`
|
|
5
5
|
namespace**, no sub-namespaces.
|
|
6
6
|
|
|
7
|
-
**[Interactive demo
|
|
7
|
+
**[Interactive demo](https://orreliquum.com/)**
|
|
8
8
|
|
|
9
9
|
```js
|
|
10
10
|
import tonus from "tonus";
|
|
@@ -123,7 +123,7 @@ is for.
|
|
|
123
123
|
| `CENSUS_GROUPS` | the field groups → `{ offset, count }`; the keys are the valid `by:` values **and** the `profile` keys |
|
|
124
124
|
| `CENSUS_ORDER` | every censused chant id, in block order — so membership is a lookup, not a `try/catch` |
|
|
125
125
|
|
|
126
|
-
Use these to pool blocks without reproducing the distance rule
|
|
126
|
+
Use these to pool blocks without reproducing the distance rule. See [the census
|
|
127
127
|
contract](census.md#distance-is-cosine-per-field-group).
|
|
128
128
|
|
|
129
129
|
## Full contents
|
|
@@ -218,7 +218,7 @@ Other fields carry only one register. Latin-only, _e.g._ `genus`, `ordinarium`,
|
|
|
218
218
|
`incipit`, `differentia`, `accentus`. English-only, _e.g._ `date`, `velocity`, `hz`.
|
|
219
219
|
|
|
220
220
|
Display strings live in exported maps (_e.g._ `SEASON_LABEL`),
|
|
221
|
-
never as label fields on objects
|
|
221
|
+
never as label fields on objects. The maps are [the appendix](#the-appendix).
|
|
222
222
|
|
|
223
223
|
### Dates
|
|
224
224
|
|
|
@@ -238,14 +238,14 @@ an ensemble) it is seeded, so the same seed yields byte-identical output.
|
|
|
238
238
|
|
|
239
239
|
### Error contract
|
|
240
240
|
|
|
241
|
-
- Query functions return `[]` on no match, never throw
|
|
241
|
+
- Query functions return `[]` on no match, never throw. But an **empty or
|
|
242
242
|
unknown-key query** throws (a mistyped filter is a bug, not an empty result):
|
|
243
243
|
`festum({ month: 12 })` and `cantus({})` throw rather than silently resolving a
|
|
244
244
|
plausible-looking answer.
|
|
245
245
|
- Builder functions throw `Error` with a descriptive message on invalid input.
|
|
246
246
|
- `notatio` throws on invalid `Chant` input.
|
|
247
247
|
- `inscriptio` throws on a non-`Score` argument or an unknown notation species.
|
|
248
|
-
- `temperamentum.tonus()` throws if `mode` is `"auto"
|
|
248
|
+
- `temperamentum.tonus()` throws if `mode` is `"auto"`. Mode must be set
|
|
249
249
|
explicitly.
|
|
250
250
|
- Malformed `comma`, ratio, or Scala input throws `RangeError`; custom scales
|
|
251
251
|
must supply 7 or 12 steps, beginning at `1/1` (a degree list) or ending at
|