tonus 0.1.8 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/BIBLIOGRAPHY.md +132 -108
  2. package/CHANGELOG.md +598 -1
  3. package/LICENSE +133 -29
  4. package/README.md +106 -83
  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/tabula.d.ts +3 -0
  83. package/dist/engines/harmonia/tabula.js +1 -0
  84. package/dist/engines/harmonia/voice.d.ts +4 -0
  85. package/dist/engines/harmonia/voice.js +8 -4
  86. package/dist/engines/imprint.js +14 -1
  87. package/dist/engines/planet/orbital.js +4 -4
  88. package/dist/engines/planet/planet.d.ts +10 -0
  89. package/dist/engines/planet/planet.js +30 -3
  90. package/dist/engines/planet/position.js +13 -10
  91. package/dist/engines/planet/types.d.ts +1 -0
  92. package/dist/engines/score/api.d.ts +2 -13
  93. package/dist/engines/score/api.js +21 -8
  94. package/dist/engines/score/articulation.js +2 -2
  95. package/dist/engines/score/cadence.d.ts +76 -0
  96. package/dist/engines/score/cadence.js +96 -0
  97. package/dist/engines/score/emitters/accidentals.d.ts +21 -0
  98. package/dist/engines/score/emitters/accidentals.js +88 -0
  99. package/dist/engines/score/emitters/atramentum.d.ts +107 -0
  100. package/dist/engines/score/emitters/atramentum.js +239 -0
  101. package/dist/engines/score/emitters/breaking.d.ts +62 -0
  102. package/dist/engines/score/emitters/breaking.js +80 -0
  103. package/dist/engines/score/emitters/moderna.d.ts +38 -0
  104. package/dist/engines/score/emitters/moderna.js +612 -0
  105. package/dist/engines/score/emitters/svg.d.ts +143 -0
  106. package/dist/engines/score/emitters/svg.js +1328 -0
  107. package/dist/engines/score/emitters/tracks.d.ts +104 -0
  108. package/dist/engines/score/emitters/tracks.js +728 -0
  109. package/dist/engines/score/infer.d.ts +3 -3
  110. package/dist/engines/score/infer.js +2 -2
  111. package/dist/engines/score/inscriptio.d.ts +69 -0
  112. package/dist/engines/score/inscriptio.js +138 -0
  113. package/dist/engines/score/ir.d.ts +2 -2
  114. package/dist/engines/score/ir.js +59 -12
  115. package/dist/engines/score/lyric.d.ts +23 -0
  116. package/dist/engines/score/lyric.js +234 -0
  117. package/dist/engines/score/meta.d.ts +2 -2
  118. package/dist/engines/score/modulation.d.ts +12 -0
  119. package/dist/engines/score/modulation.js +49 -0
  120. package/dist/engines/score/neume.js +35 -4
  121. package/dist/engines/score/parse.js +150 -9
  122. package/dist/engines/score/phrasing.js +4 -3
  123. package/dist/engines/score/prosody.d.ts +38 -0
  124. package/dist/engines/score/prosody.js +70 -6
  125. package/dist/engines/score/tabula.d.ts +37 -5
  126. package/dist/engines/score/tabula.js +18 -0
  127. package/dist/engines/score/types.d.ts +88 -1
  128. package/dist/engines/temper/api.d.ts +4 -1
  129. package/dist/engines/temper/api.js +28 -5
  130. package/dist/engines/temper/data/guido.js +6 -2
  131. package/dist/engines/temper/data/modes.d.ts +6 -0
  132. package/dist/engines/temper/data/modes.js +42 -0
  133. package/dist/engines/temper/data/tones.d.ts +1 -1
  134. package/dist/engines/temper/data/tones.js +20 -11
  135. package/dist/engines/temper/interval.js +4 -3
  136. package/dist/engines/temper/modality.d.ts +11 -2
  137. package/dist/engines/temper/modality.js +74 -2
  138. package/dist/engines/temper/modes.d.ts +1 -1
  139. package/dist/engines/temper/pitch.d.ts +1 -1
  140. package/dist/engines/temper/pitch.js +12 -2
  141. package/dist/engines/temper/scale.d.ts +53 -0
  142. package/dist/engines/temper/scale.js +107 -8
  143. package/dist/index.d.ts +26 -8
  144. package/dist/index.js +37 -4
  145. package/docs/api/calendar.md +279 -0
  146. package/docs/api/census.md +288 -0
  147. package/docs/api/chant.md +657 -0
  148. package/docs/api/heavens.md +346 -0
  149. package/docs/api/index.md +263 -0
  150. package/docs/api/score.md +873 -0
  151. package/docs/api/tuning.md +619 -0
  152. package/package.json +11 -5
  153. package/dist/data/office-matins-roman.d.ts +0 -19
  154. package/dist/data/office-matins-roman.js +0 -4383
  155. package/dist/data/office-psalms-roman.d.ts +0 -15
  156. package/dist/data/office-psalms-roman.js +0 -28
  157. package/dist/data/office-roman.d.ts +0 -19
  158. package/dist/data/office-roman.js +0 -13792
  159. package/dist/engines/chant/matutinum.d.ts +0 -33
  160. package/dist/engines/chant/matutinum.js +0 -81
  161. package/dist/engines/score/emitters/midi.d.ts +0 -65
  162. package/dist/engines/score/emitters/midi.js +0 -162
  163. package/dist/engines/score/emitters/musicxml.d.ts +0 -18
  164. package/dist/engines/score/emitters/musicxml.js +0 -166
