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,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
@@ -125,6 +125,84 @@ function bestFigure(observed, figures) {
125
125
  }
126
126
  return best;
127
127
  }
128
+ // ── The corpus catalogue (CADENTIAE) ────────────────────────────────────────
129
+ // The mining keyed tails by their last <=4 notes; the classifier reads the same
130
+ // span out of the (longer) formula window.
131
+ const TAIL = 4;
132
+ /**
133
+ * Octave-reduce a semitone offset to [-5..+6].
134
+ *
135
+ * NOT part of the family key — kept because the folded value is still worth
136
+ * reporting (it is the scale DEGREE, mode-theoretically real). As a key the
137
+ * fold made a fifth ABOVE the final share a family with a fourth BELOW:
138
+ * measured over 27,985 phrase ends, 3,499 landed on @-5, of which 2,427 were
139
+ * really +7 and 1,072 really -5. Two opposite gestures, one key. Arrival in
140
+ * the key is therefore the SIGNED offset; see cadenceKeys().
141
+ */
142
+ export function reduceArrival(semitones) {
143
+ let a = semitones % 12;
144
+ if (a > 6)
145
+ a -= 12;
146
+ if (a < -5)
147
+ a += 12;
148
+ return a;
149
+ }
150
+ /**
151
+ * THE cadence family key — one implementation, shared by every consumer.
152
+ *
153
+ * This once existed three times: here (keyed off the Phrase tree), in
154
+ * tonus-corpus `census/_shared.mjs` (keyed off flat tabula rows), and in the
155
+ * CADENTIAE miner (a character-for-character copy of the census one). They
156
+ * agreed — measured corpus-wide, 28,051 engine cadences against 27,985 census
157
+ * phrase ends with ZERO key disagreements — but agreement by luck across three
158
+ * copies is what "no second parser, no drift" forbids; hence this one shared
159
+ * function.
160
+ *
161
+ * Takes the FLAT shape, because that is what the census and the miner have; the
162
+ * engine's own detection flattens into it. A phrase end is a `phraseIndex`
163
+ * transition or the last row.
164
+ *
165
+ * A ONE-NOTE PHRASE IS A CADENCE — it has a landing but no gesture, so it is
166
+ * emitted with an empty shape rather than skipped. (The census once dropped
167
+ * these — 68 corpus-wide, all real phrases carrying a real divisio, mostly
168
+ * "::" at the chant end — a hole this shared key closes.)
169
+ */
170
+ export function cadenceKeys(rows, finalMidi) {
171
+ if (!rows.length)
172
+ return [];
173
+ const final = finalMidi ?? rows[rows.length - 1].midi;
174
+ const events = [];
175
+ for (let i = 0; i < rows.length; i++) {
176
+ const next = rows[i + 1];
177
+ if (next && next.phraseIndex === rows[i].phraseIndex)
178
+ continue; // not a phrase end
179
+ // The last <=TAIL rows of this phrase, ending at row i.
180
+ const seg = [];
181
+ for (let j = i; j >= 0 && seg.length < TAIL && rows[j].phraseIndex === rows[i].phraseIndex; j--) {
182
+ seg.unshift(rows[j]);
183
+ }
184
+ const shape = seg.slice(1).map((r, k) => r.midi - seg[k].midi);
185
+ const arrival = seg[seg.length - 1].midi - final;
186
+ events.push({
187
+ key: `${shape.join(",")} @${arrival}`,
188
+ shape,
189
+ arrival,
190
+ degree: reduceArrival(arrival),
191
+ isFinal: rows[i].divisio === "::" || !next,
192
+ phraseIndex: rows[i].phraseIndex,
193
+ });
194
+ }
195
+ return events;
196
+ }
197
+ /** The chant's closing note — the reference every arrival is measured from. */
198
+ function chantFinalMidi(phrases) {
199
+ for (let pi = phrases.length - 1; pi >= 0; pi--) {
200
+ const w = phraseFinalWindow(phrases[pi]);
201
+ if (w.length > 0)
202
+ return w[w.length - 1].midi;
203
+ }
204
+ return undefined;
205
+ }
128
206
  /**
129
207
  * Detect the cadence closing each phrase. One Cadence per phrase that carries a
130
208
  * divisio. With no mode, targets/approach are still classified but no figure is
@@ -132,6 +210,7 @@ function bestFigure(observed, figures) {
132
210
  */
