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
@@ -1,6 +1,6 @@
1
- import type { Score, Neume } from "./types.js";
1
+ import type { ScoreIR, Neume, LyricRun } from "./types.js";
2
2
  import type { Cadence } from "./cadence.js";
3
- import type { ChantType, InterpretationOptions } from "./types.js";
3
+ import type { ChantType, InterpretationOptions, WrittenShape } from "./types.js";
4
4
  export type NoteRole = "finalis" | "tenor" | "other" | null;
5
5
  export interface ChantTabulaRow {
6
6
  phraseIndex: number;
@@ -11,7 +11,25 @@ export interface ChantTabulaRow {
11
11
  /** 0-based position of this note within its neume figure. */
12
12
  neumeIndex: number;
13
13
  lyric: string;
14
+ /** Styled lyric spans; present only when GABC markup styled this syllable. */
15
+ runs?: LyricRun[];
14
16
  vowel: string;
17
+ /**
18
+ * The diphthong the sung vowel belongs to — `ae` · `oe` · `au`, and `ui` in the
19
+ * cui/hui stems — or null. `vowel` is its NUCLEUS (what the singer sustains);
20
+ * the second element is a late off-glide. Only the syllabifier can tell a
21
+ * diphthong from a hiatus, since the accent decides: `cae` is one syllable,
22
+ * `sa-é` is two.
23
+ */
24
+ diphthong: string | null;
25
+ /** True when this note's syllable bears the Latin tonic word-accent. */
26
+ accent: boolean;
27
+ /** True when this note's syllable is the first of its word. */
28
+ wordStart: boolean;
29
+ /** The engraver asked for a line break BEFORE this note (GABC `z`). */
30
+ lineBreak: boolean;
31
+ /** Inside `<nlba>` — the engraver forbade a break before this note. */
32
+ keepWithPrev: boolean;
15
33
  /** MIDI pitch number (after transpose, clamped 0–127) */
16
34
  midi: number;
17
35
  /** Pitch class 0–11 (C=0) */
@@ -22,8 +40,16 @@ export interface ChantTabulaRow {
22
40
  degree: number | null;
23
41
  hz: number;
24
42
  offset: number;
25
- /** Scientific pitch name, e.g. "D4", "Bb3" — the MusicXML step + octave. */
43
+ /** Scientific pitch name, e.g. "D4", "Bb3" — the step + octave. */
26
44
  spn: string;
45
+ /** Raw GABC pitch letter a–m — the square-note staff slot. */
46
+ staffLetter: string;
47
+ /** Staff position in half-spaces from the bottom line (a=−2 … g=4 … m=10). */
48
+ staffPosition: number;
49
+ /** Active GABC clef when this note sounded, e.g. "c4", "fb3". */
50
+ clef: string;
51
+ /** Written note shape (punctum, virga, inclinatum, …). */
52
+ shape: WrittenShape;
27
53
  /** 14-bit MIDI pitch bend for this note's microtuning (8192 = center). */
28
54
  bend: number;
29
55
  /** Solesmes quality of this note's compound beat (shared across group). */
@@ -35,9 +61,15 @@ export interface ChantTabulaRow {
35
61
  velocity: number | null;
36
62
  shapedDuration: number;
37
63
  ictus: boolean;
64
+ /** Written vertical episema / ictus mark ('), distinct from the analytical `ictus`. */
65
+ ictusSign: boolean;
66
+ /** Written horizontal episema (_), distinct from `mora`. */
67
+ episema: boolean;
38
68
  accidental: -1 | 0 | 1;
39
69
  /** How this note's accidental arose — only "explicit" prints a glyph. */
40
70
  accidentalSource: "none" | "state" | "explicit";
71
+ /** The sign to draw before this note, if any. */
72
+ accidentalSign?: -1 | 0 | 1;
41
73
  quilisma: boolean;
42
74
  liquescent: boolean;
43
75
  strophicus: boolean;
@@ -47,7 +79,7 @@ export interface ChantTabulaRow {
47
79
  divisio: string | null;
48
80
  /** Index into score.cadences[] when this note forms a cadence; null otherwise. */
49
81
  cadenceRef: number | null;
50
- /** Modal role: "final" | "tenor" | "mod" (modulation) | null if no mode or no match */
82
+ /** Modal role: "finalis" | "tenor" | "other" | null if no mode or no match */
51
83
  role: NoteRole;
52
84
  /** Guidonian short name (e.g. "g", "aa") — null for chromatic pitches with no gamut entry */
53
85
  name: string | null;
@@ -74,5 +106,5 @@ export interface TabulaOptions {
74
106
  /** Detected cadences; used to stamp each row's cadenceRef. */
75
107
  cadences?: Cadence[];
76
108
  }
77
- export declare function computeTabula(ir: Score, options?: TabulaOptions): ChantTabulaRow[];
109
+ export declare function computeTabula(ir: ScoreIR, options?: TabulaOptions): ChantTabulaRow[];
78
110
  //# sourceMappingURL=tabula.d.ts.map
@@ -2,6 +2,7 @@ import { MODES } from "../temper/modes.js";
2
2
  import { buildPhrasing, shapePhrasingForMode, applyPhrasing, } from "./phrasing.js";
3
3
  import { inferMode } from "./infer.js";
4
4
  import { CHROMA_TO_SOLFEGE as SOLFEGE_BY_PC } from "../temper/data/constants.js";
5
+ import { staffPositionForLetter } from "../../data/gabc-glyphs.js";
5
6
  export function computeTabula(ir, options = {}) {
6
7
  const modeNum = options.mode ?? inferMode(ir);
7
8
  const modeData = modeNum !== undefined ? MODES.get(modeNum) : undefined;
@@ -83,7 +84,17 @@ export function computeTabula(ir, options = {}) {
83
84
  neumeGroup: n.context.neumeGroup,
84
85
  neumeIndex: neumeIndices[i],
85
86
  lyric: n.context.lyric,
87
+ runs: n.context.runs,
86
88
  vowel: n.context.vowel,
89
+ diphthong: n.context.diphthong,
90
+ accent: n.context.accent,
91
+ // wordStart reads the note's CONTEXT syllable index (the parser's, which
92
+ // resets to 0 at each word boundary — so 0 = word start), NOT the row's
93
+ // own `syllableIndex` above (a global per-phrase counter). Two indices,
94
+ // two meanings; the per-word one is what marks words.
95
+ wordStart: n.context.syllableIndex === 0,
96
+ lineBreak: n.context.lineBreak === true,
97
+ keepWithPrev: n.context.keepWithPrev === true,
87
98
  midi: n.pitch.midi,
88
99
  pc: n.pitch.pc,
89
100
  octave: n.pitch.oct,
@@ -91,6 +102,10 @@ export function computeTabula(ir, options = {}) {
91
102
  hz: n.pitch.hz,
92
103
  offset: n.pitch.offset,
93
104
  spn: n.pitch.spn,
105
+ staffLetter: n.context.staffLetter,
106
+ staffPosition: staffPositionForLetter(n.context.staffLetter) ?? 0,
107
+ clef: n.context.clef,
108
+ shape: n.context.shape,
94
109
  bend: n.pitch.bend,
95
110
  rhythmicShape: n.performance.rhythmicShape,
96
111
  rhythmicIndex: n.performance.rhythmicIndex,
@@ -98,8 +113,11 @@ export function computeTabula(ir, options = {}) {
98
113
  velocity: velocities[i],
99
114
  shapedDuration: shapedDurations[i],
100
115
  ictus: n.context.ictus,
116
+ ictusSign: n.context.ictusSign,
117
+ episema: n.context.episema,
101
118
  accidental: n.pitch.acc,
102
119
  accidentalSource: n.context.accidentalSource,
120
+ accidentalSign: n.context.accidentalSign,
103
121
  quilisma: n.context.quilisma,
104
122
  liquescent: n.context.liquescent,
105
123
  strophicus: n.context.strophicus,
@@ -14,14 +14,59 @@ export interface Performance {
14
14
  rhythmicShape: ArsisThesis;
15
15
  rhythmicIndex: number;
16
16
  }
17
+ export type WrittenShape = "punctum" | "inclinatum" | "virga" | "virgaReversa" | "quilisma" | "oriscus" | "strophicus" | "cavum" | "linea";
18
+ /**
19
+ * One styled span of a syllable's lyric — the decoded form of GABC's text
20
+ * markup (<i>, <b>, <sc>, <c>, elisions). The spans concatenate to the
21
+ * syllable's `lyric`; style flags are absent when a span is plain.
22
+ */
23
+ export interface LyricRun {
24
+ text: string;
25
+ italic?: boolean;
26
+ bold?: boolean;
27
+ smallCaps?: boolean;
28
+ /** Rendered in the rubric color (GABC's <c>). */
29
+ rubric?: boolean;
30
+ }
17
31
  export interface Context {
18
32
  lyric: string;
33
+ /** Styled lyric spans; present only when GABC markup styled this syllable. */
34
+ runs?: LyricRun[];
19
35
  vowel: string;
36
+ /**
37
+ * The diphthong this syllable's sung vowel belongs to (`ae` · `oe` · `au`, and
38
+ * `ui` in the cui/hui stems), or null. `vowel` above is its NUCLEUS — what the
39
+ * singer sustains; the pair's second element is a late off-glide. Reported
40
+ * here because only the syllabifier can tell a diphthong from a hiatus: the
41
+ * accent decides (`cae` is one syllable, `sa-é` is two).
42
+ */
43
+ diphthong: string | null;
20
44
  syllableIndex: number;
45
+ /** True when this note's syllable bears the Latin tonic word-accent. */
46
+ accent: boolean;
21
47
  /** 0-based index of the neume figure within the syllable (GABC break markers). */
22
48
  neumeGroup: number;
49
+ /** The engraver asked for a line break BEFORE this note — GABC's `z` (and
50
+ * `Z`, its page-break sibling, honoured the same way since tonus paginates
51
+ * nothing). An editor who set a chant chose where its lines end, and that
52
+ * choice carries information a width cannot: a break at the right place is
53
+ * an editorial reading of the piece. Where it is absent the layout decides. */
54
+ lineBreak?: boolean;
55
+ keepWithPrev?: boolean;
56
+ /** Raw GABC pitch letter a–m — the staff slot for square-note rendering. */
57
+ staffLetter: string;
58
+ /** Active GABC clef when this note sounded, e.g. "c4", "fb3". */
59
+ clef: string;
60
+ /** Written note shape (punctum, virga, inclinatum, …). */
61
+ shape: WrittenShape;
23
62
  ictus: boolean;
63
+ /** Written vertical episema / ictus mark ('), distinct from the analytical `ictus`. */
64
+ ictusSign: boolean;
65
+ /** Written horizontal episema (_), distinct from `mora`. */
66
+ episema: boolean;
24
67
  accidentalSource: "none" | "state" | "explicit";
68
+ /** The sign to DRAW before this note (see parse.ts) — distinct from `accidental`. */
69
+ accidentalSign?: -1 | 0 | 1;
25
70
  quilisma: boolean;
26
71
  liquescent: boolean;
27
72
  strophicus: boolean;
@@ -116,14 +161,37 @@ export interface ParsedNote {
116
161
  type: "note";
117
162
  step: number;
118
163
  lyric: string;
164
+ /** Styled lyric spans; present only when GABC markup styled this syllable. */
165
+ runs?: LyricRun[];
119
166
  syllableIndex: number;
167
+ /** True when this note's syllable bears the Latin tonic word-accent. */
168
+ accent: boolean;
120
169
  /** 0-based index of the neume figure within the syllable (GABC break markers). */
121
170
  neumeGroup: number;
171
+ /** The engraver asked for a line break BEFORE this note — GABC's `z` (and
172
+ * `Z`, its page-break sibling, honoured the same way since tonus paginates
173
+ * nothing). An editor who set a chant chose where its lines end, and that
174
+ * choice carries information a width cannot: a break at the right place is
175
+ * an editorial reading of the piece. Where it is absent the layout decides. */
176
+ lineBreak?: boolean;
177
+ keepWithPrev?: boolean;
178
+ /** Raw GABC pitch letter a–m — the staff slot for square-note rendering. */
179
+ staffLetter: string;
180
+ /** Active GABC clef when this note sounded, e.g. "c4", "fb3". */
181
+ clef: string;
182
+ /** Written note shape (punctum, virga, inclinatum, …). */
183
+ shape: WrittenShape;
122
184
  ictus: boolean;
185
+ /** Written vertical episema / ictus mark ('), distinct from analytical `ictus`. */
186
+ ictusSign: boolean;
187
+ /** Written horizontal episema (_), distinct from `mora`. */
188
+ episema: boolean;
123
189
  weight: number;
124
190
  duration: number;
125
191
  accidental: -1 | 0 | 1;
126
192
  accidentalSource: "none" | "state" | "explicit";
193
+ /** The sign to DRAW before this note (see parse.ts) — distinct from `accidental`. */
194
+ accidentalSign?: -1 | 0 | 1;
127
195
  quilisma: boolean;
128
196
  liquescent: boolean;
129
197
  strophicus: boolean;
@@ -150,18 +218,37 @@ export interface Neume {
150
218
  }
151
219
  export interface Syllable {
152
220
  lyric: string;
221
+ /** Styled lyric spans; present only when GABC markup styled this syllable. */
222
+ runs?: LyricRun[];
153
223
  notes: Note[];
154
224
  neume: Neume;
225
+ /** Notes sung on this syllable — its melisma. 1 = syllabic, >1 = melismatic. */
226
+ melisma: number;
155
227
  }
156
228
  export interface Phrase {
157
229
  syllables: Syllable[];
158
230
  divisio?: RestEvent;
231
+ /** Total notes across this phrase's syllables. */
232
+ noteCount: number;
233
+ /** Sung syllables in this phrase (syllables carrying at least one note). */
234
+ syllableCount: number;
159
235
  /** The phrase's compound beats in order — the A/T sequence of the incise. */
160
236
  beats: CompoundBeat[];
161
237
  /** Le Guennant/Carroll rhythmic type of the incise; null if none fits. */
162
238
  rhythmicType: RhythmicType;
163
239
  }
164
- export interface Score {
240
+ /**
241
+ * What `buildIR` returns: the parsed, phrased, rhythm-classified melody before
242
+ * any analysis rides on it. Every downstream pass (meta, infer, tabula) takes
243
+ * one of these and names its parameter `ir`.
244
+ *
245
+ * This was called `Score` and shadowed the real one in api.ts — same name, a
246
+ * strict subset of its fields, and a `chant` narrowed to three strings. Nothing
247
+ * distinguished them at an import site, so the wrong one could be imported and
248
+ * still typecheck against the fields it happened to share. The public Score is
249
+ * the nine-field record in api.ts; this is its input.
250
+ */
251
+ export interface ScoreIR {
165
252
  chant: {
166
253
  incipit: string;
167
254
  mode: string | null;
@@ -1,4 +1,4 @@
1
- import type { Scale, ScaleOpts, ScalaFile, RatioResult } from "./scale.js";
1
+ import type { Scale, ScaleOpts, ScalaFile, RatioResult, Lupus } from "./scale.js";
2
2
  import type { Pitch, PitchInput } from "./pitch.js";
3
3
  import type { Step, StepVariant, Finger, Region } from "./step.js";
4
4
  import type { Interval, IntervalDirection, IntervalQuality } from "./interval.js";
@@ -60,6 +60,9 @@ export interface Temperamentum {
60
60
  ratio(input: string): RatioResult & {
61
61
  step: Step | null;
62
62
  };
63
+ /** The wolf, or null when no fifths chain built this scale. UNDOCUMENTED:
64
+ * the site is its only caller and the shape is still settling. */
65
+ lupus(): Lupus | null;
63
66
  gamut(opts?: GamutOptions): Pitch[];
64
67
  modus(mode: number): Modus;
65
68
  tonus(opts?: TonusOpts): Tonus;
@@ -1,7 +1,7 @@
1
1
  // ---------------------------------------------------------------------------
2
2
  // engines/temper/api — Temperamentum context builder
3
3
  // ---------------------------------------------------------------------------
4
- import { buildRatios, parseScala, getPtolemaicRatios, toRatio } from "./scale.js";
4
+ import { buildRatios, parseScala, getPtolemaicRatios, toRatio, wolfOf } from "./scale.js";
5
5
  import { parsePitch, toPitch } from "./pitch.js";
6
6
  import { toStep } from "./step.js";
7
7
  import { classifyInterval } from "./interval.js";
@@ -10,11 +10,20 @@ import { buildGamut } from "./gamut.js";
10
10
  import { getMode } from "./modes.js";
11
11
  import { getTone, getDifferentia } from "./data/tones.js";
12
12
  function resolveOpts(input) {
13
- if (!input)
13
+ if (input == null)
14
14
  return {};
15
15
  if (typeof input === "string")
16
16
  return { tuning: input };
17
- return input;
17
+ if (typeof input !== "object")
18
+ throw new Error(`temperamentum input must be a tuning name or an options object, got ${typeof input}`);
19
+ const opts = input;
20
+ if (opts.tuning != null && typeof opts.tuning !== "string")
21
+ throw new Error(`tuning must be a string, got ${typeof opts.tuning}`);
22
+ if (opts.mode != null &&
23
+ opts.mode !== "auto" &&
24
+ (!Number.isInteger(opts.mode) || opts.mode < 1 || opts.mode > 8))
25
+ throw new RangeError(`Unknown mode: ${opts.mode}. Supported: 1–8 or "auto".`);
26
+ return opts;
18
27
  }
19
28
  function resolveScale(opts) {
20
29
  if (!opts.scale)
@@ -49,8 +58,10 @@ function tuningToScaleOpts(opts) {
49
58
  break;
50
59
  default: {
51
60
  const ptolemaic = getPtolemaicRatios(tuning);
52
- if (ptolemaic)
53
- scalaOpts.steps = ptolemaic;
61
+ if (!ptolemaic)
62
+ throw new Error(`Unknown tuning: "${tuning}". Built-ins: pythagorean, meantone, equal, ` +
63
+ `ptolemy-intense, ptolemy-soft, ptolemy-equable — or pass a scale.`);
64
+ scalaOpts.steps = ptolemaic;
54
65
  break;
55
66
  }
56
67
  }
@@ -93,6 +104,18 @@ export function buildTemper(input) {
93
104
  neuma(inputs) {
94
105
  return buildNeume(inputs, scala);
95
106
  },
107
+ /** The wolf of THIS temperament.
108
+ *
109
+ * Null when the scale was supplied rather than stacked: a Ptolemaic
110
+ * genus and a Scala list arrive as ratios, so there is no chain of
111
+ * fifths and nothing is left over. Returning a number there would be
112
+ * inventing one.
113
+ */
114
+ lupus() {
115
+ if (scalaOpts.steps != null)
116
+ return null;
117
+ return wolfOf(scala.comma);
118
+ },
96
119
  ratio(input) {
97
120
  const result = toRatio(input);
98
121
  const folded = result.ratio >= 2 ? result.ratio / Math.pow(2, Math.floor(Math.log2(result.ratio))) : result.ratio;
@@ -4,8 +4,12 @@
4
4
  // little→index, descend the index, then spiral inward; ee (76) floats above the
5
5
  // middle fingertip (region "super"). The two b/♮ pairs (58/59, 70/71) share a
6
6
  // locus by design — one joint, two readings (fa in molle, mi in durum), the
7
- // tradition, not a duplication to fix. After Suñol [biblio: sunol-textbook] and
8
- // Apel [biblio: apel-chant]; realized in working/diagram-hand-08.html.
7
+ // tradition, not a duplication to fix. They are one distinction, named for the
8
+ // two shapes of the letter: b rotundum (the round b) against b quadratum (the
9
+ // square b), which is where ♭ and ♮ come from. A RENDERER may draw them with
10
+ // the medieval glyphs (GLYPH.flat / GLYPH.natural); these names stay letters,
11
+ // because this table names joints rather than drawing them. After Suñol
12
+ // [biblio: sunol-textbook] and Apel [biblio: apel-chant].
9
13
  // prettier-ignore
10
14
  export const GUIDONIAN_DATA = new Map([
11
15
  [43, { hand: { finger: "thumb", region: "tip" }, name: ["Γ", "Gammaut"], variants: [{ hexachord: "durum", solmization: "UT" }] }],
@@ -9,6 +9,11 @@ export interface CadenceFigure {
9
9
  id: string;
10
10
  steps: number[];
11
11
  }
12
+ export interface RecitingNote {
13
+ pc: number;
14
+ rank: "principal" | "auxiliary" | "secondary" | "pseudo" | "rare";
15
+ label?: string;
16
+ }
12
17
  export interface ModeData {
13
18
  mode: number;
14
19
  nomen: string;
@@ -17,6 +22,7 @@ export interface ModeData {
17
22
  type: "authentic" | "plagal";
18
23
  final: number;
19
24
  tenor: number;
25
+ recitingNotes: RecitingNote[];
20
26
  scalePcs: number[];
21
27
  hexachords: ("durum" | "naturale" | "molle")[];
22
28
  profile: ModeProfile;
@@ -12,6 +12,7 @@ export const MODES = new Map([
12
12
  type: "authentic",
13
13
  final: 2,
14
14
  tenor: 9,
15
+ recitingNotes: [{ pc: 9, rank: "principal", label: "psalmodic tenor" }],
15
16
  scalePcs: [2, 4, 5, 7, 9, 11, 0],
16
17
  hexachords: ["naturale"],
17
18
  profile: { mood: "serious", ethos: "gravis", phrasing: "lyrical", melodic: "falling", tendency: "melismatic" },
@@ -36,6 +37,11 @@ export const MODES = new Map([
36
37
  type: "plagal",
37
38
  final: 2,
38
39
  tenor: 5,
40
+ recitingNotes: [
41
+ { pc: 5, rank: "principal", label: "official dominant, psalmodic tenor, strong degree" },
42
+ { pc: 2, rank: "principal", label: "final; also a principal note of composition per prose" },
43
+ { pc: 7, rank: "secondary", label: "important; prose also names it a note of composition" },
44
+ ],
39
45
  scalePcs: [2, 4, 5, 7, 9, 11, 0],
40
46
  hexachords: ["naturale"],
41
47
  profile: { mood: "sad", ethos: "tristis", phrasing: "lyrical", melodic: "arch", tendency: "neumatic" },
@@ -63,6 +69,12 @@ export const MODES = new Map([
63
69
  type: "authentic",
64
70
  final: 4,
65
71
  tenor: 0,
72
+ // No Degree Summary Table exists for mode III (prose only, see
73
+ // ../../../../BIBLIOGRAPHY.md [biblio: saulnier-modes]): "the primitive
74
+ // psalmodic tenor is b, but in numerous medieval manuscripts from northern Europe
75
+ // and in the modern editions, this tenor is raised to c." tonus already
76
+ // stored the raised/practical value; this just cites the confirmation.
77
+ recitingNotes: [{ pc: 0, rank: "principal", label: "historically raised from the primitive tenor b" }],
66
78
  scalePcs: [4, 5, 7, 9, 11, 0, 2],
67
79
  hexachords: ["naturale"],
68
80
  profile: { mood: "mystic", ethos: "mysticus", phrasing: "solemn", melodic: "falling", tendency: "melismatic" },
@@ -86,6 +98,12 @@ export const MODES = new Map([
86
98
  type: "plagal",
87
99
  final: 4,
88
100
  tenor: 9,
101
+ recitingNotes: [
102
+ { pc: 9, rank: "principal", label: "psalmodic tenor" },
103
+ { pc: 7, rank: "secondary", label: "note of composition for Responsory verses (deuterus-tritus)" },
104
+ { pc: 4, rank: "secondary", label: "final; also usable as a recitation note per prose" },
105
+ { pc: 5, rank: "pseudo", label: "pseudo-tenor; strong importance but not a true note of composition per prose" },
106
+ ],
89
107
  scalePcs: [4, 5, 7, 9, 11, 0, 2],
90
108
  hexachords: ["naturale"],
91
109
  profile: { mood: "harmonious", ethos: "harmonicus", phrasing: "lyrical", melodic: "arch", tendency: "neumatic" },
@@ -109,6 +127,10 @@ export const MODES = new Map([
109
127
  type: "authentic",
110
128
  final: 5,
111
129
  tenor: 0,
130
+ recitingNotes: [
131
+ { pc: 0, rank: "principal", label: "official dominant, psalmodic tenor" },
132
+ { pc: 5, rank: "rare", label: "final; sometimes used as a recitation note" },
133
+ ],
112
134
  scalePcs: [5, 7, 9, 11, 0, 2, 4],
113
135
  hexachords: ["molle"],
114
136
  profile: { mood: "happy", ethos: "laetus", phrasing: "solemn", melodic: "rising", tendency: "melismatic" },
@@ -132,6 +154,11 @@ export const MODES = new Map([
132
154
  type: "plagal",
133
155
  final: 5,
134
156
  tenor: 9,
157
+ // Prose: "Mode VI possesses only one note of composition: f, the final
158
+ // of the pieces. For the psalmody, it resorts to a, even if it has been
159
+ // infrequently heard in the piece." So the reciting note is real but
160
+ // structurally secondary — hence "auxiliary," not "principal."
161
+ recitingNotes: [{ pc: 9, rank: "auxiliary", label: "psalmodic tenor; the mode's only note of composition is the final f, itself not used for recitation" }],
135
162
  scalePcs: [5, 7, 9, 11, 0, 2, 4],
136
163
  hexachords: ["molle"],
137
164
  profile: { mood: "devout", ethos: "devotus", phrasing: "lyrical", melodic: "arch", tendency: "neumatic" },
@@ -155,6 +182,11 @@ export const MODES = new Map([
155
182
  type: "authentic",
156
183
  final: 7,
157
184
  tenor: 2,
185
+ recitingNotes: [
186
+ { pc: 2, rank: "principal", label: "psalmodic tenor" },
187
+ { pc: 0, rank: "auxiliary", label: "strong degree" },
188
+ { pc: 11, rank: "auxiliary" },
189
+ ],
158
190
  scalePcs: [7, 9, 11, 0, 2, 4, 5],
159
191
  hexachords: ["durum"],
160
192
  profile: { mood: "angelical", ethos: "angelicus", phrasing: "solemn", melodic: "rising", tendency: "melismatic" },
@@ -179,6 +211,16 @@ export const MODES = new Map([
179
211
  type: "plagal",
180
212
  final: 7,
181
213
  tenor: 0,
214
+ // Prose: "Mode VIII has two upper notes of recitation: c (psalmodic
215
+ // tenor) and b. The lower note of recitation, g, is also the final of
216
+ // the pieces. B and g are the notes of recitation for the verses of the
217
+ // Responsories." — the final doubling as a low reciting note is a real,
218
+ // distinct case (see also mode V's f).
219
+ recitingNotes: [
220
+ { pc: 0, rank: "principal", label: "psalmodic tenor" },
221
+ { pc: 11, rank: "auxiliary" },
222
+ { pc: 7, rank: "secondary", label: "final; the mode's lower recitation note, used for Responsory verses alongside b" },
223
+ ],
182
224
  scalePcs: [7, 9, 11, 0, 2, 4, 5],
183
225
  hexachords: ["durum"],
184
226
  profile: { mood: "perfect", ethos: "perfectus", phrasing: "lyrical", melodic: "arch", tendency: "neumatic" },
@@ -4,7 +4,7 @@ export interface Differentia {
4
4
  }
5
5
  export interface PsalmTone {
6
6
  mode: number;
7
- name: string;
7
+ nomen: string;
8
8
  tenor: number;
9
9
  intonation: number[];
10
10
  flex: number[];
@@ -1,4 +1,6 @@
1
- // temper/data/tones — psalm tones (Graduale Romanum appendix)
1
+ // ---------------------------------------------------------------------------
2
+ // engines/temper/data/tones — psalm tones (Graduale Romanum appendix)
3
+ // ---------------------------------------------------------------------------
2
4
  // Pitches as MIDI numbers; tone codes follow DO convention: "1g", "6F", "4e".
3
5
  // c4 clef reference: f=53 g=55 h=57 i=59 j=60 k=62 l=64 (F3–F4)
4
6
  const f = 53, g = 55, h = 57, i = 59, j = 60, k = 62, l = 64;
@@ -6,7 +8,7 @@ const d3 = 50, e3 = 52; // below c4 clef range, needed for Tone II
6
8
  export const TONES = [
7
9
  {
8
10
  mode: 1,
9
- name: "Tonus I",
11
+ nomen: "Tonus I",
10
12
  tenor: h, // A3
11
13
  intonation: [f, h, j], // F-A-C
12
14
  flex: [h, g], // A-G
@@ -22,7 +24,7 @@ export const TONES = [
22
24
  },
23
25
  {
24
26
  mode: 2,
25
- name: "Tonus II",
27
+ nomen: "Tonus II",
26
28
  tenor: f, // F3
27
29
  intonation: [d3, f], // D3-F3
28
30
  flex: [f, e3], // F-E
@@ -37,7 +39,7 @@ export const TONES = [
37
39
  },
38
40
  {
39
41
  mode: 3,
40
- name: "Tonus III",
42
+ nomen: "Tonus III",
41
43
  tenor: i, // B3
42
44
  intonation: [g, i, j], // G-B-C
43
45
  flex: [i, h], // B-A
@@ -51,7 +53,7 @@ export const TONES = [
51
53
  },
52
54
  {
53
55
  mode: 4,
54
- name: "Tonus IV",
56
+ nomen: "Tonus IV",
55
57
  tenor: h, // A3
56
58
  intonation: [f, g, h], // F-G-A
57
59
  flex: [h, f], // A-F
@@ -66,7 +68,7 @@ export const TONES = [
66
68
  },
67
69
  {
68
70
  mode: 5,
69
- name: "Tonus V",
71
+ nomen: "Tonus V",
70
72
  tenor: j, // C4
71
73
  intonation: [h, j], // A-C
72
74
  flex: [j, i], // C-B
@@ -80,7 +82,7 @@ export const TONES = [
80
82
  },
81
83
  {
82
84
  mode: 6,
83
- name: "Tonus VI",
85
+ nomen: "Tonus VI",
84
86
  tenor: f, // F3
85
87
  intonation: [d3, e3, f], // D-E-F
86
88
  flex: [f, e3], // F-E
@@ -94,7 +96,7 @@ export const TONES = [
94
96
  },
95
97
  {
96
98
  mode: 7,
97
- name: "Tonus VII",
99
+ nomen: "Tonus VII",
98
100
  tenor: j, // C4
99
101
  intonation: [h, i, j], // A-B-C
100
102
  flex: [j, i], // C-B
@@ -109,7 +111,7 @@ export const TONES = [
109
111
  },
110
112
  {
111
113
  mode: 8,
112
- name: "Tonus VIII",
114
+ nomen: "Tonus VIII",
113
115
  tenor: j, // C4
114
116
  intonation: [h, j], // A-C
115
117
  flex: [j, h], // C-A
@@ -125,7 +127,7 @@ export const TONES = [
125
127
  // ── Tonus Peregrinus ── irregular: two different tenors
126
128
  {
127
129
  mode: 0,
128
- name: "Tonus Peregrinus",
130
+ nomen: "Tonus Peregrinus",
129
131
  tenor: j, // C4 (first half tenor)
130
132
  intonation: [h, i, j], // A-B-C
131
133
  flex: [j, i],
@@ -138,7 +140,14 @@ export const TONES = [
138
140
  ];
139
141
  /** Look up a PsalmTone by mode number (1–8, or 0 for Peregrinus) */
140
142
  export function getTone(mode) {
141
- return TONES.find(t => t.mode === mode) ?? TONES.find(t => t.mode === 8);
143
+ const tone = TONES.find(t => t.mode === mode);
144
+ // A bad mode throws rather than silently singing Tonus VIII: a
145
+ // plausible-looking wrong psalm tone is worse than no answer, and getMode
146
+ // (temper/modes.ts) already holds this line for the modes themselves.
147
+ if (!tone) {
148
+ throw new Error(`getTone: no psalm tone for mode ${mode} (expected 1–8, or 0 for the tonus peregrinus).`);
149
+ }
150
+ return tone;
142
151
  }
143
152
  /** Look up a differentia by code, falling back to the tone's defaultDiff */
144
153
  export function getDifferentia(tone, code) {
@@ -3,8 +3,8 @@
3
3
  // ---------------------------------------------------------------------------
4
4
  import { INTERVAL, UNISONUS } from "./data/constants.js";
5
5
  // The three-tier consonance taxonomy [biblio: schulter-harmony] (the same table
6
- // stated at docs/heavens.md and docs/tuning.md). Note that the perfect fourth is
7
- // deliberately NOT perfect here: in medieval counterpoint the P4 above the bass
6
+ // stated at ../../../docs/api/heavens.md and ../../../docs/api/tuning.md).
7
+ // Note that the perfect fourth is deliberately NOT perfect here: in medieval counterpoint the P4 above the bass
8
8
  // is treated as a dissonance, unlike the melodic P4. So P1/P5/P8 are perfect,
9
9
  // the thirds and sixths imperfect, and everything else — including P4 and the
10
10
  // tritone — dissonant.
@@ -35,7 +35,8 @@ export function classifyInterval(a, b) {
35
35
  semitones,
36
36
  // Nominal equal-tempered cents (100 per semitone), NOT the tuned distance —
37
37
  // the actual sounding interval depends on the temperament (see the ratio a
38
- // temperamentum reports for a nota, and docs/tuning.md on nominal vs tuned).
38
+ // temperamentum reports for a nota, and ../../../docs/api/tuning.md on
39
+ // nominal vs tuned).
39
40
  cents: semitones * 100,
40
41
  consonance: classifyConsonance(entry.class),
41
42
  };
@@ -3,9 +3,18 @@ export interface ModalAffinity {
3
3
  alias: string;
4
4
  score: number;
5
5
  }
6
+ export interface ModalAffinityOpts {
7
+ /** The chant's opening pitch class — applies the rank-weighted initials bonus. */
8
+ firstNotePc?: number;
9
+ /** The chant's closing pitch class — applies the final-note and tessitura bonuses. */
10
+ lastNotePc?: number;
11
+ /** Mean note MIDI minus the last note's MIDI — the melody's height above its close. */
12
+ tessitura?: number;
13
+ }
6
14
  /**
7
15
  * Rank a pitch-class distribution against the eight modes, best fit first.
8
- * `firstNotePc`, when given, applies the rank-weighted initials bonus.
16
+ * The optional signals sharpen the ranking: the opening note (initials bonus),
17
+ * the closing note (final-note bonus), and the tessitura (authentic/plagal).
9
18
  */
10
- export declare function computeModalAffinity(pcDistribution: Record<number, number>, firstNotePc?: number): ModalAffinity[];
19
+ export declare function computeModalAffinity(pcDistribution: Record<number, number>, opts?: ModalAffinityOpts): ModalAffinity[];
11
20
  //# sourceMappingURL=modality.d.ts.map