tonus 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BIBLIOGRAPHY.md +2 -0
- package/dist/data/compline.d.ts +38 -0
- package/dist/data/compline.js +73 -0
- package/dist/data/office-psalms.d.ts +15 -0
- package/dist/data/office-psalms.js +28 -0
- package/dist/data/prime.d.ts +20 -0
- package/dist/data/prime.js +42 -0
- package/dist/engines/chant/hour.js +88 -11
- package/dist/engines/chant/psalm.d.ts +17 -0
- package/dist/engines/chant/psalm.js +36 -0
- package/package.json +1 -1
package/BIBLIOGRAPHY.md
CHANGED
|
@@ -17,6 +17,8 @@ section of the relevant page.
|
|
|
17
17
|
[docs/chant.md](docs/chant.md#sources)
|
|
18
18
|
- **Versus Psalmorum et Canticorum (Solesmes, No. 839)** — psalm-verse
|
|
19
19
|
formulas → [docs/chant.md](docs/chant.md#sources)
|
|
20
|
+
- **Bloomfield, _Compline_ (github.com/bbloomf/compline, public domain)**
|
|
21
|
+
— reference dates & chants for the traditional Roman Compline ordo → [docs/chant.md](docs/chant.md#sources)
|
|
20
22
|
|
|
21
23
|
## Chant rhythm (score engine)
|
|
22
24
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Season } from "../engines/cal/types.js";
|
|
2
|
+
/** The invariable spine — chants sung the same every night. */
|
|
3
|
+
export declare const COMPLINE_ORDINARY: {
|
|
4
|
+
/** Deus in adjutorium — the opening versicle. */
|
|
5
|
+
readonly opening: "gregobase:501";
|
|
6
|
+
/** Nunc dimittis — the gospel canticle. */
|
|
7
|
+
readonly canticle: "gregobase:1346";
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Season → the seasonal Te lucis ante terminum (hymn) and In manus tuas
|
|
11
|
+
* (short responsory) chant ids. Feast-specific tones (Ascension, Sacred Heart,
|
|
12
|
+
* BVM octaves, …) are refinements deferred to a later pass; this maps the seven
|
|
13
|
+
* calendar seasons to their plain seasonal setting.
|
|
14
|
+
*/
|
|
15
|
+
export declare const COMPLINE_SEASONAL: Readonly<Record<Season, {
|
|
16
|
+
teLucis: string;
|
|
17
|
+
inManusTuas: string;
|
|
18
|
+
}>>;
|
|
19
|
+
/** The four seasonal Marian antiphons (simple tone). */
|
|
20
|
+
export declare const MARIAN_ANTIPHONS: {
|
|
21
|
+
readonly alma: "gregobase:1851";
|
|
22
|
+
readonly ave: "gregobase:2153";
|
|
23
|
+
readonly reginaCaeli: "gregobase:2290";
|
|
24
|
+
readonly salve: "gregobase:2435";
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* The seasonal Marian antiphon, chosen by the traditional four-way rotation.
|
|
28
|
+
* The boundaries do not align with the calendar seasons — the Alma → Ave switch
|
|
29
|
+
* falls on Candlemas (2 February), mid-season — so this uses the feast's season
|
|
30
|
+
* for the broad divisions and its date for the Candlemas cut.
|
|
31
|
+
*
|
|
32
|
+
* Advent 1 → 1 Feb Alma Redemptoris Mater
|
|
33
|
+
* 2 Feb → Holy Wednesday Ave Regina caelorum
|
|
34
|
+
* Eastertide Regina caeli
|
|
35
|
+
* Pentecost → Advent Salve Regina
|
|
36
|
+
*/
|
|
37
|
+
export declare function marianAntiphonFor(season: Season, date: Date): string;
|
|
38
|
+
//# sourceMappingURL=compline.d.ts.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// data/compline — the Ordo of Compline (completorium)
|
|
2
|
+
//
|
|
3
|
+
// Compline in the traditional Roman rite is nearly invariable: the same spine
|
|
4
|
+
// every night, varying only by liturgical season (the hymn Te lucis and the
|
|
5
|
+
// short responsory In manus tuas) and by the seasonal Marian antiphon at the
|
|
6
|
+
// end. This is an editorial ordo — it references chants already in the corpus
|
|
7
|
+
// by their gregobase: id, it does not carry any GABC of its own (cf. masses.ts,
|
|
8
|
+
// which is likewise a hand-authored profile table, not corpus-extracted).
|
|
9
|
+
//
|
|
10
|
+
// Ordo structure after the traditional Roman Compline; seasonal assignment and
|
|
11
|
+
// the Marian rotation follow standard practice, cross-checked against
|
|
12
|
+
// bbloomf/compline (public domain). See docs/chant.md and BIBLIOGRAPHY.md.
|
|
13
|
+
// Compline's psalmody (Ps 4, 30 vv. 2–6, 90, 133) is not hand-listed here — it
|
|
14
|
+
// comes from the extracted DO Tridentine scheme via
|
|
15
|
+
// `officePsalmPortions("Completorium", …)`. Only the ordo structure and the
|
|
16
|
+
// seasonal/Marian rules are editorial and live in this file.
|
|
17
|
+
/** The invariable spine — chants sung the same every night. */
|
|
18
|
+
export const COMPLINE_ORDINARY = {
|
|
19
|
+
/** Deus in adjutorium — the opening versicle. */
|
|
20
|
+
opening: "gregobase:501",
|
|
21
|
+
/** Nunc dimittis — the gospel canticle. */
|
|
22
|
+
canticle: "gregobase:1346",
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Season → the seasonal Te lucis ante terminum (hymn) and In manus tuas
|
|
26
|
+
* (short responsory) chant ids. Feast-specific tones (Ascension, Sacred Heart,
|
|
27
|
+
* BVM octaves, …) are refinements deferred to a later pass; this maps the seven
|
|
28
|
+
* calendar seasons to their plain seasonal setting.
|
|
29
|
+
*/
|
|
30
|
+
export const COMPLINE_SEASONAL = Object.freeze({
|
|
31
|
+
adv: { teLucis: "gregobase:12752", inManusTuas: "gregobase:12702" }, // Advent
|
|
32
|
+
nat: { teLucis: "gregobase:12379", inManusTuas: "gregobase:13059" }, // Christmastide
|
|
33
|
+
epi: { teLucis: "gregobase:12897", inManusTuas: "gregobase:13059" }, // after Epiphany
|
|
34
|
+
quadp: { teLucis: "gregobase:12379", inManusTuas: "gregobase:13059" }, // Septuagesima
|
|
35
|
+
quad: { teLucis: "gregobase:12673", inManusTuas: "gregobase:13059" }, // Lent
|
|
36
|
+
pasc: { teLucis: "gregobase:12092", inManusTuas: "gregobase:12649" }, // Paschaltide
|
|
37
|
+
pent: { teLucis: "gregobase:12379", inManusTuas: "gregobase:13059" }, // after Pentecost
|
|
38
|
+
});
|
|
39
|
+
/** The four seasonal Marian antiphons (simple tone). */
|
|
40
|
+
export const MARIAN_ANTIPHONS = {
|
|
41
|
+
alma: "gregobase:1851", // Alma Redemptoris Mater — Advent to Candlemas
|
|
42
|
+
ave: "gregobase:2153", // Ave Regina caelorum — Candlemas to Holy Week
|
|
43
|
+
reginaCaeli: "gregobase:2290", // Regina caeli — Eastertide
|
|
44
|
+
salve: "gregobase:2435", // Salve Regina — Trinity to Advent
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* The seasonal Marian antiphon, chosen by the traditional four-way rotation.
|
|
48
|
+
* The boundaries do not align with the calendar seasons — the Alma → Ave switch
|
|
49
|
+
* falls on Candlemas (2 February), mid-season — so this uses the feast's season
|
|
50
|
+
* for the broad divisions and its date for the Candlemas cut.
|
|
51
|
+
*
|
|
52
|
+
* Advent 1 → 1 Feb Alma Redemptoris Mater
|
|
53
|
+
* 2 Feb → Holy Wednesday Ave Regina caelorum
|
|
54
|
+
* Eastertide Regina caeli
|
|
55
|
+
* Pentecost → Advent Salve Regina
|
|
56
|
+
*/
|
|
57
|
+
export function marianAntiphonFor(season, date) {
|
|
58
|
+
if (season === "pasc")
|
|
59
|
+
return MARIAN_ANTIPHONS.reginaCaeli;
|
|
60
|
+
if (season === "adv")
|
|
61
|
+
return MARIAN_ANTIPHONS.alma;
|
|
62
|
+
if (season === "pent")
|
|
63
|
+
return MARIAN_ANTIPHONS.salve;
|
|
64
|
+
// nat / epi / quadp / quad straddle Candlemas (2 February). Alma runs from
|
|
65
|
+
// Advent through 1 February — so Christmastide and any date before Candlemas
|
|
66
|
+
// (December or January) is still Alma; 2 February onward is Ave, on through
|
|
67
|
+
// Holy Week (the tail of `quad`, before Paschaltide takes over).
|
|
68
|
+
const month = date.getUTCMonth(); // 0 = Jan, 11 = Dec
|
|
69
|
+
const beforeCandlemas = month === 11 || month === 0 ||
|
|
70
|
+
(month === 1 && date.getUTCDate() < 2);
|
|
71
|
+
return beforeCandlemas ? MARIAN_ANTIPHONS.alma : MARIAN_ANTIPHONS.ave;
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=compline.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface OfficePsalmPortion {
|
|
2
|
+
psalm: number;
|
|
3
|
+
from?: number;
|
|
4
|
+
to?: number;
|
|
5
|
+
}
|
|
6
|
+
export interface OfficePsalmEntry {
|
|
7
|
+
hour: "Prima" | "Tertia" | "Sexta" | "Nona" | "Completorium";
|
|
8
|
+
/** 0–6 (0 = Sunday); null for the feast set or hours with no weekday split. */
|
|
9
|
+
weekday: number | null;
|
|
10
|
+
/** True for the feast (Festis) psalmody. */
|
|
11
|
+
festis: boolean;
|
|
12
|
+
psalms: OfficePsalmPortion[];
|
|
13
|
+
}
|
|
14
|
+
export declare const OFFICE_PSALMS: OfficePsalmEntry[];
|
|
15
|
+
//# sourceMappingURL=office-psalms.d.ts.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// office-psalms.ts — little-hours psalmody (Prime, Terce, Sext, None, Compline)
|
|
2
|
+
// Extracted from Divinum Officium (Psalterium, [Tridentinum] section) by
|
|
3
|
+
// scripts/extract-office-psalms.mjs
|
|
4
|
+
// Generated: 2026-07-05T01:25:28.943Z
|
|
5
|
+
// Entries: 15
|
|
6
|
+
//
|
|
7
|
+
// The traditional (pre-1911) Roman little-hours psalm distribution. Each entry
|
|
8
|
+
// gives the psalm portions for one hour on one weekday (0 = Sunday), or the
|
|
9
|
+
// feast set (festis: true, weekday: null). A portion is a whole psalm or an
|
|
10
|
+
// inclusive verse range.
|
|
11
|
+
export const OFFICE_PSALMS = [
|
|
12
|
+
{ hour: "Prima", weekday: 0, festis: false, psalms: [{ psalm: 53 }, { psalm: 117 }, { psalm: 118, from: 1, to: 16 }, { psalm: 118, from: 17, to: 32 }] },
|
|
13
|
+
{ hour: "Prima", weekday: 1, festis: false, psalms: [{ psalm: 53 }, { psalm: 23 }, { psalm: 118, from: 1, to: 16 }, { psalm: 118, from: 17, to: 32 }] },
|
|
14
|
+
{ hour: "Prima", weekday: 2, festis: false, psalms: [{ psalm: 53 }, { psalm: 24 }, { psalm: 118, from: 1, to: 16 }, { psalm: 118, from: 17, to: 32 }] },
|
|
15
|
+
{ hour: "Prima", weekday: 3, festis: false, psalms: [{ psalm: 53 }, { psalm: 25 }, { psalm: 118, from: 1, to: 16 }, { psalm: 118, from: 17, to: 32 }] },
|
|
16
|
+
{ hour: "Prima", weekday: 4, festis: false, psalms: [{ psalm: 53 }, { psalm: 22 }, { psalm: 118, from: 1, to: 16 }, { psalm: 118, from: 17, to: 32 }] },
|
|
17
|
+
{ hour: "Prima", weekday: 5, festis: false, psalms: [{ psalm: 53 }, { psalm: 21 }, { psalm: 118, from: 1, to: 16 }, { psalm: 118, from: 17, to: 32 }] },
|
|
18
|
+
{ hour: "Prima", weekday: 6, festis: false, psalms: [{ psalm: 53 }, { psalm: 118, from: 1, to: 16 }, { psalm: 118, from: 17, to: 32 }] },
|
|
19
|
+
{ hour: "Prima", weekday: null, festis: true, psalms: [{ psalm: 53 }, { psalm: 118, from: 1, to: 16 }, { psalm: 118, from: 17, to: 32 }] },
|
|
20
|
+
{ hour: "Tertia", weekday: 0, festis: false, psalms: [{ psalm: 118, from: 33, to: 48 }, { psalm: 118, from: 49, to: 64 }, { psalm: 118, from: 65, to: 80 }] },
|
|
21
|
+
{ hour: "Tertia", weekday: null, festis: false, psalms: [{ psalm: 118, from: 33, to: 48 }, { psalm: 118, from: 49, to: 64 }, { psalm: 118, from: 65, to: 80 }] },
|
|
22
|
+
{ hour: "Sexta", weekday: 0, festis: false, psalms: [{ psalm: 118, from: 81, to: 96 }, { psalm: 118, from: 97, to: 112 }, { psalm: 118, from: 113, to: 128 }] },
|
|
23
|
+
{ hour: "Sexta", weekday: null, festis: false, psalms: [{ psalm: 118, from: 81, to: 96 }, { psalm: 118, from: 97, to: 112 }, { psalm: 118, from: 113, to: 128 }] },
|
|
24
|
+
{ hour: "Nona", weekday: 0, festis: false, psalms: [{ psalm: 118, from: 129, to: 144 }, { psalm: 118, from: 145, to: 160 }, { psalm: 118, from: 161, to: 176 }] },
|
|
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
|
+
{ hour: "Completorium", weekday: null, festis: false, psalms: [{ psalm: 4 }, { psalm: 30, from: 2, to: 6 }, { psalm: 90 }, { psalm: 133 }] }
|
|
27
|
+
];
|
|
28
|
+
//# sourceMappingURL=office-psalms.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Season } from "../engines/cal/types.js";
|
|
2
|
+
/** The invariable spine. */
|
|
3
|
+
export declare const PRIME_ORDINARY: {
|
|
4
|
+
/** Deus in adjutorium — the opening versicle. */
|
|
5
|
+
readonly opening: "gregobase:501";
|
|
6
|
+
/**
|
|
7
|
+
* Iam lucis orto sidere — Prime's hymn. Its variants are keyed by feast rank
|
|
8
|
+
* (ferial / Sunday / major feast) rather than by season; the ferial-and-
|
|
9
|
+
* simple-feast tone is the everyday default.
|
|
10
|
+
*/
|
|
11
|
+
readonly hymn: "gregobase:11944";
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Season → the seasonal short responsory Christe Fili Dei vivi. Only Advent and
|
|
15
|
+
* Paschaltide have their own setting; the rest of the year uses "per Annum."
|
|
16
|
+
*/
|
|
17
|
+
export declare const PRIME_SEASONAL: Readonly<Record<Season, {
|
|
18
|
+
responsory: string;
|
|
19
|
+
}>>;
|
|
20
|
+
//# sourceMappingURL=prime.d.ts.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// data/prime — the Ordo of Prime (prima)
|
|
2
|
+
//
|
|
3
|
+
// Prime, the "first hour," is like Compline mostly fixed and seasonal rather
|
|
4
|
+
// than per-feast. This ordo covers Prime's SUNG parts: the opening, the hymn
|
|
5
|
+
// Iam lucis orto sidere, the fixed psalmody, and the seasonal short responsory
|
|
6
|
+
// Christe Fili Dei vivi. Prime's recited/monotoned parts — the Athanasian Creed
|
|
7
|
+
// (Quicumque vult), the martyrology, the chapter, and the collect — are not
|
|
8
|
+
// Solesmes chant and are not in the corpus, so they are out of scope; this is a
|
|
9
|
+
// chant ordo, not a full Breviary Prime.
|
|
10
|
+
//
|
|
11
|
+
// Like data/compline.ts and data/masses.ts this is a hand-authored table that
|
|
12
|
+
// references chants already in the corpus by id; it carries no GABC of its own.
|
|
13
|
+
// See docs/chant.md and BIBLIOGRAPHY.md.
|
|
14
|
+
// Prime's psalmody (Ps 53 + a weekday-proper psalm + Ps 118 in two sections,
|
|
15
|
+
// varying by weekday per DO's Tridentine scheme) is not hand-listed here — it
|
|
16
|
+
// comes from the extracted scheme via `officePsalmPortions("Prima", weekday)`.
|
|
17
|
+
// Only the ordo structure and the seasonal responsory rule are editorial.
|
|
18
|
+
/** The invariable spine. */
|
|
19
|
+
export const PRIME_ORDINARY = {
|
|
20
|
+
/** Deus in adjutorium — the opening versicle. */
|
|
21
|
+
opening: "gregobase:501",
|
|
22
|
+
/**
|
|
23
|
+
* Iam lucis orto sidere — Prime's hymn. Its variants are keyed by feast rank
|
|
24
|
+
* (ferial / Sunday / major feast) rather than by season; the ferial-and-
|
|
25
|
+
* simple-feast tone is the everyday default.
|
|
26
|
+
*/
|
|
27
|
+
hymn: "gregobase:11944",
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Season → the seasonal short responsory Christe Fili Dei vivi. Only Advent and
|
|
31
|
+
* Paschaltide have their own setting; the rest of the year uses "per Annum."
|
|
32
|
+
*/
|
|
33
|
+
export const PRIME_SEASONAL = Object.freeze({
|
|
34
|
+
adv: { responsory: "gregobase:13127" }, // Tempore Adventus
|
|
35
|
+
nat: { responsory: "gregobase:11818" }, // per Annum
|
|
36
|
+
epi: { responsory: "gregobase:11818" },
|
|
37
|
+
quadp: { responsory: "gregobase:11818" },
|
|
38
|
+
quad: { responsory: "gregobase:11818" },
|
|
39
|
+
pasc: { responsory: "gregobase:12439" }, // Tempore Paschali
|
|
40
|
+
pent: { responsory: "gregobase:11818" },
|
|
41
|
+
});
|
|
42
|
+
//# sourceMappingURL=prime.js.map
|
|
@@ -2,15 +2,71 @@
|
|
|
2
2
|
// engines/chant/hour — Divine Office hour retrieval
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
4
|
import { resolveChant, resolveChants } from "./chant.js";
|
|
5
|
+
import { intonePortion, officePsalmPortions } from "./psalm.js";
|
|
5
6
|
import { temporaSundayId } from "../cal/date.js";
|
|
7
|
+
import { getFeast } from "../cal/calendar.js";
|
|
6
8
|
import { OFFICE_ROMAN } from "../../data/office-roman.js";
|
|
9
|
+
import { COMPLINE_ORDINARY, COMPLINE_SEASONAL, marianAntiphonFor, } from "../../data/compline.js";
|
|
10
|
+
import { PRIME_ORDINARY, PRIME_SEASONAL } from "../../data/prime.js";
|
|
7
11
|
let _roman = null;
|
|
8
12
|
function romanMap() {
|
|
9
13
|
if (!_roman)
|
|
10
14
|
_roman = new Map(OFFICE_ROMAN.map((d) => [d.feastId, d]));
|
|
11
15
|
return _roman;
|
|
12
16
|
}
|
|
17
|
+
// Compline is fixed and seasonal, not per-feast: it does not use the OfficeDay
|
|
18
|
+
// tables at all. The ordo is assembled from the season (Te lucis, In manus
|
|
19
|
+
// tuas), the fixed psalms (from the extracted DO scheme), the invariable spine
|
|
20
|
+
// (Deus in adjutorium, Nunc dimittis), and the date-driven Marian antiphon.
|
|
21
|
+
// See data/compline.ts.
|
|
22
|
+
function complineForFeast(feast) {
|
|
23
|
+
const seasonal = COMPLINE_SEASONAL[feast.season];
|
|
24
|
+
const results = [];
|
|
25
|
+
const opening = resolveChant(COMPLINE_ORDINARY.opening);
|
|
26
|
+
if (opening)
|
|
27
|
+
results.push(opening);
|
|
28
|
+
for (const p of officePsalmPortions("Completorium", feast.weekday)) {
|
|
29
|
+
results.push(...intonePortion(p));
|
|
30
|
+
}
|
|
31
|
+
const hymn = seasonal && resolveChant(seasonal.teLucis);
|
|
32
|
+
if (hymn)
|
|
33
|
+
results.push(hymn);
|
|
34
|
+
const responsory = seasonal && resolveChant(seasonal.inManusTuas);
|
|
35
|
+
if (responsory)
|
|
36
|
+
results.push(responsory);
|
|
37
|
+
const canticle = resolveChant(COMPLINE_ORDINARY.canticle);
|
|
38
|
+
if (canticle)
|
|
39
|
+
results.push(canticle);
|
|
40
|
+
const marian = resolveChant(marianAntiphonFor(feast.season, feast.date));
|
|
41
|
+
if (marian)
|
|
42
|
+
results.push(marian);
|
|
43
|
+
return results;
|
|
44
|
+
}
|
|
45
|
+
// Prime, like Compline, is a fixed+seasonal ordo, not per-feast. Covers the
|
|
46
|
+
// sung parts only (see data/prime.ts): opening, fixed psalms, the hymn Iam
|
|
47
|
+
// lucis, and the seasonal short responsory Christe Fili Dei.
|
|
48
|
+
function primeForFeast(feast) {
|
|
49
|
+
const seasonal = PRIME_SEASONAL[feast.season];
|
|
50
|
+
const results = [];
|
|
51
|
+
const opening = resolveChant(PRIME_ORDINARY.opening);
|
|
52
|
+
if (opening)
|
|
53
|
+
results.push(opening);
|
|
54
|
+
const hymn = resolveChant(PRIME_ORDINARY.hymn);
|
|
55
|
+
if (hymn)
|
|
56
|
+
results.push(hymn);
|
|
57
|
+
for (const p of officePsalmPortions("Prima", feast.weekday)) {
|
|
58
|
+
results.push(...intonePortion(p));
|
|
59
|
+
}
|
|
60
|
+
const responsory = seasonal && resolveChant(seasonal.responsory);
|
|
61
|
+
if (responsory)
|
|
62
|
+
results.push(responsory);
|
|
63
|
+
return results;
|
|
64
|
+
}
|
|
13
65
|
function chantsForFeastHour(feast, hour) {
|
|
66
|
+
if (hour === "completorium")
|
|
67
|
+
return complineForFeast(feast);
|
|
68
|
+
if (hour === "prima")
|
|
69
|
+
return primeForFeast(feast);
|
|
14
70
|
const map = romanMap();
|
|
15
71
|
const sunday = temporaSundayId(feast.id);
|
|
16
72
|
const day = map.get(feast.id) ?? (sunday ? (map.get(sunday) ?? null) : null);
|
|
@@ -79,12 +135,27 @@ export function getHour(query) {
|
|
|
79
135
|
const hour = query.hora;
|
|
80
136
|
let results;
|
|
81
137
|
if (feasts && hour) {
|
|
82
|
-
|
|
138
|
+
// Prime and Compline are seasonal/weekday ordos, identical for every feast
|
|
139
|
+
// of the day — so concurrent feasts collapse to a single ordo rather than
|
|
140
|
+
// repeating it. The other hours are genuinely per-feast.
|
|
141
|
+
results =
|
|
142
|
+
hour === "prima" || hour === "completorium"
|
|
143
|
+
? feasts[0] ? chantsForFeastHour(feasts[0], hour) : []
|
|
144
|
+
: feasts.flatMap((f) => chantsForFeastHour(f, hour));
|
|
83
145
|
}
|
|
84
146
|
else if (feasts) {
|
|
85
|
-
const hours = [
|
|
147
|
+
const hours = [
|
|
148
|
+
"matutinum", "laudes", "prima", "tertia", "sexta", "nona",
|
|
149
|
+
"vesperae", "completorium",
|
|
150
|
+
];
|
|
86
151
|
results = feasts.flatMap((f) => hours.flatMap((h) => chantsForFeastHour(f, h)));
|
|
87
152
|
}
|
|
153
|
+
else if (hour === "prima" || hour === "completorium") {
|
|
154
|
+
// Prime and Compline are seasonal ordos, not per-feast. With no feast,
|
|
155
|
+
// resolve for the default epoch (Guido d'Arezzo's era) — festum()'s anchor.
|
|
156
|
+
const [feast] = getFeast();
|
|
157
|
+
results = feast ? chantsForFeastHour(feast, hour) : [];
|
|
158
|
+
}
|
|
88
159
|
else if (hour) {
|
|
89
160
|
// Hour without feast — scan all office entries
|
|
90
161
|
results = OFFICE_ROMAN.flatMap((day) => {
|
|
@@ -119,15 +190,21 @@ export function getHour(query) {
|
|
|
119
190
|
const ids = new Set(toArray(query.id));
|
|
120
191
|
results = results.filter((c) => ids.has(c.id));
|
|
121
192
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
193
|
+
// Prime and Compline are ordered ordos — their sequence IS the content — so
|
|
194
|
+
// they keep assembly order unless the caller explicitly asks for a sort.
|
|
195
|
+
// Every other hour returns a set of chants, sorted by incipit by default.
|
|
196
|
+
const isOrderedOrdo = query.hora === "prima" || query.hora === "completorium";
|
|
197
|
+
if (query.sort || !isOrderedOrdo) {
|
|
198
|
+
const sort = query.sort ?? "incipit";
|
|
199
|
+
results.sort((a, b) => {
|
|
200
|
+
if (sort === "id")
|
|
201
|
+
return a.id.localeCompare(b.id);
|
|
202
|
+
if (sort === "mode")
|
|
203
|
+
return (String(a.mode ?? "").localeCompare(String(b.mode ?? "")) ||
|
|
204
|
+
a.incipit.localeCompare(b.incipit));
|
|
205
|
+
return a.incipit.localeCompare(b.incipit);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
131
208
|
const offset = Math.max(0, query.offset ?? 0);
|
|
132
209
|
const limit = query.limit == null ? results.length : Math.max(0, query.limit);
|
|
133
210
|
return results.slice(offset, offset + limit);
|
|
@@ -1,7 +1,24 @@
|
|
|
1
|
+
import { type OfficePsalmEntry, type OfficePsalmPortion } from "../../data/office-psalms.js";
|
|
1
2
|
import { type Chant, type PsalmusQuery } from "./types.js";
|
|
2
3
|
/**
|
|
3
4
|
* Psalm and canticle retrieval (`tonus.psalmus`) from the Psalterium,
|
|
4
5
|
* intoned to the psalm tones (modes 1-8 plus tonus peregrinus) as GABC.
|
|
5
6
|
*/
|
|
6
7
|
export declare function getPsalm(query?: PsalmusQuery): Chant[];
|
|
8
|
+
/**
|
|
9
|
+
* A contiguous verse range of one psalm, intoned — e.g. `getPsalmRange(30, 2, 6)`
|
|
10
|
+
* for Ps 30, verses 2–6. Used by the fixed office ordos (Prime, Compline), whose
|
|
11
|
+
* psalmody takes only a portion of a psalm (Ps 30:2-6; Ps 118 in sections).
|
|
12
|
+
* `lo`/`hi` are inclusive verse numbers; split verses (3a/3b) are both included.
|
|
13
|
+
*/
|
|
14
|
+
export declare function getPsalmRange(psalm: number, lo: number, hi: number, mode?: number): Chant[];
|
|
15
|
+
/** A psalm portion — whole psalm or an inclusive verse range — intoned. */
|
|
16
|
+
export declare function intonePortion(p: OfficePsalmPortion, mode?: number): Chant[];
|
|
17
|
+
/**
|
|
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
|
|
20
|
+
* weekday-specific entry, then the ferial default (weekday null), then the
|
|
21
|
+
* feast set; returns the psalm portions (not yet intoned).
|
|
22
|
+
*/
|
|
23
|
+
export declare function officePsalmPortions(hour: OfficePsalmEntry["hour"], weekday: number): OfficePsalmPortion[];
|
|
7
24
|
//# sourceMappingURL=psalm.d.ts.map
|
|
@@ -2,6 +2,7 @@
|
|
|
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
6
|
import { intone } from "./intone.js";
|
|
6
7
|
import { MODE_LABELS } from "./types.js";
|
|
7
8
|
const CANTICLE_NAMES = {
|
|
@@ -57,4 +58,39 @@ export function getPsalm(query) {
|
|
|
57
58
|
const mode = query.mode ?? 8;
|
|
58
59
|
return verses.map((v) => verseToChant(v, mode, query.differentia, query.intonatio));
|
|
59
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* A contiguous verse range of one psalm, intoned — e.g. `getPsalmRange(30, 2, 6)`
|
|
63
|
+
* for Ps 30, verses 2–6. Used by the fixed office ordos (Prime, Compline), whose
|
|
64
|
+
* psalmody takes only a portion of a psalm (Ps 30:2-6; Ps 118 in sections).
|
|
65
|
+
* `lo`/`hi` are inclusive verse numbers; split verses (3a/3b) are both included.
|
|
66
|
+
*/
|
|
67
|
+
export function getPsalmRange(psalm, lo, hi, mode = 8) {
|
|
68
|
+
return PSALMS
|
|
69
|
+
.filter((v) => {
|
|
70
|
+
if (v.psalm !== psalm)
|
|
71
|
+
return false;
|
|
72
|
+
const n = parseInt(v.verse, 10);
|
|
73
|
+
return !isNaN(n) && n >= lo && n <= hi;
|
|
74
|
+
})
|
|
75
|
+
.map((v) => verseToChant(v, mode));
|
|
76
|
+
}
|
|
77
|
+
/** A psalm portion — whole psalm or an inclusive verse range — intoned. */
|
|
78
|
+
export function intonePortion(p, mode = 8) {
|
|
79
|
+
return p.from != null && p.to != null
|
|
80
|
+
? getPsalmRange(p.psalm, p.from, p.to, mode)
|
|
81
|
+
: getPsalm({ psalm: p.psalm, mode });
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 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
|
+
* weekday-specific entry, then the ferial default (weekday null), then the
|
|
87
|
+
* feast set; returns the psalm portions (not yet intoned).
|
|
88
|
+
*/
|
|
89
|
+
export function officePsalmPortions(hour, weekday) {
|
|
90
|
+
const forHour = OFFICE_PSALMS.filter((e) => e.hour === hour);
|
|
91
|
+
const exact = forHour.find((e) => e.weekday === weekday && !e.festis);
|
|
92
|
+
const ferial = forHour.find((e) => e.weekday === null && !e.festis);
|
|
93
|
+
const festis = forHour.find((e) => e.festis);
|
|
94
|
+
return (exact ?? ferial ?? festis)?.psalms ?? [];
|
|
95
|
+
}
|
|
60
96
|
//# sourceMappingURL=psalm.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tonus",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Medieval music analysis and performance: GABC plainchant exports, liturgical calendar, tuning systems, ephemeris, and the harmony of the spheres",
|