tonus 0.1.6 → 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 +610 -1
- package/LICENSE +133 -29
- package/README.md +106 -83
- package/dist/data/am.js +2669 -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 +24 -0
- package/dist/data/corpus-overlap.js +347 -0
- 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 +757 -6394
- package/dist/data/kyriale.js +116 -116
- package/dist/data/la.js +802 -13419
- package/dist/data/lh.js +116 -3473
- package/dist/data/lu.js +935 -17632
- package/dist/data/nocturnale-romanum.d.ts +5 -0
- package/dist/data/nocturnale-romanum.js +3772 -0
- 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 +22 -4
- package/dist/engines/chant/chant.js +274 -14
- 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 +145 -10
- 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 +28 -6
- package/dist/index.js +39 -3
- 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-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/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,873 @@
|
|
|
1
|
+
# Score
|
|
2
|
+
|
|
3
|
+
`tonus.notatio` renders a chant into a score: the analyzed, tuned, and
|
|
4
|
+
rhythm-classified reading of one GABC melody. The score is data: `phrases`,
|
|
5
|
+
`tabula`, `prosody`, `cadences`, `modulations`, and `imprint`. The
|
|
6
|
+
standalone `tonus.inscriptio(score)` draws it to SVG.
|
|
7
|
+
|
|
8
|
+
- [Score](#score)
|
|
9
|
+
- [The score — `notatio`](#the-score--notatio)
|
|
10
|
+
- [Interpretation — `pondus` and `accentus`](#interpretation--pondus-and-accentus)
|
|
11
|
+
- [The note](#the-note)
|
|
12
|
+
- [The tabula](#the-tabula)
|
|
13
|
+
- [Rendering](#rendering)
|
|
14
|
+
- [inscriptio — the standalone renderer](#inscriptio--the-standalone-renderer)
|
|
15
|
+
- [theme — faces and ink](#theme--faces-and-ink)
|
|
16
|
+
- [The analysis tracks](#the-analysis-tracks)
|
|
17
|
+
- [The imprint](#the-imprint)
|
|
18
|
+
- [Prosody](#prosody)
|
|
19
|
+
- [Cadences](#cadences)
|
|
20
|
+
- [One spine, two annotations](#one-spine-two-annotations)
|
|
21
|
+
- [`finality` — how often this family closes](#finality--how-often-this-family-closes)
|
|
22
|
+
- [Modulations](#modulations)
|
|
23
|
+
- [Theory \& Context](#theory--context)
|
|
24
|
+
- [The model](#the-model)
|
|
25
|
+
- [The classification rules](#the-classification-rules)
|
|
26
|
+
- [Rhythmic types](#rhythmic-types)
|
|
27
|
+
- [Modeled and not](#modeled-and-not)
|
|
28
|
+
|
|
29
|
+
## The score — `notatio`
|
|
30
|
+
|
|
31
|
+
`notatio(chant, opts?)` builds a `Score` from a single `Chant`. Invalid
|
|
32
|
+
input throws; recoverable GABC problems land in `score.errors`, and
|
|
33
|
+
downstream fields degrade rather than throw.
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
const [feast] = tonus.festum({ date: new Date("2026-12-25") });
|
|
37
|
+
const [introit] = tonus.proprium({ feast, office: "in" }); // Puer natus est
|
|
38
|
+
const t = tonus.temperamentum({ mode: 7 });
|
|
39
|
+
|
|
40
|
+
const score = tonus.notatio(introit, { temperamentum: t });
|
|
41
|
+
// 10 phrases, 78 syllables, 159 notes, 0 errors
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The structured view is `score.phrases`; the flat view, one row per note,
|
|
45
|
+
is `score.tabula`. Phrases split at every divisio — the bars of chant
|
|
46
|
+
notation, signs of punctuation rather than measure:
|
|
47
|
+
|
|
48
|
+
| divisio | name |
|
|
49
|
+
| ------- | ---------------------------- |
|
|
50
|
+
| `,` | divisio minima (quarter bar) |
|
|
51
|
+
| `` ` `` | virgula (tick) |
|
|
52
|
+
| `;` | divisio minor (half bar) |
|
|
53
|
+
| `:` | divisio maior (full bar) |
|
|
54
|
+
| `::` | divisio finalis (double bar) |
|
|
55
|
+
|
|
56
|
+
This hierarchy is read three ways in the engine, each weighting the bars for its
|
|
57
|
+
own end: an analytic cadence weight (prosody), a phrasing strength (which zeroes
|
|
58
|
+
the virgula), and a rest duration (the divisio's pause length). Each weighting
|
|
59
|
+
is documented at its table in the code.
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
interface Score {
|
|
63
|
+
chant: Chant;
|
|
64
|
+
phrases: Phrase[];
|
|
65
|
+
errors: ParseError[];
|
|
66
|
+
tabula: ChantTabulaRow[];
|
|
67
|
+
prosody: Prosody;
|
|
68
|
+
cadences: Cadence[];
|
|
69
|
+
modulations: Modulation[];
|
|
70
|
+
imprint: Imprint;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface Phrase {
|
|
74
|
+
syllables: Syllable[];
|
|
75
|
+
divisio?: RestEvent;
|
|
76
|
+
noteCount: number; // notes across the phrase
|
|
77
|
+
syllableCount: number; // sung syllables in the phrase
|
|
78
|
+
beats: CompoundBeat[]; // the incise's arsis/thesis sequence
|
|
79
|
+
rhythmicType: RhythmicType; // Le Guennant/Carroll type, or null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface Syllable {
|
|
83
|
+
lyric: string;
|
|
84
|
+
runs?: LyricRun[]; // styled spans, present only when GABC markup styled this syllable
|
|
85
|
+
notes: Note[];
|
|
86
|
+
neume: Neume;
|
|
87
|
+
melisma: number; // notes on this syllable (1 = syllabic, >1 melismatic)
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
GABC's lyric markup is decoded at parse, so `lyric` is always clean display
|
|
92
|
+
text: the `<sp>` shortcuts arrive as real characters (`<sp>V/</sp>` → ℣,
|
|
93
|
+
`<sp>R/</sp>` → ℟, `<sp>+</sp>` → the flex †, `<sp>'ae</sp>` → ǽ, the
|
|
94
|
+
`\greheightstar` verbatim → the raised *), centering braces and layout tags
|
|
95
|
+
(`<clear>`, `<nlba>`) vanish, above-lines text (`<alt>`) is not lyric text,
|
|
96
|
+
and page cross-references (`\pageref`) to the paper books are dropped. Style
|
|
97
|
+
tags — `<i>`, `<b>`, `<sc>`, `<c>` (rubric color), `<e>` (elision) — survive
|
|
98
|
+
as `runs`, styled spans that concatenate to `lyric`; a style opened in one
|
|
99
|
+
syllable and closed several later (the common `<i>ij.</i>` and euouae
|
|
100
|
+
patterns) styles every syllable it crosses. Both notation species draw the
|
|
101
|
+
runs (italic, bold, small caps, rubric color) as SVG `<tspan>`s.
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
interface LyricRun {
|
|
105
|
+
text: string;
|
|
106
|
+
italic?: boolean;
|
|
107
|
+
bold?: boolean;
|
|
108
|
+
smallCaps?: boolean;
|
|
109
|
+
rubric?: boolean; // rendered in rubricaColor
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface Neume {
|
|
113
|
+
type: NeumeShape; // "punctum", "pes", "clivis", "torculus" …
|
|
114
|
+
intervals: number[]; // semitones between successive notes
|
|
115
|
+
hasQuilisma: boolean;
|
|
116
|
+
hasLiquescent: boolean;
|
|
117
|
+
hasStrophicus: boolean;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface RestEvent {
|
|
121
|
+
type: "rest";
|
|
122
|
+
divisio: string;
|
|
123
|
+
duration: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface ParseError {
|
|
127
|
+
message: string;
|
|
128
|
+
index?: number;
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Interpretation — `pondus` and `accentus`
|
|
133
|
+
|
|
134
|
+
Interpretation is set at build time.
|
|
135
|
+
|
|
136
|
+
- `pondus` governs articulation: note weight, duration, ornament response;
|
|
137
|
+
- `accentus` governs phrasing: velocity curves, cadence weight, tenor emphasis.
|
|
138
|
+
|
|
139
|
+
Each accepts a style name
|
|
140
|
+
or an options object with overrides. `rhythmicShape` and `rhythmicIndex`
|
|
141
|
+
are always populated by the Solesmes classifier, whatever the styles.
|
|
142
|
+
|
|
143
|
+
```js
|
|
144
|
+
tonus.notatio(chant, {
|
|
145
|
+
temperamentum: t,
|
|
146
|
+
pondus: "expressive", // style name…
|
|
147
|
+
accentus: {
|
|
148
|
+
style: "solemn",
|
|
149
|
+
overrides: {
|
|
150
|
+
/* … */
|
|
151
|
+
},
|
|
152
|
+
}, // …or opts
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
| `pondus` | articulation |
|
|
157
|
+
| -------------- | ---------------------------------------------------------------------- |
|
|
158
|
+
| `"restrained"` | minimal ornament response, flatter dynamics, the semiological approach |
|
|
159
|
+
| `"balanced"` | _default_; even articulation, moderate weight |
|
|
160
|
+
| `"expressive"` | heightened ornament response, stronger shaping |
|
|
161
|
+
| `"strict"` | full Solesmes rule fidelity, careful episema and quilisma treatment |
|
|
162
|
+
|
|
163
|
+
| `accentus` | phrasing |
|
|
164
|
+
| -------------- | --------------------------------------------------- |
|
|
165
|
+
| `"recitative"` | flat, declamatory; minimal curve, strong tenor pull |
|
|
166
|
+
| `"lyrical"` | balanced arch, moderate cadence |
|
|
167
|
+
| `"hymnic"` | measured, steady; suits metrical hymns |
|
|
168
|
+
| `"solemn"` | deep curve, strong cadence, elevated velocity |
|
|
169
|
+
|
|
170
|
+
When `accentus` is omitted, tabula shaping picks the best style per mode.
|
|
171
|
+
|
|
172
|
+
A style is a named profile of numbers; `overrides` adjusts individual
|
|
173
|
+
fields on top of the chosen style. The presets in
|
|
174
|
+
`src/engines/score/articulation.ts` and `phrasing.ts` are the reference
|
|
175
|
+
values to start from.
|
|
176
|
+
|
|
177
|
+
```js
|
|
178
|
+
tonus.notatio(chant, {
|
|
179
|
+
accentus: { style: "lyrical", overrides: { cadence: 1.0 } }, // heavier cadences
|
|
180
|
+
pondus: { style: "strict", overrides: { ictusBoost: 0 } }, // …without ictus stress
|
|
181
|
+
});
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
The `pondus` profile (`ArticulationProfile`):
|
|
185
|
+
|
|
186
|
+
| field | governs |
|
|
187
|
+
| ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------- |
|
|
188
|
+
| `weights` | per-mark weight and duration multipliers: ictus, episema (single and double), strophicus, quilisma |
|
|
189
|
+
| `weightBase`, `weightGain`, `weightSaturation` | how ornament weight accumulates and where it caps |
|
|
190
|
+
| `durationBase`, `durationGain`, `durationMin`, `durationMax` | how accumulated weight maps to note duration |
|
|
191
|
+
| `neumeArch`, `durArch` | arch shaping across a neume, in weight and duration |
|
|
192
|
+
| `ictusBoost` | extra weight on ictic notes |
|
|
193
|
+
| `ruleGain`, `contourScale` | strength of rule-driven and contour-driven shaping |
|
|
194
|
+
|
|
195
|
+
The `accentus` profile (`PhrasingProfile`):
|
|
196
|
+
|
|
197
|
+
| field | governs |
|
|
198
|
+
| --------------------------- | -------------------------------------------------- |
|
|
199
|
+
| `curve` | depth of the phrase-level velocity arch |
|
|
200
|
+
| `accent` | accent emphasis within the phrase |
|
|
201
|
+
| `cadence` | weight given to phrase-final cadences |
|
|
202
|
+
| `tenor` | pull toward the reciting tone |
|
|
203
|
+
| `baseVelocity`, `velSpread` | the velocity floor and the dynamic range above it |
|
|
204
|
+
| `contourVel`, `contourDur` | melodic-contour influence on velocity and duration |
|
|
205
|
+
| `ictusBoost` | extra velocity on ictic notes |
|
|
206
|
+
| `neumeArch`, `durArch` | arch shaping across a neume |
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
interface ScoreOpts {
|
|
210
|
+
temperamentum?: Temperamentum;
|
|
211
|
+
pondus?: string | PondusOpts; // a style from the table, or opts
|
|
212
|
+
accentus?: string | AccentusOpts;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
interface PondusOpts {
|
|
216
|
+
style?: string;
|
|
217
|
+
overrides?: Partial<ArticulationProfile>; // fields from the table above
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
interface AccentusOpts {
|
|
221
|
+
style?: string;
|
|
222
|
+
overrides?: Partial<PhrasingProfile>; // fields from the table above
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## The note
|
|
227
|
+
|
|
228
|
+
The score's unified `Note` composes four concerns into sub-objects:
|
|
229
|
+
`pitch` is the tuned identity and `step` the Guidonian annotation, both
|
|
230
|
+
from the tuning engine ([tuning.md](tuning.md)); `performance` carries the
|
|
231
|
+
interpretation; `context` the position, lyric, and ornament marks.
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
interface Note {
|
|
235
|
+
pitch: Pitch; // tuned identity — tuning.md
|
|
236
|
+
step: Step; // modal/Guidonian annotation — tuning.md
|
|
237
|
+
performance: Performance;
|
|
238
|
+
context: Context;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
interface Performance {
|
|
242
|
+
velocity: number; // 0–1 shaping factor
|
|
243
|
+
duration: number;
|
|
244
|
+
rhythmicShape: "arsic" | "thetic"; // quality of this note's compound beat
|
|
245
|
+
rhythmicIndex: number; // 1-based position within the compound beat
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
interface Context {
|
|
249
|
+
lyric: string;
|
|
250
|
+
vowel: string;
|
|
251
|
+
diphthong: string | null; // the pair the vowel belongs to: "ae" | "oe" | "au" | "ui"
|
|
252
|
+
syllableIndex: number;
|
|
253
|
+
accent: boolean; // this note's syllable bears the Latin tonic word-accent
|
|
254
|
+
neumeGroup: number; // neume figure within the syllable (0-based)
|
|
255
|
+
ictus: boolean;
|
|
256
|
+
ictusSign: boolean; // an editorial ictus mark is printed in the source
|
|
257
|
+
episema: boolean;
|
|
258
|
+
accidentalSource: "none" | "state" | "explicit";
|
|
259
|
+
quilisma: boolean;
|
|
260
|
+
liquescent: boolean;
|
|
261
|
+
strophicus: boolean;
|
|
262
|
+
oriscus: boolean;
|
|
263
|
+
mora: 0 | 1 | 2; // mora vocis: 0 none, 1 dot, 2 double dot
|
|
264
|
+
staffLetter: string; // the GABC staff letter as written
|
|
265
|
+
clef: string; // the clef in force at this note ("c3", "f4", …)
|
|
266
|
+
shape: string; // the notehead shape (punctum, inclinatum, quilisma, …)
|
|
267
|
+
weight: number; // articulation weight
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
A compound beat is the group of notes between one ictus and the next;
|
|
272
|
+
every note in the group shares its quality, arsic (rising, active) or
|
|
273
|
+
thetic (resting, retractive). The classification rules are in
|
|
274
|
+
[Theory & Context](#theory--context).
|
|
275
|
+
|
|
276
|
+
## The tabula
|
|
277
|
+
|
|
278
|
+
`score.tabula` is the flat iteration surface: one row per note, for
|
|
279
|
+
analysis, visualization, or emission.
|
|
280
|
+
|
|
281
|
+
`Harmony` exposes the same surface for voiced bodies
|
|
282
|
+
([heavens.md](heavens.md#the-tabula)). The tabula is also the rendering
|
|
283
|
+
surface — the SVG renderer ([below](#rendering)) consumes it directly, which is
|
|
284
|
+
why `hz`, `velocity`, `bend`, and the ornament flags live on each row.
|
|
285
|
+
|
|
286
|
+
```js
|
|
287
|
+
score.tabula[0];
|
|
288
|
+
// { lyric: "PU", midi: 43, hz: 97.8,
|
|
289
|
+
// name: "Γ", nomen: "Gammaut",
|
|
290
|
+
// rhythmicShape: "arsic", rhythmicIndex: 1, ictus: true,
|
|
291
|
+
// degree: 1, role: "finalis", … }
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
The first note of _Puer natus est_ sits on Gammaut, the bottom of the
|
|
295
|
+
Guidonian hand.
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
interface ChantTabulaRow {
|
|
299
|
+
// position
|
|
300
|
+
phraseIndex: number;
|
|
301
|
+
syllableIndex: number;
|
|
302
|
+
noteIndex: number;
|
|
303
|
+
accent: boolean; // this note's syllable bears the Latin tonic word-accent
|
|
304
|
+
neumeGroup: number; // which neume figure within the syllable (0-based)
|
|
305
|
+
neumeIndex: number; // position of this note within that figure
|
|
306
|
+
wordStart: boolean; // first syllable of its word
|
|
307
|
+
|
|
308
|
+
// note fields
|
|
309
|
+
midi: number;
|
|
310
|
+
pc: number;
|
|
311
|
+
octave: number;
|
|
312
|
+
accidental: -1 | 0 | 1;
|
|
313
|
+
accidentalSource: "none" | "state" | "explicit";
|
|
314
|
+
quilisma: boolean;
|
|
315
|
+
liquescent: boolean;
|
|
316
|
+
strophicus: boolean;
|
|
317
|
+
oriscus: boolean;
|
|
318
|
+
mora: 0 | 1 | 2; // mora vocis: 0 none, 1 dot, 2 double dot
|
|
319
|
+
hz: number;
|
|
320
|
+
offset: number;
|
|
321
|
+
spn: string; // scientific pitch name, "D4"
|
|
322
|
+
staffLetter: string; // the GABC staff letter as written
|
|
323
|
+
staffPosition: number; // vertical staff position (line/space index)
|
|
324
|
+
clef: string; // the clef in force at this note ("c3", "f4", …)
|
|
325
|
+
shape: string; // the notehead shape (punctum, inclinatum, quilisma, …)
|
|
326
|
+
bend: number; // 14-bit MIDI pitch bend (8192 = center)
|
|
327
|
+
velocity: number | null;
|
|
328
|
+
duration: number;
|
|
329
|
+
shapedDuration: number;
|
|
330
|
+
rhythmicShape: "arsic" | "thetic";
|
|
331
|
+
rhythmicIndex: number;
|
|
332
|
+
ictus: boolean;
|
|
333
|
+
ictusSign: boolean; // an editorial ictus mark is printed in the source
|
|
334
|
+
episema: boolean;
|
|
335
|
+
|
|
336
|
+
// step fields
|
|
337
|
+
degree: number | null;
|
|
338
|
+
role: "finalis" | "tenor" | "other" | null;
|
|
339
|
+
name: string | null; // Guidonian short name
|
|
340
|
+
nomen: string | null; // Guidonian compound name, "Delasolre"
|
|
341
|
+
hand: { finger: Finger; region: Region } | null;
|
|
342
|
+
hexachord: "durum" | "naturale" | "molle" | null;
|
|
343
|
+
solfege: string | null;
|
|
344
|
+
|
|
345
|
+
// context
|
|
346
|
+
lyric: string;
|
|
347
|
+
runs?: LyricRun[]; // styled lyric spans (see Syllable above)
|
|
348
|
+
vowel: string; // the NUCLEUS — one of a e i o u, or "" when textless
|
|
349
|
+
diphthong: string | null; // the pair it belongs to: "ae" | "oe" | "au" | "ui"
|
|
350
|
+
divisio: string | null;
|
|
351
|
+
cadenceRef: number | null; // index into score.cadences[] when this note closes one
|
|
352
|
+
neume: Neume;
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
## Rendering
|
|
357
|
+
|
|
358
|
+
The score is drawn as **SVG** — a self-contained, square-note chant staff with
|
|
359
|
+
SMuFL glyphs baked as inline paths (no external font). It consumes `score.tabula`,
|
|
360
|
+
so the interpretation applied through `pondus` and `accentus` is already in the
|
|
361
|
+
geometry. Microtuning lives on each tabula row's `bend`, `hz`, and `offset` for
|
|
362
|
+
a Web-Audio player to read directly.
|
|
363
|
+
|
|
364
|
+
### inscriptio — the standalone renderer
|
|
365
|
+
|
|
366
|
+
`tonus.inscriptio(score, opts?)` draws a `Score` and returns `{ svg, geometry }`.
|
|
367
|
+
Rendering is a standalone function that _takes_ a score, not a method on one — the
|
|
368
|
+
score analyzes, `inscriptio` inks. It throws on a non-Score or an unknown
|
|
369
|
+
notation species (the builder-function contract).
|
|
370
|
+
|
|
371
|
+
```js
|
|
372
|
+
const score = tonus.notatio(introit);
|
|
373
|
+
const { svg, geometry } = tonus.inscriptio(score, { width: 680, title: "Puer natus est" });
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Two notation species, each with its own spacing pass:
|
|
377
|
+
|
|
378
|
+
| `notation` | look |
|
|
379
|
+
| --- | --- |
|
|
380
|
+
| `"quadrata"` (default) | square-note chant staff, SMuFL glyphs baked inline |
|
|
381
|
+
| `"moderna"` | modern round-note transcription: treble-8 clef, engraved slurs |
|
|
382
|
+
|
|
383
|
+
**Layout is deterministic, and lyric widths are computed rather than measured.**
|
|
384
|
+
The same score and options give byte-identical SVG on every machine, with no
|
|
385
|
+
DOM, no canvas, and no font file — `inscriptio` runs anywhere Node does. Note
|
|
386
|
+
glyphs carry exact SMuFL advance widths; lyric text is computed from character
|
|
387
|
+
classes, since measuring it would require the font's own metrics. Line breaks,
|
|
388
|
+
system fill, and the width of the returned canvas all rest on that figure. It is
|
|
389
|
+
close, not exact: a lyric set in a face far from the assumed proportions will
|
|
390
|
+
break slightly early or late.
|
|
391
|
+
|
|
392
|
+
Two consequences worth planning around. `width` is a **request, not a promise** —
|
|
393
|
+
the canvas returned is `max(width, content)`, so a chant whose content cannot fit
|
|
394
|
+
comes back wider rather than clipped. And a caller who needs typographic
|
|
395
|
+
precision should render at a generous `width` and scale the result, rather than
|
|
396
|
+
relying on the estimate to land a tight column exactly.
|
|
397
|
+
|
|
398
|
+
Options, by group (all optional):
|
|
399
|
+
|
|
400
|
+
- **layout** — `width` wraps systems to fit (absent = a single line); `scale`
|
|
401
|
+
sets how big the chant is drawn: `"small"`, `"normal"` (default), `"large"`,
|
|
402
|
+
or a staff height in px for fitting a known column. Everything scales from it
|
|
403
|
+
— notes, lyrics, the air between systems — and it reflows the music, so a
|
|
404
|
+
larger scale means fewer notes per line. The page margin does not scale: it
|
|
405
|
+
belongs to the page rather than the notation, and scaling it gave a large
|
|
406
|
+
chant *less* usable width than a small one.
|
|
407
|
+
- **front matter** — set as the Solesmes books open a piece: `title` centers
|
|
408
|
+
over the score; `rubric` (or `annotation: "auto"` to derive the genus/mode
|
|
409
|
+
mark, e.g. _Introitus. 8._) sits upright at the left margin; `dropcap` draws
|
|
410
|
+
the initial the printed books open with, taking the first letter out of the
|
|
411
|
+
lyric and indenting the first system to hold it. Both species take the
|
|
412
|
+
title; the margin mark and the initial are **quadrata's alone** — moderna is
|
|
413
|
+
a transcription read as an edition, and carries the analysis tracks a
|
|
414
|
+
reserved cap column would fight. It ignores them rather than refusing.
|
|
415
|
+
- **theme** — the dress: `fonts` and `colors`.
|
|
416
|
+
|
|
417
|
+
### theme — faces and ink
|
|
418
|
+
|
|
419
|
+
```js
|
|
420
|
+
tonus.inscriptio(score, {
|
|
421
|
+
width: 900,
|
|
422
|
+
theme: {
|
|
423
|
+
fonts: {
|
|
424
|
+
dropcap: { family: "Pfeffer Simpelgotisch", weight: 700 },
|
|
425
|
+
title: "Junicode",
|
|
426
|
+
annotation: "Junicode",
|
|
427
|
+
lyric: { family: "Junicode", weight: 400, scale: 1.06 },
|
|
428
|
+
},
|
|
429
|
+
colors: { note: "#111", staffLine: "#111", rubrica: "#9E2B25" },
|
|
430
|
+
},
|
|
431
|
+
});
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
**`fonts`** carries four roles. A book's dropcap is very often *not* its lyric
|
|
435
|
+
face — a Lombardic or uncial initial against a text hand, which is the pairing
|
|
436
|
+
the printed books use. Each role takes a font-family string or
|
|
437
|
+
`{ family, weight?, scale? }` (`scale` adjusts that role's size, for a face
|
|
438
|
+
whose apparent size differs from the house serif).
|
|
439
|
+
|
|
440
|
+
The SVG carries font-family *references* by default, and the page hosting it
|
|
441
|
+
supplies the face (`@font-face`). A slot may instead carry
|
|
442
|
+
`embed: { base64, format? }` — the caller's own bytes — and the face then rides
|
|
443
|
+
inside the SVG's `<style>`, making the file self-contained (at the cost of its
|
|
444
|
+
size; one `@font-face` per family + weight, deduped). tonus bundles no font
|
|
445
|
+
files: with `embed` it is a conduit for data the consumer supplies, so the
|
|
446
|
+
consumer carries the face's license terms. Unset roles keep the house serif.
|
|
447
|
+
`moderna` honours the `lyric`, `title`, and `annotation` slots.
|
|
448
|
+
|
|
449
|
+
**`colors`** reach the SVG as CSS custom properties with the theme's own value
|
|
450
|
+
as the fallback — `fill="var(--tonus-note, #111)"`. A rendered chant therefore
|
|
451
|
+
carries the ink it was drawn with *and* stays themable: a host stylesheet that
|
|
452
|
+
sets the property rethemes the score without re-rendering it.
|
|
453
|
+
|
|
454
|
+
```css
|
|
455
|
+
/* the page follows its own tokens; the chant follows the page */
|
|
456
|
+
.score svg {
|
|
457
|
+
--tonus-note: var(--ink);
|
|
458
|
+
--tonus-staff-line: var(--ink);
|
|
459
|
+
--tonus-rubrica: var(--rubrica);
|
|
460
|
+
}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
The emitter's semantic classes — `note`, `lyric`, `dropcap`, `custos`,
|
|
464
|
+
`episema`, `divisio`, `clef`, `mora`, `ictus` — are stylable from the host page.
|
|
465
|
+
|
|
466
|
+
**`scale` is not part of the theme**: line breaking consumes it, so a scale
|
|
467
|
+
change re-renders while a colour change does not.
|
|
468
|
+
|
|
469
|
+
Nothing else about the layout is a caller's decision. The margin, the air
|
|
470
|
+
between systems, the notehead calibration against the staff, and the line-end
|
|
471
|
+
custos are constants. The custos appears whenever a system wraps, as it does in
|
|
472
|
+
a chant book.
|
|
473
|
+
|
|
474
|
+
**The geometry contract (public API).** `geometry` is one `NoteGeometry` per note,
|
|
475
|
+
in tabula order — the interface analysis _tracks_ build on, so they place marks
|
|
476
|
+
by index and coordinate instead of scraping the SVG. The library's own tracks
|
|
477
|
+
(below) consume exactly these anchors; a custom track downstream does the same:
|
|
478
|
+
|
|
479
|
+
```ts
|
|
480
|
+
interface NoteGeometry {
|
|
481
|
+
phraseIndex: number; syllableIndex: number; neumeGroup: number; noteIndex: number;
|
|
482
|
+
system: number; // which wrapped system the note landed in
|
|
483
|
+
x: number; y: number; // notehead anchor, svg user units
|
|
484
|
+
systemY: number; // the system's top offset within the svg
|
|
485
|
+
}
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
### The analysis tracks
|
|
489
|
+
|
|
490
|
+
`tracks` draws an analysis band beneath every system. Any track rides either
|
|
491
|
+
species, and all may ride one score — the selection is independent of the
|
|
492
|
+
notation, as `notation` itself is. One governing ink system runs through them:
|
|
493
|
+
every mark draws in the score's black, strata graded by opacity alone (the
|
|
494
|
+
liturgical red belongs to the claims — the tonarium's mode line and the
|
|
495
|
+
prosodia's accent dots), and every pressure-bearing line shares one nib law —
|
|
496
|
+
velocity as stroke width.
|
|
497
|
+
|
|
498
|
+
```js
|
|
499
|
+
tonus.inscriptio(score, { width: 680, tracks: ["prosodia"] });
|
|
500
|
+
tonus.inscriptio(score, { width: 680, tracks: ["chironomia"] });
|
|
501
|
+
tonus.inscriptio(score, { notation: "moderna", width: 680, tracks: ["tonarium"] });
|
|
502
|
+
tonus.inscriptio(score, { width: 680, tracks: ["chironomia", "tonarium"] }); // stacked
|
|
503
|
+
tonus.inscriptio(score, { width: 680, tracks: ["prosodia", "chironomia", "tonarium"] });
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
The conventional pairing is the chironomia under `quadrata` and the tonarium
|
|
507
|
+
under `moderna`; the prosodia, reading the text rather than the notation,
|
|
508
|
+
rides either as naturally. The renderer enforces none of it.
|
|
509
|
+
|
|
510
|
+
Requesting several stacks them in a fixed order — the prosodia first, directly
|
|
511
|
+
under the lyric line it reads; the chironomia next; the tonarium below —
|
|
512
|
+
whichever order they are asked for, and the page grows by the sum of the
|
|
513
|
+
bands.
|
|
514
|
+
|
|
515
|
+
- **`"prosodia"`** — how the melody treats the word, in two lanes. The upper
|
|
516
|
+
lane draws one **tent per word** — the hairpin pair's top edge, dynamics'
|
|
517
|
+
own mark for swell and release — its apex over the accented syllable, with
|
|
518
|
+
the accent's landing at the peak in the liturgical red: a **filled dot**
|
|
519
|
+
when the accent lands arsic (struck), an **open ring** when it lands thetic
|
|
520
|
+
(deferred). Accented words rise past the lane's single rule; unaccented
|
|
521
|
+
words crest on it. The lower lane is a fence on a rail, one mark per
|
|
522
|
+
syllable by how the melody treats it: a spoken syllable stands as a stem
|
|
523
|
+
(height, its notes), a syllable **recited on the tenor lies flat** — a
|
|
524
|
+
short dash floating above the rail — and a **melisma of four notes or more
|
|
525
|
+
becomes a block** as wide as its real extent and as tall as its count
|
|
526
|
+
(counts of eight or more print inside). Connected melismas join into one
|
|
527
|
+
ridge, each block's top sloping toward its neighbours, the line between
|
|
528
|
+
them crossing the gaps. A divisio drops a hairline through both lanes.
|
|
529
|
+
Accents are the book's written accents (the GABC accented vowels) — the
|
|
530
|
+
track derives none.
|
|
531
|
+
- **`"chironomia"`** — the conducting hand as one continuous line:
|
|
532
|
+
arsic beats crest, thetic beats trough, single-note theses pass through
|
|
533
|
+
shallow, and the hand picks up between close arses in a small backward loop
|
|
534
|
+
[biblio: carroll-chironomy]. Pressure is the stroke's _weight_: each note's
|
|
535
|
+
`velocity` (the `accentus` shaping) becomes nib width over solid ink, so the
|
|
536
|
+
line presses where the voice does. Pierik letters (A · T · PT) name the
|
|
537
|
+
beats — the incise's rhythmic shape is read straight off them.
|
|
538
|
+
- **`"tonarium"`** — the melodic-analysis lane, named for the book
|
|
539
|
+
that catalogued chants by mode. Four rails — the maneriae finals ladder, D on
|
|
540
|
+
the bottom (categories, not pitches) — carry the **mode line** in the
|
|
541
|
+
liturgical red: the governing mode of each phrase, its numeral above
|
|
542
|
+
(authentic-vs-plagal lives in the numeral). A modulation of kind
|
|
543
|
+
`"inflection"` steps the line solid; a `"transposition"` (the affinal frame
|
|
544
|
+
read as displacement) draws dashed. Through the rails runs the melody itself,
|
|
545
|
+
compressed to the chant's ambitus and wearing the same pressure grammar, a
|
|
546
|
+
lighter stratum — context, not message.
|
|
547
|
+
A **cadence is the melody's own ending re-inked black**: the same curve at
|
|
548
|
+
the same width turns pure black across the cadential figure and lands on a
|
|
549
|
+
terminal node — filled when the family's measured `finality` closes, open
|
|
550
|
+
when it suspends. Beneath the node, centred on it, sits the family's
|
|
551
|
+
**in-mode share**: `"3.9%"`, how often this close ends a chant in this
|
|
552
|
+
chant's mode — the frequency a singer actually meets it at. Where the chant
|
|
553
|
+
has no mode, or the family has too few occurrences in it to divide honestly,
|
|
554
|
+
the label falls back to the plain corpus share, which is the same kind of
|
|
555
|
+
number.
|
|
556
|
+
|
|
557
|
+
**Every inked cadence carries a label.** A close that does not join
|
|
558
|
+
[`CADENTIAE`](index.md#the-appendix) at all reads `"rara"` — not a gap but a
|
|
559
|
+
measurement: the catalogue holds the 110 families above fifty corpus
|
|
560
|
+
occurrences, so failing to join means rarer than anything it records. About
|
|
561
|
+
44% of cadences land there.
|
|
562
|
+
|
|
563
|
+
`rara` is a word rather than a number, so it is not read on the percentage
|
|
564
|
+
scale beside it.
|
|
565
|
+
|
|
566
|
+
The lift rides the group as `data-lift` for a caller who wants distinctiveness
|
|
567
|
+
rather than frequency, beside `data-cadentia` — the family key, which is the
|
|
568
|
+
join back to [`CADENTIAE`](index.md#the-appendix) and the provenance a margin
|
|
569
|
+
gloss can print. Crowded labels dodge to a second row.
|
|
570
|
+
|
|
571
|
+
Everywhere, confidence is opacity, and a claim below confidence 0.45 draws
|
|
572
|
+
nothing — weak claims are not inked. Every mark sits under the notation that
|
|
573
|
+
would falsify it.
|
|
574
|
+
|
|
575
|
+
## The imprint
|
|
576
|
+
|
|
577
|
+
Both `Score` and `Harmony` expose `imprint: Imprint`, analytic
|
|
578
|
+
fingerprints computed from different inputs: unweighted pitch-class counts
|
|
579
|
+
from chant phrases, presence-weighted voiced bodies from the sky. The two
|
|
580
|
+
are comparable.
|
|
581
|
+
|
|
582
|
+
```js
|
|
583
|
+
score.imprint.attractors[0];
|
|
584
|
+
// { pc: 0, weight: 0.39, pitch: { spn: "C4", … } }
|
|
585
|
+
|
|
586
|
+
score.imprint.modalAffinity.slice(0, 2);
|
|
587
|
+
// [ { mode: 7, alias: "mixolydian", score: 2.64 },
|
|
588
|
+
// { mode: 8, alias: "hypomixolydian", score: 2.18 } ]
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
The ranking reads three signals beyond the pitch-class distribution: the opening
|
|
592
|
+
note (each mode's initials, in Rockstro's ordering), the closing note (a chant
|
|
593
|
+
rests on its final, the treatises' first determinant of mode), and the tessitura
|
|
594
|
+
(how high the melody sits above its final, the classical authentic/plagal
|
|
595
|
+
separator). Together these rank the labelled mode first for a typical chant, its
|
|
596
|
+
plagal/authentic twin usually second. _Puer natus est_ (mode 7) leads with 7,
|
|
597
|
+
then its plagal twin 8.
|
|
598
|
+
|
|
599
|
+
It remains a measurement, not a confirmation: a transposed or mislabelled chant
|
|
600
|
+
will not rank its nominal mode first, which is itself a useful signal.
|
|
601
|
+
Conformance against the declared mode is read directly:
|
|
602
|
+
|
|
603
|
+
```js
|
|
604
|
+
const declared = parseInt(score.chant.mode, 10);
|
|
605
|
+
score.imprint.modalAffinity.find((m) => m.mode === declared).score;
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
```ts
|
|
609
|
+
interface Imprint {
|
|
610
|
+
pcDistribution: Record<number, number>; // fractions sum to 1
|
|
611
|
+
attractors: Attractor[]; // top pitch classes, tuned
|
|
612
|
+
vowelAttractors: VowelAttractor[]; // vowel-weighted resonances, tuned
|
|
613
|
+
modalAffinity: ModalAffinity[]; // all 8 modes ranked by fit
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
interface Attractor {
|
|
617
|
+
pc: number; // pitch class 0–11
|
|
618
|
+
weight: number; // normalized 0–1
|
|
619
|
+
pitch: Pitch; // tuned through the score/harmony's temperamentum
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
interface VowelAttractor {
|
|
623
|
+
vowel: string; // "a" | "e" | "i" | "o" | "u"
|
|
624
|
+
weight: number; // fraction of total vowel weight
|
|
625
|
+
pitch: Pitch; // the vowel's most-associated tuned pitch
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
interface ModalAffinity {
|
|
629
|
+
mode: number; // 1–8
|
|
630
|
+
alias: string; // "dorian" | "hypodorian" | …
|
|
631
|
+
score: number; // pc-distribution weight against mode's structural tones
|
|
632
|
+
}
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
## Prosody
|
|
636
|
+
|
|
637
|
+
`score.prosody` measures the chant's shape — counts, range, melisma,
|
|
638
|
+
melodic motion, contour, tessitura, rhythm, cadence. It is chant-specific;
|
|
639
|
+
`Harmony` has no prosody. For _Puer natus est_: ambitus 10 semitones, melisma
|
|
640
|
+
ratio 2.04 notes per syllable, tessitura ~5 semitones above the final, a near-
|
|
641
|
+
perfect melodic arch, mostly stepwise motion (leap rate ~5%).
|
|
642
|
+
|
|
643
|
+
```ts
|
|
644
|
+
interface Prosody {
|
|
645
|
+
noteCount: number;
|
|
646
|
+
syllableCount: number;
|
|
647
|
+
phraseCount: number;
|
|
648
|
+
noteRange: NoteRange | null;
|
|
649
|
+
ambitus: number | null;
|
|
650
|
+
melismaRatio: number; // notes ÷ syllables, whole score
|
|
651
|
+
melismaByPhrase: number[]; // per-phrase melisma density
|
|
652
|
+
melismaCadential: number; // mean notes on each phrase's final syllable
|
|
653
|
+
tessitura: number | null; // mean pitch − final, in semitones
|
|
654
|
+
intervals: IntervalStats; // melodic motion over adjacent within-phrase notes
|
|
655
|
+
arcus: Arcus | null; // the melodic arch
|
|
656
|
+
ictusRate: number;
|
|
657
|
+
rhythmicProfile: RhythmicProfile;
|
|
658
|
+
cadenceWeight: number;
|
|
659
|
+
cadenceDistribution: CadenceDistribution;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
interface IntervalStats {
|
|
663
|
+
histogram: Record<number, number>; // signed semitone interval → count
|
|
664
|
+
maxLeap: number; // largest absolute interval (semitones)
|
|
665
|
+
leapRate: number; // fraction of motions that are leaps (a 4th+)
|
|
666
|
+
motus: { step: number; skip: number; leap: number }; // 1–2 st / 3–4 / 5+
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
interface Arcus {
|
|
670
|
+
initial: number; // first note MIDI
|
|
671
|
+
peak: number; // highest note MIDI
|
|
672
|
+
final: number; // last note MIDI
|
|
673
|
+
archIndex: number; // signed: +1 rises and returns, 0 flat/monotonic
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
interface NoteRange {
|
|
677
|
+
min: number;
|
|
678
|
+
max: number;
|
|
679
|
+
span: number;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
interface RhythmicProfile {
|
|
683
|
+
arsic: number; // count of arsic notes across the score
|
|
684
|
+
thetic: number; // count of thetic notes across the score
|
|
685
|
+
avgGroupSize: number; // mean notes per compound beat
|
|
686
|
+
maxGroupSize: number; // largest compound beat observed
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
interface CadenceDistribution {
|
|
690
|
+
comma: number; // divisio minima
|
|
691
|
+
tick: number; // virgula
|
|
692
|
+
semicolon: number; // divisio minor
|
|
693
|
+
colon: number; // divisio maior
|
|
694
|
+
doubleBar: number; // divisio finalis
|
|
695
|
+
}
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
## Cadences
|
|
699
|
+
|
|
700
|
+
`score.cadences` names the melodic close of each phrase — where prosody
|
|
701
|
+
only counts the divisio bars, this identifies the figure. One `Cadence` per
|
|
702
|
+
phrase-ending divisio: its resolution `target`, the melodic `approach`, and the
|
|
703
|
+
`divisio` that tells medial from final (the double bar `::` is the final
|
|
704
|
+
cadence). Each note that forms a cadence carries a `cadenceRef` back-index on
|
|
705
|
+
the tabula.
|
|
706
|
+
|
|
707
|
+
### One spine, two annotations
|
|
708
|
+
|
|
709
|
+
Two catalogues describe a cadence, and they answer different questions. Read
|
|
710
|
+
this before deciding which field to use:
|
|
711
|
+
|
|
712
|
+
> Every cadence carries a **`signature`** — always. Some are **catalogued** by
|
|
713
|
+
> the corpus (`finality`, and everything in
|
|
714
|
+
> [`CADENTIAE`](index.md#the-appendix)). Some, on the final, are **named** by
|
|
715
|
+
> received theory (`formula`).
|
|
716
|
+
|
|
717
|
+
- **`formula`** is _tradita_: the mode's cadence figures as the treatises give
|
|
718
|
+
them ([tuning.md](tuning.md#cadence-figures)), matched in solmization
|
|
719
|
+
relative to the final — `"la-sol"`, `"mi-re"`. It fires **only on the
|
|
720
|
+
finalis**, because the received catalogue holds only final figures.
|
|
721
|
+
- **`signature`** is _inventa_: the tail's interval shape and where it lands,
|
|
722
|
+
keyed as `"2,0,-2 @0"` and mined from the corpus. It fires on **any** target,
|
|
723
|
+
so it is the one of the two that speaks about **medial** cadences.
|
|
724
|
+
|
|
725
|
+
Measured over the cadences `notatio` reports across the shipped corpus — about
|
|
726
|
+
20,500 of them — roughly 43% carry a formula, 56% join the catalogue, 31% carry
|
|
727
|
+
both, and 44% fall outside it. Neither is derivable from the other, because the
|
|
728
|
+
signature is mode-blind and the formula is mode-relative.
|
|
729
|
+
|
|
730
|
+
### `finality` — how often this family closes
|
|
731
|
+
|
|
732
|
+
`finality` is the share of **this family's** corpus occurrences that fall at a
|
|
733
|
+
final close. It is a measurement, not a property of this particular cadence,
|
|
734
|
+
and it cannot be read off the signature: of the 50 families that land **on**
|
|
735
|
+
the final, 31 do not close, and finality across the catalogue runs the whole
|
|
736
|
+
range from 0 to 1. So
|
|
737
|
+
`arrival === 0` implies nothing about whether a close is final.
|
|
738
|
+
|
|
739
|
+
It is `null` when the signature falls below the catalogue's floor — an
|
|
740
|
+
uncatalogued close, not a close that never closes.
|
|
741
|
+
|
|
742
|
+
```ts
|
|
743
|
+
interface Cadence {
|
|
744
|
+
phraseIndex: number;
|
|
745
|
+
divisio: string; // the bar that ends the phrase ("::" = final cadence)
|
|
746
|
+
target: "finalis" | "tenor" | "other";
|
|
747
|
+
approach: "descending" | "ascending" | "unison";
|
|
748
|
+
formula: string | null; // tradita: matched figure id, e.g. "la-sol"; finalis only
|
|
749
|
+
pcs: number[]; // observed pitch classes, resolution last
|
|
750
|
+
steps: (number | null)[]; // diatonic steps from the target; [] with no mode
|
|
751
|
+
confidence: number; // 0–1
|
|
752
|
+
notes: [number, number, number][]; // [phrase, syllable, note] positions
|
|
753
|
+
signature: string | null; // inventa: the family key, "shape @arrival"
|
|
754
|
+
shape: number[]; // the tail's successive semitone intervals
|
|
755
|
+
arrival: number; // SIGNED semitones from the chant's own closing note
|
|
756
|
+
finality: number | null; // the family's measured finality; null below the floor
|
|
757
|
+
}
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
A one-note phrase is a cadence — a landing with no gesture — and keys with an
|
|
761
|
+
empty shape (`" @0"`), which is why `signature` is that key rather than null.
|
|
762
|
+
|
|
763
|
+
`arrival` is signed and not octave-reduced: `@-5`, a fourth below the final,
|
|
764
|
+
and `@+7`, a fifth above, are distinct families.
|
|
765
|
+
|
|
766
|
+
## Modulations
|
|
767
|
+
|
|
768
|
+
`score.modulations` marks where the tonal centre leans away from the home
|
|
769
|
+
mode — the local, temporal counterpart to the imprint's global modal
|
|
770
|
+
affinity. Each phrase is scored against all eight modes (the imprint's
|
|
771
|
+
affinity math); a run of phrases that favours a foreign mode, by a margin,
|
|
772
|
+
becomes one `Modulation` span. The margin is calibrated against Suñol's
|
|
773
|
+
worked examples (_Christus resurgens_ modulates toward mode 3). It is
|
|
774
|
+
distribution-based: it finds where a passage leans, not a functional analysis.
|
|
775
|
+
|
|
776
|
+
`kind` says what the span is evidence OF, which matters because the three are
|
|
777
|
+
not the same phenomenon. **`inflection`** is a single phrase leaning away and
|
|
778
|
+
back — passing colour, not a shift. **`modulation`** is a sustained internal
|
|
779
|
+
excursion, two phrases or more, that returns. **`transposition`** is the whole
|
|
780
|
+
chant sitting in a foreign mode's frame: it does not close on its labelled
|
|
781
|
+
final and one foreign mode dominates most of its phrases, meaning the melody is
|
|
782
|
+
notated at a transposed position (the affinal) or the label disagrees with the
|
|
783
|
+
notation. A transposed chant is not modulating — the displacement is global —
|
|
784
|
+
so a caller displaying "modulations" should treat those spans as a re-reading of
|
|
785
|
+
the whole chant rather than an event inside it.
|
|
786
|
+
|
|
787
|
+
```ts
|
|
788
|
+
interface Modulation {
|
|
789
|
+
startPhrase: number; // first phrase of the span (inclusive)
|
|
790
|
+
endPhrase: number; // last phrase (inclusive)
|
|
791
|
+
toMode: number; // the mode the passage leans toward (1–8)
|
|
792
|
+
confidence: number; // 0–1, the averaged margin over the home mode
|
|
793
|
+
kind: "inflection" | "modulation" | "transposition";
|
|
794
|
+
}
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
## Theory & Context
|
|
798
|
+
|
|
799
|
+
The rhythm model is the Solesmes school's arsis/thesis synthesis, taken
|
|
800
|
+
from Gajard's lectures and Carroll's chironomy manuals. The full
|
|
801
|
+
treatise-level model lives at the classifier in
|
|
802
|
+
[`score/ir.ts`](https://github.com/jeffreypierce/tonus/blob/main/src/engines/score/ir.ts), which also derives Le Guennant's
|
|
803
|
+
incise rhythmic types ([below](#rhythmic-types)).
|
|
804
|
+
|
|
805
|
+
### The model
|
|
806
|
+
|
|
807
|
+
Arsis and thesis are properties of the **compound beat**, the group of
|
|
808
|
+
notes between one **ictus** and the next, not of single notes. Every
|
|
809
|
+
note in the group shares its quality, arsic (rising) or thetic (resting).
|
|
810
|
+
The ictus marks the grouping and is not itself an accent, which is why
|
|
811
|
+
tonus stores the quality as `Performance.rhythmicShape` rather than as a
|
|
812
|
+
velocity signal. Phrases, bounded by any divisio, serve as the
|
|
813
|
+
**incise**, the unit within which rhythm is judged.
|
|
814
|
+
|
|
815
|
+
### The classification rules
|
|
816
|
+
|
|
817
|
+
The classifier applies Carroll's three melodic rules in priority order
|
|
818
|
+
(_Chironomy_ Ch. 4):
|
|
819
|
+
|
|
820
|
+
1. **Incise unity.** Ictuses before the melodic apex of the incise are
|
|
821
|
+
arsic; after it, thetic. The apex is the incise's highest-pitched
|
|
822
|
+
ictus.
|
|
823
|
+
2. **Relative ictus pitch.** An ictus higher than the one before it tends
|
|
824
|
+
arsic; lower tends thetic.
|
|
825
|
+
3. **Neume slope.** When the first two are inconclusive, rising notes are
|
|
826
|
+
arsic, falling thetic.
|
|
827
|
+
|
|
828
|
+
The first compound beat of an incise is always arsic. When every rule is inconclusive, the
|
|
829
|
+
shape alternates from the previous group. Two conventional overrides
|
|
830
|
+
precede the rules: the **salicus** is always arsic — the tension toward its
|
|
831
|
+
summit is the arsic gesture — and the **doubly-dotted clivis** is always
|
|
832
|
+
thetic, as a cadential figure.
|
|
833
|
+
|
|
834
|
+
A salicus here is Cardine's: an ascent of at least three notes whose
|
|
835
|
+
**next-to-last note is an oriscus** [biblio: cardine-semiology, ch. 16]. The
|
|
836
|
+
oriscus is what makes one. An ascending group carrying only the editorial
|
|
837
|
+
Solesmes ictus is a **scandicus** that was marked for rhythm — a distinction
|
|
838
|
+
worth stating because conflating the two is, in Bevenot's word, a trap: over
|
|
839
|
+
the sung corpus tonus finds about 260 salici against about 1,900 scandici, so
|
|
840
|
+
only about an eighth of that wider set carries an oriscus at all.
|
|
841
|
+
|
|
842
|
+
Cardine's correction also decides WHICH note is principal. The printed
|
|
843
|
+
editions lengthen the oriscus itself; the manuscripts show the principal note
|
|
844
|
+
is the one **immediately following** it — the summit — so tonus prolongs that
|
|
845
|
+
note and takes the oriscus lightly. This is the one point where the rhythmic
|
|
846
|
+
layer departs from Mocquereau and Suñol, and it does so deliberately.
|
|
847
|
+
|
|
848
|
+
### Rhythmic types
|
|
849
|
+
|
|
850
|
+
Above the per-beat arsis/thesis, each phrase carries a `rhythmicType` — Le
|
|
851
|
+
Guennant's taxonomy (via Carroll) of how the incise's compound beats chain, and
|
|
852
|
+
the `beats` sequence it reads. The observable types are modeled: **IV** (a single
|
|
853
|
+
arsis to a single thesis), **V** (several arses to one thesis), **VI** (one arsis
|
|
854
|
+
to several theses), **VII** (regular A–T alternation), and **VIII** (a
|
|
855
|
+
contraction — two simple rhythms overlapping at a shared ictus, after Suñol).
|
|
856
|
+
Types I–III use sub-beat cells that never surface in isolation and are not
|
|
857
|
+
labeled; an incise that fits no type is `null`. The classification rules live at
|
|
858
|
+
the data — see `classifyRhythmicType` in
|
|
859
|
+
[`score/ir.ts`](https://github.com/jeffreypierce/tonus/blob/main/src/engines/score/ir.ts).
|
|
860
|
+
|
|
861
|
+
### Modeled and not
|
|
862
|
+
|
|
863
|
+
tonus models the compound-beat classification, the per-note rhythmic index,
|
|
864
|
+
mode-specific cadence figures ([above](#cadences)), and the incise rhythmic types
|
|
865
|
+
(above). It does not yet model Carroll's textual rules (word-accent → arsic,
|
|
866
|
+
word-final → thetic) or accentual (spondaic vs. dactylic) cadences.
|
|
867
|
+
|
|
868
|
+
## Sources
|
|
869
|
+
|
|
870
|
+
Sources for this page are in the central [bibliography](https://github.com/jeffreypierce/tonus/blob/main/BIBLIOGRAPHY.md):
|
|
871
|
+
`carroll-chironomy`, `carroll-applied`, `gajard-rhythm`, `mocquereau-nombre`,
|
|
872
|
+
`cardine-semiology`, `desrocquettes-values`, `sunol-textbook`, `homan-cadence`,
|
|
873
|
+
`pierik-spirit`, `apel-chant`, `liber-usualis`, `bravura-smufl`.
|