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
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,281 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to tonus. Newest first.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 0.9.1 — 2026-08-24
|
|
6
|
+
|
|
7
|
+
Moderna reads the pitch it was given: the whole page had been sitting a sixth
|
|
8
|
+
too high, and a transposed chant said nothing about its own accidentals.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Moderna drew every note a sixth above where it belonged.** `writtenY`
|
|
13
|
+
measured the note against a reference term that had not been normalised the
|
|
14
|
+
same way (`4 * 7` against a note term carrying `+2`), so sounding E4 computed
|
|
15
|
+
as 14 staff steps instead of 0. Every note of every moderna score sat above
|
|
16
|
+
the top line. The port was faithful — the same expression is in the locked
|
|
17
|
+
reference generator, which was fed pitches an octave below today's corpus, so
|
|
18
|
+
two errors cancelled; when the corpus moved to true octaves only one of them
|
|
19
|
+
moved. The clef's octave lift is now stated once, explicitly.
|
|
20
|
+
- **A transposed chromatic rendered as a silent wrong pitch.** `temperamentum`'s
|
|
21
|
+
`transpose` moves what a chant sounds, and moderna reads sounding pitch — but
|
|
22
|
+
the accidental was parsed and discarded, so `Ab3` and `A3` landed on one slot
|
|
23
|
+
with no sign drawn. The page read a semitone off with nothing to say so.
|
|
24
|
+
Spelling now follows `spn`: flats stay flats, sharps stay sharps. Signs
|
|
25
|
+
written in the GABC are unaffected — `computeAccidentals` remains
|
|
26
|
+
authoritative wherever it has an opinion, keyed on the degree a sign alters
|
|
27
|
+
rather than the row that carries it.
|
|
28
|
+
- **Moderna had no ledger lines at all.** A note outside the five rendered as a
|
|
29
|
+
head floating in blank space. One line per staff line the note has passed,
|
|
30
|
+
drawn behind the head: the spaces immediately outside (steps 9 and −1) get
|
|
31
|
+
none, and a line-note and the space above it share one.
|
|
32
|
+
- **`midiToGabc` threw on every B-flat** — the one accidental chant sings, and
|
|
33
|
+
the whole reason `parse.ts` carries a flat state machine. It now spells it
|
|
34
|
+
`ix`, and `gabcToMidi` reads that back, so the two stay inverses. Other
|
|
35
|
+
chromatic pitch classes still throw. Verified over 222 round-trips.
|
|
36
|
+
- **The GABC clef tables disagreed about which clefs exist.** `temper/gabc.ts`
|
|
37
|
+
knew six (`c1`–`c4`, `f3`, `f4`) while `parse.ts` accepted sixteen, so a
|
|
38
|
+
`cb2` the parser reads happily raised "Unknown clef". `CLEFS` is now derived
|
|
39
|
+
rather than hand-listed; all six original entries are reproduced exactly.
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- **A low moderna chant is written an octave up.** One clef throughout — what
|
|
44
|
+
floats is the written octave, the move the hand already makes when it lifts a
|
|
45
|
+
chant onto the gamut's fingers. Chant sounds below this staff (corpus median
|
|
46
|
+
50–62 against a window of 52–65), and transposing down pushes it further: at
|
|
47
|
+
−4 an unlifted score put 43.6% of all notes under the bottom line, now 9.8%.
|
|
48
|
+
Untransposed moderna output moves for most chants as a result, always toward
|
|
49
|
+
the staff (7.8% off-staff to 5.8%). Whole octaves only, and one lift for the
|
|
50
|
+
whole chant — a page that changed register partway would read as two pieces.
|
|
51
|
+
The limit is honest: an ambitus straddling the window cannot be aligned by
|
|
52
|
+
any multiple of 12, and 59 of 200 measured chants are wider than the staff.
|
|
53
|
+
`notatio`'s `spn` remains the authority on what actually sounds.
|
|
54
|
+
- **Quadrata is untouched by all of this**, by construction: it draws from
|
|
55
|
+
staff position, which transposition does not move. Verified byte-identical.
|
|
56
|
+
|
|
57
|
+
### Packaging
|
|
58
|
+
|
|
59
|
+
- `.npmignore` removed — it contradicted the `files` allowlist, and two
|
|
60
|
+
mechanisms deciding what ships is one more than can be reasoned about.
|
|
61
|
+
Measured before and after: the tarball is byte-identical at 216 files.
|
|
62
|
+
- `"sideEffects": false` declared, and verified true — nothing in `dist/` runs
|
|
63
|
+
at module scope and no data file is read at import time.
|
|
64
|
+
- The description no longer claims "and performance"; the voice lives in
|
|
65
|
+
tonus-sonus.
|
|
66
|
+
|
|
67
|
+
## 0.9.0 — 2026-08-19
|
|
68
|
+
|
|
69
|
+
The tracks stop promising a contract they never kept, and the geometry says
|
|
70
|
+
where the ink is.
|
|
71
|
+
|
|
72
|
+
### Added
|
|
73
|
+
|
|
74
|
+
- **`Syllable.neumes` — the syllable's figures, each classified.** A syllable
|
|
75
|
+
is not a neume; it carries neumes. GABC marks the figures with `!`, `/` and
|
|
76
|
+
`//`, and the parser has always recorded them, but classification read the
|
|
77
|
+
whole syllable, so a melisma of three ordinary figures was named once and
|
|
78
|
+
that name was `compound`: 18,718 of the 27,643 compounds, 67.7% of them.
|
|
79
|
+
`neume` still reads the syllable as one figure and is unchanged; `neumes`
|
|
80
|
+
names them individually. Compound falls from 17.8% to **8.3%**, and the
|
|
81
|
+
torculus count more than doubles (4,521 → 10,607).
|
|
82
|
+
|
|
83
|
+
The salicus reports at syllable scope, and the exception is the point. Its
|
|
84
|
+
rule reads the oriscus on the next-to-last note of an ascent, and 41 of the
|
|
85
|
+
corpus's 255 salici are written across a figure boundary with the oriscus in
|
|
86
|
+
one figure and the summit in the next. Splitting first severed them and the
|
|
87
|
+
count fell to 251, narrowing Cardine's definition by refactoring rather than
|
|
88
|
+
by ruling. Held at 255, with 35 more recovered: figures that are a salicus in
|
|
89
|
+
their own right inside a syllable that is not one.
|
|
90
|
+
|
|
91
|
+
- **Six neume names for figures that restate a pitch.** `classifyShape` had no
|
|
92
|
+
case for the unison beyond a two-note group, so every repercussive figure
|
|
93
|
+
fell to `compound`: 12,675 of them, 6.6% of the corpus. `NeumeShape` gains
|
|
94
|
+
`distropha`, `tristropha`, `tristropha flexa`, `pressus`, `pressus maior`,
|
|
95
|
+
and `scandicus subpunctis` (the mirror of `pes subpunctis`, which was the
|
|
96
|
+
only form named past three intervals). 4,154 groups that read `compound` now
|
|
97
|
+
carry a name, and `compound` falls from 17.8% to 15.9%.
|
|
98
|
+
|
|
99
|
+
The names rest on contour, which is what both emitters already read: quadrata
|
|
100
|
+
breathes strophae apart on the staff position alone, and moderna merges a
|
|
101
|
+
strophic run into one slur. `hasStrophicus` is untouched and still reports
|
|
102
|
+
the GABC marker, which is the narrower fact.
|
|
103
|
+
|
|
104
|
+
This does not reopen the salicus ruling. That one turns on an ornament the
|
|
105
|
+
printed edition may have resolved away, so reading it from the ictus would
|
|
106
|
+
invent what the source withheld; a restated pitch is a contour the source
|
|
107
|
+
states outright.
|
|
108
|
+
|
|
109
|
+
The praepunctis family is absent by choice. Measured over the corpus, the
|
|
110
|
+
long figures are genuine compound melismas (`[up,down,down,up]` 643,
|
|
111
|
+
`[up,down,down,down]` 524) rather than textbook praepunctis forms, and a name
|
|
112
|
+
matching nothing is worse than the gap.
|
|
113
|
+
|
|
114
|
+
- **`NoteGeometry` reports the figure's ink extent**, as `inkLeft` and
|
|
115
|
+
`inkRight` beside the anchor. `x` is an ANCHOR, and what it anchors differs by
|
|
116
|
+
notation: quadrata's square glyphs start at it and run right, so `x` is the
|
|
117
|
+
figure's left edge and a mark drawn anchor-to-anchor sits left of the notes it
|
|
118
|
+
names and stops short of the last one; moderna centres its noteheads on it.
|
|
119
|
+
These are the numbers the in-house tracks have always consumed, now reported
|
|
120
|
+
rather than measured privately. A caller placing a playhead or an overlay no
|
|
121
|
+
longer has to read the drawn glyph back out of the SVG to find the middle of a
|
|
122
|
+
notehead.
|
|
123
|
+
|
|
124
|
+
Quadrata measures the edges from the glyph's bounding box as it places;
|
|
125
|
+
moderna derives them from the anchor, so they straddle it evenly.
|
|
126
|
+
|
|
127
|
+
### Changed
|
|
128
|
+
|
|
129
|
+
- **The rhythm model names a note by its own figure.** `applyCompoundBeats`
|
|
130
|
+
annotated every note of a melisma with the whole syllable's name, so the two
|
|
131
|
+
conventional overrides (salicus → arsic, doubly-dotted clivis → thetic) could
|
|
132
|
+
not see a clivis inside a syllable that read `compound`. Measured over the
|
|
133
|
+
corpus: 5 phrases of 26,803 change `rhythmicType`, and 75 notes of 400,948
|
|
134
|
+
change arsis/thesis (0.019%). Small because most newly-visible clivises carry
|
|
135
|
+
no double mora, so the thetic rule still does not fire on them.
|
|
136
|
+
|
|
137
|
+
- **The docs no longer offer a downstream track contract.** `score.md` framed
|
|
138
|
+
the geometry as the interface analysis tracks build on, and said a custom
|
|
139
|
+
track downstream does the same. It could not: the tracks consume a private
|
|
140
|
+
surface (the tabula row, the lyric baseline, per-system right edges, the
|
|
141
|
+
scale factor, and band room only an emitter can reserve). The geometry is
|
|
142
|
+
documented for what it is and is used for: locating a note on the drawn page.
|
|
143
|
+
The three tracks stay in the library, and the tracks section is rewritten as
|
|
144
|
+
reference rather than prose.
|
|
145
|
+
|
|
146
|
+
- **The metrics documentation stops dumping six interfaces in a row.** The
|
|
147
|
+
section ended in fifty-odd unbroken lines of TypeScript, its meaning carried
|
|
148
|
+
in trailing comments. The five composite fields are now grouped by the
|
|
149
|
+
question each answers, and the two that were enumerations wearing interfaces
|
|
150
|
+
are tables: `motus` with the semitone ranges its three names mean, and
|
|
151
|
+
`cadenceDistribution` with each divisio's GABC mark and analytic weight —
|
|
152
|
+
the bar-line hierarchy stated once, where `metrics.ts` already cites it.
|
|
153
|
+
|
|
154
|
+
- **Corrected counts in the tracks documentation.** The cadence catalogue holds
|
|
155
|
+
110 families, not 122; the corpus carries about 26,800 cadences, not 20,500.
|
|
156
|
+
The `rara` share is stated both ways it can be read: about 43% of the
|
|
157
|
+
corpus's cadences, but about a third of the labels a page prints.
|
|
158
|
+
|
|
159
|
+
- **A corpus measurement the library can answer for itself is checked against
|
|
160
|
+
the library.** A count written into prose is a copy, and a copy goes stale on
|
|
161
|
+
the next re-bake with nothing to catch it: the catalogue held 122 families for
|
|
162
|
+
as long as it took someone to notice it held 110, in three places at once.
|
|
163
|
+
`tests/comments.test.mjs` gains the class — the book table, the ledger totals,
|
|
164
|
+
the catalogue's size and population, the censused count, the calendar's size —
|
|
165
|
+
each failure naming the file, the stale figure and the live value. Illustrative
|
|
166
|
+
counts come out of the comments that carried them; calibration provenance
|
|
167
|
+
stays, because a figure recording what a constant was fitted against is the
|
|
168
|
+
evidence for that constant, not decoration.
|
|
169
|
+
|
|
170
|
+
Three figures were already wrong. `chant.md` gave `listings - count` as 580
|
|
171
|
+
extra rows "over the 683 chants printed in more than one book", but 683
|
|
172
|
+
multi-book chants produce 696 extra rows — two measures of different things in
|
|
173
|
+
one sentence, now stated as the rule they illustrate. `score.md` had a cadence
|
|
174
|
+
breakdown failing inclusion–exclusion, and read its 43% as the share that
|
|
175
|
+
joins the catalogue where it is the share that does not.
|
|
176
|
+
|
|
177
|
+
### Fixed
|
|
178
|
+
|
|
179
|
+
- **The staff was anchored an octave below the gamut.** `guido.ts` fixes Γ
|
|
180
|
+
(gamma ut) at midi 43 and runs two octaves up from it, so within tonus the
|
|
181
|
+
gamut is an absolute frame. The GABC parser anchored the staff an octave under
|
|
182
|
+
it: a c4 chant opened on E2 at 82.5 Hz, and most of the corpus read at or
|
|
183
|
+
below the floor of the medieval system. Measured over 400 chants, 346 sat
|
|
184
|
+
below Γ; none do now. *Ab occultis meis* spans midi 52–65, opening on E3 and
|
|
185
|
+
landing on Elami, Fefaut, Gesolreut.
|
|
186
|
+
|
|
187
|
+
Nothing caught it because the tabula was self-consistent — `spn`, `hz` and
|
|
188
|
+
`octave` all derive from `midi`, so they reported a wrong number faithfully.
|
|
189
|
+
It showed on the Guidonian hand, where a joint is a fixed place: orreliquum
|
|
190
|
+
had grown a per-chant octave lift to haul chants back onto the figure, itself
|
|
191
|
+
replacing an earlier manual control. Two compensations for one constant. A
|
|
192
|
+
downstream consumer carrying such a lift should now remove it.
|
|
193
|
+
|
|
194
|
+
**This moves output data**: every `midi`, `hz`, `spn` and `octave` in the
|
|
195
|
+
tabula shifts by twelve semitones. The census is unaffected — its pitch fields
|
|
196
|
+
are final-relative, and `blocks.bin` rebuilds bit-identical.
|
|
197
|
+
|
|
198
|
+
- **`NoteGeometry.noteIndex` addressed the wrong note.** Both emitters filled
|
|
199
|
+
it from the row's `neumeIndex` (position within the neume FIGURE) where
|
|
200
|
+
`ChantTabulaRow.noteIndex` and `Cadence.notes[*][2]` mean position within the
|
|
201
|
+
SYLLABLE. The documented tuple join therefore misaddressed every syllable
|
|
202
|
+
carrying a second figure: 17 of 159 notes on *Puer natus est*. `noteIndex`
|
|
203
|
+
now carries the syllable position the tabula means, and `neumeIndex` is
|
|
204
|
+
reported alongside it, so the three address one note. A consumer joining by
|
|
205
|
+
array index (the safe join) is unaffected.
|
|
206
|
+
|
|
207
|
+
- **A wrapped cadence closed twice.** A cadence figure crossing a system break
|
|
208
|
+
is re-inked in both, because the claim spans them, but it CLOSES once. Drawn
|
|
209
|
+
per system, the terminal node landed on the earlier fragment's last sample (a
|
|
210
|
+
landing mid-figure) and the label repeated: at width 680 that subject drew 7
|
|
211
|
+
nodes under quadrata and 8 under moderna for 6 confident cadences. The node
|
|
212
|
+
and label now draw only in the system holding the figure's last note.
|
|
213
|
+
|
|
214
|
+
- **The tonarium's mode line admitted claims the rest of the renderer
|
|
215
|
+
refuses.** It gated modulations at confidence 0.4 where the documented floor,
|
|
216
|
+
the cadence path, and the ink doctrine all say nothing below 0.45 draws.
|
|
217
|
+
|
|
218
|
+
- **A flat was drawn on the line of the note it was written before, not the
|
|
219
|
+
line of the note it alters.** Solesmes prints an accidental on the line of
|
|
220
|
+
the pitch it inflects, so a B-flat sign sits on the B line; where the sign is
|
|
221
|
+
printed and which degree it alters are separate facts, and both emitters took
|
|
222
|
+
both from the row carrying the sign. On *Felices sensus* (gregobase:1180) two
|
|
223
|
+
flats governing B-flats were drawn on the G and A lines, where the Liber
|
|
224
|
+
Usualis plate (p. 1637) has them on the B line. Corpus-wide, 3,505 of 4,358
|
|
225
|
+
explicit signs sat on a degree they do not alter. The mark now carries the
|
|
226
|
+
altered degree, and horizontal placement is untouched: the sign still reads
|
|
227
|
+
before the figure it governs.
|
|
228
|
+
|
|
229
|
+
- **A written sign could be dropped.** Repeat-suppression compared the
|
|
230
|
+
sign-carrying row's pitch class against the row immediately before it, which
|
|
231
|
+
measured the wrong pitch (the sign's neighbour, not the degree it alters) and
|
|
232
|
+
applied a weaker rule than the books', which restate once another pitch has
|
|
233
|
+
intervened. *Felices sensus* wrote three flats and printed two: the third
|
|
234
|
+
followed another A, though it opens a new phrase and governs a new B-flat.
|
|
235
|
+
Suppression is now keyed on the altered degree and on nothing else, and it
|
|
236
|
+
holds back only a sign with no music between it and the last mark of that
|
|
237
|
+
degree. A sign whose alteration is never found within its lookahead prints
|
|
238
|
+
rather than borrowing the coordinate of the line it was written on: that line
|
|
239
|
+
is a different fact, and keying both into one map let a found degree and an
|
|
240
|
+
unfound sign's carrying line collide. All three of the fixture's flats print.
|
|
241
|
+
|
|
242
|
+
- **A natural's line was found by the wrong test.** The lookahead for the
|
|
243
|
+
governed degree read `accidental !== 0`, which a natural never satisfies: it
|
|
244
|
+
restores a pitch rather than altering one. Naturals are now read against the
|
|
245
|
+
state they set, so a B-natural sign sits on the B line.
|
|
246
|
+
|
|
247
|
+
## 0.8.0 — 2026-08-18
|
|
248
|
+
|
|
249
|
+
### Changed
|
|
250
|
+
|
|
251
|
+
- **`prosody` is renamed `metrics`, because that is what it measures.**
|
|
252
|
+
|
|
253
|
+
- **CI runs the test suite** in place of the retired site deploy.
|
|
254
|
+
|
|
255
|
+
## 0.7.0 — 2026-08-17
|
|
256
|
+
|
|
257
|
+
The site leaves for its own repo, and the fonts are named rather than bundled.
|
|
258
|
+
|
|
259
|
+
### Changed
|
|
260
|
+
|
|
261
|
+
- **The site migrated to orreliquum.** The playground, its diagrams and the hand
|
|
262
|
+
figure now live in the site's own repo, which consumes tonus as a dependency;
|
|
263
|
+
this repo keeps the library and `docs/api`.
|
|
264
|
+
|
|
265
|
+
- **Junicode is named, not bundled**, and the font note is stated as a recipe.
|
|
266
|
+
tonus ships no font files: the `fonts` option emits family references, and a
|
|
267
|
+
slot's `embed` carries the caller's own bytes.
|
|
268
|
+
|
|
269
|
+
- **The glyph extractor is JavaScript**, and Bravura lives in the repo.
|
|
270
|
+
|
|
271
|
+
- **The signs get their own documentation page**, and the removed exports leave
|
|
272
|
+
the table.
|
|
273
|
+
|
|
274
|
+
### Fixed
|
|
275
|
+
|
|
276
|
+
- **A correction to the Guidonian hand data.**
|
|
277
|
+
|
|
278
|
+
- **0.6.1 — the tagged 0.6.0 never reached the registry.** A republish under a
|
|
279
|
+
new patch version, the registry having lagged the git tag.
|
|
6
280
|
|
|
7
281
|
## 0.6.0 — 2026-08-16
|
|
8
282
|
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ belong to which day and returns analysis and notation, and calculates the
|
|
|
8
8
|
_music of the spheres_ proposed by Pythagoras, Boethius, and more as real
|
|
9
9
|
intervals.
|
|
10
10
|
|
|
11
|
-
**[Interactive demo
|
|
11
|
+
**[Interactive demo](https://orreliquum.com/)**
|
|
12
12
|
|
|
13
13
|
Everything is computed locally and deterministically: the same question always
|
|
14
14
|
returns the same answer, from data that ships inside the package, with no network
|
|
@@ -72,8 +72,8 @@ its final, dominant, ambitus, and ethos, with its psalm tones.
|
|
|
72
72
|
**The calendar** is Tridentine, resolved against Easter by
|
|
73
73
|
the Julian or the Gregorian computus.
|
|
74
74
|
|
|
75
|
-
**The repertoire** is 2,187 chants across ten books
|
|
76
|
-
the _Nocturnale Romanum_ night office
|
|
75
|
+
**The repertoire** is 2,187 chants across ten books, nine Solesmes editions and
|
|
76
|
+
the _Nocturnale Romanum_ night office in
|
|
77
77
|
[GABC](https://gregorio-project.github.io/) notation: Mass propers, the Kyriale,
|
|
78
78
|
the Office hours, the psalter, and the Matins nocturns.
|
|
79
79
|
|
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
// ReferenceError whenever ordinary.js loaded first).
|
|
9
9
|
//
|
|
10
10
|
// ── A FILTER THAT LEFT AND CAME BACK ───────────────────────────────────────
|
|
11
|
-
// This filter was once retired because its data had a genre-shaped hole:
|
|
12
|
-
// of the corpus undated,
|
|
13
|
-
// deleted the Hartker repertory from Epiphany Matins — wrong
|
|
14
|
-
// liturgy on the exact question the library exists to answer. The
|
|
15
|
-
// never the problem; the coverage was. The corpus side closed it
|
|
16
|
-
// gap-fill + matcher v2: composite respond+verse, the incipit-entry
|
|
17
|
-
// containment, genre disambiguation — all gold-gated):
|
|
18
|
-
// now carry a dated witness
|
|
11
|
+
// This filter was once retired because its data had a genre-shaped hole: a
|
|
12
|
+
// third of the corpus undated, and RESPONSORIES almost all of it, so
|
|
13
|
+
// `before: 1098` deleted the Hartker repertory from Epiphany Matins — wrong
|
|
14
|
+
// about the liturgy on the exact question the library exists to answer. The
|
|
15
|
+
// rule was never the problem; the coverage was. The corpus side closed it
|
|
16
|
+
// (crosswalk gap-fill + matcher v2: composite respond+verse, the incipit-entry
|
|
17
|
+
// rule, containment, genre disambiguation — all gold-gated): the great majority
|
|
18
|
+
// of shipped records now carry a dated witness, responsories included. What remains
|
|
19
19
|
// undated is deliberate (psalter, formulas) or editorial territory (hymns,
|
|
20
20
|
// modern propers) — not a genre bias. So the evidence law stands: an undated
|
|
21
21
|
// chant is EXCLUDED under any cutoff. Silence is not evidence.
|
|
@@ -97,10 +97,10 @@ function primeForFeast(feast) {
|
|
|
97
97
|
*
|
|
98
98
|
* A monastic weekday in the temporale mostly has no proper antiphons — the
|
|
99
99
|
* office-monastic table has the day but leaves antLaudes/antVespera/antMatutinum
|
|
100
|
-
* empty
|
|
100
|
+
* empty in most of its entries, because those antiphons live in the psalter,
|
|
101
101
|
* not in the propers. Without a fallback the hour silently returns a partial
|
|
102
|
-
* ordo, which is why Lauds resolved on
|
|
103
|
-
* the temporale.
|
|
102
|
+
* ordo, which is why Lauds resolved on well under a third of the year's days
|
|
103
|
+
* and essentially never in the temporale.
|
|
104
104
|
*
|
|
105
105
|
* ── WHY THE COMMUNE COMES BEFORE THE FERIAL CYCLE ───────────────────────────
|
|
106
106
|
* A saint's day is not a feria. When the rubrics give a saint no proper
|
|
@@ -50,7 +50,13 @@ function buildHalf(syllables, tenor, cadenceNotes, prefixNotes) {
|
|
|
50
50
|
}
|
|
51
51
|
else {
|
|
52
52
|
const note = noteAssignment[noteIdx++] ?? tenor;
|
|
53
|
-
|
|
53
|
+
// TEXT FIRST, then its notes — `syl(note)`, which is what GABC means and
|
|
54
|
+
// what every corpus chant encodes. Emitted the other way round, each
|
|
55
|
+
// whitespace-separated word opened with an empty-text syllable that took
|
|
56
|
+
// syllableIndex 0, so the word's real first syllable never scored
|
|
57
|
+
// wordStart and the renderer hyphenated between every pair: a whole verse
|
|
58
|
+
// set as a single word. The word-aware metrics read the same lie.
|
|
59
|
+
tokens.push(`${syl}(${note})`);
|
|
54
60
|
}
|
|
55
61
|
}
|
|
56
62
|
return tokens.join("");
|
|
@@ -59,10 +59,10 @@ export interface Cadence {
|
|
|
59
59
|
*
|
|
60
60
|
* NOT part of the family key — kept because the folded value is still worth
|
|
61
61
|
* reporting (it is the scale DEGREE, mode-theoretically real). As a key the
|
|
62
|
-
* fold made a fifth ABOVE the final share a family with a fourth BELOW
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* the key is therefore the SIGNED offset; see cadenceKeys().
|
|
62
|
+
* fold made a fifth ABOVE the final share a family with a fourth BELOW — two
|
|
63
|
+
* opposite gestures under one key. The measurement behind that ruling is
|
|
64
|
+
* stated once, in `data/cadentiae.ts`'s header, beside the table it governs.
|
|
65
|
+
* Arrival in the key is therefore the SIGNED offset; see cadenceKeys().
|
|
66
66
|
*/
|
|
67
67
|
export declare function reduceArrival(semitones: number): number;
|
|
68
68
|
/** One phrase-end event: the family key, and whether it closes the chant. */
|
|
@@ -91,6 +91,12 @@ export interface CadenceKeyEvent {
|
|
|
91
91
|
* copies is what "no second parser, no drift" forbids; hence this one shared
|
|
92
92
|
* function.
|
|
93
93
|
*
|
|
94
|
+
* That 27,985 is the population of the run that PROVED the agreement, and it
|
|
95
|
+
* does not match `CADENTIAE_POPULATION.ends`: the table was rebaked afterwards
|
|
96
|
+
* against the sung corpus. Two measurements of different populations, not a
|
|
97
|
+
* contradiction — the figure here is fixed to its proof and does not track the
|
|
98
|
+
* table.
|
|
99
|
+
*
|
|
94
100
|
* Takes the FLAT shape, because that is what the census and the miner have; the
|
|
95
101
|
* engine's own detection flattens into it. A phrase end is a `phraseIndex`
|
|
96
102
|
* transition or the last row.
|
|
@@ -134,10 +134,10 @@ const TAIL = 4;
|
|
|
134
134
|
*
|
|
135
135
|
* NOT part of the family key — kept because the folded value is still worth
|
|
136
136
|
* reporting (it is the scale DEGREE, mode-theoretically real). As a key the
|
|
137
|
-
* fold made a fifth ABOVE the final share a family with a fourth BELOW
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* the key is therefore the SIGNED offset; see cadenceKeys().
|
|
137
|
+
* fold made a fifth ABOVE the final share a family with a fourth BELOW — two
|
|
138
|
+
* opposite gestures under one key. The measurement behind that ruling is
|
|
139
|
+
* stated once, in `data/cadentiae.ts`'s header, beside the table it governs.
|
|
140
|
+
* Arrival in the key is therefore the SIGNED offset; see cadenceKeys().
|
|
141
141
|
*/
|
|
142
142
|
export function reduceArrival(semitones) {
|
|
143
143
|
let a = semitones % 12;
|
|
@@ -158,6 +158,12 @@ export function reduceArrival(semitones) {
|
|
|
158
158
|
* copies is what "no second parser, no drift" forbids; hence this one shared
|
|
159
159
|
* function.
|
|
160
160
|
*
|
|
161
|
+
* That 27,985 is the population of the run that PROVED the agreement, and it
|
|
162
|
+
* does not match `CADENTIAE_POPULATION.ends`: the table was rebaked afterwards
|
|
163
|
+
* against the sung corpus. Two measurements of different populations, not a
|
|
164
|
+
* contradiction — the figure here is fixed to its proof and does not track the
|
|
165
|
+
* table.
|
|
166
|
+
*
|
|
161
167
|
* Takes the FLAT shape, because that is what the census and the miner have; the
|
|
162
168
|
* engine's own detection flattens into it. A phrase end is a `phraseIndex`
|
|
163
169
|
* transition or the last row.
|
|
@@ -9,6 +9,15 @@ export interface AccidentalMark {
|
|
|
9
9
|
glyph?: string;
|
|
10
10
|
/** Text label (kind "cents") — e.g. "−3.9". */
|
|
11
11
|
label?: string;
|
|
12
|
+
/** The DEGREE THE SIGN ALTERS (kind "glyph", standard mode), as the two
|
|
13
|
+
* emitters address a line: `degree` is quadrata's staffPosition, `degreeSpn`
|
|
14
|
+
* is moderna's written pitch. A flat is printed on the line of the pitch it
|
|
15
|
+
* inflects — a B-flat sign sits on the B line — while its horizontal place
|
|
16
|
+
* is before the figure where it takes effect. Those are two facts, and the
|
|
17
|
+
* carrying row supplies only the second. Both absent when the sign alters
|
|
18
|
+
* nothing found ahead. */
|
|
19
|
+
degree?: number;
|
|
20
|
+
degreeSpn?: string;
|
|
12
21
|
}
|
|
13
22
|
export type AccidentalGlyphSet = "modern" | "medieval";
|
|
14
23
|
/**
|
|
@@ -8,6 +8,12 @@ const GLYPHS_BY_SET = {
|
|
|
8
8
|
modern: { [-1]: "E260", 0: "E261", 1: "E262" },
|
|
9
9
|
medieval: { [-1]: "E9E0", 0: "E9E1", 1: "E9E3" },
|
|
10
10
|
};
|
|
11
|
+
// How far ahead a written sign is read for the degree it alters. A flat is
|
|
12
|
+
// written before the figure it governs, not always before the note: on
|
|
13
|
+
// gregobase:1180 one sign precedes its B-flat by four notes within the
|
|
14
|
+
// syllable. Bounded so a sign whose alteration never arrives does not reach
|
|
15
|
+
// across the chant and borrow an unrelated note's line.
|
|
16
|
+
const SCOPE = 12;
|
|
11
17
|
// HEJI comma glyphs (Extended Helmholtz–Ellis, U+E2C0–). The syntonic-comma
|
|
12
18
|
// arrows: one comma down / up. Higher-order commas are out of scope for 0.2.
|
|
13
19
|
const HEJI_COMMA_DOWN = "E2C2"; // one syntonic comma lower
|
|
@@ -24,21 +30,78 @@ function fromPythagorean(row) {
|
|
|
24
30
|
*/
|
|
25
31
|
export function computeAccidentals(rows, mode, centsBaseline = "pythagorean", glyphSet = "modern") {
|
|
26
32
|
if (mode === "standard") {
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
// WHERE a sign is printed and WHICH DEGREE it alters are different facts,
|
|
34
|
+
// and the row carrying the sign supplies only the first. In
|
|
35
|
+
// `fe(jx)cit(ih)` the flat is written before the I while it governs the J:
|
|
36
|
+
// horizontally it precedes the figure (so the singer reads it in time),
|
|
37
|
+
// vertically it belongs to the pitch it inflects. Solesmes prints a flat
|
|
38
|
+
// on the LINE OF THE PITCH IT ALTERS — a B-flat sign sits on the B line
|
|
39
|
+
// [biblio: liber-usualis, Rules for Interpretation; the plate for
|
|
40
|
+
// gregobase:1180 at p. 1637 shows both of that chant's flats on the B
|
|
41
|
+
// line, over an A and a G]. Reading `row.accidental` here drew nothing at
|
|
42
|
+
// all in the `fe(jx)` case (the I is unaltered); reading the carrying
|
|
43
|
+
// row's staffPosition drew the flat on whatever line happened to precede
|
|
44
|
+
// — corpus-wide, 80% of signs sat on a degree they do not alter.
|
|
45
|
+
const alteredOf = (from) => {
|
|
46
|
+
// The sign governs the next note in its scope that CARRIES THE STATE it
|
|
47
|
+
// sets. A flat looks for the note it lowers; a natural looks for the note
|
|
48
|
+
// it restores, which is unaltered by definition (`accidental === 0`) and
|
|
49
|
+
// is marked "state" because the sign, not the clef, put it there.
|
|
50
|
+
// Reading only `accidental !== 0` found the flats and missed every
|
|
51
|
+
// natural's B.
|
|
52
|
+
//
|
|
53
|
+
// The scan opens AT the carrying row, which is the common case: a sign
|
|
54
|
+
// written directly before the note it alters is carried by that note.
|
|
55
|
+
// The two facts coincide there; where they part, the scan walks on.
|
|
56
|
+
const sign = rows[from].accidentalSign ?? rows[from].accidental;
|
|
57
|
+
for (let j = from; j < rows.length && j < from + SCOPE; j++) {
|
|
58
|
+
const r = rows[j];
|
|
59
|
+
if (sign === 0 ? r.accidentalSource === "state" : r.accidental === sign)
|
|
60
|
+
return r;
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
};
|
|
64
|
+
// Restate a sign once another pitch has intervened since that DEGREE was
|
|
65
|
+
// last marked. Comparing the carrying row against the row before it was
|
|
66
|
+
// wrong twice over: it measured the wrong pitch (the sign's neighbour,
|
|
67
|
+
// not the degree it alters), and "the immediately-preceding row" is a
|
|
68
|
+
// weaker rule than the books', which restate after an intervening pitch.
|
|
69
|
+
// On gregobase:1180 the third flat was dropped because the note before it
|
|
70
|
+
// was another A — though it opens a new phrase and governs a new B-flat.
|
|
71
|
+
//
|
|
72
|
+
// SUPPRESSION IS KEYED ON THE ALTERED DEGREE, AND ON NOTHING ELSE. A sign
|
|
73
|
+
// whose alteration is never found has no degree to key on, and it is not
|
|
74
|
+
// keyed on the line it happens to be written on: that line is a different
|
|
75
|
+
// coordinate, and the two sharing one map made a found degree and an
|
|
76
|
+
// unfound sign's carrying line collide. Such a sign always prints —
|
|
77
|
+
// rare, and an unexplained sign on the page is the safer failure.
|
|
78
|
+
const lastMarked = new Map();
|
|
79
|
+
return rows.map((row, i) => {
|
|
80
|
+
if (row.accidentalSource !== "explicit")
|
|
34
81
|
return null;
|
|
35
|
-
// Draw the SIGN the source wrote, not this note's own alteration. In
|
|
36
|
-
// `fe(jx)cit(ih)` the flat is printed before the I while it governs J —
|
|
37
|
-
// where a sign is printed and which degree it alters are different facts.
|
|
38
|
-
// Reading `row.accidental` here drew nothing in that case, because the I
|
|
39
|
-
// is unaltered.
|
|
40
82
|
const sign = row.accidentalSign ?? row.accidental;
|
|
41
|
-
|
|
83
|
+
const altered = alteredOf(i);
|
|
84
|
+
const degree = altered?.staffPosition;
|
|
85
|
+
const mark = {
|
|
86
|
+
kind: "glyph", glyph: GLYPHS_BY_SET[glyphSet][sign],
|
|
87
|
+
degree, degreeSpn: altered?.spn,
|
|
88
|
+
};
|
|
89
|
+
if (degree === undefined)
|
|
90
|
+
return mark;
|
|
91
|
+
const since = lastMarked.get(degree);
|
|
92
|
+
lastMarked.set(degree, i);
|
|
93
|
+
// What the books restate after is INTERVENING MUSIC, and the sign that
|
|
94
|
+
// says nothing new is the one with none: the two signs adjacent, the
|
|
95
|
+
// second merely re-marking a degree the first has just marked. Asking
|
|
96
|
+
// instead whether an intervening row LEFT the degree suppressed a real
|
|
97
|
+
// restatement whenever the music between the signs stayed on the altered
|
|
98
|
+
// degree — the commonest case there is, since that degree is what the
|
|
99
|
+
// signs are both about.
|
|
100
|
+
if (since !== undefined &&
|
|
101
|
+
rows.slice(since + 1, i).every((r) => r.accidentalSource === "explicit")) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
return mark;
|
|
42
105
|
});
|
|
43
106
|
}
|
|
44
107
|
if (mode === "heji") {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
// draw.
|
|
25
25
|
//
|
|
26
26
|
// ─── HOW THIS MEETS THE PAGE ───────────────────────────────────────────────
|
|
27
|
-
// The site (orreliquum
|
|
27
|
+
// The site (orreliquum) keeps a system of its own, stated at the top of
|
|
28
28
|
// its stylesheet. The two are separate and they AGREE BY SHARING NUMBERS,
|
|
29
29
|
// not by one importing the other:
|
|
30
30
|
//
|
|
@@ -25,8 +25,8 @@ export interface BreakQuery {
|
|
|
25
25
|
* Set when the caller has ALREADY consumed `next.lineBreak` itself. Quadrata
|
|
26
26
|
* honours `z` in its own block (it must repeat the clef and place a custos
|
|
27
27
|
* before the staff advances), so asking here too would break the same system
|
|
28
|
-
* twice — measured,
|
|
29
|
-
*
|
|
28
|
+
* twice — measured, a Graduale chant drew one custos per system where the
|
|
29
|
+
* last system needs none.
|
|
30
30
|
*/
|
|
31
31
|
forcedHandled?: boolean;
|
|
32
32
|
}
|
|
@@ -52,8 +52,8 @@ export function decideBreak(q) {
|
|
|
52
52
|
// be tested on its own: a single figure wider than a whole line (a 36-note
|
|
53
53
|
// melisma at 420px) can never be rescued by breaking, but the line before it
|
|
54
54
|
// should still end rather than run on. Folding this into the `need` test
|
|
55
|
-
// below let such a syllable extend a line that was already full
|
|
56
|
-
//
|
|
55
|
+
// below let such a syllable extend a line that was already full, and a
|
|
56
|
+
// Graduale render overran its width.
|
|
57
57
|
if (q.x > q.boundary)
|
|
58
58
|
return { break: true, reason: "width" };
|
|
59
59
|
return q.x + q.need > q.boundary
|