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.
- package/BIBLIOGRAPHY.md +132 -108
- package/CHANGELOG.md +598 -1
- package/LICENSE +133 -29
- package/README.md +106 -83
- package/dist/data/am.js +2666 -11196
- package/dist/data/ams.d.ts +5 -0
- package/dist/data/ams.js +122 -0
- package/dist/data/attestation.d.ts +19 -0
- package/dist/data/attestation.js +15716 -0
- package/dist/data/attestation.json +15711 -0
- package/dist/data/cadentiae.d.ts +43 -0
- package/dist/data/cadentiae.js +174 -0
- package/dist/data/cal.js +48 -0
- package/dist/data/census.d.ts +12 -0
- package/dist/data/census.js +36 -0
- package/dist/data/commune-office.d.ts +4 -0
- package/dist/data/commune-office.js +2371 -0
- package/dist/data/commune-office.json +2365 -0
- package/dist/data/commune.js +181 -11
- package/dist/data/corpus-overlap.d.ts +17 -0
- package/dist/data/corpus-overlap.js +299 -5
- package/dist/data/cot.d.ts +5 -0
- package/dist/data/cot.js +172 -0
- package/dist/data/cse.d.ts +5 -0
- package/dist/data/cse.js +122 -0
- package/dist/data/gabc-glyphs.d.ts +45 -0
- package/dist/data/gabc-glyphs.js +122 -0
- package/dist/data/gr.js +754 -6394
- package/dist/data/kyriale.js +116 -116
- package/dist/data/la.js +799 -13419
- package/dist/data/lh.js +113 -3473
- package/dist/data/lu.js +931 -17631
- package/dist/data/nocturnale-romanum.js +1659 -10411
- package/dist/data/office-ferial.d.ts +4 -0
- package/dist/data/office-ferial.js +396 -0
- package/dist/data/office-ferial.json +391 -0
- package/dist/data/office-monastic.d.ts +17 -1
- package/dist/data/office-monastic.js +1403 -466
- package/dist/data/office-psalms-monastic.d.ts +13 -1
- package/dist/data/office-psalms-monastic.js +9 -0
- package/dist/data/propers.js +1 -1
- package/dist/data/psalms.js +22919 -5
- package/dist/data/psm.d.ts +5 -0
- package/dist/data/psm.js +122 -0
- package/dist/data/seasonal-respbreve.d.ts +5 -0
- package/dist/data/seasonal-respbreve.js +41 -0
- package/dist/data/seasonal-respbreve.json +35 -0
- package/dist/data/smufl-glyphs.d.ts +17 -0
- package/dist/data/smufl-glyphs.js +1546 -0
- package/dist/data/smufl-glyphs.json +1530 -0
- package/dist/engines/cal/calendar.d.ts +3 -2
- package/dist/engines/cal/calendar.js +105 -29
- package/dist/engines/cal/data/eras.d.ts +35 -0
- package/dist/engines/cal/data/eras.js +128 -0
- package/dist/engines/cal/date.js +44 -0
- package/dist/engines/cal/types.d.ts +15 -3
- package/dist/engines/cal/types.js +5 -5
- package/dist/engines/census/census.d.ts +7 -0
- package/dist/engines/census/census.js +179 -0
- package/dist/engines/census/types.d.ts +55 -0
- package/dist/engines/census/types.js +8 -0
- package/dist/engines/chant/attest.d.ts +39 -0
- package/dist/engines/chant/attest.js +90 -0
- package/dist/engines/chant/chant.d.ts +16 -4
- package/dist/engines/chant/chant.js +220 -30
- package/dist/engines/chant/data/compline.js +2 -1
- package/dist/engines/chant/data/masses.d.ts +56 -4
- package/dist/engines/chant/data/masses.js +305 -80
- package/dist/engines/chant/data/prime.js +1 -1
- package/dist/engines/chant/hour.js +279 -58
- package/dist/engines/chant/ordinary.d.ts +2 -0
- package/dist/engines/chant/ordinary.js +336 -56
- package/dist/engines/chant/propers.js +55 -5
- package/dist/engines/chant/psalm.d.ts +4 -4
- package/dist/engines/chant/psalm.js +25 -11
- package/dist/engines/chant/syllabify.d.ts +1 -0
- package/dist/engines/chant/syllabify.js +90 -17
- package/dist/engines/chant/types.d.ts +115 -12
- package/dist/engines/chant/types.js +38 -3
- package/dist/engines/harmonia/api.js +4 -0
- package/dist/engines/harmonia/data/doctrines.js +3 -1
- package/dist/engines/harmonia/tabula.d.ts +3 -0
- package/dist/engines/harmonia/tabula.js +1 -0
- package/dist/engines/harmonia/voice.d.ts +4 -0
- package/dist/engines/harmonia/voice.js +8 -4
- package/dist/engines/imprint.js +14 -1
- package/dist/engines/planet/orbital.js +4 -4
- package/dist/engines/planet/planet.d.ts +10 -0
- package/dist/engines/planet/planet.js +30 -3
- package/dist/engines/planet/position.js +13 -10
- package/dist/engines/planet/types.d.ts +1 -0
- package/dist/engines/score/api.d.ts +2 -13
- package/dist/engines/score/api.js +21 -8
- package/dist/engines/score/articulation.js +2 -2
- package/dist/engines/score/cadence.d.ts +76 -0
- package/dist/engines/score/cadence.js +96 -0
- package/dist/engines/score/emitters/accidentals.d.ts +21 -0
- package/dist/engines/score/emitters/accidentals.js +88 -0
- package/dist/engines/score/emitters/atramentum.d.ts +107 -0
- package/dist/engines/score/emitters/atramentum.js +239 -0
- package/dist/engines/score/emitters/breaking.d.ts +62 -0
- package/dist/engines/score/emitters/breaking.js +80 -0
- package/dist/engines/score/emitters/moderna.d.ts +38 -0
- package/dist/engines/score/emitters/moderna.js +612 -0
- package/dist/engines/score/emitters/svg.d.ts +143 -0
- package/dist/engines/score/emitters/svg.js +1328 -0
- package/dist/engines/score/emitters/tracks.d.ts +104 -0
- package/dist/engines/score/emitters/tracks.js +728 -0
- package/dist/engines/score/infer.d.ts +3 -3
- package/dist/engines/score/infer.js +2 -2
- package/dist/engines/score/inscriptio.d.ts +69 -0
- package/dist/engines/score/inscriptio.js +138 -0
- package/dist/engines/score/ir.d.ts +2 -2
- package/dist/engines/score/ir.js +59 -12
- package/dist/engines/score/lyric.d.ts +23 -0
- package/dist/engines/score/lyric.js +234 -0
- package/dist/engines/score/meta.d.ts +2 -2
- package/dist/engines/score/modulation.d.ts +12 -0
- package/dist/engines/score/modulation.js +49 -0
- package/dist/engines/score/neume.js +35 -4
- package/dist/engines/score/parse.js +150 -9
- package/dist/engines/score/phrasing.js +4 -3
- package/dist/engines/score/prosody.d.ts +38 -0
- package/dist/engines/score/prosody.js +70 -6
- package/dist/engines/score/tabula.d.ts +37 -5
- package/dist/engines/score/tabula.js +18 -0
- package/dist/engines/score/types.d.ts +88 -1
- package/dist/engines/temper/api.d.ts +4 -1
- package/dist/engines/temper/api.js +28 -5
- package/dist/engines/temper/data/guido.js +6 -2
- package/dist/engines/temper/data/modes.d.ts +6 -0
- package/dist/engines/temper/data/modes.js +42 -0
- package/dist/engines/temper/data/tones.d.ts +1 -1
- package/dist/engines/temper/data/tones.js +20 -11
- package/dist/engines/temper/interval.js +4 -3
- package/dist/engines/temper/modality.d.ts +11 -2
- package/dist/engines/temper/modality.js +74 -2
- package/dist/engines/temper/modes.d.ts +1 -1
- package/dist/engines/temper/pitch.d.ts +1 -1
- package/dist/engines/temper/pitch.js +12 -2
- package/dist/engines/temper/scale.d.ts +53 -0
- package/dist/engines/temper/scale.js +107 -8
- package/dist/index.d.ts +26 -8
- package/dist/index.js +37 -4
- package/docs/api/calendar.md +279 -0
- package/docs/api/census.md +288 -0
- package/docs/api/chant.md +657 -0
- package/docs/api/heavens.md +346 -0
- package/docs/api/index.md +263 -0
- package/docs/api/score.md +873 -0
- package/docs/api/tuning.md +619 -0
- package/package.json +11 -5
- package/dist/data/office-matins-roman.d.ts +0 -19
- package/dist/data/office-matins-roman.js +0 -4383
- package/dist/data/office-psalms-roman.d.ts +0 -15
- package/dist/data/office-psalms-roman.js +0 -28
- package/dist/data/office-roman.d.ts +0 -19
- package/dist/data/office-roman.js +0 -13792
- package/dist/engines/chant/matutinum.d.ts +0 -33
- package/dist/engines/chant/matutinum.js +0 -81
- package/dist/engines/score/emitters/midi.d.ts +0 -65
- package/dist/engines/score/emitters/midi.js +0 -162
- package/dist/engines/score/emitters/musicxml.d.ts +0 -18
- 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
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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)
|
|
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): [
|
|
161
|
-
// for 1800–2050
|
|
162
|
-
// are J (days from J2000): −73048.5 ≈ 1800, 18626.5 ≈
|
|
163
|
-
// use the precise set, outside fall back to the
|
|
164
|
-
|
|
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.
|
|
170
|
-
//
|
|
171
|
-
|
|
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
|
}
|
|
@@ -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 {
|
|
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
|
|
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
|