133
211
  export function detectCadences(phrases, modeData) {
134
212
  const cadences = [];
213
+ const finalMidi = chantFinalMidi(phrases);
135
214
  for (let pi = 0; pi < phrases.length; pi++) {
136
215
  const phrase = phrases[pi];
137
216
  if (!phrase.divisio)
@@ -162,6 +241,15 @@ export function detectCadences(phrases, modeData) {
162
241
  }
163
242
  }
164
243
  }
244
+ // The corpus classification, from THE shared key function — the engine does
245
+ // not compute this itself. `cadenceKeys` takes flat rows, so the window is
246
+ // projected into that shape; one phrase in, one event out.
247
+ const ev = cadenceKeys(window.map((w) => ({ phraseIndex: 0, midi: w.midi, divisio: null })), finalMidi)[0];
248
+ const shape = ev?.shape ?? [];
249
+ const arrival = ev?.arrival ?? 0;
250
+ // A 1-note phrase has a landing but no gesture: a real cadence with an
251
+ // empty shape, which is why `signature` is the key rather than null.
252
+ const signature = ev?.key ?? null;
165
253
  cadences.push({
166
254
  phraseIndex: pi,
167
255
  divisio,
@@ -172,6 +260,14 @@ export function detectCadences(phrases, modeData) {
172
260
  steps,
173
261
  confidence: Math.round(confidence * 100) / 100,
174
262
  notes: window.map((w) => [pi, w.syllableIndex, w.noteIndex]),
263
+ signature,
264
+ shape,
265
+ arrival,
266
+ // Left null here on purpose. Detection is a pure pass over the phrase
267
+ // tree; the corpus catalogue is generated data, and reaching for it from
268
+ // inside the detector would put a baked artifact in the detection path.
269
+ // buildScore joins it, where MODES is already joined.
270
+ finality: null,
175
271
  });
176
272
  }
177
273
  return cadences;
