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.
Files changed (161) hide show
  1. package/BIBLIOGRAPHY.md +132 -108
  2. package/CHANGELOG.md +610 -1
  3. package/LICENSE +133 -29
  4. package/README.md +106 -83
  5. package/dist/data/am.js +2669 -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 +24 -0
  21. package/dist/data/corpus-overlap.js +347 -0
  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 +757 -6394
  29. package/dist/data/kyriale.js +116 -116
  30. package/dist/data/la.js +802 -13419
  31. package/dist/data/lh.js +116 -3473
  32. package/dist/data/lu.js +935 -17632
  33. package/dist/data/nocturnale-romanum.d.ts +5 -0
  34. package/dist/data/nocturnale-romanum.js +3772 -0
  35. package/dist/data/office-ferial.d.ts +4 -0
  36. package/dist/data/office-ferial.js +396 -0
  37. package/dist/data/office-ferial.json +391 -0
  38. package/dist/data/office-monastic.d.ts +17 -1
  39. package/dist/data/office-monastic.js +1403 -466
  40. package/dist/data/office-psalms-monastic.d.ts +13 -1
  41. package/dist/data/office-psalms-monastic.js +9 -0
  42. package/dist/data/propers.js +1 -1
  43. package/dist/data/psalms.js +22919 -5
  44. package/dist/data/psm.d.ts +5 -0
  45. package/dist/data/psm.js +122 -0
  46. package/dist/data/seasonal-respbreve.d.ts +5 -0
  47. package/dist/data/seasonal-respbreve.js +41 -0
  48. package/dist/data/seasonal-respbreve.json +35 -0
  49. package/dist/data/smufl-glyphs.d.ts +17 -0
  50. package/dist/data/smufl-glyphs.js +1546 -0
  51. package/dist/data/smufl-glyphs.json +1530 -0
  52. package/dist/engines/cal/calendar.d.ts +3 -2
  53. package/dist/engines/cal/calendar.js +105 -29
  54. package/dist/engines/cal/data/eras.d.ts +35 -0
  55. package/dist/engines/cal/data/eras.js +128 -0
  56. package/dist/engines/cal/date.js +44 -0
  57. package/dist/engines/cal/types.d.ts +15 -3
  58. package/dist/engines/cal/types.js +5 -5
  59. package/dist/engines/census/census.d.ts +7 -0
  60. package/dist/engines/census/census.js +179 -0
  61. package/dist/engines/census/types.d.ts +55 -0
  62. package/dist/engines/census/types.js +8 -0
  63. package/dist/engines/chant/attest.d.ts +39 -0
  64. package/dist/engines/chant/attest.js +90 -0
  65. package/dist/engines/chant/chant.d.ts +22 -4
  66. package/dist/engines/chant/chant.js +274 -14
  67. package/dist/engines/chant/data/compline.js +2 -1
  68. package/dist/engines/chant/data/masses.d.ts +56 -4
  69. package/dist/engines/chant/data/masses.js +305 -80
  70. package/dist/engines/chant/data/prime.js +1 -1
  71. package/dist/engines/chant/hour.js +279 -58
  72. package/dist/engines/chant/ordinary.d.ts +2 -0
  73. package/dist/engines/chant/ordinary.js +336 -56
  74. package/dist/engines/chant/propers.js +55 -5
  75. package/dist/engines/chant/psalm.d.ts +4 -4
  76. package/dist/engines/chant/psalm.js +25 -11
  77. package/dist/engines/chant/syllabify.d.ts +1 -0
  78. package/dist/engines/chant/syllabify.js +90 -17
  79. package/dist/engines/chant/types.d.ts +145 -10
  80. package/dist/engines/chant/types.js +38 -3
  81. package/dist/engines/harmonia/api.js +4 -0
  82. package/dist/engines/harmonia/data/doctrines.js +3 -1
  83. package/dist/engines/harmonia/tabula.d.ts +3 -0
  84. package/dist/engines/harmonia/tabula.js +1 -0
  85. package/dist/engines/harmonia/voice.d.ts +4 -0
  86. package/dist/engines/harmonia/voice.js +8 -4
  87. package/dist/engines/imprint.js +14 -1
  88. package/dist/engines/planet/orbital.js +4 -4
  89. package/dist/engines/planet/planet.d.ts +10 -0
  90. package/dist/engines/planet/planet.js +30 -3
  91. package/dist/engines/planet/position.js +13 -10
  92. package/dist/engines/planet/types.d.ts +1 -0
  93. package/dist/engines/score/api.d.ts +2 -13
  94. package/dist/engines/score/api.js +21 -8
  95. package/dist/engines/score/articulation.js +2 -2
  96. package/dist/engines/score/cadence.d.ts +76 -0
  97. package/dist/engines/score/cadence.js +96 -0
  98. package/dist/engines/score/emitters/accidentals.d.ts +21 -0
  99. package/dist/engines/score/emitters/accidentals.js +88 -0
  100. package/dist/engines/score/emitters/atramentum.d.ts +107 -0
  101. package/dist/engines/score/emitters/atramentum.js +239 -0
  102. package/dist/engines/score/emitters/breaking.d.ts +62 -0
  103. package/dist/engines/score/emitters/breaking.js +80 -0
  104. package/dist/engines/score/emitters/moderna.d.ts +38 -0
  105. package/dist/engines/score/emitters/moderna.js +612 -0
  106. package/dist/engines/score/emitters/svg.d.ts +143 -0
  107. package/dist/engines/score/emitters/svg.js +1328 -0
  108. package/dist/engines/score/emitters/tracks.d.ts +104 -0
  109. package/dist/engines/score/emitters/tracks.js +728 -0
  110. package/dist/engines/score/infer.d.ts +3 -3
  111. package/dist/engines/score/infer.js +2 -2
  112. package/dist/engines/score/inscriptio.d.ts +69 -0
  113. package/dist/engines/score/inscriptio.js +138 -0
  114. package/dist/engines/score/ir.d.ts +2 -2
  115. package/dist/engines/score/ir.js +59 -12
  116. package/dist/engines/score/lyric.d.ts +23 -0
  117. package/dist/engines/score/lyric.js +234 -0
  118. package/dist/engines/score/meta.d.ts +2 -2
  119. package/dist/engines/score/modulation.d.ts +12 -0
  120. package/dist/engines/score/modulation.js +49 -0
  121. package/dist/engines/score/neume.js +35 -4
  122. package/dist/engines/score/parse.js +150 -9
  123. package/dist/engines/score/phrasing.js +4 -3
  124. package/dist/engines/score/prosody.d.ts +38 -0
  125. package/dist/engines/score/prosody.js +70 -6
  126. package/dist/engines/score/tabula.d.ts +37 -5
  127. package/dist/engines/score/tabula.js +18 -0
  128. package/dist/engines/score/types.d.ts +88 -1
  129. package/dist/engines/temper/api.d.ts +4 -1
  130. package/dist/engines/temper/api.js +28 -5
  131. package/dist/engines/temper/data/guido.js +6 -2
  132. package/dist/engines/temper/data/modes.d.ts +6 -0
  133. package/dist/engines/temper/data/modes.js +42 -0
  134. package/dist/engines/temper/data/tones.d.ts +1 -1
  135. package/dist/engines/temper/data/tones.js +20 -11
  136. package/dist/engines/temper/interval.js +4 -3
  137. package/dist/engines/temper/modality.d.ts +11 -2
  138. package/dist/engines/temper/modality.js +74 -2
  139. package/dist/engines/temper/modes.d.ts +1 -1
  140. package/dist/engines/temper/pitch.d.ts +1 -1
  141. package/dist/engines/temper/pitch.js +12 -2
  142. package/dist/engines/temper/scale.d.ts +53 -0
  143. package/dist/engines/temper/scale.js +107 -8
  144. package/dist/index.d.ts +28 -6
  145. package/dist/index.js +39 -3
  146. package/docs/api/calendar.md +279 -0
  147. package/docs/api/census.md +288 -0
  148. package/docs/api/chant.md +657 -0
  149. package/docs/api/heavens.md +346 -0
  150. package/docs/api/index.md +263 -0
  151. package/docs/api/score.md +873 -0
  152. package/docs/api/tuning.md +619 -0
  153. package/package.json +11 -5
  154. package/dist/data/office-psalms-roman.d.ts +0 -15
  155. package/dist/data/office-psalms-roman.js +0 -28
  156. package/dist/data/office-roman.d.ts +0 -19
  157. package/dist/data/office-roman.js +0 -13792
  158. package/dist/engines/score/emitters/midi.d.ts +0 -65
  159. package/dist/engines/score/emitters/midi.js +0 -162
  160. package/dist/engines/score/emitters/musicxml.d.ts +0 -18
  161. 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`.