@@ -0,0 +1,619 @@
1
+ # Tuning
2
+
3
+ `tonus.temperamentum` is the library's model of medieval pitch. It builds
4
+ a tuning context (temperament, mode, A4) and returns helper functions to tune and analyze pitch content. The engine depends on nothing
5
+ else in the library and may be used alone; every pitch elsewhere in tonus
6
+ is resolved through one of these contexts. The default tuning is
7
+ Pythagorean, as in the treatises.
8
+
9
+ - [Tuning](#tuning)
10
+ - [The context — `temperamentum`](#the-context--temperamentum)
11
+ - [Pitch input](#pitch-input)
12
+ - [Pitches — `nota`](#pitches--nota)
13
+ - [Steps — `gradus`](#steps--gradus)
14
+ - [Intervals — `intervallum`](#intervals--intervallum)
15
+ - [Neumes — `neuma`](#neumes--neuma)
16
+ - [Ratios — `ratio`](#ratios--ratio)
17
+ - [The gamut — `gamut`](#the-gamut--gamut)
18
+ - [Modes — `modus`](#modes--modus)
19
+ - [Cadence figures](#cadence-figures)
20
+ - [The corpus catalogue — `CADENTIAE`](#the-corpus-catalogue--cadentiae)
21
+ - [Lift — how mode-bound a close is](#lift--how-mode-bound-a-close-is)
22
+ - [Psalm tones — `tonus`](#psalm-tones--tonus)
23
+ - [Theory \& Context](#theory--context)
24
+ - [The presets](#the-presets)
25
+
26
+ ## The context — `temperamentum`
27
+
28
+ `temperamentum(input?)` returns a `Temperamentum`. The input is a tuning
29
+ name, an options object, or nothing; with nothing, the context is
30
+ Pythagorean, mode `"auto"`, A4 = 440 Hz. Invalid input throws.
31
+
32
+ ```js
33
+ tonus.temperamentum(); // pythagorean, mode auto, A4=440
34
+ tonus.temperamentum("pythagorean"); // string shorthand
35
+ tonus.temperamentum({ tuning: "meantone", comma: "1/4" });
36
+ tonus.temperamentum({ tuning: "ptolemy-intense" }); // just intonation
37
+ tonus.temperamentum({ tuning: "equal", mode: 3, a4: 415 });
38
+ tonus.temperamentum({ scale: ["1/1", "9/8", "5/4" /* … */] }); // custom array
39
+ tonus.temperamentum({ scale: "! meanquar.scl\n…" }); // Scala file
40
+ ```
41
+
42
+ **`TemperamentumOpts`**
43
+
44
+ | Field | Type | Default | Description |
45
+ | ----------- | -------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------- |
46
+ | `tuning` | `Tuning` | `"pythagorean"` | Base temperament |
47
+ | `mode` | `number \| "auto"` | `"auto"` | Gregorian mode (1–8); `"auto"` resolves from chant, falls back to mode 1 (final D) |
48
+ | `a4` | `number` | `440` | A4 reference frequency in Hz |
49
+ | `root` | `number` | mode finalis | Root pitch class override (0–11) |
50
+ | `transpose` | `number` | `0` | Output semitone transposition |
51
+ | `comma` | `number \| string` | — | Meantone comma (`0.25`, `"1/4"`, `"1/3"`); ignored for other tunings |
52
+ | `scale` | `string \| string[]` | — | Scala `.scl` file string or array of 7/12 ratio/cent values, beginning at `1/1` (degree list) or ending at `2/1` (Scala convention); implies custom tuning, name from the Scala description |
53
+
54
+ The tuning presets:
55
+
56
+ | Name | Description |
57
+ | ------------------- | -------------------------------------------------------------------------------------------- |
58
+ | `"pythagorean"` | Pure fifths (3/2), no tempering. Default |
59
+ | `"meantone"` | Tempered fifths; `comma` controls the amount (default 1/4) |
60
+ | `"equal"` | 12-tone equal temperament |
61
+ | `"ptolemy-intense"` | Ptolemy's intense diatonic (_syntonon_) — classical just intonation, pure major thirds (5/4) |
62
+ | `"ptolemy-soft"` | Ptolemy's soft diatonic (_malakon_) — septimal tuning, 8/7 whole tone |
63
+ | `"ptolemy-equable"` | Ptolemy's equable diatonic (_homalon_) — undecimal tuning, neutral 12/11 seconds |
64
+
65
+ What each preset is, and when to choose it, is treated in
66
+ [Theory & Context](#theory--context). Any other string is accepted as a
67
+ custom tuning name (for instance, from a Scala file description).
68
+
69
+ ```ts
70
+ interface Temperamentum {
71
+ // resolved
72
+ tuning: Tuning;
73
+ mode: number | "auto";
74
+ a4: number;
75
+ root: number;
76
+ transpose: number;
77
+ comma: number;
78
+
79
+ // computed
80
+ ratios: number[]; // frequency ratios relative to root, one per pitch class
81
+ cents: number[]; // cent values per pitch class, relative to root
82
+
83
+ // methods
84
+ nota(input: PitchInput): Pitch;
85
+ gradus(input: PitchInput): Step;
86
+ intervallum(a: PitchInput, b: PitchInput): Interval;
87
+ ratio(input: string): RatioResult & { step: Step | null };
88
+ neuma(inputs: PitchInput[]): Neume;
89
+ gamut(opts?: GamutOptions): Pitch[];
90
+ modus(mode: number): Modus;
91
+ tonus(opts?: TonusOpts): Tonus;
92
+ }
93
+ ```
94
+
95
+ ## Pitch input
96
+
97
+ Every method that takes a pitch accepts the same forms. A number is a MIDI
98
+ note (a non-integer or a value past 127 reads as a frequency in Hz); a
99
+ string is scientific pitch notation or a GABC letter; an object addresses
100
+ one system explicitly (`{ hz: 293.33 }` for an exact frequency). All inputs resolve to a pitch
101
+ first; `transpose` is applied last as a uniform output shift.
102
+
103
+ ```js
104
+ t.nota(62); // MIDI
105
+ t.nota("D4"); // scientific pitch notation
106
+ t.nota(293.33); // frequency (any non-integer, or > 127)
107
+ t.nota({ gabc: "d", clef: "c4" }); // GABC letter under a clef
108
+ t.nota({ solmization: "RE", hexachord: "naturale" }); // hexachordal address
109
+ ```
110
+
111
+ ```ts
112
+ type PitchInput = number | string | PitchObject;
113
+
114
+ interface PitchObject {
115
+ midi?: number;
116
+ spn?: string;
117
+ hz?: number;
118
+ gabc?: string;
119
+ clef?: string; // default "c4", only with gabc
120
+ solfege?: string; // modern fixed-do: "DO" | "RE" | "MI" | "FA" | "SOL" | "LA" | "SI"
121
+ solmization?: string; // medieval: "UT" | "RE" | "MI" | "FA" | "SOL" | "LA"
122
+ hexachord?: "durum" | "naturale" | "molle"; // default "naturale", only with solmization
123
+ }
124
+ ```
125
+
126
+ ## Pitches — `nota`
127
+
128
+ `nota` resolves any pitch input to a tuned `Pitch`. The frequency comes
129
+ through the scale; the same note name lands on a different frequency in a
130
+ different tuning.
131
+
132
+ ```js
133
+ const t = tonus.temperamentum({ mode: 1 }); // pythagorean
134
+
135
+ t.nota("D4");
136
+ // { midi: 62, pc: 2, oct: 4, acc: 0, spn: "D4",
137
+ // hz: 293.33, offset: -1.96, bend: 8112, ratio: 1 }
138
+
139
+ tonus.temperamentum({ root: 0 }).nota("C4").hz; // 260.74 pythagorean
140
+ tonus.temperamentum({ tuning: "meantone", root: 0 }).nota("C4").hz; // 263.18 quarter-comma
141
+ tonus.temperamentum({ tuning: "equal", root: 0 }).nota("C4").hz; // 261.63 equal
142
+ ```
143
+
144
+ `offset` is the distance in cents from the equal-tempered pitch of the same
145
+ MIDI number; `bend` is the corresponding 14-bit MIDI pitch bend. `Pitch` is
146
+ the tuned identity type used everywhere in tonus: in `Neume.pitches`, as
147
+ `note.pitch` in the score engine, and on `Attractor.pitch` in the imprint.
148
+
149
+ ```ts
150
+ interface Pitch {
151
+ midi: number;
152
+ pc: number; // pitch class 0–11
153
+ oct: number;
154
+ acc: -1 | 0 | 1; // flat, natural, sharp
155
+ spn: string; // scientific pitch name, e.g. "D4"
156
+ hz: number; // frequency in Hz (through the scale)
157
+ offset: number; // cents from 12-TET
158
+ bend: number; // 14-bit MIDI pitch bend, 8192 = center
159
+ ratio: number; // scale ratio for this pc
160
+ }
161
+ ```
162
+
163
+ ## Steps — `gradus`
164
+
165
+ `gradus` returns the modal and Guidonian identity of a pitch class: its
166
+ letter, its compound name, its hexachord and solmization syllable, the
167
+ mutations available to it, its position on the Guidonian hand, and its
168
+ degree and role in the current mode. `Step` carries no tuning data; that is
169
+ `Pitch`'s office.
170
+
171
+ ```js
172
+ const t = tonus.temperamentum({ mode: 1 });
173
+
174
+ t.gradus("D4");
175
+ // { pc: 2, name: "d", nomen: "Delasolre",
176
+ // hexachord: "naturale", solmization: "RE",
177
+ // variants: [ { hexachord: "durum", solmization: "SOL" },
178
+ // { hexachord: "naturale", solmization: "RE" },
179
+ // { hexachord: "molle", solmization: "LA" } ],
180
+ // hand: { finger: "middle", region: "tip" },
181
+ // degree: 1, role: "finalis" }
182
+ ```
183
+
184
+ The `variants` array lists the step's solmization in every hexachord that
185
+ contains it; these are the mutations a medieval singer had available at
186
+ that step. `degree` and `role` read against the resolved mode (`"auto"`
187
+ falls back to mode 1); for a chromatic pitch outside the mode's scale they
188
+ are `null`.
189
+
190
+ The hand position names the joint a singer would point to. The loci follow the
191
+ canonical counter-clockwise spiral (Γ ut at the thumb tip, across the finger
192
+ bases, up the little finger, back across the tips, in), not a linear fill:
193
+
194
+ | `hand` field | values |
195
+ | ------------ | ----------------------------------- |
196
+ | `finger` | thumb, index, middle, ring, pinky |
197
+ | `region` | base, mid, top, tip, super |
198
+
199
+ ```ts
200
+ interface StepVariant {
201
+ hexachord: "durum" | "naturale" | "molle";
202
+ solmization: string;
203
+ }
204
+
205
+ interface Step {
206
+ pc: number; // pitch class 0–11
207
+ name: string; // "d" (Guidonian) or SPN letter fallback
208
+ nomen: string | null; // "Delasolre"; null out of gamut
209
+ hexachord: "durum" | "naturale" | "molle" | null;
210
+ solmization: string | null; // null out of gamut
211
+ variants: StepVariant[]; // available mutations across hexachords
212
+ hand: { finger: string; region: string } | null; // Guidonian hand position
213
+ degree: number | null; // 1–7 diatonic degree in mode
214
+ role: "finalis" | "tenor" | "other" | null;
215
+ }
216
+ ```
217
+
218
+ ## Intervals — `intervallum`
219
+
220
+ `intervallum` classifies the interval between two pitches: its Latin name,
221
+ its Greek alias where one exists, its quality, class, direction, and
222
+ consonance grade.
223
+
224
+ ```js
225
+ t.intervallum("D4", "A4");
226
+ // { nomen: "Quinta", alias: "Diapente", quality: "perfect",
227
+ // class: "P5", direction: "up", semitones: 7, cents: 700,
228
+ // consonance: "perfect" }
229
+ ```
230
+
231
+ `semitones` and `cents` describe the interval class in nominal
232
+ (equal-tempered) terms. The tuned distance between two particular pitches
233
+ is the ratio of their frequencies; take it from `nota`.
234
+
235
+ | field | values |
236
+ | ------------ | ---------------------------------------------------------------- |
237
+ | `class` | `P1` `m2` `M2` `m3` `M3` `P4` `TT` `P5` `m6` `M6` `m7` `M7` `P8` |
238
+ | `quality` | perfect, major, minor, augmented |
239
+ | `direction` | up, down, unison |
240
+ | `consonance` | perfect, imperfect, dissonant |
241
+
242
+ ```ts
243
+ interface Interval {
244
+ nomen: string; // e.g. "Quinta", "Semitonium"
245
+ alias?: string; // e.g. "Diapente", "Diatessaron"
246
+ quality: string;
247
+ class: string;
248
+ direction: string;
249
+ semitones: number;
250
+ cents: number; // nominal class value
251
+ consonance: string;
252
+ }
253
+ ```
254
+
255
+ ## Neumes — `neuma`
256
+
257
+ The neume is chant's unit of notation: a group of notes written as one
258
+ figure and sung on one syllable. `neuma` classifies a sequence of pitches
259
+ as a neume shape, under the Solesmes nomenclature, and returns the tuned
260
+ pitches with the intervals between them.
261
+
262
+ ```js
263
+ t.neuma(["D4", "F4", "E4"]);
264
+ // shape: "torculus" — up, then down
265
+ // intervals: Tertia minor, Semitonium
266
+ ```
267
+
268
+ Shapes that match no simple figure classify as `"compound"`.
269
+
270
+ | `shape` | figure | `shape` | figure |
271
+ | ----------- | ------------------ | -------------------- | --------------------------- |
272
+ | `punctum` | a single note | `torculus resupinus` | torculus, then rising |
273
+ | `pes` | two notes, rising | `porrectus flexus` | porrectus, then falling |
274
+ | `clivis` | two notes, falling | `scandicus flexus` | scandicus, then falling |
275
+ | `torculus` | three: up, down | `climacus resupinus` | climacus, then rising |
276
+ | `porrectus` | three: down, up | `pes subpunctis` | pes, then descending points |
277
+ | `scandicus` | three, rising | `compound` | any figure not above |
278
+ | `climacus` | three, falling | | |
279
+
280
+ ```ts
281
+ interface Neume {
282
+ pitches: Pitch[];
283
+ intervals: Interval[];
284
+ shape: string; // a value from the table above
285
+ }
286
+ ```
287
+
288
+ ## Ratios — `ratio`
289
+
290
+ `ratio` converts between cents, decimal ratios, and colon display
291
+ notation, following the Scala conventions: a period means cents
292
+ (`"701.955"`); a slash, colon, or bare integer means a ratio (`"3/2"`,
293
+ `"3:2"`, `"2"`). When the ratio corresponds to a degree of the current
294
+ scale, the matching `Step` is returned with it.
295
+
296
+ ```js
297
+ t.ratio("3/2");
298
+ // { ratio: 1.5, cents: 701.96, display: "3:2",
299
+ // step: { nomen: "Alamire", degree: 5, role: "tenor", … } }
300
+ ```
301
+
302
+ In mode 1 the pure fifth lands on the tenor; the step field says so.
303
+
304
+ ```ts
305
+ interface RatioResult {
306
+ ratio: number; // decimal frequency ratio
307
+ cents: number; // interval in cents
308
+ display: string; // colon notation, e.g. "3:2"
309
+ }
310
+ ```
311
+
312
+ ## The gamut — `gamut`
313
+
314
+ The gamut is the medieval note-space: the ordered range of singable
315
+ pitches Guido's system named from Gammaut upward. `gamut` returns the
316
+ tuned pitches of the scale across a span. With a mode set, the default
317
+ span is the mode's ambitus; mode 1 yields twelve diatonic pitches from D3
318
+ to A4. The `chromatic` option adds the chromatic degrees; `span` sets
319
+ explicit MIDI bounds.
320
+
321
+ ```js
322
+ const g = t.gamut(); // 12 pitches, D3 … A4 (mode 1 ambitus)
323
+ t.gamut({ span: [48, 72], chromatic: true });
324
+ ```
325
+
326
+ ```ts
327
+ interface GamutOptions {
328
+ span?: [number, number]; // [lowest, highest] MIDI
329
+ chromatic?: boolean; // include chromatic pitches, default false
330
+ }
331
+ ```
332
+
333
+ ## Modes — `modus`
334
+
335
+ `modus` returns the full profile of one of the eight modes, as the
336
+ medieval tonaries describe them:
337
+
338
+ - its identity — Latin name, modern alias, _maneria_ (the family: Protus,
339
+ Deuterus, Tritus, Tetrardus), and authentic or plagal type;
340
+ - its structure — finalis, tenor, scale degrees, ambitus, and the species
341
+ of fifth and fourth that build it;
342
+ - its practice — hexachords in rank order, melodic profile, its cadence
343
+ figures, and permitted modulations.
344
+
345
+ Each mode also carries its traditional **ethos** (the character medieval theory
346
+ ascribed to it) as both a Latin epithet (`gravis`, `tristis`, …) and an English
347
+ gloss. The full set and its source are at the data, in
348
+ [`temper/data/modes.ts`](https://github.com/jeffreypierce/tonus/blob/main/src/engines/temper/data/modes.ts).
349
+
350
+ The `modulations` fields (`regular`, `conceded`, and `initials`) are the
351
+ mode's tonal centres and valid openings, each list **ordered by importance**
352
+ (after Rockstro's Grove table [`rockstro-grove`](https://github.com/jeffreypierce/tonus/blob/main/BIBLIOGRAPHY.md)). The
353
+ modal-affinity scorer reads that order; the reasoning is documented at the
354
+ data, in [`temper/data/modes.ts`](https://github.com/jeffreypierce/tonus/blob/main/src/engines/temper/data/modes.ts).
355
+
356
+ `modus` resolves the mode's structural pitches through its own
357
+ temperamentum: the **finalis** and **reciting** tone as tuned notes (pitch +
358
+ Guidonian step), and **ambitusNotes**, the mode's diatonic range walked out
359
+ note by note. The raw `ModeData` fields (`final`, `tenor`, `cadences`, …)
360
+ remain alongside — `Modus` extends `ModeData`.
361
+
362
+ ```js
363
+ t.modus(1);
364
+ // { mode: 1, nomen: "Protus Authenticus", alias: "dorian",
365
+ // maneria: "Protus", type: "authentic",
366
+ // final: 2, tenor: 9,
367
+ // finalis: { pitch: <D4 293.33 Hz>, step: { … role: "finalis" } },
368
+ // reciting: { pitch: <A4>, step: { … role: "tenor" } },
369
+ // ambitusNotes: [ <D4>, <E4>, <F4>, … ],
370
+ // profile: { mood: "serious", ethos: "gravis", … }, … }
371
+ ```
372
+
373
+ ```ts
374
+ interface ModeProfile {
375
+ mood: string; // English gloss of the ethos, e.g. "serious"
376
+ ethos: string; // traditional Latin epithet, e.g. "gravis"
377
+ phrasing: "recitative" | "lyrical" | "hymnic" | "solemn";
378
+ melodic: "rising" | "falling" | "arch" | "neutral";
379
+ tendency: "melismatic" | "neumatic" | "syllabic" | "neutral";
380
+ }
381
+
382
+ interface CadenceFigure {
383
+ id: string; // solmization, e.g. "sol-fa-re"
384
+ steps: number[]; // diatonic steps relative to the final; resolution (0) last
385
+ }
386
+
387
+ interface ModeData {
388
+ mode: number;
389
+ nomen: string; // "Protus Authenticus"
390
+ alias: string; // "dorian" (modern name)
391
+ maneria: string; // "Protus" — the mode family
392
+ type: "authentic" | "plagal";
393
+ final: number; // finalis pitch class (C=0)
394
+ tenor: number; // reciting tone pitch class
395
+ recitingNotes: { pc: number; rank: string; label: string }[]; // tenor + secondary reciting pitches
396
+ scalePcs: number[]; // 7 diatonic pitch classes
397
+ hexachords: ("durum" | "naturale" | "molle")[]; // rank-ordered
398
+ profile: ModeProfile;
399
+ cadences: CadenceFigure[]; // characteristic cadence figures
400
+ modulations: {
401
+ regular: number[];
402
+ conceded: number[];
403
+ initials: number[];
404
+ };
405
+ ambitus: {
406
+ lowest: number;
407
+ highest: number;
408
+ span: number;
409
+ };
410
+ species: {
411
+ fifth: [number, number];
412
+ fourth: [number, number];
413
+ };
414
+ }
415
+
416
+ interface TunedNote {
417
+ pitch: Pitch; // tuned through the temperamentum
418
+ step: Step; // Guidonian annotation
419
+ }
420
+
421
+ interface Modus extends ModeData {
422
+ finalis: TunedNote; // the final, tuned
423
+ reciting: TunedNote; // the tenor / reciting tone, tuned
424
+ ambitusNotes: TunedNote[]; // every diatonic step across the mode's range
425
+ }
426
+ ```
427
+
428
+ ### Cadence figures
429
+
430
+ Each mode carries the melodic figures its phrases characteristically close
431
+ on, in `modus(n).cadences` — the shapes by which a chant comes to rest, stored
432
+ as diatonic steps relative to the final. The score engine reads them to name a
433
+ phrase's cadence ([score.md](score.md#cadences)).
434
+
435
+ | Maneria (final) | Figures (solmization → final) |
436
+ | --------------- | --------------------------------- |
437
+ | Protus (Re) | mi-re, ut-re, sol-fa-re, mi-fa-re |
438
+ | Deuterus (Mi) | fa-mi, re-mi, sol-fa-mi |
439
+ | Tritus (Fa) | mi-fa, fa-mi-fa, la-sol-fa |
440
+ | Tetrardus (Sol) | la-sol, fa-sol, ut-sol, ut-ti-sol |
441
+
442
+ The catalogue is an editorial synthesis covering the final cadences; its step
443
+ encoding, sources, and known gaps (medial cadences are not yet included) are
444
+ documented at the data — see `CadenceFigure` in
445
+ [`temper/data/modes.ts`](https://github.com/jeffreypierce/tonus/blob/main/src/engines/temper/data/modes.ts).
446
+
447
+ This is the **tradita** half — what the treatises say. Its counterpart is the
448
+ corpus tally below, and they are not interchangeable: see [one spine, two
449
+ annotations](score.md#one-spine-two-annotations).
450
+
451
+ ### The corpus catalogue — `CADENTIAE`
452
+
453
+ Where the figures above are received, [`CADENTIAE`](index.md#the-appendix) is
454
+ **mined**: every phrase-end in the sung corpus, grouped into families by what
455
+ the melody actually did. A family is a **shape** — the closing tail's
456
+ successive semitone intervals — and an **arrival**, where it landed relative
457
+ to the chant's own closing note. Together they are the key, `"2,0,-2 @0"`, and
458
+ the key is the family's whole name.
459
+
460
+ About 110 families clear the floor of 50 occurrences, covering about 57% of all
461
+ phrase-ends. The rest of the tail is real but too thin to characterise.
462
+
463
+ ```ts
464
+ interface CadentiaFamilia {
465
+ key: string; // "shape @arrival" — the name, and the join
466
+ shape: number[]; // successive semitone intervals of the closing tail
467
+ arrival: number; // SIGNED semitones from the chant's own closing note
468
+ n: number; // corpus occurrences
469
+ share: number; // n over ALL phrase-ends (CADENTIAE_POPULATION.ends)
470
+ finality: number; // share of those occurrences at a final close
471
+ modes: Record<string, number>; // occurrences by mode digit ("?" = mode-less)
472
+ }
473
+ ```
474
+
475
+ `share` is taken against **every** phrase-end, not against the ~57% that
476
+ cleared the floor. The denominator ships beside the table:
477
+
478
+ ```ts
479
+ CADENTIAE_POPULATION.ends; // 26787 — all phrase-ends, sung corpus
480
+ CADENTIAE_POPULATION.byMode; // the same total per mode digit
481
+ ```
482
+
483
+ #### Lift — how mode-bound a close is
484
+
485
+ `modes` and `byMode` share a denominator, so a family's **lift** in a given
486
+ mode is one division: how much more (or less) that mode reaches for this close
487
+ than the corpus at large.
488
+
489
+ ```js
490
+ import { CADENTIAE, CADENTIAE_POPULATION as POP } from "tonus";
491
+
492
+ const fam = CADENTIAE.find((f) => f.key === "2,0,-2 @0");
493
+ const lift = (f, mode) =>
494
+ (f.modes[String(mode)] / POP.byMode[String(mode)]) / f.share;
495
+
496
+ lift(fam, 6); // 2.15
497
+ lift(fam, 4); // 0.07
498
+ ```
499
+
500
+ The commonest family in the corpus — about 1,100 occurrences, about 4% of all
501
+ phrase-ends, closing a little under half the time — is not reached for evenly.
502
+ Mode 6 reaches for this close more than twice as often as the corpus does;
503
+ mode 4 almost never. That is the figure the tonarium prints under a cadence
504
+ ([score.md](score.md#the-analysis-tracks)).
505
+
506
+ **The ratio is not baked.** The table exports the vocabulary — counts and
507
+ their denominators — and leaves the arithmetic to the caller. Two cautions when
508
+ you take it: below roughly ten in-mode occurrences the ratio is one or two
509
+ chants deciding a number that reads like a measurement, and a mode-less chant
510
+ (`"?"`) has no denominator at all. In both cases fall back to `share`.
511
+
512
+ ## Psalm tones — `tonus`
513
+
514
+ `tonus` returns the recitation formula of the context's mode as tuned
515
+ pitches: the **intonatio** (the opening rise), the **mediatio** (the
516
+ cadence at the verse's colon), and the **terminatio** (the closing
517
+ cadence). The termination varies by **differentia**, the cadential variant
518
+ chosen to lead back into the antiphon; the mode's default differentia is
519
+ used when none is given. The mode must be set explicitly — with mode
520
+ `"auto"`, `tonus()` throws.
521
+
522
+ ```js
523
+ const t = tonus.temperamentum({ mode: 1 });
524
+
525
+ t.tonus();
526
+ // { mode: 1, differentia: "1g",
527
+ // intonatio: [F3, A3, C4],
528
+ // mediatio: [C4, A3, G3, A3],
529
+ // terminatio: [C4, A3, G3, A3, G3] } // each entry a tuned Pitch
530
+
531
+ t.tonus({ differentia: "1f" });
532
+ ```
533
+
534
+ To sing psalm texts to these formulas, use
535
+ [`tonus.psalmus`](chant.md#psalms--psalmus), which returns
536
+ GABC-encoded verses pointed to the tone.
537
+
538
+ ```ts
539
+ interface TonusOpts {
540
+ differentia?: string; // e.g. "6F", "4e"; mode comes from Temperamentum, throws if "auto"
541
+ }
542
+
543
+ interface Tonus {
544
+ mode: number;
545
+ differentia: string;
546
+ intonatio: Pitch[]; // opening intonation formula
547
+ mediatio: Pitch[]; // mediant cadence at the verse colon
548
+ terminatio: Pitch[]; // termination cadence (per differentia)
549
+ }
550
+ ```
551
+
552
+ ## Theory & Context
553
+
554
+ `temperamentum` represents a tuning as a table of twelve frequency ratios,
555
+ one per pitch class, relative to the context's root and anchored at the
556
+ reference A4. The presets fill that table from history: the Pythagorean
557
+ division the treatises teach, the meantone compromise of the Renaissance,
558
+ Ptolemy's three diatonic shades, and the modern equal division.
559
+
560
+ ### The presets
561
+
562
+ Six ways to fill the table, five historical and one modern. Each has a
563
+ character; the interval arithmetic, the commas that drive it, and the
564
+ editorial choices behind it are documented at the builder, in
565
+ [`temper/scale.ts`](https://github.com/jeffreypierce/tonus/blob/main/src/engines/temper/scale.ts).
566
+
567
+ - **`"pythagorean"`** — the medieval default. All intervals from the pure
568
+ fifth and the octave: perfect melodic fifths and fourths, a narrow keen
569
+ semitone, a wide third that never has to be a consonance. Correct for
570
+ unaccompanied chant, and the tuning the treatises teach.
571
+
572
+ ```js
573
+ tonus.temperamentum(); // pythagorean, A4 = 440
574
+ ```
575
+
576
+ - **`"meantone"`** — the Renaissance compromise. Narrows each fifth to buy
577
+ purer thirds; `comma` sets how far (`"1/4"`, the default, gives pure major
578
+ thirds and the 16th-century sound; `"1/3"` pure minor thirds; `"1/6"` leans
579
+ baroque). One fifth is left over as the _wolf_.
580
+
581
+ ```js
582
+ tonus.temperamentum({ tuning: "meantone", comma: "1/4" });
583
+ ```
584
+
585
+ - **The three Ptolemaic diatonics** — antiquity's just intonations, Ptolemy's
586
+ three "shades" of the diatonic. `"ptolemy-intense"` is classical just
587
+ intonation (pure thirds; coherent with the Ptolemy doctrina in
588
+ [`harmonia`](heavens.md)); `"ptolemy-soft"` is septimal and dark;
589
+ `"ptolemy-equable"` has neutral, near-equal steps outside the Latin tradition.
590
+ Each tunes one fixed scale, so pairing one with a non-default mode can surface
591
+ unexpected intervals, including a wolf.
592
+
593
+ ```js
594
+ tonus.temperamentum({ tuning: "ptolemy-intense" });
595
+ ```
596
+
597
+ - **`"equal"`** — twelve identical semitones; nothing pure, nothing unusable.
598
+ Anachronistic for chant by some seven centuries, included as a reference point.
599
+
600
+ ```js
601
+ tonus.temperamentum({ tuning: "equal", a4: 415 }); // equal at baroque pitch
602
+ ```
603
+
604
+ Any 7- or 12-step scale can also be supplied directly as ratios or cents, or as
605
+ a [Scala `.scl`](https://www.huygens-fokker.org/scala/scl_format.html) file:
606
+
607
+ ```js
608
+ tonus.temperamentum({
609
+ scale: ["1/1", "9/8", "5/4", "4/3", "3/2", "5/3", "15/8"],
610
+ });
611
+ tonus.temperamentum({ scale: sclFileString }); // name taken from the file
612
+ ```
613
+
614
+ ## Sources
615
+
616
+ Sources for this page are in the central [bibliography](https://github.com/jeffreypierce/tonus/blob/main/BIBLIOGRAPHY.md):
617
+ `boethius-institutione`, `ptolemy-harmonics`, `schulter-harmony`,
618
+ `rockstro-grove`, `niedermeyer-ortigue`, `bragers-treatise`,
619
+ `sunol-textbook`, `saulnier-modes`, `liber-usualis`, `scala-format`.
package/package.json CHANGED
@@ -1,11 +1,10 @@
1
1
  {
2
2
  "name": "tonus",
3
- "version": "0.1.8",
4
- "private": false,
3
+ "version": "0.5.0",
5
4
  "type": "module",
6
5
  "description": "Medieval music analysis and performance: GABC plainchant exports, liturgical calendar, tuning systems, ephemeris, and the harmony of the spheres",
7
6
  "author": "Jeffrey Pierce <jeffrey@jeffreypierce.net>",
8
- "license": "MIT",
7
+ "license": "PolyForm-Noncommercial-1.0.0",
9
8
  "repository": {
10
9
  "type": "git",
11
10
  "url": "git+https://github.com/jeffreypierce/tonus.git"
@@ -39,6 +38,7 @@
39
38
  "types": "./dist/index.d.ts",
40
39
  "files": [
41
40
  "dist",
41
+ "docs/api",
42
42
  "!dist/**/*.js.map",
43
43
  "!dist/**/*.d.ts.map",
44
44
  "LICENSE",
@@ -46,10 +46,16 @@
46
46
  "BIBLIOGRAPHY.md"
47
47
  ],
48
48
  "scripts": {
49
- "build": "tsc && cp src/data/psalms.json dist/data/psalms.json",
49
+ "build": "tsc && cp src/data/psalms.json dist/data/psalms.json && cp src/data/smufl-glyphs.json dist/data/smufl-glyphs.json && cp src/data/office-ferial.json dist/data/office-ferial.json && cp src/data/commune-office.json dist/data/commune-office.json && cp src/data/seasonal-respbreve.json dist/data/seasonal-respbreve.json && cp src/data/attestation.json dist/data/attestation.json && node scripts/bake-data.mjs",
50
50
  "clean": "rm -rf dist",
51
51
  "test": "npm run build && node --test --test-reporter=spec tests/*.test.mjs",
52
- "prepublishOnly": "npm run clean && npm run build"
52
+ "prepublishOnly": "npm run clean && npm run build",
53
+ "check:links": "node scripts/check-doc-links.mjs",
54
+ "site": "npm run build && node scripts/vendor-ink.mjs && rm -rf docs/dist && cp -R dist docs/dist",
55
+ "serve": "npm run site && node scripts/serve.mjs",
56
+ "diagrams": "npm run site && node scripts/diagram-lab.mjs",
57
+ "lab": "npm run build && node scripts/render-lab.mjs",
58
+ "stress": "npm run build && node scripts/render-stress.mjs"
53
59
  },
54
60
  "devDependencies": {
55
61
  "@types/node": "^22.0.0",
@@ -1,19 +0,0 @@
1
- export interface MatinsChant {
2
- id: string;
3
- type: string;
4
- pos: string;
5
- }
6
- export interface MatinsNocturn {
7
- n: number;
8
- chants: MatinsChant[];
9
- }
10
- export interface MatinsDay {
11
- id: string;
12
- tonusFeastId: string | null;
13
- name: string;
14
- rank: string;
15
- nocturns: MatinsNocturn[];
16
- redirectedFrom: string | null;
17
- }
18
- export declare const MATINS_ROMAN: MatinsDay[];
19
- //# sourceMappingURL=office-matins-roman.d.ts.map