tonus 0.1.8 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/BIBLIOGRAPHY.md +138 -108
  2. package/CHANGELOG.md +664 -1
  3. package/LICENSE +133 -29
  4. package/README.md +107 -80
  5. package/dist/data/am.js +2666 -11196
  6. package/dist/data/ams.d.ts +5 -0
  7. package/dist/data/ams.js +122 -0
  8. package/dist/data/attestation.d.ts +19 -0
  9. package/dist/data/attestation.js +15716 -0
  10. package/dist/data/attestation.json +15711 -0
  11. package/dist/data/cadentiae.d.ts +43 -0
  12. package/dist/data/cadentiae.js +174 -0
  13. package/dist/data/cal.js +48 -0
  14. package/dist/data/census.d.ts +12 -0
  15. package/dist/data/census.js +36 -0
  16. package/dist/data/commune-office.d.ts +4 -0
  17. package/dist/data/commune-office.js +2371 -0
  18. package/dist/data/commune-office.json +2365 -0
  19. package/dist/data/commune.js +181 -11
  20. package/dist/data/corpus-overlap.d.ts +17 -0
  21. package/dist/data/corpus-overlap.js +299 -5
  22. package/dist/data/cot.d.ts +5 -0
  23. package/dist/data/cot.js +172 -0
  24. package/dist/data/cse.d.ts +5 -0
  25. package/dist/data/cse.js +122 -0
  26. package/dist/data/gabc-glyphs.d.ts +45 -0
  27. package/dist/data/gabc-glyphs.js +122 -0
  28. package/dist/data/gr.js +754 -6394
  29. package/dist/data/kyriale.js +116 -116
  30. package/dist/data/la.js +799 -13419
  31. package/dist/data/lh.js +113 -3473
  32. package/dist/data/lu.js +931 -17631
  33. package/dist/data/nocturnale-romanum.js +1659 -10411
  34. package/dist/data/office-ferial.d.ts +4 -0
  35. package/dist/data/office-ferial.js +396 -0
  36. package/dist/data/office-ferial.json +391 -0
  37. package/dist/data/office-monastic.d.ts +17 -1
  38. package/dist/data/office-monastic.js +1403 -466
  39. package/dist/data/office-psalms-monastic.d.ts +13 -1
  40. package/dist/data/office-psalms-monastic.js +9 -0
  41. package/dist/data/propers.js +1 -1
  42. package/dist/data/psalms.js +22919 -5
  43. package/dist/data/psm.d.ts +5 -0
  44. package/dist/data/psm.js +122 -0
  45. package/dist/data/seasonal-respbreve.d.ts +5 -0
  46. package/dist/data/seasonal-respbreve.js +41 -0
  47. package/dist/data/seasonal-respbreve.json +35 -0
  48. package/dist/data/smufl-glyphs.d.ts +17 -0
  49. package/dist/data/smufl-glyphs.js +1546 -0
  50. package/dist/data/smufl-glyphs.json +1530 -0
  51. package/dist/engines/cal/calendar.d.ts +3 -2
  52. package/dist/engines/cal/calendar.js +105 -29
  53. package/dist/engines/cal/data/eras.d.ts +35 -0
  54. package/dist/engines/cal/data/eras.js +128 -0
  55. package/dist/engines/cal/date.js +44 -0
  56. package/dist/engines/cal/types.d.ts +15 -3
  57. package/dist/engines/cal/types.js +5 -5
  58. package/dist/engines/census/census.d.ts +7 -0
  59. package/dist/engines/census/census.js +179 -0
  60. package/dist/engines/census/types.d.ts +55 -0
  61. package/dist/engines/census/types.js +8 -0
  62. package/dist/engines/chant/attest.d.ts +39 -0
  63. package/dist/engines/chant/attest.js +90 -0
  64. package/dist/engines/chant/chant.d.ts +16 -4
  65. package/dist/engines/chant/chant.js +220 -30
  66. package/dist/engines/chant/data/compline.js +2 -1
  67. package/dist/engines/chant/data/masses.d.ts +56 -4
  68. package/dist/engines/chant/data/masses.js +305 -80
  69. package/dist/engines/chant/data/prime.js +1 -1
  70. package/dist/engines/chant/hour.js +279 -58
  71. package/dist/engines/chant/ordinary.d.ts +2 -0
  72. package/dist/engines/chant/ordinary.js +336 -56
  73. package/dist/engines/chant/propers.js +55 -5
  74. package/dist/engines/chant/psalm.d.ts +4 -4
  75. package/dist/engines/chant/psalm.js +25 -11
  76. package/dist/engines/chant/syllabify.d.ts +1 -0
  77. package/dist/engines/chant/syllabify.js +90 -17
  78. package/dist/engines/chant/types.d.ts +115 -12
  79. package/dist/engines/chant/types.js +38 -3
  80. package/dist/engines/harmonia/api.js +4 -0
  81. package/dist/engines/harmonia/data/doctrines.js +3 -1
  82. package/dist/engines/harmonia/data/zodiac.d.ts +47 -0
  83. package/dist/engines/harmonia/data/zodiac.js +163 -0
  84. package/dist/engines/harmonia/tabula.d.ts +3 -0
  85. package/dist/engines/harmonia/tabula.js +1 -0
  86. package/dist/engines/harmonia/voice.d.ts +4 -0
  87. package/dist/engines/harmonia/voice.js +8 -4
  88. package/dist/engines/imprint.js +14 -1
  89. package/dist/engines/planet/orbital.js +4 -4
  90. package/dist/engines/planet/planet.d.ts +10 -0
  91. package/dist/engines/planet/planet.js +30 -3
  92. package/dist/engines/planet/position.js +13 -10
  93. package/dist/engines/planet/types.d.ts +1 -0
  94. package/dist/engines/score/api.d.ts +2 -13
  95. package/dist/engines/score/api.js +21 -8
  96. package/dist/engines/score/articulation.js +2 -2
  97. package/dist/engines/score/cadence.d.ts +76 -0
  98. package/dist/engines/score/cadence.js +96 -0
  99. package/dist/engines/score/emitters/accidentals.d.ts +21 -0
  100. package/dist/engines/score/emitters/accidentals.js +88 -0
  101. package/dist/engines/score/emitters/atramentum.d.ts +107 -0
  102. package/dist/engines/score/emitters/atramentum.js +239 -0
  103. package/dist/engines/score/emitters/breaking.d.ts +62 -0
  104. package/dist/engines/score/emitters/breaking.js +80 -0
  105. package/dist/engines/score/emitters/moderna.d.ts +38 -0
  106. package/dist/engines/score/emitters/moderna.js +612 -0
  107. package/dist/engines/score/emitters/svg.d.ts +143 -0
  108. package/dist/engines/score/emitters/svg.js +1335 -0
  109. package/dist/engines/score/emitters/tracks.d.ts +104 -0
  110. package/dist/engines/score/emitters/tracks.js +728 -0
  111. package/dist/engines/score/infer.d.ts +3 -3
  112. package/dist/engines/score/infer.js +2 -2
  113. package/dist/engines/score/inscriptio.d.ts +69 -0
  114. package/dist/engines/score/inscriptio.js +138 -0
  115. package/dist/engines/score/ir.d.ts +2 -2
  116. package/dist/engines/score/ir.js +59 -12
  117. package/dist/engines/score/lyric.d.ts +23 -0
  118. package/dist/engines/score/lyric.js +234 -0
  119. package/dist/engines/score/meta.d.ts +2 -2
  120. package/dist/engines/score/modulation.d.ts +12 -0
  121. package/dist/engines/score/modulation.js +49 -0
  122. package/dist/engines/score/neume.js +35 -4
  123. package/dist/engines/score/parse.js +150 -9
  124. package/dist/engines/score/phrasing.js +4 -3
  125. package/dist/engines/score/prosody.d.ts +38 -0
  126. package/dist/engines/score/prosody.js +70 -6
  127. package/dist/engines/score/tabula.d.ts +37 -5
  128. package/dist/engines/score/tabula.js +18 -0
  129. package/dist/engines/score/types.d.ts +88 -1
  130. package/dist/engines/temper/api.d.ts +4 -1
  131. package/dist/engines/temper/api.js +28 -5
  132. package/dist/engines/temper/data/guido.js +6 -2
  133. package/dist/engines/temper/data/modes.d.ts +6 -0
  134. package/dist/engines/temper/data/modes.js +42 -0
  135. package/dist/engines/temper/data/tones.d.ts +1 -1
  136. package/dist/engines/temper/data/tones.js +20 -11
  137. package/dist/engines/temper/interval.js +4 -3
  138. package/dist/engines/temper/modality.d.ts +11 -2
  139. package/dist/engines/temper/modality.js +74 -2
  140. package/dist/engines/temper/modes.d.ts +1 -1
  141. package/dist/engines/temper/pitch.d.ts +1 -1
  142. package/dist/engines/temper/pitch.js +12 -2
  143. package/dist/engines/temper/scale.d.ts +53 -0
  144. package/dist/engines/temper/scale.js +107 -8
  145. package/dist/index.d.ts +26 -8
  146. package/dist/index.js +40 -4
  147. package/docs/api/calendar.md +279 -0
  148. package/docs/api/census.md +288 -0
  149. package/docs/api/chant.md +657 -0
  150. package/docs/api/heavens.md +396 -0
  151. package/docs/api/index.md +265 -0
  152. package/docs/api/score.md +906 -0
  153. package/docs/api/tuning.md +619 -0
  154. package/package.json +13 -5
  155. package/dist/data/office-matins-roman.d.ts +0 -19
  156. package/dist/data/office-matins-roman.js +0 -4383
  157. package/dist/data/office-psalms-roman.d.ts +0 -15
  158. package/dist/data/office-psalms-roman.js +0 -28
  159. package/dist/data/office-roman.d.ts +0 -19
  160. package/dist/data/office-roman.js +0 -13792
  161. package/dist/engines/chant/matutinum.d.ts +0 -33
  162. package/dist/engines/chant/matutinum.js +0 -81
  163. package/dist/engines/score/emitters/midi.d.ts +0 -65
  164. package/dist/engines/score/emitters/midi.js +0 -162
  165. package/dist/engines/score/emitters/musicxml.d.ts +0 -18
  166. package/dist/engines/score/emitters/musicxml.js +0 -166