@@ -0,0 +1,21 @@
1
+ import type { ChantTabulaRow } from "../tabula.js";
2
+ export type AccidentalMode = "standard" | "heji" | "cents";
3
+ export type CentsBaseline = "pythagorean" | "et";
4
+ /** One note's intonation mark, for the emitter to place before/above the head. */
5
+ export interface AccidentalMark {
6
+ /** "glyph" → a notehead-preceding accidental; "cents" → a label floating above the staff. */
7
+ kind: "glyph" | "cents";
8
+ /** Glyph codepoint (kind "glyph") — a standard or HEJI accidental. */
9
+ glyph?: string;
10
+ /** Text label (kind "cents") — e.g. "−3.9". */
11
+ label?: string;
12
+ }
13
+ export type AccidentalGlyphSet = "modern" | "medieval";
14
+ /**
15
+ * Compute the intonation mark for each tabula row, or null where none applies.
16
+ * Repeat-suppression (standard mode) and per-phrase pitch-class suppression
17
+ * (cents mode) keep the staff from being carpeted.
18
+ * @throws Error when `heji` is asked of a non-just (meantone) tuning.
19
+ */
20
+ export declare function computeAccidentals(rows: ChantTabulaRow[], mode: AccidentalMode, centsBaseline?: CentsBaseline, glyphSet?: AccidentalGlyphSet): (AccidentalMark | null)[];
21
+ //# sourceMappingURL=accidentals.d.ts.map
@@ -0,0 +1,88 @@
1
+ import { pythagoreanCentsByPc } from "../../temper/scale.js";
2
+ // The Pythagorean chain's ET-cents deviation per pitch class, derived from the
3
+ // SAME E♭–G♯ chain the engine tunes with (temper/scale.ts) so baseline and
4
+ // engine can never disagree — a hardcoded copy once drifted and broke the
5
+ // heji channel for every flatted chant under the default tuning.
6
+ const PYTH_BASELINE = pythagoreanCentsByPc();
7
+ const GLYPHS_BY_SET = {
8
+ modern: { [-1]: "E260", 0: "E261", 1: "E262" },
9
+ medieval: { [-1]: "E9E0", 0: "E9E1", 1: "E9E3" },
10
+ };
11
+ // HEJI comma glyphs (Extended Helmholtz–Ellis, U+E2C0–). The syntonic-comma
12
+ // arrows: one comma down / up. Higher-order commas are out of scope for 0.2.
13
+ const HEJI_COMMA_DOWN = "E2C2"; // one syntonic comma lower
14
+ const HEJI_COMMA_UP = "E2C7"; // one syntonic comma higher
15
+ /** Deviation of a note from the Pythagorean chain, in cents. */
16
+ function fromPythagorean(row) {
17
+ return row.offset - (PYTH_BASELINE[row.pc] ?? 0);
18
+ }
19
+ /**
20
+ * Compute the intonation mark for each tabula row, or null where none applies.
21
+ * Repeat-suppression (standard mode) and per-phrase pitch-class suppression
22
+ * (cents mode) keep the staff from being carpeted.
23
+ * @throws Error when `heji` is asked of a non-just (meantone) tuning.
24
+ */
25
+ export function computeAccidentals(rows, mode, centsBaseline = "pythagorean", glyphSet = "modern") {
26
+ if (mode === "standard") {
27
+ // A glyph before any note whose pitch is explicitly inflected — but not on
28
+ // an immediately-repeated same pitch (restate only after another pitch).
29
+ let prevPc = null;
30
+ return rows.map((row) => {
31
+ const repeat = row.pc === prevPc;
32
+ prevPc = row.pc;
33
+ if (row.accidentalSource !== "explicit" || repeat)
34
+ return null;
35
+ // Draw the SIGN the source wrote, not this note's own alteration. In
36
+ // `fe(jx)cit(ih)` the flat is printed before the I while it governs J —
37
+ // where a sign is printed and which degree it alters are different facts.
38
+ // Reading `row.accidental` here drew nothing in that case, because the I
39
+ // is unaltered.
40
+ const sign = row.accidentalSign ?? row.accidental;
41
+ return { kind: "glyph", glyph: GLYPHS_BY_SET[glyphSet][sign] };
42
+ });
43
+ }
44
+ if (mode === "heji") {
45
+ // A tuning is just-expressible when its departures from Pythagorean are
46
+ // whole syntonic commas (±21.5¢). An irrational tempering (meantone) leaves
47
+ // fractional deviations everywhere — heji cannot notate it. Detect that.
48
+ const devs = rows.map(fromPythagorean);
49
+ const SYNTONIC = 21.506;
50
+ const tempered = devs.some((d) => {
51
+ if (Math.abs(d) < 0.5)
52
+ return false; // on the pure-fifth chain
53
+ const commas = d / SYNTONIC;
54
+ return Math.abs(commas - Math.round(commas)) > 0.1; // not a whole comma
55
+ });
56
+ if (tempered) {
57
+ throw new Error("inscriptio: accidentals \"heji\" needs a just-expressible tuning; the " +
58
+ "current temperament tempers by fractional commas (e.g. meantone). Use " +
59
+ "accidentals: \"cents\" for tempered tunings.");
60
+ }
61
+ return rows.map((row) => {
62
+ const d = fromPythagorean(row);
63
+ if (Math.abs(d) < 0.5)
64
+ return null; // Pythagorean baseline — no arrow
65
+ const commas = Math.round(d / SYNTONIC);
66
+ if (commas === 0)
67
+ return null;
68
+ return { kind: "glyph", glyph: commas < 0 ? HEJI_COMMA_DOWN : HEJI_COMMA_UP };
69
+ });
70
+ }
71
+ // cents — signed deviation label, stated once per pitch class PER PHRASE.
72
+ // A deviating pitch is labelled at its first appearance in each phrase and
73
+ // rides silently through the phrase's repeats; the next phrase restates it
74
+ // (the reader's memory is the phrase, and chant has no measures to carry it).
75
+ const seen = new Set();
76
+ return rows.map((row) => {
77
+ const value = centsBaseline === "et" ? row.offset : fromPythagorean(row);
78
+ if (Math.abs(value) < 0.5)
79
+ return null; // effectively in tune
80
+ const key = `${row.phraseIndex}:${row.pc}`;
81
+ if (seen.has(key))
82
+ return null; // already labelled in this phrase
83
+ seen.add(key);
84
+ const sign = value < 0 ? "−" : "+"; // real minus sign
85
+ return { kind: "cents", label: `${sign}${Math.abs(value).toFixed(1)}` };
86
+ });
87
+ }
88
+ //# sourceMappingURL=accidentals.js.map