tonus 0.1.8 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BIBLIOGRAPHY.md +132 -108
- package/CHANGELOG.md +598 -1
- package/LICENSE +133 -29
- package/README.md +106 -83
- package/dist/data/am.js +2666 -11196
- package/dist/data/ams.d.ts +5 -0
- package/dist/data/ams.js +122 -0
- package/dist/data/attestation.d.ts +19 -0
- package/dist/data/attestation.js +15716 -0
- package/dist/data/attestation.json +15711 -0
- package/dist/data/cadentiae.d.ts +43 -0
- package/dist/data/cadentiae.js +174 -0
- package/dist/data/cal.js +48 -0
- package/dist/data/census.d.ts +12 -0
- package/dist/data/census.js +36 -0
- package/dist/data/commune-office.d.ts +4 -0
- package/dist/data/commune-office.js +2371 -0
- package/dist/data/commune-office.json +2365 -0
- package/dist/data/commune.js +181 -11
- package/dist/data/corpus-overlap.d.ts +17 -0
- package/dist/data/corpus-overlap.js +299 -5
- package/dist/data/cot.d.ts +5 -0
- package/dist/data/cot.js +172 -0
- package/dist/data/cse.d.ts +5 -0
- package/dist/data/cse.js +122 -0
- package/dist/data/gabc-glyphs.d.ts +45 -0
- package/dist/data/gabc-glyphs.js +122 -0
- package/dist/data/gr.js +754 -6394
- package/dist/data/kyriale.js +116 -116
- package/dist/data/la.js +799 -13419
- package/dist/data/lh.js +113 -3473
- package/dist/data/lu.js +931 -17631
- package/dist/data/nocturnale-romanum.js +1659 -10411
- package/dist/data/office-ferial.d.ts +4 -0
- package/dist/data/office-ferial.js +396 -0
- package/dist/data/office-ferial.json +391 -0
- package/dist/data/office-monastic.d.ts +17 -1
- package/dist/data/office-monastic.js +1403 -466
- package/dist/data/office-psalms-monastic.d.ts +13 -1
- package/dist/data/office-psalms-monastic.js +9 -0
- package/dist/data/propers.js +1 -1
- package/dist/data/psalms.js +22919 -5
- package/dist/data/psm.d.ts +5 -0
- package/dist/data/psm.js +122 -0
- package/dist/data/seasonal-respbreve.d.ts +5 -0
- package/dist/data/seasonal-respbreve.js +41 -0
- package/dist/data/seasonal-respbreve.json +35 -0
- package/dist/data/smufl-glyphs.d.ts +17 -0
- package/dist/data/smufl-glyphs.js +1546 -0
- package/dist/data/smufl-glyphs.json +1530 -0
- package/dist/engines/cal/calendar.d.ts +3 -2
- package/dist/engines/cal/calendar.js +105 -29
- package/dist/engines/cal/data/eras.d.ts +35 -0
- package/dist/engines/cal/data/eras.js +128 -0
- package/dist/engines/cal/date.js +44 -0
- package/dist/engines/cal/types.d.ts +15 -3
- package/dist/engines/cal/types.js +5 -5
- package/dist/engines/census/census.d.ts +7 -0
- package/dist/engines/census/census.js +179 -0
- package/dist/engines/census/types.d.ts +55 -0
- package/dist/engines/census/types.js +8 -0
- package/dist/engines/chant/attest.d.ts +39 -0
- package/dist/engines/chant/attest.js +90 -0
- package/dist/engines/chant/chant.d.ts +16 -4
- package/dist/engines/chant/chant.js +220 -30
- package/dist/engines/chant/data/compline.js +2 -1
- package/dist/engines/chant/data/masses.d.ts +56 -4
- package/dist/engines/chant/data/masses.js +305 -80
- package/dist/engines/chant/data/prime.js +1 -1
- package/dist/engines/chant/hour.js +279 -58
- package/dist/engines/chant/ordinary.d.ts +2 -0
- package/dist/engines/chant/ordinary.js +336 -56
- package/dist/engines/chant/propers.js +55 -5
- package/dist/engines/chant/psalm.d.ts +4 -4
- package/dist/engines/chant/psalm.js +25 -11
- package/dist/engines/chant/syllabify.d.ts +1 -0
- package/dist/engines/chant/syllabify.js +90 -17
- package/dist/engines/chant/types.d.ts +115 -12
- package/dist/engines/chant/types.js +38 -3
- package/dist/engines/harmonia/api.js +4 -0
- package/dist/engines/harmonia/data/doctrines.js +3 -1
- package/dist/engines/harmonia/tabula.d.ts +3 -0
- package/dist/engines/harmonia/tabula.js +1 -0
- package/dist/engines/harmonia/voice.d.ts +4 -0
- package/dist/engines/harmonia/voice.js +8 -4
- package/dist/engines/imprint.js +14 -1
- package/dist/engines/planet/orbital.js +4 -4
- package/dist/engines/planet/planet.d.ts +10 -0
- package/dist/engines/planet/planet.js +30 -3
- package/dist/engines/planet/position.js +13 -10
- package/dist/engines/planet/types.d.ts +1 -0
- package/dist/engines/score/api.d.ts +2 -13
- package/dist/engines/score/api.js +21 -8
- package/dist/engines/score/articulation.js +2 -2
- package/dist/engines/score/cadence.d.ts +76 -0
- package/dist/engines/score/cadence.js +96 -0
- package/dist/engines/score/emitters/accidentals.d.ts +21 -0
- package/dist/engines/score/emitters/accidentals.js +88 -0
- package/dist/engines/score/emitters/atramentum.d.ts +107 -0
- package/dist/engines/score/emitters/atramentum.js +239 -0
- package/dist/engines/score/emitters/breaking.d.ts +62 -0
- package/dist/engines/score/emitters/breaking.js +80 -0
- package/dist/engines/score/emitters/moderna.d.ts +38 -0
- package/dist/engines/score/emitters/moderna.js +612 -0
- package/dist/engines/score/emitters/svg.d.ts +143 -0
- package/dist/engines/score/emitters/svg.js +1328 -0
- package/dist/engines/score/emitters/tracks.d.ts +104 -0
- package/dist/engines/score/emitters/tracks.js +728 -0
- package/dist/engines/score/infer.d.ts +3 -3
- package/dist/engines/score/infer.js +2 -2
- package/dist/engines/score/inscriptio.d.ts +69 -0
- package/dist/engines/score/inscriptio.js +138 -0
- package/dist/engines/score/ir.d.ts +2 -2
- package/dist/engines/score/ir.js +59 -12
- package/dist/engines/score/lyric.d.ts +23 -0
- package/dist/engines/score/lyric.js +234 -0
- package/dist/engines/score/meta.d.ts +2 -2
- package/dist/engines/score/modulation.d.ts +12 -0
- package/dist/engines/score/modulation.js +49 -0
- package/dist/engines/score/neume.js +35 -4
- package/dist/engines/score/parse.js +150 -9
- package/dist/engines/score/phrasing.js +4 -3
- package/dist/engines/score/prosody.d.ts +38 -0
- package/dist/engines/score/prosody.js +70 -6
- package/dist/engines/score/tabula.d.ts +37 -5
- package/dist/engines/score/tabula.js +18 -0
- package/dist/engines/score/types.d.ts +88 -1
- package/dist/engines/temper/api.d.ts +4 -1
- package/dist/engines/temper/api.js +28 -5
- package/dist/engines/temper/data/guido.js +6 -2
- package/dist/engines/temper/data/modes.d.ts +6 -0
- package/dist/engines/temper/data/modes.js +42 -0
- package/dist/engines/temper/data/tones.d.ts +1 -1
- package/dist/engines/temper/data/tones.js +20 -11
- package/dist/engines/temper/interval.js +4 -3
- package/dist/engines/temper/modality.d.ts +11 -2
- package/dist/engines/temper/modality.js +74 -2
- package/dist/engines/temper/modes.d.ts +1 -1
- package/dist/engines/temper/pitch.d.ts +1 -1
- package/dist/engines/temper/pitch.js +12 -2
- package/dist/engines/temper/scale.d.ts +53 -0
- package/dist/engines/temper/scale.js +107 -8
- package/dist/index.d.ts +26 -8
- package/dist/index.js +37 -4
- package/docs/api/calendar.md +279 -0
- package/docs/api/census.md +288 -0
- package/docs/api/chant.md +657 -0
- package/docs/api/heavens.md +346 -0
- package/docs/api/index.md +263 -0
- package/docs/api/score.md +873 -0
- package/docs/api/tuning.md +619 -0
- package/package.json +11 -5
- package/dist/data/office-matins-roman.d.ts +0 -19
- package/dist/data/office-matins-roman.js +0 -4383
- package/dist/data/office-psalms-roman.d.ts +0 -15
- package/dist/data/office-psalms-roman.js +0 -28
- package/dist/data/office-roman.d.ts +0 -19
- package/dist/data/office-roman.js +0 -13792
- package/dist/engines/chant/matutinum.d.ts +0 -33
- package/dist/engines/chant/matutinum.js +0 -81
- package/dist/engines/score/emitters/midi.d.ts +0 -65
- package/dist/engines/score/emitters/midi.js +0 -162
- package/dist/engines/score/emitters/musicxml.d.ts +0 -18
- package/dist/engines/score/emitters/musicxml.js +0 -166
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# Calendar
|
|
2
|
+
|
|
3
|
+
`tonus.festum` resolves a date to its place in the liturgical year. It
|
|
4
|
+
returns the feasts that fall on the day, ordered by precedence, each
|
|
5
|
+
carrying its rank in the rubrics' own vocabulary, its season, and the
|
|
6
|
+
kyriale masses appropriate to it. The calendar is the Tridentine
|
|
7
|
+
Roman rite (1570–1962), extracted from
|
|
8
|
+
[Divinum Officium](https://github.com/DivinumOfficium/divinum-officium):
|
|
9
|
+
650 entries across the sanctorale (fixed feasts) and the temporale
|
|
10
|
+
(movable feasts), resolved against Easter computed by the Gregorian computus
|
|
11
|
+
from 1583 and the Julian computus before it.
|
|
12
|
+
|
|
13
|
+
- [Calendar](#calendar)
|
|
14
|
+
- [The day's feasts — `festum`](#the-days-feasts--festum)
|
|
15
|
+
- [The day as of a year — `before`](#the-day-as-of-a-year--before)
|
|
16
|
+
- [Rank — `ritus` and `grade`](#rank--ritus-and-grade)
|
|
17
|
+
- [Seasons — the temporale](#seasons--the-temporale)
|
|
18
|
+
- [The year's anchors — `pascha`](#the-years-anchors--pascha)
|
|
19
|
+
- [Theory \& Context](#theory--context)
|
|
20
|
+
- [The calendar's era](#the-calendars-era)
|
|
21
|
+
|
|
22
|
+
## The day's feasts — `festum`
|
|
23
|
+
|
|
24
|
+
`festum(query?)` returns `Feast[]`. A date query returns every entry that
|
|
25
|
+
falls on the day: the primary feast first, concurrent feasts after it, in
|
|
26
|
+
order of dignity. A range query (`from`/`to`) walks each day and flattens
|
|
27
|
+
the results. A query with filters but no date scans the default liturgical
|
|
28
|
+
year, Advent to Advent. With no argument at all, the day resolves to
|
|
29
|
+
tonus's default epoch, **1 June 991**, the symbolic birthday of Guido
|
|
30
|
+
d'Arezzo (see [the conventions note](index.md#conventions)).
|
|
31
|
+
Empty matches return `[]`; an invalid range throws.
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
tonus.festum({ date: new Date("2026-12-25") });
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
[
|
|
39
|
+
{
|
|
40
|
+
id: "12-25",
|
|
41
|
+
nomen: "In Nativitate Domini",
|
|
42
|
+
ritus: "Duplex I classis",
|
|
43
|
+
grade: "duplex-i",
|
|
44
|
+
season: "nat",
|
|
45
|
+
tempus: "Tempus Nativitatis",
|
|
46
|
+
seasonStart: "2026-12-25",
|
|
47
|
+
seasonEnd: "2027-01-10",
|
|
48
|
+
date: "2026-12-25",
|
|
49
|
+
weekday: 5,
|
|
50
|
+
masses: [2, 3],
|
|
51
|
+
marian: false,
|
|
52
|
+
apostolic: false,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: "Adv4-5",
|
|
56
|
+
nomen: "Feria VI infra Hebdomadam IV Adventus",
|
|
57
|
+
ritus: "Feria major",
|
|
58
|
+
grade: "feria-major",
|
|
59
|
+
season: "nat",
|
|
60
|
+
tempus: "Tempus Nativitatis",
|
|
61
|
+
seasonStart: "2026-12-25",
|
|
62
|
+
seasonEnd: "2027-01-10",
|
|
63
|
+
date: "2026-12-25",
|
|
64
|
+
weekday: 5,
|
|
65
|
+
masses: [16],
|
|
66
|
+
marian: false,
|
|
67
|
+
apostolic: false,
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The privileged feast leads; the concurrent Advent feria follows, carrying only
|
|
73
|
+
the ferial rubric's mass (`masses: [16]`).
|
|
74
|
+
|
|
75
|
+
Precedence decides what comes first when feasts collide. On November 30,
|
|
76
|
+
2025, St. Andrew falls on the first Sunday of Advent; the privileged
|
|
77
|
+
Sunday wins the day and the Apostle follows it:
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
tonus.festum({ date: new Date("2025-11-30") });
|
|
81
|
+
// Dominica I Adventus [semiduplex-i]
|
|
82
|
+
// S. Andreæ Apostoli [duplex-ii]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The other query forms:
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
tonus.festum({ from: advent1, to: epiphany }); // range, day by day
|
|
89
|
+
tonus.festum({ nomen: "Dominica I Adventus" }); // partial match, case-insensitive
|
|
90
|
+
tonus.festum({ season: "pasc" }); // liturgical-year scan, filtered
|
|
91
|
+
tonus.festum({ grade: "duplex-i", marian: true });
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### The day as of a year — `before`
|
|
95
|
+
|
|
96
|
+
`before` resolves the day as it stood in a given year: the calendar holds
|
|
97
|
+
only the feasts instituted by then, and precedence runs over those. On most
|
|
98
|
+
days that returns the temporale or the feria in place of a modern feast.
|
|
99
|
+
Institution dates are in `cal/data/eras.ts`. A day with no feast yet
|
|
100
|
+
instituted returns `[]`.
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
tonus.festum({ date: new Date("2026-07-01"), before: 1100 });
|
|
104
|
+
// the feria — the Precious Blood was not instituted until 1849
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The feast returned **carries the view** (`feast.before`), and every chant
|
|
108
|
+
verb reads it back: `proprium`, `ordinarium`, and `officium`
|
|
109
|
+
serve only chants attested by the same year, without being told the year
|
|
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
|
|
112
|
+
[chant.md](chant.md#the-repertoire-as-of-a-date--the-era-view).
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
interface FeastQuery {
|
|
116
|
+
date?: Date;
|
|
117
|
+
from?: Date;
|
|
118
|
+
to?: Date;
|
|
119
|
+
nomen?: string; // partial match, case-insensitive
|
|
120
|
+
season?: Season;
|
|
121
|
+
grade?: Grade;
|
|
122
|
+
marian?: boolean;
|
|
123
|
+
apostolic?: boolean;
|
|
124
|
+
before?: number; // the era view: the day as of this year
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
interface Feast {
|
|
128
|
+
id: string; // "MM-DD" (sancti) or DO stem, e.g. "Adv1-0" (tempora)
|
|
129
|
+
nomen: string; // Latin feast name, "In Nativitate Domini"
|
|
130
|
+
ritus: string; // the Tridentine rank verbatim, incl. octave detail
|
|
131
|
+
grade: Grade; // canonical grade code; precedence via GRADE_ORDER
|
|
132
|
+
season: Season; // machine code (DO Tempora stem)
|
|
133
|
+
tempus: string; // Latin season name, "Tempus Adventus"
|
|
134
|
+
seasonStart: Date;
|
|
135
|
+
seasonEnd: Date;
|
|
136
|
+
date: Date;
|
|
137
|
+
weekday: number; // 0 = Sunday (UTC)
|
|
138
|
+
masses: number[]; // the masses the day's Kyriale rubric appoints
|
|
139
|
+
marian: boolean;
|
|
140
|
+
apostolic: boolean;
|
|
141
|
+
before?: number; // the era view this feast was resolved under, if any
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The `masses` list is derived from the Kyriale's own printed rubric — one
|
|
146
|
+
category per mass, by RANK: "In Paschal Time", "For feasts of the I class",
|
|
147
|
+
"For Sundays throughout the Year", "For ferias". A day resolves to exactly
|
|
148
|
+
one rubric (a BVM feast is "of the Blessed Virgin" even in Paschaltide),
|
|
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
|
|
151
|
+
numbering is the book's invitation to choose, and `ordinarium` rotates
|
|
152
|
+
among them by year. The book's per-mass nicknames (_Orbis factor_ for
|
|
153
|
+
Sundays, and so on) record customary use, which disagrees with the rubric for 9
|
|
154
|
+
of the 18; `ordinarium` selects on the rubric.
|
|
155
|
+
|
|
156
|
+
## Rank — `ritus` and `grade`
|
|
157
|
+
|
|
158
|
+
`ritus` is the rank string verbatim
|
|
159
|
+
from the Divinum Officium `[Rank]` line (`"Duplex majus"`, `"Semiduplex
|
|
160
|
+
II classis"`, `"Duplex I classis cum Octava privilegiata I ordinis"`).
|
|
161
|
+
It preserves the
|
|
162
|
+
octave detail, if present. `grade` is the canonical code the ritus reduces to for
|
|
163
|
+
sorting, filtering, and mass selection.
|
|
164
|
+
|
|
165
|
+
The order is classis-primary. A first-class day outranks any
|
|
166
|
+
non-first-class feast regardless of the duplex/semiduplex axis, so a plain
|
|
167
|
+
Duplex feast never displaces a Lent Sunday:
|
|
168
|
+
|
|
169
|
+
| # | `grade` | reduces from `ritus` | who it is |
|
|
170
|
+
| --- | -------------------- | ------------------------------------ | ----------------------------------------------------------- |
|
|
171
|
+
| 1 | `triduum` | Feria privilegiata Duplex I classis | Maundy Thu, Good Fri, Holy Sat |
|
|
172
|
+
| 2 | `duplex-i` | Duplex I classis (+ octave variants) | Christmas, Easter, Pentecost |
|
|
173
|
+
| 3 | `duplex-majus-i` | Duplex majus I classis | Low Sunday |
|
|
174
|
+
| 4 | `semiduplex-i` | Semiduplex I classis | Lent Sundays, Palm Sunday, Easter/Pentecost octave weekdays |
|
|
175
|
+
| 5 | `feria-privilegiata` | Feria privilegiata | Ash Wednesday, Holy Week Mon–Wed |
|
|
176
|
+
| 6 | `duplex-ii` | Duplex II classis (+ octave) | second-class feasts |
|
|
177
|
+
| 7 | `semiduplex-ii` | Semiduplex II classis | later Advent Sundays, octave days |
|
|
178
|
+
| 8 | `duplex-majus` | Duplex majus | |
|
|
179
|
+
| 9 | `duplex` | Duplex | |
|
|
180
|
+
| 10 | `semiduplex` | Semiduplex | Sundays throughout the year, semiduplex feasts |
|
|
181
|
+
| 11 | `simplex` | Simplex | |
|
|
182
|
+
| 12 | `feria-major` | Feria major | Advent/Lent ferias, Ember days |
|
|
183
|
+
| 13 | `vigilia` | Vigilia | |
|
|
184
|
+
| 14 | `feria` | Feria | weekdays with no feast |
|
|
185
|
+
|
|
186
|
+
Four Sundays are graded above their Divinum Officium rank, which marks Advent I
|
|
187
|
+
and the three Septuagesima-block Sundays plain `"Semiduplex"`. Advent I resolves
|
|
188
|
+
to `semiduplex-i`; Septuagesima, Sexagesima, and Quinquagesima to
|
|
189
|
+
`semiduplex-ii`, matching the classes DO gives the Lent and late-Advent Sundays.
|
|
190
|
+
`ritus` stays verbatim.
|
|
191
|
+
|
|
192
|
+
## Seasons — the temporale
|
|
193
|
+
|
|
194
|
+
Each feast carries the pair `season` (code) and `tempus` (the Latin
|
|
195
|
+
season name). The codes are one-to-one with the Divinum Officium Tempora
|
|
196
|
+
stems, so a date's season and the stem of any Tempora feast on it agree by
|
|
197
|
+
construction.
|
|
198
|
+
|
|
199
|
+
| `season` | `tempus` | English | Span |
|
|
200
|
+
| -------- | ----------------------- | -------------------- | --------------------------------------------------- |
|
|
201
|
+
| `adv` | Tempus Adventus | Advent | Advent I Sunday → Christmas |
|
|
202
|
+
| `nat` | Tempus Nativitatis | Christmastide | Christmas → 1st Sunday after Epiphany |
|
|
203
|
+
| `epi` | Tempus post Epiphaniam | Time after Epiphany | there → Septuagesima |
|
|
204
|
+
| `quadp` | Tempus Septuagesimæ | Septuagesima | Septuagesima Sunday → Ash Wednesday |
|
|
205
|
+
| `quad` | Tempus Quadragesimæ | Lent | Ash Wednesday → Easter |
|
|
206
|
+
| `pasc` | Tempus Paschale | Paschaltide | Easter → Trinity Sunday (Pentecost octave included) |
|
|
207
|
+
| `pent` | Tempus post Pentecosten | Time after Pentecost | Trinity Sunday → next Advent |
|
|
208
|
+
|
|
209
|
+
A feast's `id` carries a nominal week number, but `season` is always
|
|
210
|
+
derived from the date; overflow entries, such as the Epiphany weeks
|
|
211
|
+
resumed before Septuagesima, take the season of the day they fall on.
|
|
212
|
+
|
|
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
|
|
215
|
+
Benedicamus dismissal appears only where the selected mass carries a
|
|
216
|
+
setting ([chant.md](chant.md#the-ordinary--ordinarium)).
|
|
217
|
+
|
|
218
|
+
## The year's anchors — `pascha`
|
|
219
|
+
|
|
220
|
+
`pascha(year)` returns the movable anchors of one liturgical year as
|
|
221
|
+
UTC-midnight dates. Easter is computed by the Gregorian (Gauss/Butcher)
|
|
222
|
+
computus from 1583, and by the Julian computus with Julian→Gregorian
|
|
223
|
+
day-number conversion before that, so years reaching into the medieval
|
|
224
|
+
period stay correct. Everything else anchors to Easter, except Advent,
|
|
225
|
+
which anchors to the first Sunday on or after November 27, and the fixed
|
|
226
|
+
Christmas-cycle dates. A non-finite year throws.
|
|
227
|
+
|
|
228
|
+
```js
|
|
229
|
+
tonus.pascha(2026);
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
```js
|
|
233
|
+
{ year: 2026,
|
|
234
|
+
septuagesima: 2026-02-01, ashWednesday: 2026-02-18,
|
|
235
|
+
firstLentSunday: 2026-02-22, palmSunday: 2026-03-29,
|
|
236
|
+
goodFriday: 2026-04-03, easter: 2026-04-05,
|
|
237
|
+
ascension: 2026-05-14, pentecost: 2026-05-24,
|
|
238
|
+
trinitySunday: 2026-05-31, corpusChristi: 2026-06-04,
|
|
239
|
+
adventFirstSunday: 2026-11-29, gaudete: 2026-12-13,
|
|
240
|
+
christmas: 2026-12-25, epiphany: 2026-01-06,
|
|
241
|
+
baptism: 2026-01-11 }
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
interface Pascha {
|
|
246
|
+
year: number;
|
|
247
|
+
septuagesima: Date;
|
|
248
|
+
ashWednesday: Date;
|
|
249
|
+
firstLentSunday: Date;
|
|
250
|
+
palmSunday: Date;
|
|
251
|
+
goodFriday: Date;
|
|
252
|
+
easter: Date;
|
|
253
|
+
ascension: Date;
|
|
254
|
+
pentecost: Date;
|
|
255
|
+
trinitySunday: Date;
|
|
256
|
+
corpusChristi: Date;
|
|
257
|
+
adventFirstSunday: Date;
|
|
258
|
+
gaudete: Date;
|
|
259
|
+
christmas: Date;
|
|
260
|
+
epiphany: Date;
|
|
261
|
+
baptism: Date;
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Theory & Context
|
|
266
|
+
|
|
267
|
+
### The calendar's era
|
|
268
|
+
|
|
269
|
+
The calendar's structure is medieval: the temporale from Advent through the
|
|
270
|
+
season after Pentecost (Septuagesima included), the eight-hour office cursus,
|
|
271
|
+
and the duplex/semiduplex/simplex dignity system. The data is the Tridentine
|
|
272
|
+
codification (1570–1962) via Divinum Officium, continuous with late-medieval
|
|
273
|
+
usage and carrying feasts as recent as the 1950s. tonus describes this calendar
|
|
274
|
+
as Tridentine Roman, continuous with medieval practice.
|
|
275
|
+
|
|
276
|
+
## Sources
|
|
277
|
+
|
|
278
|
+
Sources for this page are in the central [bibliography](https://github.com/jeffreypierce/tonus/blob/main/BIBLIOGRAPHY.md):
|
|
279
|
+
`divinum-officium`, `computus`, `liber-usualis`.
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
# Census
|
|
2
|
+
|
|
3
|
+
`census` measures one chant against the corpus that holds it: how typical it
|
|
4
|
+
is, where it is unusual, and what it is near.
|
|
5
|
+
|
|
6
|
+
- [Census](#census)
|
|
7
|
+
- [The method](#the-method)
|
|
8
|
+
- [What a block holds](#what-a-block-holds)
|
|
9
|
+
- [How the measurement works](#how-the-measurement-works)
|
|
10
|
+
- [Distance is cosine per field group](#distance-is-cosine-per-field-group)
|
|
11
|
+
- [Profile and typicality](#profile-and-typicality)
|
|
12
|
+
- [Balance — distance and deviance](#balance--distance-and-deviance)
|
|
13
|
+
- [Neighbors, and `by`](#neighbors-and-by)
|
|
14
|
+
- [The era view](#the-era-view)
|
|
15
|
+
- [What the census is not](#what-the-census-is-not)
|
|
16
|
+
|
|
17
|
+
## The method
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
tonus.census({ id: "gregobase:1210" });
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Everything comes back in one call — profile, balance, neighbors:
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
{
|
|
27
|
+
id: "gregobase:1210",
|
|
28
|
+
by: "all",
|
|
29
|
+
profile: {
|
|
30
|
+
modal: { values: [...12], typicality: 0.99 },
|
|
31
|
+
degreeHist: { values: [...15], typicality: … },
|
|
32
|
+
melodic: { values: [...121], typicality: 0.70 },
|
|
33
|
+
trigram: { values: [...16], typicality: … },
|
|
34
|
+
cadenceFinal: { values: [...16], typicality: … },
|
|
35
|
+
cadenceMedial: { values: [...16], typicality: … },
|
|
36
|
+
chironomy: { values: [...6], typicality: … },
|
|
37
|
+
textual: { values: [...7], typicality: … },
|
|
38
|
+
},
|
|
39
|
+
balance: {
|
|
40
|
+
distance: 0.091,
|
|
41
|
+
deviantGroups: ["degreeHist", "melodic"],
|
|
42
|
+
},
|
|
43
|
+
neighbors: [
|
|
44
|
+
{ id: "gregobase:34", similarity: 0.999 },
|
|
45
|
+
…
|
|
46
|
+
],
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
interface CensusQuery {
|
|
52
|
+
id: string; // the chant to census
|
|
53
|
+
k?: number; // how many neighbors, default 8; 0 returns none
|
|
54
|
+
by?: CensusBy; // which field group similarity is measured on, default "all"
|
|
55
|
+
before?: number; // restrict neighbors to chants attested by this year
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The census covers the **2,187 chants tonus ships** — the same population
|
|
60
|
+
`cantus({ id })` addresses, one block per chant. An id with no block throws
|
|
61
|
+
rather than returning an empty answer, because a silent nothing reads as "this
|
|
62
|
+
chant is unlike everything," which is a different claim.
|
|
63
|
+
|
|
64
|
+
## What a block holds
|
|
65
|
+
|
|
66
|
+
The corpus pipeline censuses every shipped chant into 221 float32s, grouped by
|
|
67
|
+
what they describe:
|
|
68
|
+
|
|
69
|
+
| group | floats | what it measures |
|
|
70
|
+
| --------------- | -----: | --------------------------------------------------------------------------------- |
|
|
71
|
+
| `modal` | 12 | affinity to each of the eight modes, the final's and tenor's pitch-class, ambitus |
|
|
72
|
+
| `degreeHist` | 15 | how long the melody dwells on each scale degree, final-relative |
|
|
73
|
+
| `melodic` | 121 | the interval bigram table — which step follows which |
|
|
74
|
+
| `trigram` | 16 | three-note motifs, against the corpus's commonest |
|
|
75
|
+
| `cadenceFinal` | 16 | how the chant closes, keyed by cadence signature |
|
|
76
|
+
| `cadenceMedial` | 16 | how its interior phrases land |
|
|
77
|
+
| `chironomy` | 6 | the melodic arc in quarters, phrase length, melisma density |
|
|
78
|
+
| `textual` | 7 | vowel distribution by sung duration, accent rate, melisma mean |
|
|
79
|
+
|
|
80
|
+
Four more fields ride in the block and are **not** similarity dimensions:
|
|
81
|
+
`flags` (a bitfield), `attest` (dating — that is what `before` reads),
|
|
82
|
+
`extras`, and `reserve`. `by` will not accept them.
|
|
83
|
+
|
|
84
|
+
## How the measurement works
|
|
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
|
|
88
|
+
what a chant is.
|
|
89
|
+
|
|
90
|
+
Each float is a named measurement, not a learned one: time spent on the
|
|
91
|
+
subfinal, how often a rising second follows a falling third. When the census
|
|
92
|
+
calls two chants near, the profile says in what respect.
|
|
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
|
|
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
|
|
99
|
+
supplies the usage.
|
|
100
|
+
|
|
101
|
+
The reference is the mean block over all 2,187 chants, group by group. Because
|
|
102
|
+
blocks are sums of durations and counts, they add: a season's blocks, summed and
|
|
103
|
+
divided by their count, are the season's mean profile in the same 221 slots.
|
|
104
|
+
|
|
105
|
+
## Distance is cosine per field group
|
|
106
|
+
|
|
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
|
|
110
|
+
computing a distance, and if you compute it differently from the rule below
|
|
111
|
+
your numbers will not agree with `census()`'s. Nothing will error.
|
|
112
|
+
|
|
113
|
+
The rule, in three lines:
|
|
114
|
+
|
|
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
|
|
117
|
+
dimension one vote, no tunable weights.
|
|
118
|
+
3. Ties break to the lower id, so the same question always has the same answer.
|
|
119
|
+
|
|
120
|
+
Cosine on the whole vector is dominated by the 121-float `melodic` block and by
|
|
121
|
+
sheer magnitude, so a long Tract would neighbor other long chants for being
|
|
122
|
+
long. Per-group cosine asks about **shape within each dimension**.
|
|
123
|
+
|
|
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 —
|
|
126
|
+
so you can pool a set without guessing at either.
|
|
127
|
+
|
|
128
|
+
### Reading the numbers
|
|
129
|
+
|
|
130
|
+
**Similarity is not comparable across `by` values.** A 0.94 on `cadenceFinal`
|
|
131
|
+
and a 0.94 on `melodic` are not the same amount of alike: the groups have
|
|
132
|
+
different widths and different natural spreads. Rank within one `by`; never
|
|
133
|
+
threshold across two.
|
|
134
|
+
|
|
135
|
+
**A centroid must be pooled per group, then compared per group.** Averaging the
|
|
136
|
+
flat 221 and taking one cosine is the exact mistake rule 1 exists to prevent.
|
|
137
|
+
Pooling the 178 Communions both ways gives different winners, and the flat
|
|
138
|
+
version collapses the top of the field into a tie around 0.99 where the
|
|
139
|
+
per-group version spreads from about 0.85 down to 0.65. That compression comes
|
|
140
|
+
from one wide block outvoting the other eight.
|
|
141
|
+
|
|
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
|
|
144
|
+
unfiltered one. Chants that were ranked out by later material rise into it.
|
|
145
|
+
Typicality is unaffected: it is always measured against the whole shipped
|
|
146
|
+
corpus (see [Profile and typicality](#profile-and-typicality)).
|
|
147
|
+
|
|
148
|
+
### Worked example — pooling a genus
|
|
149
|
+
|
|
150
|
+
Reproducing the rule in full. This gives the same numbers `census()` gives,
|
|
151
|
+
which is the point of printing it:
|
|
152
|
+
|
|
153
|
+
```js
|
|
154
|
+
import tonus, { CENSUS_GROUPS, CENSUS_ORDER } from "tonus";
|
|
155
|
+
|
|
156
|
+
const GROUPS = Object.keys(CENSUS_GROUPS);
|
|
157
|
+
|
|
158
|
+
const cosine = (a, b) => {
|
|
159
|
+
let dot = 0, na = 0, nb = 0;
|
|
160
|
+
for (let i = 0; i < a.length; i++) {
|
|
161
|
+
dot += a[i] * b[i]; na += a[i] ** 2; nb += b[i] ** 2;
|
|
162
|
+
}
|
|
163
|
+
return na && nb ? dot / Math.sqrt(na * nb) : 0;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// Pool a set of chants into a centroid — per group, never the flat 221.
|
|
167
|
+
function centroid(ids) {
|
|
168
|
+
const sums = Object.fromEntries(
|
|
169
|
+
GROUPS.map((g) => [g, new Array(CENSUS_GROUPS[g].count).fill(0)]),
|
|
170
|
+
);
|
|
171
|
+
for (const id of ids) {
|
|
172
|
+
const { profile } = tonus.census({ id, k: 0 }); // k: 0 — profile only
|
|
173
|
+
for (const g of GROUPS) profile[g].values.forEach((v, i) => { sums[g][i] += v; });
|
|
174
|
+
}
|
|
175
|
+
for (const g of GROUPS) sums[g] = sums[g].map((v) => v / ids.length);
|
|
176
|
+
return sums;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Compare against it the same way: cosine per group, then the mean for `all`.
|
|
180
|
+
function similarity(id, c) {
|
|
181
|
+
const { profile } = tonus.census({ id, k: 0 });
|
|
182
|
+
const per = Object.fromEntries(GROUPS.map((g) => [g, cosine(profile[g].values, c[g])]));
|
|
183
|
+
per.all = GROUPS.reduce((s, g) => s + per[g], 0) / GROUPS.length;
|
|
184
|
+
return per;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Every censused Communion, pooled — then: which Communion is most a Communion?
|
|
188
|
+
const ids = CENSUS_ORDER.filter((id) => tonus.cantus({ id })[0]?.office === "co");
|
|
189
|
+
const c = centroid(ids); // 178 chants
|
|
190
|
+
const ranked = ids
|
|
191
|
+
.map((id) => ({ id, s: similarity(id, c) }))
|
|
192
|
+
.sort((a, b) => b.s.all - a.s.all);
|
|
193
|
+
|
|
194
|
+
// 0.953 Quinque prudentes
|
|
195
|
+
// 0.951 Domus mea
|
|
196
|
+
// 0.944 Joseph fili David
|
|
197
|
+
// …
|
|
198
|
+
// 0.746 Exiit sermo
|
|
199
|
+
// 0.735 Tollite hostias
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
The per-group breakdown is where the answer becomes legible. _Quinque
|
|
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
|
|
205
|
+
`cadenceFinal` is only about 0.82, so the one thing it does unlike a typical
|
|
206
|
+
Communion is end. A chant is typical of its genus in some dimensions and not
|
|
207
|
+
others.
|
|
208
|
+
|
|
209
|
+
## Profile and typicality
|
|
210
|
+
|
|
211
|
+
Each group's `typicality` is its cosine against the corpus mean for that group:
|
|
212
|
+
1.0 is "uses this dimension exactly as the corpus does on average," lower means
|
|
213
|
+
"unlike the rest."
|
|
214
|
+
|
|
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
|
|
218
|
+
interval
|
|
219
|
+
vocabulary is its own. One chant can be conventional in one dimension and
|
|
220
|
+
distinctive in another, which is the reason the groups are kept apart.
|
|
221
|
+
|
|
222
|
+
Typicality is always measured against the **whole shipped corpus**, never the
|
|
223
|
+
filtered pool: `before` restricts who may be a neighbor, it does not move
|
|
224
|
+
the mean.
|
|
225
|
+
|
|
226
|
+
## Balance — distance and deviance
|
|
227
|
+
|
|
228
|
+
```js
|
|
229
|
+
balance: { distance: 0.091, deviantGroups: ["degreeHist", "melodic"] }
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
`distance` is 1 minus the mean typicality across all groups: 0 is a chant at
|
|
233
|
+
the corpus mean, 1 has nothing in common with it.
|
|
234
|
+
|
|
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
|
|
237
|
+
is "given how typical this chant is overall, where does it depart from
|
|
238
|
+
itself?", which is what makes the answer legible for a chant that is unusual
|
|
239
|
+
everywhere or nowhere.
|
|
240
|
+
|
|
241
|
+
## Neighbors, and `by`
|
|
242
|
+
|
|
243
|
+
```js
|
|
244
|
+
tonus.census({ id: "gregobase:1210", k: 3 });
|
|
245
|
+
// Ab occultis meis (Graduale, mode 2) →
|
|
246
|
+
// Justus ut palma Graduale, mode 2
|
|
247
|
+
// Requiem Graduale, mode 2
|
|
248
|
+
// Domine refugium Graduale, mode 2
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Nothing tells the census what genre or mode a chant is. It recovers them from
|
|
252
|
+
melodic shape alone.
|
|
253
|
+
|
|
254
|
+
`by` changes what _near_ means:
|
|
255
|
+
|
|
256
|
+
```js
|
|
257
|
+
tonus.census({ id: "gregobase:1210", k: 3, by: "cadenceFinal" });
|
|
258
|
+
// chants that CLOSE the same way — crossing genre and mode freely
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Asked on `all`, a mode-2 Gradual finds mode-2 Graduals. Asked on
|
|
262
|
+
`cadenceFinal`, it finds an Alleluia, an Introit and a Communion in modes 1
|
|
263
|
+
and 4 that happen to end with the same gesture. Both answers are correct; they
|
|
264
|
+
are answers to different questions.
|
|
265
|
+
|
|
266
|
+
`k` bounds the result (default 8, `0` returns none, larger than the corpus
|
|
267
|
+
returns all 2,186 others).
|
|
268
|
+
|
|
269
|
+
## The era view
|
|
270
|
+
|
|
271
|
+
```js
|
|
272
|
+
tonus.census({ id: "gregobase:1210", before: 1100 });
|
|
273
|
+
```
|
|
274
|
+
|
|
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
|
|
277
|
+
[`cantus({ before })`](chant.md#the-repertoire-as-of-a-date--the-era-view),
|
|
278
|
+
through the same admissibility door: **evidence, not existence**, so a chant
|
|
279
|
+
with no dated witness is excluded rather than assumed old.
|
|
280
|
+
|
|
281
|
+
The seed chant itself is never filtered — you asked about it by name.
|
|
282
|
+
|
|
283
|
+
## What the census is not
|
|
284
|
+
|
|
285
|
+
It is not a similarity search over Gregorian chant at large. The blocks
|
|
286
|
+
describe the chants tonus ships, which is the assignment-driven corpus: what
|
|
287
|
+
some day of the calendar calls for. A melody's neighbors are its neighbors
|
|
288
|
+
_within that repertoire_.
|