tonus 0.1.8 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/BIBLIOGRAPHY.md +138 -108
  2. package/CHANGELOG.md +664 -1
  3. package/LICENSE +133 -29
  4. package/README.md +107 -80
  5. package/dist/data/am.js +2666 -11196
  6. package/dist/data/ams.d.ts +5 -0
  7. package/dist/data/ams.js +122 -0
  8. package/dist/data/attestation.d.ts +19 -0
  9. package/dist/data/attestation.js +15716 -0
  10. package/dist/data/attestation.json +15711 -0
  11. package/dist/data/cadentiae.d.ts +43 -0
  12. package/dist/data/cadentiae.js +174 -0
  13. package/dist/data/cal.js +48 -0
  14. package/dist/data/census.d.ts +12 -0
  15. package/dist/data/census.js +36 -0
  16. package/dist/data/commune-office.d.ts +4 -0
  17. package/dist/data/commune-office.js +2371 -0
  18. package/dist/data/commune-office.json +2365 -0
  19. package/dist/data/commune.js +181 -11
  20. package/dist/data/corpus-overlap.d.ts +17 -0
  21. package/dist/data/corpus-overlap.js +299 -5
  22. package/dist/data/cot.d.ts +5 -0
  23. package/dist/data/cot.js +172 -0
  24. package/dist/data/cse.d.ts +5 -0
  25. package/dist/data/cse.js +122 -0
  26. package/dist/data/gabc-glyphs.d.ts +45 -0
  27. package/dist/data/gabc-glyphs.js +122 -0
  28. package/dist/data/gr.js +754 -6394
  29. package/dist/data/kyriale.js +116 -116
  30. package/dist/data/la.js +799 -13419
  31. package/dist/data/lh.js +113 -3473
  32. package/dist/data/lu.js +931 -17631
  33. package/dist/data/nocturnale-romanum.js +1659 -10411
  34. package/dist/data/office-ferial.d.ts +4 -0
  35. package/dist/data/office-ferial.js +396 -0
  36. package/dist/data/office-ferial.json +391 -0
  37. package/dist/data/office-monastic.d.ts +17 -1
  38. package/dist/data/office-monastic.js +1403 -466
  39. package/dist/data/office-psalms-monastic.d.ts +13 -1
  40. package/dist/data/office-psalms-monastic.js +9 -0
  41. package/dist/data/propers.js +1 -1
  42. package/dist/data/psalms.js +22919 -5
  43. package/dist/data/psm.d.ts +5 -0
  44. package/dist/data/psm.js +122 -0
  45. package/dist/data/seasonal-respbreve.d.ts +5 -0
  46. package/dist/data/seasonal-respbreve.js +41 -0
  47. package/dist/data/seasonal-respbreve.json +35 -0
  48. package/dist/data/smufl-glyphs.d.ts +17 -0
  49. package/dist/data/smufl-glyphs.js +1546 -0
  50. package/dist/data/smufl-glyphs.json +1530 -0
  51. package/dist/engines/cal/calendar.d.ts +3 -2
  52. package/dist/engines/cal/calendar.js +105 -29
  53. package/dist/engines/cal/data/eras.d.ts +35 -0
  54. package/dist/engines/cal/data/eras.js +128 -0
  55. package/dist/engines/cal/date.js +44 -0
  56. package/dist/engines/cal/types.d.ts +15 -3
  57. package/dist/engines/cal/types.js +5 -5
  58. package/dist/engines/census/census.d.ts +7 -0
  59. package/dist/engines/census/census.js +179 -0
  60. package/dist/engines/census/types.d.ts +55 -0
  61. package/dist/engines/census/types.js +8 -0
  62. package/dist/engines/chant/attest.d.ts +39 -0
  63. package/dist/engines/chant/attest.js +90 -0
  64. package/dist/engines/chant/chant.d.ts +16 -4
  65. package/dist/engines/chant/chant.js +220 -30
  66. package/dist/engines/chant/data/compline.js +2 -1
  67. package/dist/engines/chant/data/masses.d.ts +56 -4
  68. package/dist/engines/chant/data/masses.js +305 -80
  69. package/dist/engines/chant/data/prime.js +1 -1
  70. package/dist/engines/chant/hour.js +279 -58
  71. package/dist/engines/chant/ordinary.d.ts +2 -0
  72. package/dist/engines/chant/ordinary.js +336 -56
  73. package/dist/engines/chant/propers.js +55 -5
  74. package/dist/engines/chant/psalm.d.ts +4 -4
  75. package/dist/engines/chant/psalm.js +25 -11
  76. package/dist/engines/chant/syllabify.d.ts +1 -0
  77. package/dist/engines/chant/syllabify.js +90 -17
  78. package/dist/engines/chant/types.d.ts +115 -12
  79. package/dist/engines/chant/types.js +38 -3
  80. package/dist/engines/harmonia/api.js +4 -0
  81. package/dist/engines/harmonia/data/doctrines.js +3 -1
  82. package/dist/engines/harmonia/data/zodiac.d.ts +47 -0
  83. package/dist/engines/harmonia/data/zodiac.js +163 -0
  84. package/dist/engines/harmonia/tabula.d.ts +3 -0
  85. package/dist/engines/harmonia/tabula.js +1 -0
  86. package/dist/engines/harmonia/voice.d.ts +4 -0
  87. package/dist/engines/harmonia/voice.js +8 -4
  88. package/dist/engines/imprint.js +14 -1
  89. package/dist/engines/planet/orbital.js +4 -4
  90. package/dist/engines/planet/planet.d.ts +10 -0
  91. package/dist/engines/planet/planet.js +30 -3
  92. package/dist/engines/planet/position.js +13 -10
  93. package/dist/engines/planet/types.d.ts +1 -0
  94. package/dist/engines/score/api.d.ts +2 -13
  95. package/dist/engines/score/api.js +21 -8
  96. package/dist/engines/score/articulation.js +2 -2
  97. package/dist/engines/score/cadence.d.ts +76 -0
  98. package/dist/engines/score/cadence.js +96 -0
  99. package/dist/engines/score/emitters/accidentals.d.ts +21 -0
  100. package/dist/engines/score/emitters/accidentals.js +88 -0
  101. package/dist/engines/score/emitters/atramentum.d.ts +107 -0
  102. package/dist/engines/score/emitters/atramentum.js +239 -0
  103. package/dist/engines/score/emitters/breaking.d.ts +62 -0
  104. package/dist/engines/score/emitters/breaking.js +80 -0
  105. package/dist/engines/score/emitters/moderna.d.ts +38 -0
  106. package/dist/engines/score/emitters/moderna.js +612 -0
  107. package/dist/engines/score/emitters/svg.d.ts +143 -0
  108. package/dist/engines/score/emitters/svg.js +1335 -0
  109. package/dist/engines/score/emitters/tracks.d.ts +104 -0
  110. package/dist/engines/score/emitters/tracks.js +728 -0
  111. package/dist/engines/score/infer.d.ts +3 -3
  112. package/dist/engines/score/infer.js +2 -2
  113. package/dist/engines/score/inscriptio.d.ts +69 -0
  114. package/dist/engines/score/inscriptio.js +138 -0
  115. package/dist/engines/score/ir.d.ts +2 -2
  116. package/dist/engines/score/ir.js +59 -12
  117. package/dist/engines/score/lyric.d.ts +23 -0
  118. package/dist/engines/score/lyric.js +234 -0
  119. package/dist/engines/score/meta.d.ts +2 -2
  120. package/dist/engines/score/modulation.d.ts +12 -0
  121. package/dist/engines/score/modulation.js +49 -0
  122. package/dist/engines/score/neume.js +35 -4
  123. package/dist/engines/score/parse.js +150 -9
  124. package/dist/engines/score/phrasing.js +4 -3
  125. package/dist/engines/score/prosody.d.ts +38 -0
  126. package/dist/engines/score/prosody.js +70 -6
  127. package/dist/engines/score/tabula.d.ts +37 -5
  128. package/dist/engines/score/tabula.js +18 -0
  129. package/dist/engines/score/types.d.ts +88 -1
  130. package/dist/engines/temper/api.d.ts +4 -1
  131. package/dist/engines/temper/api.js +28 -5
  132. package/dist/engines/temper/data/guido.js +6 -2
  133. package/dist/engines/temper/data/modes.d.ts +6 -0
  134. package/dist/engines/temper/data/modes.js +42 -0
  135. package/dist/engines/temper/data/tones.d.ts +1 -1
  136. package/dist/engines/temper/data/tones.js +20 -11
  137. package/dist/engines/temper/interval.js +4 -3
  138. package/dist/engines/temper/modality.d.ts +11 -2
  139. package/dist/engines/temper/modality.js +74 -2
  140. package/dist/engines/temper/modes.d.ts +1 -1
  141. package/dist/engines/temper/pitch.d.ts +1 -1
  142. package/dist/engines/temper/pitch.js +12 -2
  143. package/dist/engines/temper/scale.d.ts +53 -0
  144. package/dist/engines/temper/scale.js +107 -8
  145. package/dist/index.d.ts +26 -8
  146. package/dist/index.js +40 -4
  147. package/docs/api/calendar.md +279 -0
  148. package/docs/api/census.md +288 -0
  149. package/docs/api/chant.md +657 -0
  150. package/docs/api/heavens.md +396 -0
  151. package/docs/api/index.md +265 -0
  152. package/docs/api/score.md +906 -0
  153. package/docs/api/tuning.md +619 -0
  154. package/package.json +13 -5
  155. package/dist/data/office-matins-roman.d.ts +0 -19
  156. package/dist/data/office-matins-roman.js +0 -4383
  157. package/dist/data/office-psalms-roman.d.ts +0 -15
  158. package/dist/data/office-psalms-roman.js +0 -28
  159. package/dist/data/office-roman.d.ts +0 -19
  160. package/dist/data/office-roman.js +0 -13792
  161. package/dist/engines/chant/matutinum.d.ts +0 -33
  162. package/dist/engines/chant/matutinum.js +0 -81
  163. package/dist/engines/score/emitters/midi.d.ts +0 -65
  164. package/dist/engines/score/emitters/midi.js +0 -162
  165. package/dist/engines/score/emitters/musicxml.d.ts +0 -18
  166. package/dist/engines/score/emitters/musicxml.js +0 -166