@@ -0,0 +1,906 @@
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
+ #### The recommended face — Junicode
450
+
451
+ The plates are drawn in
452
+ **[Junicode](https://github.com/psb1558/Junicode-font)** (OFL), which the house
453
+ stack already asks for first. Load it and add the dress:
454
+
455
+ ```css
456
+ @font-face {
457
+ font-family: "Junicode";
458
+ src: url("JunicodeVF-Roman.woff2") format("woff2-variations");
459
+ font-weight: 300 700;
460
+ }
461
+ ```
462
+
463
+ ```js
464
+ tonus.inscriptio(score, {
465
+ width: 900,
466
+ theme: {
467
+ fonts: {
468
+ dropcap: { family: "Junicode", weight: 700 },
469
+ title: { family: "Junicode", weight: 620 },
470
+ annotation: { family: "Junicode", weight: 640 },
471
+ lyric: { family: "Junicode", weight: 560, scale: 1.06 },
472
+ },
473
+ },
474
+ dropcap: true, annotation: "auto", // a role shows only once its feature is on
475
+ });
476
+ ```
477
+
478
+ Reference it rather than `embed` it unless one SVG must travel alone: the face
479
+ is 196 KB base64'd, which triples a typical chant and repeats in every file,
480
+ where a reference is cached once.
481
+
482
+ **`colors`** reach the SVG as CSS custom properties with the theme's own value
483
+ as the fallback — `fill="var(--tonus-note, #111)"`. A rendered chant therefore
484
+ carries the ink it was drawn with *and* stays themable: a host stylesheet that
485
+ sets the property rethemes the score without re-rendering it.
486
+
487
+ ```css
488
+ /* the page follows its own tokens; the chant follows the page */
489
+ .score svg {
490
+ --tonus-note: var(--ink);
491
+ --tonus-staff-line: var(--ink);
492
+ --tonus-rubrica: var(--rubrica);
493
+ }
494
+ ```
495
+
496
+ The emitter's semantic classes — `note`, `lyric`, `dropcap`, `custos`,
497
+ `episema`, `divisio`, `clef`, `mora`, `ictus` — are stylable from the host page.
498
+
499
+ **`scale` is not part of the theme**: line breaking consumes it, so a scale
500
+ change re-renders while a colour change does not.
501
+
502
+ Nothing else about the layout is a caller's decision. The margin, the air
503
+ between systems, the notehead calibration against the staff, and the line-end
504
+ custos are constants. The custos appears whenever a system wraps, as it does in
505
+ a chant book.
506
+
507
+ **The geometry contract (public API).** `geometry` is one `NoteGeometry` per note,
508
+ in tabula order — the interface analysis _tracks_ build on, so they place marks
509
+ by index and coordinate instead of scraping the SVG. The library's own tracks
510
+ (below) consume exactly these anchors; a custom track downstream does the same:
511
+
512
+ ```ts
513
+ interface NoteGeometry {
514
+ phraseIndex: number; syllableIndex: number; neumeGroup: number; noteIndex: number;
515
+ system: number; // which wrapped system the note landed in
516
+ x: number; y: number; // notehead anchor, svg user units
517
+ systemY: number; // the system's top offset within the svg
518
+ }
519
+ ```
520
+
521
+ ### The analysis tracks
522
+
523
+ `tracks` draws an analysis band beneath every system. Any track rides either
524
+ species, and all may ride one score — the selection is independent of the
525
+ notation, as `notation` itself is. One governing ink system runs through them:
526
+ every mark draws in the score's black, strata graded by opacity alone (the
527
+ liturgical red belongs to the claims — the tonarium's mode line and the
528
+ prosodia's accent dots), and every pressure-bearing line shares one nib law —
529
+ velocity as stroke width.
530
+
531
+ ```js
532
+ tonus.inscriptio(score, { width: 680, tracks: ["prosodia"] });
533
+ tonus.inscriptio(score, { width: 680, tracks: ["chironomia"] });
534
+ tonus.inscriptio(score, { notation: "moderna", width: 680, tracks: ["tonarium"] });
535
+ tonus.inscriptio(score, { width: 680, tracks: ["chironomia", "tonarium"] }); // stacked
536
+ tonus.inscriptio(score, { width: 680, tracks: ["prosodia", "chironomia", "tonarium"] });
537
+ ```
538
+
539
+ The conventional pairing is the chironomia under `quadrata` and the tonarium
540
+ under `moderna`; the prosodia, reading the text rather than the notation,
541
+ rides either as naturally. The renderer enforces none of it.
542
+
543
+ Requesting several stacks them in a fixed order — the prosodia first, directly
544
+ under the lyric line it reads; the chironomia next; the tonarium below —
545
+ whichever order they are asked for, and the page grows by the sum of the
546
+ bands.
547
+
548
+ - **`"prosodia"`** — how the melody treats the word, in two lanes. The upper
549
+ lane draws one **tent per word** — the hairpin pair's top edge, dynamics'
550
+ own mark for swell and release — its apex over the accented syllable, with
551
+ the accent's landing at the peak in the liturgical red: a **filled dot**
552
+ when the accent lands arsic (struck), an **open ring** when it lands thetic
553
+ (deferred). Accented words rise past the lane's single rule; unaccented
554
+ words crest on it. The lower lane is a fence on a rail, one mark per
555
+ syllable by how the melody treats it: a spoken syllable stands as a stem
556
+ (height, its notes), a syllable **recited on the tenor lies flat** — a
557
+ short dash floating above the rail — and a **melisma of four notes or more
558
+ becomes a block** as wide as its real extent and as tall as its count
559
+ (counts of eight or more print inside). Connected melismas join into one
560
+ ridge, each block's top sloping toward its neighbours, the line between
561
+ them crossing the gaps. A divisio drops a hairline through both lanes.
562
+ Accents are the book's written accents (the GABC accented vowels) — the
563
+ track derives none.
564
+ - **`"chironomia"`** — the conducting hand as one continuous line:
565
+ arsic beats crest, thetic beats trough, single-note theses pass through
566
+ shallow, and the hand picks up between close arses in a small backward loop
567
+ [biblio: carroll-chironomy]. Pressure is the stroke's _weight_: each note's
568
+ `velocity` (the `accentus` shaping) becomes nib width over solid ink, so the
569
+ line presses where the voice does. Pierik letters (A · T · PT) name the
570
+ beats — the incise's rhythmic shape is read straight off them.
571
+ - **`"tonarium"`** — the melodic-analysis lane, named for the book
572
+ that catalogued chants by mode. Four rails — the maneriae finals ladder, D on
573
+ the bottom (categories, not pitches) — carry the **mode line** in the
574
+ liturgical red: the governing mode of each phrase, its numeral above
575
+ (authentic-vs-plagal lives in the numeral). A modulation of kind
576
+ `"inflection"` steps the line solid; a `"transposition"` (the affinal frame
577
+ read as displacement) draws dashed. Through the rails runs the melody itself,
578
+ compressed to the chant's ambitus and wearing the same pressure grammar, a
579
+ lighter stratum — context, not message.
580
+ A **cadence is the melody's own ending re-inked black**: the same curve at
581
+ the same width turns pure black across the cadential figure and lands on a
582
+ terminal node — filled when the family's measured `finality` closes, open
583
+ when it suspends. Beneath the node, centred on it, sits the family's
584
+ **in-mode share**: `"3.9%"`, how often this close ends a chant in this
585
+ chant's mode — the frequency a singer actually meets it at. Where the chant
586
+ has no mode, or the family has too few occurrences in it to divide honestly,
587
+ the label falls back to the plain corpus share, which is the same kind of
588
+ number.
589
+
590
+ **Every inked cadence carries a label.** A close that does not join
591
+ [`CADENTIAE`](index.md#the-appendix) at all reads `"rara"` — not a gap but a
592
+ measurement: the catalogue holds the 110 families above fifty corpus
593
+ occurrences, so failing to join means rarer than anything it records. About
594
+ 44% of cadences land there.
595
+
596
+ `rara` is a word rather than a number, so it is not read on the percentage
597
+ scale beside it.
598
+
599
+ The lift rides the group as `data-lift` for a caller who wants distinctiveness
600
+ rather than frequency, beside `data-cadentia` — the family key, which is the
601
+ join back to [`CADENTIAE`](index.md#the-appendix) and the provenance a margin
602
+ gloss can print. Crowded labels dodge to a second row.
603
+
604
+ Everywhere, confidence is opacity, and a claim below confidence 0.45 draws
605
+ nothing — weak claims are not inked. Every mark sits under the notation that
606
+ would falsify it.
607
+
608
+ ## The imprint
609
+
610
+ Both `Score` and `Harmony` expose `imprint: Imprint`, analytic
611
+ fingerprints computed from different inputs: unweighted pitch-class counts
612
+ from chant phrases, presence-weighted voiced bodies from the sky. The two
613
+ are comparable.
614
+
615
+ ```js
616
+ score.imprint.attractors[0];
617
+ // { pc: 0, weight: 0.39, pitch: { spn: "C4", … } }
618
+
619
+ score.imprint.modalAffinity.slice(0, 2);
620
+ // [ { mode: 7, alias: "mixolydian", score: 2.64 },
621
+ // { mode: 8, alias: "hypomixolydian", score: 2.18 } ]
622
+ ```
623
+
624
+ The ranking reads three signals beyond the pitch-class distribution: the opening
625
+ note (each mode's initials, in Rockstro's ordering), the closing note (a chant
626
+ rests on its final, the treatises' first determinant of mode), and the tessitura
627
+ (how high the melody sits above its final, the classical authentic/plagal
628
+ separator). Together these rank the labelled mode first for a typical chant, its
629
+ plagal/authentic twin usually second. _Puer natus est_ (mode 7) leads with 7,
630
+ then its plagal twin 8.
631
+
632
+ It remains a measurement, not a confirmation: a transposed or mislabelled chant
633
+ will not rank its nominal mode first, which is itself a useful signal.
634
+ Conformance against the declared mode is read directly:
635
+
636
+ ```js
637
+ const declared = parseInt(score.chant.mode, 10);
638
+ score.imprint.modalAffinity.find((m) => m.mode === declared).score;
639
+ ```
640
+
641
+ ```ts
642
+ interface Imprint {
643
+ pcDistribution: Record<number, number>; // fractions sum to 1
644
+ attractors: Attractor[]; // top pitch classes, tuned
645
+ vowelAttractors: VowelAttractor[]; // vowel-weighted resonances, tuned
646
+ modalAffinity: ModalAffinity[]; // all 8 modes ranked by fit
647
+ }
648
+
649
+ interface Attractor {
650
+ pc: number; // pitch class 0–11
651
+ weight: number; // normalized 0–1
652
+ pitch: Pitch; // tuned through the score/harmony's temperamentum
653
+ }
654
+
655
+ interface VowelAttractor {
656
+ vowel: string; // "a" | "e" | "i" | "o" | "u"
657
+ weight: number; // fraction of total vowel weight
658
+ pitch: Pitch; // the vowel's most-associated tuned pitch
659
+ }
660
+
661
+ interface ModalAffinity {
662
+ mode: number; // 1–8
663
+ alias: string; // "dorian" | "hypodorian" | …
664
+ score: number; // pc-distribution weight against mode's structural tones
665
+ }
666
+ ```
667
+
668
+ ## Prosody
669
+
670
+ `score.prosody` measures the chant's shape — counts, range, melisma,
671
+ melodic motion, contour, tessitura, rhythm, cadence. It is chant-specific;
672
+ `Harmony` has no prosody. For _Puer natus est_: ambitus 10 semitones, melisma
673
+ ratio 2.04 notes per syllable, tessitura ~5 semitones above the final, a near-
674
+ perfect melodic arch, mostly stepwise motion (leap rate ~5%).
675
+
676
+ ```ts
677
+ interface Prosody {
678
+ noteCount: number;
679
+ syllableCount: number;
680
+ phraseCount: number;
681
+ noteRange: NoteRange | null;
682
+ ambitus: number | null;
683
+ melismaRatio: number; // notes ÷ syllables, whole score
684
+ melismaByPhrase: number[]; // per-phrase melisma density
685
+ melismaCadential: number; // mean notes on each phrase's final syllable
686
+ tessitura: number | null; // mean pitch − final, in semitones
687
+ intervals: IntervalStats; // melodic motion over adjacent within-phrase notes
688
+ arcus: Arcus | null; // the melodic arch
689
+ ictusRate: number;
690
+ rhythmicProfile: RhythmicProfile;
691
+ cadenceWeight: number;
692
+ cadenceDistribution: CadenceDistribution;
693
+ }
694
+
695
+ interface IntervalStats {
696
+ histogram: Record<number, number>; // signed semitone interval → count
697
+ maxLeap: number; // largest absolute interval (semitones)
698
+ leapRate: number; // fraction of motions that are leaps (a 4th+)
699
+ motus: { step: number; skip: number; leap: number }; // 1–2 st / 3–4 / 5+
700
+ }
701
+
702
+ interface Arcus {
703
+ initial: number; // first note MIDI
704
+ peak: number; // highest note MIDI
705
+ final: number; // last note MIDI
706
+ archIndex: number; // signed: +1 rises and returns, 0 flat/monotonic
707
+ }
708
+
709
+ interface NoteRange {
710
+ min: number;
711
+ max: number;
712
+ span: number;
713
+ }
714
+
715
+ interface RhythmicProfile {
716
+ arsic: number; // count of arsic notes across the score
717
+ thetic: number; // count of thetic notes across the score
718
+ avgGroupSize: number; // mean notes per compound beat
719
+ maxGroupSize: number; // largest compound beat observed
720
+ }
721
+
722
+ interface CadenceDistribution {
723
+ comma: number; // divisio minima
724
+ tick: number; // virgula
725
+ semicolon: number; // divisio minor
726
+ colon: number; // divisio maior
727
+ doubleBar: number; // divisio finalis
728
+ }
729
+ ```
730
+
731
+ ## Cadences
732
+
733
+ `score.cadences` names the melodic close of each phrase — where prosody
734
+ only counts the divisio bars, this identifies the figure. One `Cadence` per
735
+ phrase-ending divisio: its resolution `target`, the melodic `approach`, and the
736
+ `divisio` that tells medial from final (the double bar `::` is the final
737
+ cadence). Each note that forms a cadence carries a `cadenceRef` back-index on
738
+ the tabula.
739
+
740
+ ### One spine, two annotations
741
+
742
+ Two catalogues describe a cadence, and they answer different questions. Read
743
+ this before deciding which field to use:
744
+
745
+ > Every cadence carries a **`signature`** — always. Some are **catalogued** by
746
+ > the corpus (`finality`, and everything in
747
+ > [`CADENTIAE`](index.md#the-appendix)). Some, on the final, are **named** by
748
+ > received theory (`formula`).
749
+
750
+ - **`formula`** is _tradita_: the mode's cadence figures as the treatises give
751
+ them ([tuning.md](tuning.md#cadence-figures)), matched in solmization
752
+ relative to the final — `"la-sol"`, `"mi-re"`. It fires **only on the
753
+ finalis**, because the received catalogue holds only final figures.
754
+ - **`signature`** is _inventa_: the tail's interval shape and where it lands,
755
+ keyed as `"2,0,-2 @0"` and mined from the corpus. It fires on **any** target,
756
+ so it is the one of the two that speaks about **medial** cadences.
757
+
758
+ Measured over the cadences `notatio` reports across the shipped corpus — about
759
+ 20,500 of them — roughly 43% carry a formula, 56% join the catalogue, 31% carry
760
+ both, and 44% fall outside it. Neither is derivable from the other, because the
761
+ signature is mode-blind and the formula is mode-relative.
762
+
763
+ ### `finality` — how often this family closes
764
+
765
+ `finality` is the share of **this family's** corpus occurrences that fall at a
766
+ final close. It is a measurement, not a property of this particular cadence,
767
+ and it cannot be read off the signature: of the 50 families that land **on**
768
+ the final, 31 do not close, and finality across the catalogue runs the whole
769
+ range from 0 to 1. So
770
+ `arrival === 0` implies nothing about whether a close is final.
771
+
772
+ It is `null` when the signature falls below the catalogue's floor — an
773
+ uncatalogued close, not a close that never closes.
774
+
775
+ ```ts
776
+ interface Cadence {
777
+ phraseIndex: number;
778
+ divisio: string; // the bar that ends the phrase ("::" = final cadence)
779
+ target: "finalis" | "tenor" | "other";
780
+ approach: "descending" | "ascending" | "unison";
781
+ formula: string | null; // tradita: matched figure id, e.g. "la-sol"; finalis only
782
+ pcs: number[]; // observed pitch classes, resolution last
783
+ steps: (number | null)[]; // diatonic steps from the target; [] with no mode
784
+ confidence: number; // 0–1
785
+ notes: [number, number, number][]; // [phrase, syllable, note] positions
786
+ signature: string | null; // inventa: the family key, "shape @arrival"
787
+ shape: number[]; // the tail's successive semitone intervals
788
+ arrival: number; // SIGNED semitones from the chant's own closing note
789
+ finality: number | null; // the family's measured finality; null below the floor
790
+ }
791
+ ```
792
+
793
+ A one-note phrase is a cadence — a landing with no gesture — and keys with an
794
+ empty shape (`" @0"`), which is why `signature` is that key rather than null.
795
+
796
+ `arrival` is signed and not octave-reduced: `@-5`, a fourth below the final,
797
+ and `@+7`, a fifth above, are distinct families.
798
+
799
+ ## Modulations
800
+
801
+ `score.modulations` marks where the tonal centre leans away from the home
802
+ mode — the local, temporal counterpart to the imprint's global modal
803
+ affinity. Each phrase is scored against all eight modes (the imprint's
804
+ affinity math); a run of phrases that favours a foreign mode, by a margin,
805
+ becomes one `Modulation` span. The margin is calibrated against Suñol's
806
+ worked examples (_Christus resurgens_ modulates toward mode 3). It is
807
+ distribution-based: it finds where a passage leans, not a functional analysis.
808
+
809
+ `kind` says what the span is evidence OF, which matters because the three are
810
+ not the same phenomenon. **`inflection`** is a single phrase leaning away and
811
+ back — passing colour, not a shift. **`modulation`** is a sustained internal
812
+ excursion, two phrases or more, that returns. **`transposition`** is the whole
813
+ chant sitting in a foreign mode's frame: it does not close on its labelled
814
+ final and one foreign mode dominates most of its phrases, meaning the melody is
815
+ notated at a transposed position (the affinal) or the label disagrees with the
816
+ notation. A transposed chant is not modulating — the displacement is global —
817
+ so a caller displaying "modulations" should treat those spans as a re-reading of
818
+ the whole chant rather than an event inside it.
819
+
820
+ ```ts
821
+ interface Modulation {
822
+ startPhrase: number; // first phrase of the span (inclusive)
823
+ endPhrase: number; // last phrase (inclusive)
824
+ toMode: number; // the mode the passage leans toward (1–8)
825
+ confidence: number; // 0–1, the averaged margin over the home mode
826
+ kind: "inflection" | "modulation" | "transposition";
827
+ }
828
+ ```
829
+
830
+ ## Theory & Context
831
+
832
+ The rhythm model is the Solesmes school's arsis/thesis synthesis, taken
833
+ from Gajard's lectures and Carroll's chironomy manuals. The full
834
+ treatise-level model lives at the classifier in
835
+ [`score/ir.ts`](https://github.com/jeffreypierce/tonus/blob/main/src/engines/score/ir.ts), which also derives Le Guennant's
836
+ incise rhythmic types ([below](#rhythmic-types)).
837
+
838
+ ### The model
839
+
840
+ Arsis and thesis are properties of the **compound beat**, the group of
841
+ notes between one **ictus** and the next, not of single notes. Every
842
+ note in the group shares its quality, arsic (rising) or thetic (resting).
843
+ The ictus marks the grouping and is not itself an accent, which is why
844
+ tonus stores the quality as `Performance.rhythmicShape` rather than as a
845
+ velocity signal. Phrases, bounded by any divisio, serve as the
846
+ **incise**, the unit within which rhythm is judged.
847
+
848
+ ### The classification rules
849
+
850
+ The classifier applies Carroll's three melodic rules in priority order
851
+ (_Chironomy_ Ch. 4):
852
+
853
+ 1. **Incise unity.** Ictuses before the melodic apex of the incise are
854
+ arsic; after it, thetic. The apex is the incise's highest-pitched
855
+ ictus.
856
+ 2. **Relative ictus pitch.** An ictus higher than the one before it tends
857
+ arsic; lower tends thetic.
858
+ 3. **Neume slope.** When the first two are inconclusive, rising notes are
859
+ arsic, falling thetic.
860
+
861
+ The first compound beat of an incise is always arsic. When every rule is inconclusive, the
862
+ shape alternates from the previous group. Two conventional overrides
863
+ precede the rules: the **salicus** is always arsic — the tension toward its
864
+ summit is the arsic gesture — and the **doubly-dotted clivis** is always
865
+ thetic, as a cadential figure.
866
+
867
+ A salicus here is Cardine's: an ascent of at least three notes whose
868
+ **next-to-last note is an oriscus** [biblio: cardine-semiology, ch. 16]. The
869
+ oriscus is what makes one. An ascending group carrying only the editorial
870
+ Solesmes ictus is a **scandicus** that was marked for rhythm — a distinction
871
+ worth stating because conflating the two is, in Bevenot's word, a trap: over
872
+ the sung corpus tonus finds about 260 salici against about 1,900 scandici, so
873
+ only about an eighth of that wider set carries an oriscus at all.
874
+
875
+ Cardine's correction also decides WHICH note is principal. The printed
876
+ editions lengthen the oriscus itself; the manuscripts show the principal note
877
+ is the one **immediately following** it — the summit — so tonus prolongs that
878
+ note and takes the oriscus lightly. This is the one point where the rhythmic
879
+ layer departs from Mocquereau and Suñol, and it does so deliberately.
880
+
881
+ ### Rhythmic types
882
+
883
+ Above the per-beat arsis/thesis, each phrase carries a `rhythmicType` — Le
884
+ Guennant's taxonomy (via Carroll) of how the incise's compound beats chain, and
885
+ the `beats` sequence it reads. The observable types are modeled: **IV** (a single
886
+ arsis to a single thesis), **V** (several arses to one thesis), **VI** (one arsis
887
+ to several theses), **VII** (regular A–T alternation), and **VIII** (a
888
+ contraction — two simple rhythms overlapping at a shared ictus, after Suñol).
889
+ Types I–III use sub-beat cells that never surface in isolation and are not
890
+ labeled; an incise that fits no type is `null`. The classification rules live at
891
+ the data — see `classifyRhythmicType` in
892
+ [`score/ir.ts`](https://github.com/jeffreypierce/tonus/blob/main/src/engines/score/ir.ts).
893
+
894
+ ### Modeled and not
895
+
896
+ tonus models the compound-beat classification, the per-note rhythmic index,
897
+ mode-specific cadence figures ([above](#cadences)), and the incise rhythmic types
898
+ (above). It does not yet model Carroll's textual rules (word-accent → arsic,
899
+ word-final → thetic) or accentual (spondaic vs. dactylic) cadences.
900
+
901
+ ## Sources
902
+
903
+ Sources for this page are in the central [bibliography](https://github.com/jeffreypierce/tonus/blob/main/BIBLIOGRAPHY.md):
904
+ `carroll-chironomy`, `carroll-applied`, `gajard-rhythm`, `mocquereau-nombre`,
905
+ `cardine-semiology`, `desrocquettes-values`, `sunol-textbook`, `homan-cadence`,
906
+ `pierik-spirit`, `apel-chant`, `liber-usualis`, `bravura-smufl`.