tonus 0.1.4 → 0.1.6
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 +143 -96
- package/CHANGELOG.md +62 -0
- package/README.md +7 -9
- package/dist/data/am.d.ts +5 -0
- package/dist/data/am.js +14299 -0
- package/dist/data/office-monastic.d.ts +3 -0
- package/dist/data/office-monastic.js +6956 -0
- package/dist/data/office-psalms-monastic.d.ts +3 -0
- package/dist/data/office-psalms-monastic.js +21 -0
- package/dist/data/{office-psalms.d.ts → office-psalms-roman.d.ts} +1 -1
- package/dist/data/{office-psalms.js → office-psalms-roman.js} +3 -3
- package/dist/engines/cal/calendar.js +16 -1
- package/dist/engines/cal/date.js +5 -0
- package/dist/engines/chant/chant.js +2 -0
- package/dist/{data → engines/chant/data}/compline.d.ts +1 -1
- package/dist/{data → engines/chant/data}/masses.d.ts +1 -1
- package/dist/{data → engines/chant/data}/masses.js +1 -1
- package/dist/{data → engines/chant/data}/prime.d.ts +1 -1
- package/dist/{data → engines/chant/data}/prime.js +1 -1
- package/dist/engines/chant/hour.js +37 -22
- package/dist/engines/chant/intone.js +6 -1
- package/dist/engines/chant/ordinary.js +1 -1
- package/dist/engines/chant/psalm.d.ts +4 -4
- package/dist/engines/chant/psalm.js +6 -4
- package/dist/engines/chant/types.d.ts +5 -1
- package/dist/engines/harmonia/api.js +11 -0
- package/dist/engines/harmonia/data/doctrines.js +27 -1
- package/dist/engines/imprint.js +7 -1
- package/dist/engines/planet/position.js +51 -5
- package/dist/engines/score/articulation.js +60 -3
- package/dist/engines/score/cadence.js +3 -3
- package/dist/engines/score/emitters/midi.js +6 -2
- package/dist/engines/score/infer.js +13 -1
- package/dist/engines/score/ir.d.ts +2 -1
- package/dist/engines/score/ir.js +54 -12
- package/dist/engines/score/modulation.js +2 -2
- package/dist/engines/score/parse.js +16 -8
- package/dist/engines/score/phrasing.js +26 -0
- package/dist/engines/score/prosody.js +5 -0
- package/dist/engines/score/tabula.d.ts +2 -0
- package/dist/engines/score/tabula.js +1 -0
- package/dist/engines/score/types.d.ts +11 -2
- package/dist/engines/temper/api.js +1 -1
- package/dist/engines/temper/data/guido.d.ts +3 -2
- package/dist/engines/temper/data/guido.js +30 -25
- package/dist/engines/temper/data/modes.js +3 -0
- package/dist/{data → engines/temper/data}/tones.js +1 -1
- package/dist/engines/temper/gabc.js +25 -2
- package/dist/engines/temper/guido.d.ts +3 -2
- package/dist/engines/temper/interval.js +12 -0
- package/dist/engines/temper/modality.js +5 -4
- package/dist/engines/temper/scale.js +33 -2
- package/dist/engines/temper/step.d.ts +2 -2
- package/dist/index.d.ts +3 -3
- package/package.json +2 -1
- package/dist/data/office.d.ts +0 -12
- package/dist/data/office.js +0 -13052
- /package/dist/{data → engines/chant/data}/compline.js +0 -0
- /package/dist/{data → engines/temper/data}/tones.d.ts +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const OFFICE_PSALMS_MONASTIC = [
|
|
2
|
+
{ hour: "Prima", weekday: 0, festis: false, psalms: [{ psalm: 118, from: 1, to: 8 }, { psalm: 118, from: 9, to: 16 }, { psalm: 118, from: 17, to: 24 }, { psalm: 118, from: 25, to: 32 }] },
|
|
3
|
+
{ hour: "Prima", weekday: 1, festis: false, psalms: [{ psalm: 1 }, { psalm: 2 }, { psalm: 6 }] },
|
|
4
|
+
{ hour: "Prima", weekday: 2, festis: false, psalms: [{ psalm: 7 }, { psalm: 8 }, { psalm: 9, from: 2, to: 19 }] },
|
|
5
|
+
{ hour: "Prima", weekday: 3, festis: false, psalms: [{ psalm: 9, from: 20, to: 39 }, { psalm: 10 }, { psalm: 11 }] },
|
|
6
|
+
{ hour: "Prima", weekday: 4, festis: false, psalms: [{ psalm: 12 }, { psalm: 13 }, { psalm: 14 }] },
|
|
7
|
+
{ hour: "Prima", weekday: 5, festis: false, psalms: [{ psalm: 15 }, { psalm: 16 }, { psalm: 17, from: 2, to: 25 }] },
|
|
8
|
+
{ hour: "Prima", weekday: 6, festis: false, psalms: [{ psalm: 17, from: 26, to: 51 }, { psalm: 18 }, { psalm: 19 }] },
|
|
9
|
+
{ hour: "Prima", weekday: null, festis: true, psalms: [{ psalm: 118, from: 1, to: 8 }, { psalm: 118, from: 9, to: 16 }, { psalm: 118, from: 17, to: 24 }, { psalm: 118, from: 25, to: 32 }] },
|
|
10
|
+
{ hour: "Tertia", weekday: 0, festis: false, psalms: [{ psalm: 118, from: 33, to: 40 }, { psalm: 118, from: 41, to: 48 }, { psalm: 118, from: 49, to: 56 }] },
|
|
11
|
+
{ hour: "Tertia", weekday: 1, festis: false, psalms: [{ psalm: 118, from: 105, to: 112 }, { psalm: 118, from: 113, to: 120 }, { psalm: 118, from: 121, to: 128 }] },
|
|
12
|
+
{ hour: "Tertia", weekday: null, festis: false, psalms: [{ psalm: 119 }, { psalm: 120 }, { psalm: 121 }] },
|
|
13
|
+
{ hour: "Sexta", weekday: 0, festis: false, psalms: [{ psalm: 118, from: 57, to: 64 }, { psalm: 118, from: 65, to: 72 }, { psalm: 118, from: 73, to: 80 }] },
|
|
14
|
+
{ hour: "Sexta", weekday: 1, festis: false, psalms: [{ psalm: 118, from: 129, to: 136 }, { psalm: 118, from: 137, to: 144 }, { psalm: 118, from: 145, to: 152 }] },
|
|
15
|
+
{ hour: "Sexta", weekday: null, festis: false, psalms: [{ psalm: 122 }, { psalm: 123 }, { psalm: 124 }] },
|
|
16
|
+
{ hour: "Nona", weekday: 0, festis: false, psalms: [{ psalm: 118, from: 81, to: 88 }, { psalm: 118, from: 89, to: 96 }, { psalm: 118, from: 97, to: 104 }] },
|
|
17
|
+
{ hour: "Nona", weekday: 1, festis: false, psalms: [{ psalm: 118, from: 153, to: 160 }, { psalm: 118, from: 161, to: 168 }, { psalm: 118, from: 169, to: 176 }] },
|
|
18
|
+
{ hour: "Nona", weekday: null, festis: false, psalms: [{ psalm: 125 }, { psalm: 126 }, { psalm: 127 }] },
|
|
19
|
+
{ hour: "Completorium", weekday: null, festis: false, psalms: [{ psalm: 4 }, { psalm: 90 }, { psalm: 133 }] }
|
|
20
|
+
];
|
|
21
|
+
//# sourceMappingURL=office-psalms-monastic.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// office-psalms.ts — little-hours psalmody (Prime, Terce, Sext, None, Compline)
|
|
1
|
+
// office-psalms-roman.ts — Roman little-hours psalmody (Prime, Terce, Sext, None, Compline)
|
|
2
2
|
// Extracted from Divinum Officium (Psalterium, [Tridentinum] section) by
|
|
3
3
|
// scripts/extract-office-psalms.mjs
|
|
4
|
-
// Generated: 2026-07-
|
|
4
|
+
// Generated: 2026-07-08T04:45:29.223Z
|
|
5
5
|
// Entries: 15
|
|
6
6
|
//
|
|
7
7
|
// The traditional (pre-1911) Roman little-hours psalm distribution. Each entry
|
|
@@ -25,4 +25,4 @@ export const OFFICE_PSALMS = [
|
|
|
25
25
|
{ hour: "Nona", weekday: null, festis: false, psalms: [{ psalm: 118, from: 129, to: 144 }, { psalm: 118, from: 145, to: 160 }, { psalm: 118, from: 161, to: 176 }] },
|
|
26
26
|
{ hour: "Completorium", weekday: null, festis: false, psalms: [{ psalm: 4 }, { psalm: 30, from: 2, to: 6 }, { psalm: 90 }, { psalm: 133 }] }
|
|
27
27
|
];
|
|
28
|
-
//# sourceMappingURL=office-psalms.js.map
|
|
28
|
+
//# sourceMappingURL=office-psalms-roman.js.map
|
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
2
|
// engines/cal/calendar — liturgical feast lookup
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
|
+
// The era, and why it is what it is. The calendar's *structure* is medieval —
|
|
5
|
+
// the temporale from Advent through the season after Pentecost (Septuagesima
|
|
6
|
+
// included), the eight-hour office cursus, the duplex/semiduplex/simplex
|
|
7
|
+
// dignity system. The *data* is the Tridentine codification (1570–1962) drawn
|
|
8
|
+
// from Divinum Officium [biblio: divinum-officium], substantially continuous
|
|
9
|
+
// with late-medieval Roman usage but carrying feasts instituted as late as the
|
|
10
|
+
// 1950s (Queenship of Mary 1954, Immaculate Heart 1944).
|
|
11
|
+
//
|
|
12
|
+
// Decision: those post-medieval feasts are KEPT, not pruned. Pruning would
|
|
13
|
+
// break the DO data's integrity and demand per-feast historical adjudication;
|
|
14
|
+
// instead tonus states its actual era plainly. The honest description is
|
|
15
|
+
// "Tridentine Roman, continuous with medieval practice" — not "a medieval
|
|
16
|
+
// calendar." (Per-feast era metadata — medieval / tridentine / modern — is
|
|
17
|
+
// noted as future work.) The rank system this data carries is documented at
|
|
18
|
+
// `ritus`/`Grade` in ./types.ts; Easter reckoning at pascha() in ./date.ts.
|
|
4
19
|
import { CAL } from "../../data/cal.js";
|
|
5
|
-
import { MASSES } from "
|
|
20
|
+
import { MASSES } from "../chant/data/masses.js";
|
|
6
21
|
import { isoDate, startOfDay, addDays, subDays, firstSundayOnOrAfter, nextSunday, pascha, resolveEntryId, DEFAULT_EPOCH, } from "./date.js";
|
|
7
22
|
import { TEMPUS_NAMES, entryGrade, gradeOrder, BVM_FEAST_IDS, APOSTOLIC_FEAST_IDS, } from "./types.js";
|
|
8
23
|
const _calCache = new Map();
|
package/dist/engines/cal/date.js
CHANGED
|
@@ -37,6 +37,11 @@ export function parseMonthDay(year, mmdd) {
|
|
|
37
37
|
return new Date(Date.UTC(year, m - 1, d));
|
|
38
38
|
}
|
|
39
39
|
// ── Easter ──
|
|
40
|
+
// Split at the Gregorian reform (1583): from 1583 on, the Gauss/Butcher
|
|
41
|
+
// Gregorian computus; before it, the classical 19-year Julian cycle with a
|
|
42
|
+
// Julian→Gregorian day-number conversion [biblio: computus]. This keeps Easter
|
|
43
|
+
// correct for date queries reaching back into the medieval period — the era the
|
|
44
|
+
// calendar is built for (see the era note in ./calendar.ts).
|
|
40
45
|
export function pascha(year) {
|
|
41
46
|
if (year < 1583)
|
|
42
47
|
return paschaJulian(year);
|
|
@@ -3,6 +3,7 @@ import { GR_DATA, GR_SOURCE } from "../../data/gr.js";
|
|
|
3
3
|
import { LU_DATA, LU_SOURCE } from "../../data/lu.js";
|
|
4
4
|
import { LA_DATA, LA_SOURCE } from "../../data/la.js";
|
|
5
5
|
import { LH_DATA, LH_SOURCE } from "../../data/lh.js";
|
|
6
|
+
import { AM_DATA, AM_SOURCE } from "../../data/am.js";
|
|
6
7
|
function modusOf(mode) {
|
|
7
8
|
return mode != null ? (MODE_LABELS[mode] ?? null) : null;
|
|
8
9
|
}
|
|
@@ -63,6 +64,7 @@ const CORPUS = [
|
|
|
63
64
|
...LU_DATA.map((c) => withLabels(c, LU_SOURCE)),
|
|
64
65
|
...LA_DATA.map((c) => withLabels(c, LA_SOURCE)),
|
|
65
66
|
...LH_DATA.map((c) => withLabels(c, LH_SOURCE)),
|
|
67
|
+
...AM_DATA.map((c) => withLabels(c, AM_SOURCE)),
|
|
66
68
|
];
|
|
67
69
|
let _byId = null;
|
|
68
70
|
function byId() {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Describes which masses are appropriate for a given season, grade, and day.
|
|
4
4
|
// Mass 0 has two ad-lib variants (index 0a/0b); all others are single entries.
|
|
5
|
-
import { GRADE_ORDER, } from "
|
|
5
|
+
import { GRADE_ORDER, } from "../../cal/types.js";
|
|
6
6
|
// Inclusive slice of GRADE_ORDER from `high` (more solemn) to `low`.
|
|
7
7
|
// e.g. gradesFrom("duplex-i", "duplex-ii") lists every grade between them.
|
|
8
8
|
function gradesFrom(high, low) {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// Solesmes chant and are not in the corpus, so they are out of scope; this is a
|
|
9
9
|
// chant ordo, not a full Breviary Prime.
|
|
10
10
|
//
|
|
11
|
-
// Like
|
|
11
|
+
// Like compline.ts and masses.ts (its siblings here) this is a hand-authored table that
|
|
12
12
|
// references chants already in the corpus by id; it carries no GABC of its own.
|
|
13
13
|
// See docs/chant.md and BIBLIOGRAPHY.md.
|
|
14
14
|
// Prime's psalmody (Ps 53 + a weekday-proper psalm + Ps 118 in two sections,
|
|
@@ -6,10 +6,17 @@ import { intonePortion, officePsalmPortions } from "./psalm.js";
|
|
|
6
6
|
import { temporaSundayId } from "../cal/date.js";
|
|
7
7
|
import { getFeast } from "../cal/calendar.js";
|
|
8
8
|
import { OFFICE_ROMAN } from "../../data/office-roman.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { OFFICE_MONASTIC } from "../../data/office-monastic.js";
|
|
10
|
+
import { COMPLINE_ORDINARY, COMPLINE_SEASONAL, marianAntiphonFor, } from "./data/compline.js";
|
|
11
|
+
import { PRIME_ORDINARY, PRIME_SEASONAL } from "./data/prime.js";
|
|
11
12
|
let _roman = null;
|
|
12
|
-
|
|
13
|
+
let _monastic = null;
|
|
14
|
+
function officeMap(rite) {
|
|
15
|
+
if (rite === "monasticum") {
|
|
16
|
+
if (!_monastic)
|
|
17
|
+
_monastic = new Map(OFFICE_MONASTIC.map((d) => [d.feastId, d]));
|
|
18
|
+
return _monastic;
|
|
19
|
+
}
|
|
13
20
|
if (!_roman)
|
|
14
21
|
_roman = new Map(OFFICE_ROMAN.map((d) => [d.feastId, d]));
|
|
15
22
|
return _roman;
|
|
@@ -29,14 +36,17 @@ const SEASONAL_ORDO_HOURS = new Set([
|
|
|
29
36
|
// tables at all. The ordo is assembled from the season (Te lucis, In manus
|
|
30
37
|
// tuas), the fixed psalms (from the extracted DO scheme), the invariable spine
|
|
31
38
|
// (Deus in adjutorium, Nunc dimittis), and the date-driven Marian antiphon.
|
|
32
|
-
// See data/compline.ts.
|
|
33
|
-
function complineForFeast(feast) {
|
|
39
|
+
// See ./data/compline.ts.
|
|
40
|
+
function complineForFeast(feast, rite) {
|
|
34
41
|
const seasonal = COMPLINE_SEASONAL[feast.season];
|
|
35
42
|
const results = [];
|
|
36
43
|
const opening = resolveChant(COMPLINE_ORDINARY.opening);
|
|
37
44
|
if (opening)
|
|
38
45
|
results.push(opening);
|
|
39
|
-
|
|
46
|
+
// Monastic Compline uses a fixed three-psalm set (4, 90, 133); the Roman rite
|
|
47
|
+
// adds Ps 30 vv. 2–6. The difference is entirely in the psalm scheme — the
|
|
48
|
+
// rest of the ordo (spine, hymn, In manus tuas, Marian antiphon) is shared.
|
|
49
|
+
for (const p of officePsalmPortions("Completorium", feast.weekday, rite)) {
|
|
40
50
|
results.push(...intonePortion(p));
|
|
41
51
|
}
|
|
42
52
|
const hymn = seasonal && resolveChant(seasonal.teLucis);
|
|
@@ -54,9 +64,9 @@ function complineForFeast(feast) {
|
|
|
54
64
|
return results;
|
|
55
65
|
}
|
|
56
66
|
// Prime, like Compline, is a fixed+seasonal ordo, not per-feast. Covers the
|
|
57
|
-
// sung parts only (see data/prime.ts): opening, fixed psalms, the hymn Iam
|
|
67
|
+
// sung parts only (see ./data/prime.ts): opening, fixed psalms, the hymn Iam
|
|
58
68
|
// lucis, and the seasonal short responsory Christe Fili Dei.
|
|
59
|
-
function primeForFeast(feast) {
|
|
69
|
+
function primeForFeast(feast, rite) {
|
|
60
70
|
const seasonal = PRIME_SEASONAL[feast.season];
|
|
61
71
|
const results = [];
|
|
62
72
|
const opening = resolveChant(PRIME_ORDINARY.opening);
|
|
@@ -65,7 +75,9 @@ function primeForFeast(feast) {
|
|
|
65
75
|
const hymn = resolveChant(PRIME_ORDINARY.hymn);
|
|
66
76
|
if (hymn)
|
|
67
77
|
results.push(hymn);
|
|
68
|
-
|
|
78
|
+
// The monastic Prime psalmody is weekday-varied across the psalter (vs. the
|
|
79
|
+
// Roman Ps-118 pattern) — a psalm-scheme difference; the ordo spine is shared.
|
|
80
|
+
for (const p of officePsalmPortions("Prima", feast.weekday, rite)) {
|
|
69
81
|
results.push(...intonePortion(p));
|
|
70
82
|
}
|
|
71
83
|
const responsory = seasonal && resolveChant(seasonal.responsory);
|
|
@@ -73,12 +85,12 @@ function primeForFeast(feast) {
|
|
|
73
85
|
results.push(responsory);
|
|
74
86
|
return results;
|
|
75
87
|
}
|
|
76
|
-
function chantsForFeastHour(feast, hour) {
|
|
88
|
+
function chantsForFeastHour(feast, hour, rite) {
|
|
77
89
|
if (hour === "completorium")
|
|
78
|
-
return complineForFeast(feast);
|
|
90
|
+
return complineForFeast(feast, rite);
|
|
79
91
|
if (hour === "prima")
|
|
80
|
-
return primeForFeast(feast);
|
|
81
|
-
const map =
|
|
92
|
+
return primeForFeast(feast, rite);
|
|
93
|
+
const map = officeMap(rite);
|
|
82
94
|
const sunday = temporaSundayId(feast.id);
|
|
83
95
|
const day = map.get(feast.id) ?? (sunday ? (map.get(sunday) ?? null) : null);
|
|
84
96
|
if (!day)
|
|
@@ -111,7 +123,7 @@ function chantsForFeastHour(feast, hour) {
|
|
|
111
123
|
// repeat the psalms once per feast).
|
|
112
124
|
if (feast.date) {
|
|
113
125
|
const hourName = hour === "tertia" ? "Tertia" : hour === "sexta" ? "Sexta" : "Nona";
|
|
114
|
-
for (const p of officePsalmPortions(hourName, feast.weekday)) {
|
|
126
|
+
for (const p of officePsalmPortions(hourName, feast.weekday, rite)) {
|
|
115
127
|
results.push(...intonePortion(p));
|
|
116
128
|
}
|
|
117
129
|
}
|
|
@@ -147,34 +159,37 @@ export function getHour(query) {
|
|
|
147
159
|
return [];
|
|
148
160
|
const feasts = toArray(query.feast);
|
|
149
161
|
const hour = query.hora;
|
|
162
|
+
const rite = query.rite ?? "romanum";
|
|
150
163
|
let results;
|
|
151
164
|
if (feasts && hour) {
|
|
152
165
|
// Prime and Compline are seasonal/weekday ordos, identical for every feast
|
|
153
166
|
// of the day — so concurrent feasts collapse to a single ordo rather than
|
|
154
167
|
// repeating it. The other hours are genuinely per-feast.
|
|
155
168
|
results = SEASONAL_ORDO_HOURS.has(hour)
|
|
156
|
-
? feasts[0] ? chantsForFeastHour(feasts[0], hour) : []
|
|
157
|
-
: feasts.flatMap((f) => chantsForFeastHour(f, hour));
|
|
169
|
+
? feasts[0] ? chantsForFeastHour(feasts[0], hour, rite) : []
|
|
170
|
+
: feasts.flatMap((f) => chantsForFeastHour(f, hour, rite));
|
|
158
171
|
}
|
|
159
172
|
else if (feasts) {
|
|
160
173
|
const hours = [
|
|
161
174
|
"matutinum", "laudes", "prima", "tertia", "sexta", "nona",
|
|
162
175
|
"vesperae", "completorium",
|
|
163
176
|
];
|
|
164
|
-
results = feasts.flatMap((f) => hours.flatMap((h) => chantsForFeastHour(f, h)));
|
|
177
|
+
results = feasts.flatMap((f) => hours.flatMap((h) => chantsForFeastHour(f, h, rite)));
|
|
165
178
|
}
|
|
166
179
|
else if (hour && SEASONAL_ORDO_HOURS.has(hour)) {
|
|
167
180
|
// Prime and Compline are seasonal ordos, not per-feast. With no feast,
|
|
168
181
|
// resolve for the default epoch (Guido d'Arezzo's era) — festum()'s anchor.
|
|
169
182
|
const [feast] = getFeast();
|
|
170
|
-
results = feast ? chantsForFeastHour(feast, hour) : [];
|
|
183
|
+
results = feast ? chantsForFeastHour(feast, hour, rite) : [];
|
|
171
184
|
}
|
|
172
185
|
else if (hour) {
|
|
173
|
-
// Hour without feast — survey per-feast content across
|
|
174
|
-
// mockFeast has no date, so the little hours return only
|
|
175
|
-
|
|
186
|
+
// Hour without feast — survey per-feast content across the office entries of
|
|
187
|
+
// the chosen rite. mockFeast has no date, so the little hours return only
|
|
188
|
+
// their responsories.
|
|
189
|
+
const table = rite === "monasticum" ? OFFICE_MONASTIC : OFFICE_ROMAN;
|
|
190
|
+
results = table.flatMap((day) => {
|
|
176
191
|
const mockFeast = { id: day.feastId };
|
|
177
|
-
return chantsForFeastHour(mockFeast, hour);
|
|
192
|
+
return chantsForFeastHour(mockFeast, hour, rite);
|
|
178
193
|
});
|
|
179
194
|
}
|
|
180
195
|
else {
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
2
|
// engines/chant/intone — GABC generation for psalm verses sung to psalm tones
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
|
+
// Psalmody structure follows the Liber Usualis rules [biblio: liber-usualis]:
|
|
5
|
+
// each verse is intonation → tenor (reciting note) → mediation → final cadence,
|
|
6
|
+
// with the flex a subdivision of a long first half. The intonation is sung on
|
|
7
|
+
// the first verse only (unless repeated, as in the Magnificat) — see the
|
|
8
|
+
// `intonation` option; `inDirectum` recites straight through with no mediant.
|
|
4
9
|
import { syllabifyPhrase } from "./syllabify.js";
|
|
5
|
-
import { getTone, getDifferentia } from "
|
|
10
|
+
import { getTone, getDifferentia } from "../temper/data/tones.js";
|
|
6
11
|
import { midiToGabc } from "../temper/gabc.js";
|
|
7
12
|
const DEFAULT_CLEF = "c4";
|
|
8
13
|
const CLEF = `(${DEFAULT_CLEF}) `;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
2
|
// engines/chant/ordinary — Mass ordinary (kyriale) selection
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
|
-
import { MASSES, AD_LIB } from "
|
|
4
|
+
import { MASSES, AD_LIB } from "./data/masses.js";
|
|
5
5
|
import { KYRIALE } from "../../data/kyriale.js";
|
|
6
6
|
import { MODE_LABELS, ORDINARY_LABELS, } from "./types.js";
|
|
7
7
|
import { gradeOrder, PENITENTIAL_SEASONS, } from "../cal/types.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type OfficePsalmEntry, type OfficePsalmPortion } from "../../data/office-psalms.js";
|
|
2
|
-
import { type Chant, type PsalmusQuery } from "./types.js";
|
|
1
|
+
import { type OfficePsalmEntry, type OfficePsalmPortion } from "../../data/office-psalms-roman.js";
|
|
2
|
+
import { type Chant, type PsalmusQuery, type Rite } from "./types.js";
|
|
3
3
|
/**
|
|
4
4
|
* Psalm and canticle retrieval (`tonus.psalmus`) from the Psalterium,
|
|
5
5
|
* intoned to the psalm tones (modes 1-8 plus tonus peregrinus) as GABC.
|
|
@@ -16,9 +16,9 @@ export declare function getPsalmRange(psalm: number, lo: number, hi: number, mod
|
|
|
16
16
|
export declare function intonePortion(p: OfficePsalmPortion, mode?: number): Chant[];
|
|
17
17
|
/**
|
|
18
18
|
* The little-hours psalmody for one hour on a given weekday (0 = Sunday), from
|
|
19
|
-
* the extracted DO Tridentine scheme (`office-psalms.ts`). Prefers the
|
|
19
|
+
* the extracted DO Tridentine scheme (`office-psalms-roman.ts`). Prefers the
|
|
20
20
|
* weekday-specific entry, then the ferial default (weekday null), then the
|
|
21
21
|
* feast set; returns the psalm portions (not yet intoned).
|
|
22
22
|
*/
|
|
23
|
-
export declare function officePsalmPortions(hour: OfficePsalmEntry["hour"], weekday: number): OfficePsalmPortion[];
|
|
23
|
+
export declare function officePsalmPortions(hour: OfficePsalmEntry["hour"], weekday: number, rite?: Rite): OfficePsalmPortion[];
|
|
24
24
|
//# sourceMappingURL=psalm.d.ts.map
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// engines/chant/psalm — psalm and canticle retrieval as intoned Chant[]
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
4
|
import { PSALMS } from "../../data/psalms.js";
|
|
5
|
-
import { OFFICE_PSALMS, } from "../../data/office-psalms.js";
|
|
5
|
+
import { OFFICE_PSALMS, } from "../../data/office-psalms-roman.js";
|
|
6
|
+
import { OFFICE_PSALMS_MONASTIC } from "../../data/office-psalms-monastic.js";
|
|
6
7
|
import { intone } from "./intone.js";
|
|
7
8
|
import { MODE_LABELS } from "./types.js";
|
|
8
9
|
const CANTICLE_NAMES = {
|
|
@@ -82,12 +83,13 @@ export function intonePortion(p, mode = 8) {
|
|
|
82
83
|
}
|
|
83
84
|
/**
|
|
84
85
|
* The little-hours psalmody for one hour on a given weekday (0 = Sunday), from
|
|
85
|
-
* the extracted DO Tridentine scheme (`office-psalms.ts`). Prefers the
|
|
86
|
+
* the extracted DO Tridentine scheme (`office-psalms-roman.ts`). Prefers the
|
|
86
87
|
* weekday-specific entry, then the ferial default (weekday null), then the
|
|
87
88
|
* feast set; returns the psalm portions (not yet intoned).
|
|
88
89
|
*/
|
|
89
|
-
export function officePsalmPortions(hour, weekday) {
|
|
90
|
-
const
|
|
90
|
+
export function officePsalmPortions(hour, weekday, rite = "romanum") {
|
|
91
|
+
const scheme = rite === "monasticum" ? OFFICE_PSALMS_MONASTIC : OFFICE_PSALMS;
|
|
92
|
+
const forHour = scheme.filter((e) => e.hour === hour);
|
|
91
93
|
const exact = forHour.find((e) => e.weekday === weekday && !e.festis);
|
|
92
94
|
const ferial = forHour.find((e) => e.weekday === null && !e.festis);
|
|
93
95
|
const festis = forHour.find((e) => e.festis);
|
|
@@ -2,7 +2,7 @@ import type { Season, Grade, Feast } from "../cal/types.js";
|
|
|
2
2
|
export type { Season, Grade, Feast };
|
|
3
3
|
export type OfficeCode = "an" | "al" | "ca" | "co" | "gr" | "hy" | "in" | "of" | "ps" | "re" | "rb" | "se" | "tr" | "tp" | "or";
|
|
4
4
|
export type OrdinaryCode = "ky" | "gl" | "cr" | "sa" | "ag" | "be" | "it" | "as" | "va";
|
|
5
|
-
export type ChantSource = "gr" | "lu" | "la" | "lh";
|
|
5
|
+
export type ChantSource = "gr" | "lu" | "la" | "lh" | "am";
|
|
6
6
|
export type CanonicalHour = "matutinum" | "laudes" | "prima" | "tertia" | "sexta" | "nona" | "vesperae" | "completorium";
|
|
7
7
|
export declare const MODE_LABELS: Readonly<Record<string, string>>;
|
|
8
8
|
export declare const OFFICE_LABELS: Readonly<Record<OfficeCode, string>>;
|
|
@@ -54,9 +54,13 @@ export interface OrdinariumQuery extends CantusQuery {
|
|
|
54
54
|
ordinary?: OrdinaryCode;
|
|
55
55
|
mass?: number;
|
|
56
56
|
}
|
|
57
|
+
/** Which rite's Office to assemble. `romanum` (default) is the Tridentine Roman
|
|
58
|
+
* cursus; `monasticum` is the Benedictine cursus (Antiphonale Monasticum). */
|
|
59
|
+
export type Rite = "romanum" | "monasticum";
|
|
57
60
|
export interface OfficiumQuery extends CantusQuery {
|
|
58
61
|
feast?: Feast | Feast[];
|
|
59
62
|
hora?: CanonicalHour;
|
|
63
|
+
rite?: Rite;
|
|
60
64
|
}
|
|
61
65
|
export interface PsalmusQuery {
|
|
62
66
|
psalm?: number | string;
|
|
@@ -20,6 +20,14 @@ function resolveScale(temper) {
|
|
|
20
20
|
}
|
|
21
21
|
return buildRatios();
|
|
22
22
|
}
|
|
23
|
+
// Reduce a time range's per-frame voicings to one aggregate body list for the
|
|
24
|
+
// top-level `bodies`. Note the deliberate asymmetry: the DYNAMICS (presence,
|
|
25
|
+
// motion) are mean-averaged across the range, but the REPRESENTATIVE identity —
|
|
26
|
+
// pitch, vowel, zodiac, retrograde — is taken from the first frame, not averaged
|
|
27
|
+
// (a pitch has no meaningful mean, and averaging a wrapping zodiac longitude is
|
|
28
|
+
// nonsense). So an aggregate body sounds the range's *starting* pitch with its
|
|
29
|
+
// *mean* loudness. Callers wanting the pitch to track the range should read the
|
|
30
|
+
// per-frame `frames` instead. (Aspects are likewise taken from frame 0; see below.)
|
|
23
31
|
function averageBodies(frames) {
|
|
24
32
|
if (frames.length === 0)
|
|
25
33
|
return [];
|
|
@@ -71,6 +79,9 @@ export function buildHarmonia(input, opts = {}) {
|
|
|
71
79
|
frames.push({ date: cosmos.date, bodies: vb, aspects: va });
|
|
72
80
|
}
|
|
73
81
|
const aggregateBodies = averageBodies(perCosmosBodies);
|
|
82
|
+
// Aspects are the first frame's, not merged across the range — an aspect forms
|
|
83
|
+
// and dissolves over time, so there is no meaningful "average" set. Like the
|
|
84
|
+
// representative pitch above, this reflects the range's start.
|
|
74
85
|
const aggregateAspects = frames[0]?.aspects ?? [];
|
|
75
86
|
const imprint = computeImprintFromBodies(aggregateBodies, scale);
|
|
76
87
|
const harmony = {
|
|
@@ -10,12 +10,38 @@
|
|
|
10
10
|
// center, not a voiced body (except Pliny, where Earth is a boundary tone).
|
|
11
11
|
// The Sun holds the mese position in all four systems.
|
|
12
12
|
//
|
|
13
|
-
//
|
|
13
|
+
// The ratios are reconstructed from the primary texts [biblio:
|
|
14
|
+
// doctrina-primaries] through Godwin's syntheses [biblio: godwin-harmonies,
|
|
15
|
+
// godwin-spheres]: for each author the Greek tonal framework is identified
|
|
16
|
+
// (conjunct/disjunct tetrachords, or the fixed Greater Perfect System),
|
|
17
|
+
// bodies are mapped to Greek tone-names from the source's explicit
|
|
18
|
+
// assignments, and ratios are derived by Pythagorean interval arithmetic
|
|
19
|
+
// (tone 9/8, limma 256/243, fourth 4/3, fifth 3/2) normalized to the mese
|
|
20
|
+
// (Sun = 1/1), then verified computationally for span and closure.
|
|
14
21
|
//
|
|
15
22
|
// Taxonomy follows Godwin's classification:
|
|
16
23
|
// Type A — intervals represent distances between spheres
|
|
17
24
|
// Type B — intervals represent speeds/symbolic scale degrees
|
|
18
25
|
// Type C — intervals map to fixed tones of the Greater Perfect System
|
|
26
|
+
//
|
|
27
|
+
// Decisions recorded from the derivation:
|
|
28
|
+
// 1. Boethius transmits Nicomachus's ratios; tonus credits the doctrina to
|
|
29
|
+
// Boethius as the medieval authority.
|
|
30
|
+
// 2. Pliny's Sun sits at hypate meson, not the Greek mese; tonus keeps the
|
|
31
|
+
// Sun as the structural center regardless, and greekName reflects
|
|
32
|
+
// Pliny's actual position.
|
|
33
|
+
// 3. Ptolemy's aspect–consonance mapping carries into interval grading — a
|
|
34
|
+
// Jupiter–Sun aspect sounds a fifth, Mars–Sun a tone.
|
|
35
|
+
// 4. Pythagoras's Fixed Stars complete the octave in the data but are never
|
|
36
|
+
// voiced: no caelum body, no classical vowel.
|
|
37
|
+
// 5. Voices are stored in sphere order, outermost first; sort by ratio for
|
|
38
|
+
// scale views.
|
|
39
|
+
//
|
|
40
|
+
// The single pitch that separates Pythagoras from Boethius is Venus — the
|
|
41
|
+
// diagnostic of the whole tradition: a whole tone above the Sun in the
|
|
42
|
+
// disjunct system (9/8, B durum), a semitone in the conjunct (256/243, B
|
|
43
|
+
// molle). This is the same durum/molle distinction the GABC parser tracks as
|
|
44
|
+
// the `bmolle` accidental. See the per-doctrina notes below.
|
|
19
45
|
// ---------------------------------------------------------------------------
|
|
20
46
|
// ── Pythagoras ──
|
|
21
47
|
//
|
package/dist/engines/imprint.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { toPitch } from "./temper/pitch.js";
|
|
2
2
|
import { computeModalAffinity } from "./temper/modality.js";
|
|
3
3
|
const DEFAULT_TOP = 5;
|
|
4
|
+
// Attractors are pitch *classes*; they're rendered at a fixed octave (4) purely
|
|
5
|
+
// so they carry a concrete Pitch for display. The octave is not meaningful — do
|
|
6
|
+
// not read it as register.
|
|
4
7
|
const DEFAULT_MIDI_OCTAVE = 4;
|
|
5
8
|
const VOWELS = ["a", "e", "i", "o", "u"];
|
|
6
9
|
function pitchForPc(pc, scale) {
|
|
@@ -70,7 +73,10 @@ function computeVowelAttractors(phrases, scale) {
|
|
|
70
73
|
// A note's contribution to the pc-distribution is raised where it carries more
|
|
71
74
|
// structural weight: on an ictus (the rhythmic footfall) and, above all, when it
|
|
72
75
|
// is a cadence's resolution. Cadence notes are passed in — the imprint sits below
|
|
73
|
-
// the score engine, so it cannot detect them itself.
|
|
76
|
+
// the score engine, so it cannot detect them itself. The principle that ictus and
|
|
77
|
+
// cadence notes are the modally load-bearing ones is Solesmes doctrine [biblio:
|
|
78
|
+
// mocquereau-nombre]; the magnitudes (1.5×, 2×) are a tuned editorial weighting,
|
|
79
|
+
// not a figure from any source — cadence weighted above ictus by intent.
|
|
74
80
|
const ICTUS_WEIGHT = 1.5;
|
|
75
81
|
const CADENCE_WEIGHT = 2;
|
|
76
82
|
/** Build an Imprint from chant phrases, weighting structural notes more. */
|
|
@@ -1,12 +1,33 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
2
|
// engines/planet/position — Julian date, astro state, helio/geo position engine
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
|
+
// Two source models sit side by side, and which one runs depends on the body:
|
|
5
|
+
//
|
|
6
|
+
// • Planets — the JPL/Standish Keplerian element tables [biblio: standish-jpl]
|
|
7
|
+
// in orbital.ts, solved with the iterative Newton kepler() solver. See
|
|
8
|
+
// planetPos.
|
|
9
|
+
// • Sun and Moon — Paul Schlyter's tutorial formulae [biblio:
|
|
10
|
+
// schlyter-positions], a lighter model with its own constants, a one-step
|
|
11
|
+
// eccentric-anomaly approximation (the Sun), and a named perturbation series
|
|
12
|
+
// (the Moon). See sunPos / moonPos.
|
|
13
|
+
//
|
|
14
|
+
// The split is deliberate: the JPL tables don't include the Moon, and Schlyter's
|
|
15
|
+
// Sun is accurate enough at the Sun's tiny eccentricity to skip Newton iteration.
|
|
16
|
+
// So the reader should not expect the same constants or solver across bodies.
|
|
17
|
+
//
|
|
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.
|
|
4
21
|
import { sinDeg, cosDeg, atan2Deg, kepler, wrapAngle, toAu, toCartesian, toSpherical, toEquatorial } from "./math.js";
|
|
5
22
|
import { ORBITAL_ELEMENTS } from "./orbital.js";
|
|
6
23
|
const MS_PER_DAY = 86400000;
|
|
7
24
|
// Precomputed Earth radius in AU (used for Moon distance conversion)
|
|
8
25
|
const EARTH_ELEM = ORBITAL_ELEMENTS.get("Earth");
|
|
9
26
|
export const EARTH_RADIUS_AU = toAu(EARTH_ELEM.radius);
|
|
27
|
+
// Mean obliquity of the ecliptic (the tilt used to rotate ecliptic → equatorial
|
|
28
|
+
// coordinates), in degrees. eps0 is the J2000 value 23°26′21.406″; the arcsecond
|
|
29
|
+
// series is the standard IAU 2006 expansion in Julian centuries T from J2000.
|
|
30
|
+
// (Standard astronomy; source not yet catalogued — TBD.)
|
|
10
31
|
function meanObliquity(T) {
|
|
11
32
|
const eps0 = 23 + 26 / 60 + 21.406 / 3600;
|
|
12
33
|
const sec = -46.836769 * T -
|
|
@@ -17,8 +38,14 @@ function meanObliquity(T) {
|
|
|
17
38
|
return eps0 + sec / 3600;
|
|
18
39
|
}
|
|
19
40
|
export function getState(ts) {
|
|
41
|
+
// 2440587.5 is the Julian Date of the Unix epoch; 2451545 is J2000.0 and 36525
|
|
42
|
+
// days is a Julian century — so _T is centuries from J2000 in UT.
|
|
20
43
|
const JD = 2440587.5 + ts / MS_PER_DAY;
|
|
21
44
|
const _T = (JD - 2451545) / 36525;
|
|
45
|
+
// ΔT (TT − UT), in seconds: the drift between civil UT and the uniform
|
|
46
|
+
// Terrestrial Time the ephemerides are reckoned in. This is a coarse local fit
|
|
47
|
+
// valid near the present, not a long-baseline model — a limitation for deep
|
|
48
|
+
// medieval dates. (Standard-form polynomial; source not yet catalogued — TBD.)
|
|
22
49
|
const dT = 64.7 + 64.7 * _T - 0.6 * _T * _T; // seconds
|
|
23
50
|
const TT = JD + dT / 86400;
|
|
24
51
|
const J = TT - 2451545.0;
|
|
@@ -26,12 +53,16 @@ export function getState(ts) {
|
|
|
26
53
|
const eps = meanObliquity(T);
|
|
27
54
|
return { JD, TT, TS: ts, J, T, eps };
|
|
28
55
|
}
|
|
29
|
-
// ── Sun position ──
|
|
56
|
+
// ── Sun position ── (Schlyter's solar elements [biblio: schlyter-positions])
|
|
30
57
|
export function sunPos(state) {
|
|
31
58
|
const { J, eps } = state;
|
|
59
|
+
// omega = longitude of perihelion, e = eccentricity, M = mean anomaly, all as
|
|
60
|
+
// Schlyter's linear-in-J elements (J = days from J2000).
|
|
32
61
|
const omega = 282.9404 + 4.70935e-5 * J;
|
|
33
62
|
const e = 0.016709 - 1.151e-9 * J;
|
|
34
63
|
const M = 356.047 + 0.9856002585 * J;
|
|
64
|
+
// Eccentric anomaly by a single first-order step, not Newton iteration: the
|
|
65
|
+
// Sun's eccentricity is small enough (~0.017) that one correction suffices.
|
|
35
66
|
const E = M + (180 / Math.PI) * e * sinDeg(M) * (1 + e * cosDeg(M));
|
|
36
67
|
const xp = cosDeg(E) - e;
|
|
37
68
|
const yp = Math.sqrt(1 - e * e) * sinDeg(E);
|
|
@@ -54,9 +85,13 @@ export function sunPos(state) {
|
|
|
54
85
|
},
|
|
55
86
|
};
|
|
56
87
|
}
|
|
57
|
-
// ── Moon position ──
|
|
88
|
+
// ── Moon position ── (Schlyter's lunar elements [biblio: schlyter-positions])
|
|
58
89
|
export function moonPos(state, sun) {
|
|
59
90
|
const { J, eps } = state;
|
|
91
|
+
// Orbital elements (J = days from J2000): Omega = ascending node, I =
|
|
92
|
+
// inclination, omega = argument of perigee, a = semi-major axis IN EARTH RADII
|
|
93
|
+
// (not AU — the Moon is measured from Earth; the AU conversion happens at the
|
|
94
|
+
// end via EARTH_RADIUS_AU), e = eccentricity, M = mean anomaly.
|
|
60
95
|
const Omega = wrapAngle(125.1228 - 0.0529538083 * J);
|
|
61
96
|
const I = 5.1454;
|
|
62
97
|
const omega = wrapAngle(318.0634 + 0.1643573223 * J);
|
|
@@ -73,12 +108,18 @@ export function moonPos(state, sun) {
|
|
|
73
108
|
const y = (cw * sO + sw * cO * cI) * xh + (-sw * sO + cw * cO * cI) * yh;
|
|
74
109
|
const z = sw * sI * xh + cw * sI * yh;
|
|
75
110
|
const [lonE, latE, distE] = toSpherical(x, y, z);
|
|
111
|
+
// The perturbation arguments: Lm = Moon's mean longitude, Ms/Ls = Sun's mean
|
|
112
|
+
// anomaly/longitude, D = mean elongation (Moon − Sun), F = argument of latitude
|
|
113
|
+
// (Moon − node). The Sun's disturbing pull is expressed as sines of these.
|
|
76
114
|
const Lm = wrapAngle(Omega + omega + M);
|
|
77
115
|
const Ms = sun.orbit.M;
|
|
78
116
|
const Ls = sun.orbit.L;
|
|
79
117
|
const D = wrapAngle(Lm - Ls);
|
|
80
118
|
const F = wrapAngle(Lm - Omega);
|
|
81
|
-
// Lunar perturbations
|
|
119
|
+
// Lunar perturbations, in degrees — Schlyter's truncated series of the classical
|
|
120
|
+
// named terms. The three largest in longitude are the evection (−1.274·sin(M−2D),
|
|
121
|
+
// the Sun stretching the Moon's ellipse), the variation (+0.658·sin(2D)), and
|
|
122
|
+
// the annual equation (−0.186·sin Ms); the remainder are smaller corrections.
|
|
82
123
|
const lonPerturb = -1.274 * sinDeg(M - 2 * D) + 0.658 * sinDeg(2 * D) - 0.186 * sinDeg(Ms) -
|
|
83
124
|
0.059 * sinDeg(2 * M - 2 * D) - 0.057 * sinDeg(M - 2 * D + Ms) +
|
|
84
125
|
0.053 * sinDeg(M + 2 * D) + 0.046 * sinDeg(2 * D - Ms) +
|
|
@@ -116,12 +157,17 @@ export function planetPos(name, state, sun) {
|
|
|
116
157
|
throw new Error(`Unknown body: ${name}`);
|
|
117
158
|
const { J, T, eps } = state;
|
|
118
159
|
const oe = body.datasets;
|
|
119
|
-
//
|
|
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.
|
|
120
164
|
const dataset = J > -73048.5 && J < 18626.5 ? oe[1] : oe[0];
|
|
121
165
|
const [a, e, I, L, wBar, Omega] = dataset.map(([x0, x1]) => x0 + x1 * T);
|
|
122
166
|
const omega = wBar - Omega; // argument of periapsis
|
|
123
167
|
let M = L - wBar; // mean anomaly
|
|
124
|
-
//
|
|
168
|
+
// 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]
|
|
125
171
|
if (oe[2]) {
|
|
126
172
|
const [b, c, s, f] = oe[2];
|
|
127
173
|
M += b * T * T + c * cosDeg(f * T) + s * sinDeg(f * T);
|