@@ -0,0 +1,163 @@
1
+ // ---------------------------------------------------------------------------
2
+ // engines/harmonia/data/zodiac — what the twelve signs MEAN
3
+ // ---------------------------------------------------------------------------
4
+ //
5
+ // `caelum` answers WHERE: a body's ecliptic longitude becomes `zodiac` (the
6
+ // index), `sign` and `signum`. This table answers WHAT IT MEANS — the doctrine
7
+ // a medieval reader brought to that position.
8
+ //
9
+ // DELIBERATELY NO DATES. When the Sun enters a sign is the ephemeris's
10
+ // business and changes with precession; a table that carried "March 21" would
11
+ // be wrong for most of the period this library models, and wrong differently
12
+ // every century. The index is the join, and `caelum` computes it.
13
+ //
14
+ // The join: `SIGNS[i]`, `SIGNA[i]` (engines/planet/planet.ts) and `ZODIACA[i]`
15
+ // are one list three ways, so `ZODIACA[body.zodiac]` is always that body's
16
+ // doctrine. The two name arrays live with the ephemeris because drawing the
17
+ // wheel needs them without any of this; the meanings live here because nothing
18
+ // in computing a position depends on them.
19
+ //
20
+ // Sources, per field:
21
+ // domicile Ptolemy, Tetrabiblos I.17 [biblio: ptolemy-tetrabiblos],
22
+ // transmitted whole through the medieval Latin tradition and
23
+ // uncontested across sources.
24
+ // exaltation Tetrabiblos I.19 and the standard medieval list, THE SEVEN
25
+ // PLANETS ONLY. The lunar nodes' exaltations (Caput Draconis in
26
+ // Gemini, Cauda in Sagittarius) are omitted because the nodes are
27
+ // not tonus bodies. The exaltation DEGREES (Sun at 19° Arietis
28
+ // and the rest) are omitted as more precision than any consumer
29
+ // here reads — five signs simply have no exaltation, and that
30
+ // absence is the tradition's, not an omission.
31
+ // element, the Ptolemaic triplicities and quadruplicities. The Latin
32
+ // quality quality terms are the scholastic mobile/fixum/commune, where
33
+ // Ptolemy writes tropic/solid/bicorporeal.
34
+ // humor the Galenic element→humor square — fire/cholera, earth/
35
+ // melancholia, air/sanguis, water/phlegma — ubiquitous in
36
+ // medieval medicine.
37
+ // melothesia the homo signorum of medieval calendars and medical
38
+ // manuscripts, Aries at the head down to Pisces at the feet (the
39
+ // Très Riches Heures anatomical man is the familiar picture).
40
+ // Practice, not decoration: phlebotomy was timed against it.
41
+ //
42
+ // Where the tradition wobbles, the entry says so in `variant` rather than
43
+ // picking a winner quietly. Uncertainty is data.
44
+ export const ZODIACA = [
45
+ {
46
+ index: 0, sign: "Aries", signum: "Aries", symbol: "♈",
47
+ element: "fire", elementum: "ignis",
48
+ quality: "cardinal", qualitas: "mobile",
49
+ nature: { heat: "calidum", moisture: "siccum" },
50
+ genus: "masculinum", humor: "cholera", temperament: "cholericum",
51
+ domicile: "Mars", exaltation: "Sun",
52
+ melothesia: { latin: "caput", english: "the head and face" },
53
+ },
54
+ {
55
+ index: 1, sign: "Taurus", signum: "Taurus", symbol: "♉",
56
+ element: "earth", elementum: "terra",
57
+ quality: "fixed", qualitas: "fixum",
58
+ nature: { heat: "frigidum", moisture: "siccum" },
59
+ genus: "femininum", humor: "melancholia", temperament: "melancholicum",
60
+ domicile: "Venus", exaltation: "Moon",
61
+ melothesia: { latin: "collum", english: "the neck and throat" },
62
+ },
63
+ {
64
+ index: 2, sign: "Gemini", signum: "Gemini", symbol: "♊",
65
+ element: "air", elementum: "aer",
66
+ quality: "mutable", qualitas: "commune",
67
+ nature: { heat: "calidum", moisture: "humidum" },
68
+ genus: "masculinum", humor: "sanguis", temperament: "sanguineum",
69
+ domicile: "Mercury", exaltation: null,
70
+ melothesia: { latin: "umeri", english: "the shoulders, arms and hands" },
71
+ },
72
+ {
73
+ index: 3, sign: "Cancer", signum: "Cancer", symbol: "♋",
74
+ element: "water", elementum: "aqua",
75
+ quality: "cardinal", qualitas: "mobile",
76
+ nature: { heat: "frigidum", moisture: "humidum" },
77
+ genus: "femininum", humor: "phlegma", temperament: "phlegmaticum",
78
+ domicile: "Moon", exaltation: "Jupiter",
79
+ melothesia: { latin: "pectus", english: "the breast and chest" },
80
+ variant: "the melothesia wobbles here more than anywhere: some manuscripts "
81
+ + "extend Cancer to the lungs and stomach",
82
+ },
83
+ {
84
+ index: 4, sign: "Leo", signum: "Leo", symbol: "♌",
85
+ element: "fire", elementum: "ignis",
86
+ quality: "fixed", qualitas: "fixum",
87
+ nature: { heat: "calidum", moisture: "siccum" },
88
+ genus: "masculinum", humor: "cholera", temperament: "cholericum",
89
+ domicile: "Sun", exaltation: null,
90
+ melothesia: { latin: "cor", english: "the heart, back and sides" },
91
+ },
92
+ {
93
+ index: 5, sign: "Virgo", signum: "Virgo", symbol: "♍",
94
+ element: "earth", elementum: "terra",
95
+ quality: "mutable", qualitas: "commune",
96
+ nature: { heat: "frigidum", moisture: "siccum" },
97
+ genus: "femininum", humor: "melancholia", temperament: "melancholicum",
98
+ domicile: "Mercury", exaltation: "Mercury",
99
+ melothesia: { latin: "venter", english: "the belly and bowels" },
100
+ },
101
+ {
102
+ index: 6, sign: "Libra", signum: "Libra", symbol: "♎",
103
+ element: "air", elementum: "aer",
104
+ quality: "cardinal", qualitas: "mobile",
105
+ nature: { heat: "calidum", moisture: "humidum" },
106
+ genus: "masculinum", humor: "sanguis", temperament: "sanguineum",
107
+ domicile: "Venus", exaltation: "Saturn",
108
+ melothesia: { latin: "renes", english: "the kidneys and loins" },
109
+ },
110
+ {
111
+ index: 7, sign: "Scorpio", signum: "Scorpius", symbol: "♏",
112
+ element: "water", elementum: "aqua",
113
+ quality: "fixed", qualitas: "fixum",
114
+ nature: { heat: "frigidum", moisture: "humidum" },
115
+ genus: "femininum", humor: "phlegma", temperament: "phlegmaticum",
116
+ domicile: "Mars", exaltation: null,
117
+ melothesia: { latin: "genitalia", english: "the genitals" },
118
+ },
119
+ {
120
+ index: 8, sign: "Sagittarius", signum: "Sagittarius", symbol: "♐",
121
+ element: "fire", elementum: "ignis",
122
+ quality: "mutable", qualitas: "commune",
123
+ nature: { heat: "calidum", moisture: "siccum" },
124
+ genus: "masculinum", humor: "cholera", temperament: "cholericum",
125
+ domicile: "Jupiter", exaltation: null,
126
+ melothesia: { latin: "femora", english: "the thighs" },
127
+ },
128
+ {
129
+ index: 9, sign: "Capricorn", signum: "Capricornus", symbol: "♑",
130
+ element: "earth", elementum: "terra",
131
+ quality: "cardinal", qualitas: "mobile",
132
+ nature: { heat: "frigidum", moisture: "siccum" },
133
+ genus: "femininum", humor: "melancholia", temperament: "melancholicum",
134
+ domicile: "Saturn", exaltation: "Mars",
135
+ melothesia: { latin: "genua", english: "the knees" },
136
+ },
137
+ {
138
+ index: 10, sign: "Aquarius", signum: "Aquarius", symbol: "♒",
139
+ element: "air", elementum: "aer",
140
+ quality: "fixed", qualitas: "fixum",
141
+ nature: { heat: "calidum", moisture: "humidum" },
142
+ genus: "masculinum", humor: "sanguis", temperament: "sanguineum",
143
+ domicile: "Saturn", exaltation: null,
144
+ melothesia: { latin: "crura", english: "the shins and ankles" },
145
+ },
146
+ {
147
+ index: 11, sign: "Pisces", signum: "Pisces", symbol: "♓",
148
+ element: "water", elementum: "aqua",
149
+ quality: "mutable", qualitas: "commune",
150
+ nature: { heat: "frigidum", moisture: "humidum" },
151
+ genus: "femininum", humor: "phlegma", temperament: "phlegmaticum",
152
+ domicile: "Jupiter", exaltation: "Venus",
153
+ melothesia: { latin: "pedes", english: "the feet" },
154
+ },
155
+ ];
156
+ // THE LUNAR PROHIBITION, recorded here rather than exported: while the Moon
157
+ // stands in a sign, that sign's member is not to be touched. It is the reason
158
+ // the zodiac man is in a physician's calendar at all — it was "dangerous, if
159
+ // not fatal, to treat that member if the Moon was in the sign at the time" —
160
+ // and it is one line of doctrine that any consumer can state for itself from
161
+ // `ZODIACA[moon.zodiac].melothesia`. A constant holding a sentence of English
162
+ // would be prose pretending to be data.
163
+ //# sourceMappingURL=zodiac.js.map
@@ -11,6 +11,9 @@ export interface HarmonyTabulaRow {
11
11
  oct: number;
12
12
  spn: string;
13
13
  hz: number;
14
+ /** The doctrina's ratio against the mese, [num, den] — what the sphere IS
15
+ * in the scheme, of which spn and hz are the sounding. */
16
+ ratio: readonly [number, number];
14
17
  presence: number;
15
18
  motion: number;
16
19
  velocity: number;
@@ -15,6 +15,7 @@ export function computeHarmonyTabula(bodies, aspects) {
15
15
  oct: b.nota.pitch.oct,
16
16
  spn: b.nota.pitch.spn,
17
17
  hz: b.nota.pitch.hz,
18
+ ratio: b.ratio,
18
19
  presence: b.presence,
19
20
  motion: b.motion,
20
21
  velocity: b.nota.performance.velocity,
@@ -13,6 +13,10 @@ export interface VoicedBody extends Body {
13
13
  presence: number;
14
14
  motion: number;
15
15
  greekName: string;
16
+ /** The doctrina's own ratio for this sphere, [num, den] against the mese.
17
+ * The pitch is DERIVED from it, so this is the primary datum and the note
18
+ * name is the reading — a caller comparing doctrinae wants the fraction. */
19
+ ratio: readonly [number, number];
16
20
  vowel: PlanetVowel;
17
21
  }
18
22
  export declare function voiceBodies(bodies: Body[], doctrina: Doctrina, scale: Scale): VoicedBody[];
@@ -5,9 +5,8 @@ import { computePresence, computeMotion } from "./presence.js";
5
5
  // reference. Ratios in the doctrina are multiplied by this anchor to produce Hz.
6
6
  const ANCHOR_MIDI = 69;
7
7
  function voiceOne(body, voice, vowel, scale) {
8
- const pliny = voice.greekName === "proslambanomenos" && body.name === "Earth";
9
- const presence = pliny ? 1 : computePresence(body);
10
- const motion = pliny ? 0 : computeMotion(body);
8
+ const presence = computePresence(body);
9
+ const motion = computeMotion(body);
11
10
  // Direct Hz from anchor (A4) and doctrina ratio — bypass scale quantization
12
11
  // so the ratio's pure Hz relationship is preserved.
13
12
  const ratioVal = voice.ratio[0] / voice.ratio[1];
@@ -28,6 +27,7 @@ function voiceOne(body, voice, vowel, scale) {
28
27
  presence,
29
28
  motion,
30
29
  greekName: voice.greekName,
30
+ ratio: voice.ratio,
31
31
  vowel,
32
32
  };
33
33
  }
@@ -41,9 +41,13 @@ export function voiceBodies(bodies, doctrina, scale) {
41
41
  const voice = voiceByBody.get(body.name);
42
42
  if (!voice)
43
43
  continue; // body not part of this doctrina (e.g. Earth in Boethius)
44
+ // Only the seven vowel-bearing planets voice. Earth stays silent even
45
+ // under Pliny, whose table assigns it proslambanomenos — the ratio is
46
+ // kept in the doctrina as documentation, but no classical vowel exists
47
+ // for Earth, and tonus does not invent one.
44
48
  const vowel = PLANET_VOWELS[body.name];
45
49
  if (!vowel)
46
- continue; // body has no classical vowel mapping (e.g. Earth, FixedStars)
50
+ continue;
47
51
  result.push(voiceOne(body, voice, vowel, scale));
48
52
  }
49
53
  return result;
@@ -84,6 +84,10 @@ export function computeImprint(phrases, scale, opts = {}) {
84
84
  const cadenceNotes = opts.cadenceNotes;
85
85
  const pcCounts = new Array(12).fill(0);
86
86
  let total = 0;
87
+ // For the tessitura signal: the mean MIDI of every note, and the last note.
88
+ let midiSum = 0;
89
+ let noteCount = 0;
90
+ let lastNote = null;
87
91
  for (let pi = 0; pi < phrases.length; pi++) {
88
92
  const phrase = phrases[pi];
89
93
  for (let si = 0; si < phrase.syllables.length; si++) {
@@ -97,6 +101,9 @@ export function computeImprint(phrases, scale, opts = {}) {
97
101
  w *= CADENCE_WEIGHT;
98
102
  pcCounts[note.pitch.pc] += w;
99
103
  total += w;
104
+ midiSum += note.pitch.midi;
105
+ noteCount++;
106
+ lastNote = { pc: note.pitch.pc, midi: note.pitch.midi };
100
107
  }
101
108
  }
102
109
  }
@@ -105,11 +112,17 @@ export function computeImprint(phrases, scale, opts = {}) {
105
112
  pcDistribution[pc] = total > 0 ? pcCounts[pc] / total : 0;
106
113
  }
107
114
  const firstNotePc = phrases[0]?.syllables[0]?.notes[0]?.pitch.pc;
115
+ // Tessitura = the melody's mean height above where it comes to rest.
116
+ const tessitura = lastNote && noteCount > 0 ? midiSum / noteCount - lastNote.midi : undefined;
108
117
  return {
109
118
  pcDistribution,
110
119
  attractors: computeAttractors(pcDistribution, scale),
111
120
  vowelAttractors: computeVowelAttractors(phrases, scale),
112
- modalAffinity: computeModalAffinity(pcDistribution, firstNotePc),
121
+ modalAffinity: computeModalAffinity(pcDistribution, {
122
+ firstNotePc,
123
+ lastNotePc: lastNote?.pc,
124
+ tessitura,
125
+ }),
113
126
  };
114
127
  }
115
128
  /** Build an Imprint from voiced planetary bodies (presence-weighted pc counts). */
@@ -34,7 +34,7 @@ export const ORBITAL_ELEMENTS = new Map([
34
34
  datasets: [
35
35
  [
36
36
  [0.72333566, 0.0000039],
37
- [0.00676399, -0.00005107],
37
+ [0.00677672, -0.00004107],
38
38
  [3.39467605, -0.0007889],
39
39
  [181.9790995, 58517.81538729],
40
40
  [131.60246718, 0.00268329],
@@ -42,7 +42,7 @@ export const ORBITAL_ELEMENTS = new Map([
42
42
  ],
43
43
  [
44
44
  [0.72332102, -0.00000026],
45
- [-0.00005107, 0.01673163],
45
+ [0.00676399, -0.00005107],
46
46
  [3.39777545, 0.00043494],
47
47
  [181.9797085, 58517.8156026],
48
48
  [131.76755713, 0.05679648],
@@ -68,7 +68,7 @@ export const ORBITAL_ELEMENTS = new Map([
68
68
  [0, 0],
69
69
  ],
70
70
  [
71
- [1.00000018, 1.52371243],
71
+ [1.00000018, -0.00000003],
72
72
  [0.01673163, -0.00003661],
73
73
  [-0.00054346, -0.01337178],
74
74
  [100.46691572, 35999.37306329],
@@ -129,7 +129,7 @@ export const ORBITAL_ELEMENTS = new Map([
129
129
  [14.27495244, 0.18199196],
130
130
  [100.29282654, 0.13024619],
131
131
  ],
132
- [0.00012452, 0.0606406, -0.35635438, 38.35125],
132
+ [-0.00012452, 0.0606406, -0.35635438, 38.35125],
133
133
  ],
134
134
  radius: 69911,
135
135
  rotation_period: 0.41354,
@@ -1,4 +1,14 @@
1
1
  import type { Cosmos, CosmosQuery } from "./types.js";
2
+ /** The twelve signs, in ecliptic order from the vernal point — the machine
3
+ * codes. Exported because a body's `sign` is one of these and a consumer
4
+ * drawing the zodiac needs the same twelve in the same order; copying them out
5
+ * is how a second, drifting list gets written. */
6
+ export declare const SIGNS: readonly ["Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces"];
7
+ /** The same twelve as the books name them. Eight are already their own Latin
8
+ * nominative; the four that differ are the ones English clipped — Scorpius for
9
+ * Scorpio, and the -us/-i endings the others dropped. Parallel to a body's
10
+ * `name`/`nomen` pair: the code stays English, the Latin rides beside it. */
11
+ export declare const SIGNA: readonly ["Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra", "Scorpius", "Sagittarius", "Capricornus", "Aquarius", "Pisces"];
2
12
  /**
3
13
  * Ephemeris lookup (`tonus.caelum`). Computes geocentric and heliocentric
4
14
  * positions, zodiac signs, retrogradation, and aspects for the classical
@@ -10,12 +10,25 @@ import { latinName } from "./types.js";
10
10
  import { DEFAULT_EPOCH } from "../epoch.js";
11
11
  const MS_PER_DAY = 86400000;
12
12
  const ALL_BODIES = ["Sun", "Moon", "Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn"];
13
- const SIGNS = [
13
+ /** The twelve signs, in ecliptic order from the vernal point — the machine
14
+ * codes. Exported because a body's `sign` is one of these and a consumer
15
+ * drawing the zodiac needs the same twelve in the same order; copying them out
16
+ * is how a second, drifting list gets written. */
17
+ export const SIGNS = [
14
18
  "Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
15
19
  "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces",
16
20
  ];
21
+ /** The same twelve as the books name them. Eight are already their own Latin
22
+ * nominative; the four that differ are the ones English clipped — Scorpius for
23
+ * Scorpio, and the -us/-i endings the others dropped. Parallel to a body's
24
+ * `name`/`nomen` pair: the code stays English, the Latin rides beside it. */
25
+ export const SIGNA = [
26
+ "Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
27
+ "Libra", "Scorpius", "Sagittarius", "Capricornus", "Aquarius", "Pisces",
28
+ ];
17
29
  const zodiac = (lon) => Math.floor(wrapAngle(lon) / 30) % 12;
18
30
  const sign = (lon) => SIGNS[zodiac(lon)];
31
+ const signum = (lon) => SIGNA[zodiac(lon)];
19
32
  function computeSpeed(geoLon, name, ts) {
20
33
  const nextState = getState(ts + MS_PER_DAY);
21
34
  const nextSun = sunPos(nextState);
@@ -50,6 +63,7 @@ function buildSun(ts) {
50
63
  apparentDiameter: app.apparentDiameter,
51
64
  zodiac: zodiac(pos.geo.lon),
52
65
  sign: sign(pos.geo.lon),
66
+ signum: signum(pos.geo.lon),
53
67
  };
54
68
  }
55
69
  function buildMoon(ts) {
@@ -82,6 +96,7 @@ function buildMoon(ts) {
82
96
  apparentDiameter: app.apparentDiameter,
83
97
  zodiac: zodiac(pos.geo.lon),
84
98
  sign: sign(pos.geo.lon),
99
+ signum: signum(pos.geo.lon),
85
100
  distEarthRadii: pos.distEarthRadii,
86
101
  };
87
102
  }
@@ -115,8 +130,14 @@ function buildPlanet(name, ts) {
115
130
  elongation: app.elongation,
116
131
  phase: app.phase,
117
132
  apparentDiameter: app.apparentDiameter,
118
- zodiac: zodiac(pos.helio.lon),
119
- sign: sign(pos.helio.lon),
133
+ // GEOCENTRIC, as the Sun's and Moon's are: a sign placement says where a
134
+ // body appears from here, which is the only frame in which "Mars in
135
+ // Sagittarius" means anything. Reading the heliocentric longitude put a
136
+ // body's own sign at odds with its own geo.lon — Mercury reported in Aries
137
+ // while appearing in Taurus.
138
+ zodiac: zodiac(pos.geo.lon),
139
+ sign: sign(pos.geo.lon),
140
+ signum: signum(pos.geo.lon),
120
141
  };
121
142
  }
122
143
  function buildEarth(ts) {
@@ -142,6 +163,7 @@ function buildEarth(ts) {
142
163
  apparentDiameter: 0,
143
164
  zodiac: zodiac(pos.helio.lon),
144
165
  sign: sign(pos.helio.lon),
166
+ signum: signum(pos.helio.lon),
145
167
  };
146
168
  }
147
169
  const BODY_BUILDERS = {
@@ -192,7 +214,12 @@ export function getCosmos(query = {}) {
192
214
  }
193
215
  return frames;
194
216
  }
217
+ if (query.feast != null &&
218
+ (typeof query.feast !== "object" || !(query.feast.date instanceof Date)))
219
+ throw new Error("caelum: feast must be a Feast (from tonus.festum) — its date places the sky");
195
220
  const date = query.date ?? query.feast?.date ?? DEFAULT_EPOCH;
221
+ if (!(date instanceof Date) || Number.isNaN(date.getTime()))
222
+ throw new Error(`caelum: date must be a Date — e.g. new Date("2026-12-25") (UTC-canonical)`);
196
223
  return snapshotAt(date, requested, query.orbLimit);
197
224
  }
198
225
  //# sourceMappingURL=planet.js.map
@@ -16,8 +16,8 @@
16
16
  // So the reader should not expect the same constants or solver across bodies.
17
17
  //
18
18
  // A few time-scale models here are standard astronomy but not yet catalogued in
19
- // BIBLIOGRAPHY.md (marked "source TBD" at each): the ΔT (TT−UT) polynomial and
20
- // the mean-obliquity expansion.
19
+ // ../../../BIBLIOGRAPHY.md (marked "source TBD" at each): the ΔT (TT−UT)
20
+ // polynomial and the mean-obliquity expansion.
21
21
  import { sinDeg, cosDeg, atan2Deg, kepler, wrapAngle, toAu, toCartesian, toSpherical, toEquatorial } from "./math.js";
22
22
  import { ORBITAL_ELEMENTS } from "./orbital.js";
23
23
  const MS_PER_DAY = 86400000;
@@ -157,18 +157,21 @@ export function planetPos(name, state, sun) {
157
157
  throw new Error(`Unknown body: ${name}`);
158
158
  const { J, T, eps } = state;
159
159
  const oe = body.datasets;
160
- // Two Standish element sets per body (see orbital.ts): [1] is fitted tightly
161
- // for 1800–2050, [0] trades accuracy for 3000 BC–3000 AD coverage. The bounds
162
- // are J (days from J2000): −73048.5 ≈ 1800, 18626.5 ≈ 2050. Inside the window
163
- // use the precise set, outside fall back to the long-range one.
164
- const dataset = J > -73048.5 && J < 18626.5 ? oe[1] : oe[0];
160
+ // Two Standish element sets per body (see orbital.ts): [0] is Table 1, fitted
161
+ // tightly for 1800–2050; [1] is Table 2a, trading accuracy for 3000 BC–3000 AD
162
+ // coverage. The bounds are J (days from J2000): −73048.5 ≈ 1800, 18626.5 ≈
163
+ // 2050. Inside the window use the precise set, outside fall back to the
164
+ // long-range one — which is the set tonus's medieval epoch runs on.
165
+ const inWindow = J > -73048.5 && J < 18626.5;
166
+ const dataset = inWindow ? oe[0] : oe[1];
165
167
  const [a, e, I, L, wBar, Omega] = dataset.map(([x0, x1]) => x0 + x1 * T);
166
168
  const omega = wBar - Omega; // argument of periapsis
167
169
  let M = L - wBar; // mean anomaly
168
170
  // Standish's great-inequality correction for the outer planets (Jupiter–Neptune):
169
- // a secular b·T² plus a long-period cos/sin term at frequency f. Only bodies
170
- // that carry a datasets[2] (see orbital.ts) get it. [biblio: standish-jpl]
171
- if (oe[2]) {
171
+ // a secular b·T² plus a long-period cos/sin term at frequency f. Table 2b is
172
+ // defined for use with the Table 2a elements only, so it applies exactly when
173
+ // the long-range set is in play. [biblio: standish-jpl]
174
+ if (!inWindow && oe[2]) {
172
175
  const [b, c, s, f] = oe[2];
173
176
  M += b * T * T + c * cosDeg(f * T) + s * sinDeg(f * T);
174
177
  }
@@ -33,6 +33,7 @@ export interface Body {
33
33
  };
34
34
  zodiac: number;
35
35
  sign: string;
36
+ signum: string;
36
37
  distEarthRadii?: number;
37
38
  }
38
39
  export interface Aspect {
@@ -3,8 +3,6 @@ import { type Prosody } from "./prosody.js";
3
3
  import { type Cadence } from "./cadence.js";
4
4
  import { type Modulation } from "./modulation.js";
5
5
  import { type ChantTabulaRow } from "./tabula.js";
6
- import { type MidiOpts, type MidiEmitResult } from "./emitters/midi.js";
7
- import { type MusicXmlOpts, type MusicXmlEmitResult } from "./emitters/musicxml.js";
8
6
  import type { Chant } from "../chant/types.js";
9
7
  import type { Temperamentum } from "../temper/api.js";
10
8
  import type { ArticulationProfile, PhrasingProfile, ParseError, Phrase as IRPhrase } from "./types.js";
@@ -36,14 +34,6 @@ export interface Score {
36
34
  /** Passages where the tonal centre leans away from the home mode. */
37
35
  modulations: Modulation[];
38
36
  imprint: Imprint;
39
- /**
40
- * Emit a Standard MIDI File from the score's tabula. Returns the file bytes
41
- * by default; `format: "json"` returns the event structure, `"both"` an
42
- * object. The score's pondus/accentus reach the output via the tabula.
43
- */
44
- midi(opts?: MidiOpts): Uint8Array | MidiEmitResult;
45
- /** Emit a MusicXML 4.0 partwise document from the score's tabula. */
46
- musicxml(opts?: MusicXmlOpts): MusicXmlEmitResult;
47
37
  }
48
38
  /**
49
39
  * Score builder (`tonus.notatio`). Parses a chant's GABC into a musical
@@ -55,8 +45,7 @@ export interface Score {
55
45
  */
56
46
  export declare function buildScore(chant: Chant, opts?: ScoreOpts): Score;
57
47
  export type { ParseError };
58
- export type { Cadence, CadenceTarget, CadenceApproach } from "./cadence.js";
48
+ export type { Cadence, CadenceTarget, CadenceApproach, CadenceKeyEvent } from "./cadence.js";
49
+ export { cadenceKeys } from "./cadence.js";
59
50
  export type { Modulation } from "./modulation.js";
60
- export type { MidiOpts, MidiEmitResult, MidiJsonResult, MidiJsonEvent } from "./emitters/midi.js";
61
- export type { MusicXmlOpts, MusicXmlEmitResult } from "./emitters/musicxml.js";
62
51
  //# sourceMappingURL=api.d.ts.map
@@ -11,8 +11,7 @@ import { detectCadences } from "./cadence.js";
11
11
  import { detectModulations } from "./modulation.js";
12
12
  import { computeTabula } from "./tabula.js";
13
13
  import { MODES } from "../temper/modes.js";
14
- import { toMidi } from "./emitters/midi.js";
15
- import { toMusicXML } from "./emitters/musicxml.js";
14
+ import { cadentiaFamilia } from "../../data/cadentiae.js";
16
15
  const PONDUS_TO_ARTICULATION = {
17
16
  restrained: "restrained",
18
17
  balanced: "balanced",
@@ -40,6 +39,8 @@ function resolveAccentus(input) {
40
39
  * @throws Error on invalid Chant input or unparseable GABC.
41
40
  */
42
41
  export function buildScore(chant, opts) {
42
+ if (!chant || typeof chant !== "object" || typeof chant.gabc !== "string")
43
+ throw new Error("notatio needs a Chant with a gabc string — build one with tonus.cantus({ gabc })");
43
44
  const pondus = resolvePondus(opts?.pondus);
44
45
  const accentus = resolveAccentus(opts?.accentus);
45
46
  const parsed = parseGABC(chant.gabc, {
@@ -62,10 +63,24 @@ export function buildScore(chant, opts) {
62
63
  // Cadence detection runs here, where the resolved mode (and its cadence
63
64
  // figures) is in hand. Pure data — mirrors the arsis/thesis pass in ir.ts.
64
65
  const cadences = detectCadences(ir.phrases, meta.mode != null ? MODES.get(meta.mode) : undefined);
66
+ // The corpus join, on the same footing as MODES above: a resolved table
67
+ // meeting detected data. The detector computes the signature and stops, so
68
+ // this is the one place a cadence learns how often its family closes.
69
+ // Below the catalogue's floor there is no family, and finality stays null —
70
+ // an uncatalogued close, not a close that never closes.
71
+ for (const cadence of cadences) {
72
+ cadence.finality = cadence.signature
73
+ ? (cadentiaFamilia(cadence.signature)?.finality ?? null)
74
+ : null;
75
+ }
65
76
  // Modulation: where the tonal centre leans away from the home mode.
66
77
  const modulations = detectModulations(ir.phrases, meta.mode ?? undefined);
67
78
  const tabula = computeTabula(ir, {
68
79
  mode: meta.mode ?? undefined,
80
+ // The office gate: a chant that names its liturgical type gets phrasing
81
+ // even when its mode must be inferred — the gate computeTabula always had,
82
+ // now actually fed.
83
+ office: chant.office,
69
84
  a4Hz: opts?.temperamentum?.a4,
70
85
  transpose: opts?.temperamentum?.transpose,
71
86
  cadences,
@@ -93,12 +108,10 @@ export function buildScore(chant, opts) {
93
108
  return `${pi}:${si}:${ni}`;
94
109
  })),
95
110
  }),
96
- midi(emitOpts) {
97
- return toMidi(tabula, emitOpts);
98
- },
99
- musicxml(emitOpts) {
100
- return toMusicXML(tabula, chant, emitOpts);
101
- },
102
111
  };
103
112
  }
113
+ // THE cadence family key, exported as a FUNCTION and not only a type: the
114
+ // census and the CADENTIAE miner need to key a flat tabula, and re-deriving
115
+ // the algorithm is exactly the fork this shared export forbids.
116
+ export { cadenceKeys } from "./cadence.js";
104
117
  //# sourceMappingURL=api.js.map
@@ -3,8 +3,8 @@
3
3
  // ---------------------------------------------------------------------------
4
4
  // The pondus ("weight") tables. Each GABC performance mark — episema, quilisma,
5
5
  // liquescent, strophicus, oriscus, ictus — carries a weight delta and a
6
- // duration delta that the parser folds into a note's rhythmicShape (see the
7
- // tanh compressor in parse.ts). The signs encode the semiological reading of
6
+ // duration delta that the parser folds into a note's weight and duration (see
7
+ // the tanh compressor in parse.ts). The signs encode the semiological reading of
8
8
  // the mark [biblio: cardine-semiology], and the durational values the Solesmes
9
9
  // rhythmic tradition [biblio: desrocquettes-values, liber-usualis]: POSITIVE
10
10
  // lengthens/stresses, NEGATIVE lightens/shortens. So an episema lengthens
@@ -28,7 +28,83 @@ export interface Cadence {
28
28
  confidence: number;
29
29
  /** Note positions forming this cadence: [phraseIndex, syllableIndex, noteIndex]. */
30
30
  notes: Array<[number, number, number]>;
31
+ /**
32
+ * The corpus-catalogue key, "shape @arrival" (e.g. "2,0,-2 @0" — see
33
+ * CADENTIAE). A single-note phrase keys with an empty shape (" @0"):
34
+ * a landing with no gesture is still a cadence.
35
+ */
36
+ signature: string | null;
37
+ /** Interval signature of the closing tail (<=4 notes), in semitones. */
38
+ shape: number[];
39
+ /** Closing note minus the CHANT'S OWN closing note (its sounded final —
40
+ * not the labeled mode's final, which may disagree on a transposed or
41
+ * mislabeled chant), in SIGNED semitones — not octave-reduced. */
42
+ arrival: number;
43
+ /**
44
+ * The catalogued family's measured finality: the share of THIS FAMILY's
45
+ * corpus occurrences that fall at a final close. null when the signature is
46
+ * below the catalogue's floor (about a third of cadences), and null on a
47
+ * cadence taken straight from `detectCadences` — the join happens in the
48
+ * score builder, not the detector.
49
+ *
50
+ * A measurement, not a name, which is why it rides here while `familia`
51
+ * does not: the signature is already the family's name, but how often that
52
+ * family CLOSES cannot be read off the signature. Families landing on the
53
+ * final range from 0.054 to 1.000, so `arrival === 0` does not imply a close.
54
+ */
55
+ finality: number | null;
31
56
  }
57
+ /**
58
+ * Octave-reduce a semitone offset to [-5..+6].
59
+ *
60
+ * NOT part of the family key — kept because the folded value is still worth
61
+ * reporting (it is the scale DEGREE, mode-theoretically real). As a key the
62
+ * fold made a fifth ABOVE the final share a family with a fourth BELOW:
63
+ * measured over 27,985 phrase ends, 3,499 landed on @-5, of which 2,427 were
64
+ * really +7 and 1,072 really -5. Two opposite gestures, one key. Arrival in
65
+ * the key is therefore the SIGNED offset; see cadenceKeys().
66
+ */
67
+ export declare function reduceArrival(semitones: number): number;
68
+ /** One phrase-end event: the family key, and whether it closes the chant. */
69
+ export interface CadenceKeyEvent {
70
+ /** `"<interval,interval,…> @<signed arrival>"` — empty shape for a 1-note phrase. */
71
+ key: string;
72
+ /** Shape only: the tail's successive semitone intervals. */
73
+ shape: number[];
74
+ /** Signed semitone offset of the landing note from the chant's closing note. */
75
+ arrival: number;
76
+ /** The arrival octave-reduced to [-5..+6] — the scale degree, not the key. */
77
+ degree: number;
78
+ /** A chant end, or a full-bar "::" — as opposed to an interior phrase end. */
79
+ isFinal: boolean;
80
+ /** Index of the phrase this closes. */
81
+ phraseIndex: number;
82
+ }
83
+ /**
84
+ * THE cadence family key — one implementation, shared by every consumer.
85
+ *
86
+ * This once existed three times: here (keyed off the Phrase tree), in
87
+ * tonus-corpus `census/_shared.mjs` (keyed off flat tabula rows), and in the
88
+ * CADENTIAE miner (a character-for-character copy of the census one). They
89
+ * agreed — measured corpus-wide, 28,051 engine cadences against 27,985 census
90
+ * phrase ends with ZERO key disagreements — but agreement by luck across three
91
+ * copies is what "no second parser, no drift" forbids; hence this one shared
92
+ * function.
93
+ *
94
+ * Takes the FLAT shape, because that is what the census and the miner have; the
95
+ * engine's own detection flattens into it. A phrase end is a `phraseIndex`
96
+ * transition or the last row.
97
+ *
98
+ * A ONE-NOTE PHRASE IS A CADENCE — it has a landing but no gesture, so it is
99
+ * emitted with an empty shape rather than skipped. (The census once dropped
100
+ * these — 68 corpus-wide, all real phrases carrying a real divisio, mostly
101
+ * "::" at the chant end — a hole this shared key closes.)
102
+ */
103
+ export declare function cadenceKeys(rows: readonly {
104
+ phraseIndex: number;
105
+ midi: number;
106
+ divisio: string | null;
107
+ }[], finalMidi?: number): CadenceKeyEvent[];
32
108
  /**
33
109
  * Detect the cadence closing each phrase. One Cadence per phrase that carries a
34
110
  * divisio. With no mode, targets/approach are still classified but no figure is