tonus 0.1.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 +99 -0
- package/LICENSE +29 -0
- package/README.md +108 -0
- package/dist/data/cal.d.ts +10 -0
- package/dist/data/cal.js +3862 -0
- package/dist/data/commune.d.ts +17 -0
- package/dist/data/commune.js +1333 -0
- package/dist/data/gr.d.ts +5 -0
- package/dist/data/gr.js +13449 -0
- package/dist/data/kyriale.d.ts +11 -0
- package/dist/data/kyriale.js +971 -0
- package/dist/data/la.d.ts +5 -0
- package/dist/data/la.js +14229 -0
- package/dist/data/lh.d.ts +5 -0
- package/dist/data/lh.js +3619 -0
- package/dist/data/lu.d.ts +5 -0
- package/dist/data/lu.js +23779 -0
- package/dist/data/masses.d.ts +18 -0
- package/dist/data/masses.js +297 -0
- package/dist/data/office-roman.d.ts +19 -0
- package/dist/data/office-roman.js +13792 -0
- package/dist/data/office.d.ts +12 -0
- package/dist/data/office.js +13052 -0
- package/dist/data/propers.d.ts +13 -0
- package/dist/data/propers.js +7584 -0
- package/dist/data/psalms.d.ts +4 -0
- package/dist/data/psalms.js +10 -0
- package/dist/data/psalms.json +22918 -0
- package/dist/data/tones.d.ts +20 -0
- package/dist/data/tones.js +153 -0
- package/dist/data/types.d.ts +3 -0
- package/dist/data/types.js +2 -0
- package/dist/engines/cal/calendar.d.ts +21 -0
- package/dist/engines/cal/calendar.js +265 -0
- package/dist/engines/cal/date.d.ts +31 -0
- package/dist/engines/cal/date.js +141 -0
- package/dist/engines/cal/types.d.ts +66 -0
- package/dist/engines/cal/types.js +189 -0
- package/dist/engines/chant/chant.d.ts +10 -0
- package/dist/engines/chant/chant.js +135 -0
- package/dist/engines/chant/hour.d.ts +8 -0
- package/dist/engines/chant/hour.js +135 -0
- package/dist/engines/chant/intone.d.ts +8 -0
- package/dist/engines/chant/intone.js +84 -0
- package/dist/engines/chant/ordinary.d.ts +7 -0
- package/dist/engines/chant/ordinary.js +232 -0
- package/dist/engines/chant/propers.d.ts +8 -0
- package/dist/engines/chant/propers.js +107 -0
- package/dist/engines/chant/psalm.d.ts +7 -0
- package/dist/engines/chant/psalm.js +60 -0
- package/dist/engines/chant/syllabify.d.ts +20 -0
- package/dist/engines/chant/syllabify.js +192 -0
- package/dist/engines/chant/types.d.ts +76 -0
- package/dist/engines/chant/types.js +34 -0
- package/dist/engines/epoch.d.ts +2 -0
- package/dist/engines/epoch.js +14 -0
- package/dist/engines/harmonia/api.d.ts +35 -0
- package/dist/engines/harmonia/api.js +90 -0
- package/dist/engines/harmonia/aspects.d.ts +8 -0
- package/dist/engines/harmonia/aspects.js +15 -0
- package/dist/engines/harmonia/data/doctrines.d.ts +16 -0
- package/dist/engines/harmonia/data/doctrines.js +154 -0
- package/dist/engines/harmonia/data/vowels.d.ts +10 -0
- package/dist/engines/harmonia/data/vowels.js +21 -0
- package/dist/engines/harmonia/presence.d.ts +13 -0
- package/dist/engines/harmonia/presence.js +48 -0
- package/dist/engines/harmonia/tabula.d.ts +28 -0
- package/dist/engines/harmonia/tabula.js +32 -0
- package/dist/engines/harmonia/voice.d.ts +19 -0
- package/dist/engines/harmonia/voice.js +51 -0
- package/dist/engines/imprint.d.ts +30 -0
- package/dist/engines/imprint.js +152 -0
- package/dist/engines/planet/appearance.d.ts +40 -0
- package/dist/engines/planet/appearance.js +84 -0
- package/dist/engines/planet/aspects.d.ts +5 -0
- package/dist/engines/planet/aspects.js +41 -0
- package/dist/engines/planet/math.d.ts +13 -0
- package/dist/engines/planet/math.js +56 -0
- package/dist/engines/planet/orbital.d.ts +25 -0
- package/dist/engines/planet/orbital.js +223 -0
- package/dist/engines/planet/planet.d.ts +13 -0
- package/dist/engines/planet/planet.js +198 -0
- package/dist/engines/planet/position.d.ts +62 -0
- package/dist/engines/planet/position.js +156 -0
- package/dist/engines/planet/types.d.ts +61 -0
- package/dist/engines/planet/types.js +14 -0
- package/dist/engines/score/api.d.ts +54 -0
- package/dist/engines/score/api.js +87 -0
- package/dist/engines/score/articulation.d.ts +6 -0
- package/dist/engines/score/articulation.js +112 -0
- package/dist/engines/score/emitters/midi.d.ts +65 -0
- package/dist/engines/score/emitters/midi.js +158 -0
- package/dist/engines/score/emitters/musicxml.d.ts +18 -0
- package/dist/engines/score/emitters/musicxml.js +166 -0
- package/dist/engines/score/infer.d.ts +4 -0
- package/dist/engines/score/infer.js +77 -0
- package/dist/engines/score/ir.d.ts +4 -0
- package/dist/engines/score/ir.js +177 -0
- package/dist/engines/score/meta.d.ts +19 -0
- package/dist/engines/score/meta.js +34 -0
- package/dist/engines/score/neume.d.ts +3 -0
- package/dist/engines/score/neume.js +26 -0
- package/dist/engines/score/parse.d.ts +3 -0
- package/dist/engines/score/parse.js +359 -0
- package/dist/engines/score/phrasing.d.ts +24 -0
- package/dist/engines/score/phrasing.js +257 -0
- package/dist/engines/score/prosody.d.ts +35 -0
- package/dist/engines/score/prosody.js +109 -0
- package/dist/engines/score/tabula.d.ts +70 -0
- package/dist/engines/score/tabula.js +109 -0
- package/dist/engines/score/types.d.ts +159 -0
- package/dist/engines/score/types.js +2 -0
- package/dist/engines/temper/api.d.ts +60 -0
- package/dist/engines/temper/api.js +130 -0
- package/dist/engines/temper/data/constants.d.ts +27 -0
- package/dist/engines/temper/data/constants.js +150 -0
- package/dist/engines/temper/data/guido.d.ts +14 -0
- package/dist/engines/temper/data/guido.js +29 -0
- package/dist/engines/temper/data/modes.d.ts +38 -0
- package/dist/engines/temper/data/modes.js +158 -0
- package/dist/engines/temper/gabc.d.ts +5 -0
- package/dist/engines/temper/gabc.js +53 -0
- package/dist/engines/temper/gamut.d.ts +9 -0
- package/dist/engines/temper/gamut.js +24 -0
- package/dist/engines/temper/guido.d.ts +16 -0
- package/dist/engines/temper/guido.js +48 -0
- package/dist/engines/temper/interval.d.ts +15 -0
- package/dist/engines/temper/interval.js +31 -0
- package/dist/engines/temper/modes.d.ts +6 -0
- package/dist/engines/temper/modes.js +13 -0
- package/dist/engines/temper/neume.d.ts +14 -0
- package/dist/engines/temper/neume.js +59 -0
- package/dist/engines/temper/pitch.d.ts +40 -0
- package/dist/engines/temper/pitch.js +129 -0
- package/dist/engines/temper/scale.d.ts +37 -0
- package/dist/engines/temper/scale.js +217 -0
- package/dist/engines/temper/step.d.ts +23 -0
- package/dist/engines/temper/step.js +53 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +27 -0
- package/package.json +60 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export type Season = "adv" | "nat" | "epi" | "quadp" | "quad" | "pasc" | "pent";
|
|
2
|
+
export declare const SEASON_LABELS: Readonly<Record<Season, string>>;
|
|
3
|
+
export declare const TEMPUS_NAMES: Readonly<Record<Season, string>>;
|
|
4
|
+
export declare const PENITENTIAL_SEASONS: ReadonlySet<Season>;
|
|
5
|
+
export type Grade = "triduum" | "duplex-i" | "duplex-majus-i" | "semiduplex-i" | "feria-privilegiata" | "duplex-ii" | "semiduplex-ii" | "duplex-majus" | "duplex" | "semiduplex" | "simplex" | "feria-major" | "vigilia" | "feria";
|
|
6
|
+
export declare const GRADE_ORDER: readonly Grade[];
|
|
7
|
+
export declare const GRADE_NAMES: Readonly<Record<Grade, string>>;
|
|
8
|
+
export declare const RITUS_TO_GRADE: Readonly<Record<string, Grade>>;
|
|
9
|
+
/** Reduce a Tridentine ritus string to its canonical Grade. */
|
|
10
|
+
export declare function ritusToGrade(ritus: string): Grade;
|
|
11
|
+
export declare const PRIVILEGED_SUNDAYS: Readonly<Record<string, Grade>>;
|
|
12
|
+
/**
|
|
13
|
+
* Grade for a calendar entry: the per-id privileged-Sunday override when
|
|
14
|
+
* one exists, otherwise the ritus reduction.
|
|
15
|
+
*/
|
|
16
|
+
export declare function entryGrade(id: string | undefined, ritus: string): Grade;
|
|
17
|
+
/** Precedence index (0 = highest). Use for sorting and comparison. */
|
|
18
|
+
export declare function gradeOrder(grade: Grade): number;
|
|
19
|
+
/** Sort comparator: earlier in GRADE_ORDER (higher dignity) sorts first. */
|
|
20
|
+
export declare function compareGrade(a: Grade, b: Grade): number;
|
|
21
|
+
export declare const BVM_FEAST_IDS: ReadonlySet<string>;
|
|
22
|
+
export declare const APOSTOLIC_FEAST_IDS: ReadonlySet<string>;
|
|
23
|
+
export interface Feast {
|
|
24
|
+
id: string;
|
|
25
|
+
nomen: string;
|
|
26
|
+
ritus: string;
|
|
27
|
+
grade: Grade;
|
|
28
|
+
season: Season;
|
|
29
|
+
tempus: string;
|
|
30
|
+
seasonStart: Date;
|
|
31
|
+
seasonEnd: Date;
|
|
32
|
+
date: Date;
|
|
33
|
+
weekday: number;
|
|
34
|
+
masses: number[];
|
|
35
|
+
marian: boolean;
|
|
36
|
+
apostolic: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface FeastQuery {
|
|
39
|
+
date?: Date;
|
|
40
|
+
from?: Date;
|
|
41
|
+
to?: Date;
|
|
42
|
+
nomen?: string;
|
|
43
|
+
season?: Season;
|
|
44
|
+
grade?: Grade;
|
|
45
|
+
marian?: boolean;
|
|
46
|
+
apostolic?: boolean;
|
|
47
|
+
}
|
|
48
|
+
export interface Pascha {
|
|
49
|
+
year: number;
|
|
50
|
+
septuagesima: Date;
|
|
51
|
+
ashWednesday: Date;
|
|
52
|
+
firstLentSunday: Date;
|
|
53
|
+
palmSunday: Date;
|
|
54
|
+
goodFriday: Date;
|
|
55
|
+
easter: Date;
|
|
56
|
+
ascension: Date;
|
|
57
|
+
pentecost: Date;
|
|
58
|
+
trinitySunday: Date;
|
|
59
|
+
corpusChristi: Date;
|
|
60
|
+
adventFirstSunday: Date;
|
|
61
|
+
gaudete: Date;
|
|
62
|
+
christmas: Date;
|
|
63
|
+
epiphany: Date;
|
|
64
|
+
baptism: Date;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// engines/cal/types — calendar types and constants
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
export const SEASON_LABELS = Object.freeze({
|
|
5
|
+
adv: "Advent",
|
|
6
|
+
nat: "Christmastide",
|
|
7
|
+
epi: "Time after Epiphany",
|
|
8
|
+
quadp: "Septuagesima",
|
|
9
|
+
quad: "Lent",
|
|
10
|
+
pasc: "Paschaltide",
|
|
11
|
+
pent: "Time after Pentecost",
|
|
12
|
+
});
|
|
13
|
+
// Authentic Latin season names (the books' own headings). Feast.tempus
|
|
14
|
+
// carries these; SEASON_LABELS above is the English reference map.
|
|
15
|
+
export const TEMPUS_NAMES = Object.freeze({
|
|
16
|
+
adv: "Tempus Adventus",
|
|
17
|
+
nat: "Tempus Nativitatis",
|
|
18
|
+
epi: "Tempus post Epiphaniam",
|
|
19
|
+
quadp: "Tempus Septuagesimæ",
|
|
20
|
+
quad: "Tempus Quadragesimæ",
|
|
21
|
+
pasc: "Tempus Paschale",
|
|
22
|
+
pent: "Tempus post Pentecosten",
|
|
23
|
+
});
|
|
24
|
+
// Penitential seasons: Gloria and (in quadp/quad) Alleluia are suppressed.
|
|
25
|
+
export const PENITENTIAL_SEASONS = new Set([
|
|
26
|
+
"adv",
|
|
27
|
+
"quadp",
|
|
28
|
+
"quad",
|
|
29
|
+
]);
|
|
30
|
+
// High → low precedence. Position in this array IS the dignity — earlier
|
|
31
|
+
// outranks later. There is no separate numeric rank field; precedence is read
|
|
32
|
+
// from this order via gradeOrder / compareGrade.
|
|
33
|
+
export const GRADE_ORDER = [
|
|
34
|
+
"triduum",
|
|
35
|
+
"duplex-i",
|
|
36
|
+
"duplex-majus-i",
|
|
37
|
+
"semiduplex-i",
|
|
38
|
+
"feria-privilegiata",
|
|
39
|
+
"duplex-ii",
|
|
40
|
+
"semiduplex-ii",
|
|
41
|
+
"duplex-majus",
|
|
42
|
+
"duplex",
|
|
43
|
+
"semiduplex",
|
|
44
|
+
"simplex",
|
|
45
|
+
"feria-major",
|
|
46
|
+
"vigilia",
|
|
47
|
+
"feria",
|
|
48
|
+
];
|
|
49
|
+
// Canonical Latin name per grade (display reference map, like SEASON_LABELS).
|
|
50
|
+
// Feast objects don't carry this — `ritus` is the on-object Latin carrier;
|
|
51
|
+
// use this map when the canonical grade name is wanted instead (it differs
|
|
52
|
+
// from ritus only for octave compounds, the Triduum, and the privileged-
|
|
53
|
+
// Sunday overrides).
|
|
54
|
+
export const GRADE_NAMES = Object.freeze({
|
|
55
|
+
triduum: "Triduum Sacrum",
|
|
56
|
+
"duplex-i": "Duplex I classis",
|
|
57
|
+
"duplex-majus-i": "Duplex majus I classis",
|
|
58
|
+
"semiduplex-i": "Semiduplex I classis",
|
|
59
|
+
"feria-privilegiata": "Feria privilegiata",
|
|
60
|
+
"duplex-ii": "Duplex II classis",
|
|
61
|
+
"semiduplex-ii": "Semiduplex II classis",
|
|
62
|
+
"duplex-majus": "Duplex majus",
|
|
63
|
+
duplex: "Duplex",
|
|
64
|
+
semiduplex: "Semiduplex",
|
|
65
|
+
simplex: "Simplex",
|
|
66
|
+
"feria-major": "Feria major",
|
|
67
|
+
vigilia: "Vigilia",
|
|
68
|
+
feria: "Feria",
|
|
69
|
+
});
|
|
70
|
+
// Exact reduction of every ritus string the extractor produces to its
|
|
71
|
+
// canonical Grade. Compounds ("… cum Octava …") reduce to their base
|
|
72
|
+
// grade; the Triduum's privileged-feria ritus reduces to `triduum`.
|
|
73
|
+
export const RITUS_TO_GRADE = Object.freeze({
|
|
74
|
+
"Feria privilegiata Duplex I classis": "triduum",
|
|
75
|
+
"Duplex I classis": "duplex-i",
|
|
76
|
+
"Duplex I classis cum Octava communi": "duplex-i",
|
|
77
|
+
"Duplex I classis cum Octava privilegiata I ordinis": "duplex-i",
|
|
78
|
+
"Duplex I classis cum Octava privilegiata II ordinis": "duplex-i",
|
|
79
|
+
"Duplex I classis cum Octava privilegiata III ordinis": "duplex-i",
|
|
80
|
+
"Duplex majus I classis": "duplex-majus-i",
|
|
81
|
+
"Semiduplex I classis": "semiduplex-i",
|
|
82
|
+
"Feria privilegiata": "feria-privilegiata",
|
|
83
|
+
"Duplex II classis": "duplex-ii",
|
|
84
|
+
"Duplex II classis cum Octava simplici": "duplex-ii",
|
|
85
|
+
"Semiduplex II classis": "semiduplex-ii",
|
|
86
|
+
"Duplex majus": "duplex-majus",
|
|
87
|
+
Duplex: "duplex",
|
|
88
|
+
Semiduplex: "semiduplex",
|
|
89
|
+
Simplex: "simplex",
|
|
90
|
+
"Feria major": "feria-major",
|
|
91
|
+
Vigilia: "vigilia",
|
|
92
|
+
Feria: "feria",
|
|
93
|
+
});
|
|
94
|
+
// Ordered, most-specific-first fallback matcher for ritus strings not in the
|
|
95
|
+
// exact table above (future data). Longer patterns first so "Feria
|
|
96
|
+
// privilegiata Duplex I classis" wins over "Feria privilegiata", and
|
|
97
|
+
// "Duplex I classis" over "Duplex".
|
|
98
|
+
const RITUS_PATTERNS = [
|
|
99
|
+
["Feria privilegiata Duplex I classis", "triduum"],
|
|
100
|
+
["Duplex majus I classis", "duplex-majus-i"],
|
|
101
|
+
["Duplex I classis", "duplex-i"],
|
|
102
|
+
["Duplex II classis", "duplex-ii"],
|
|
103
|
+
["Semiduplex I classis", "semiduplex-i"],
|
|
104
|
+
["Semiduplex II classis", "semiduplex-ii"],
|
|
105
|
+
["Feria privilegiata", "feria-privilegiata"],
|
|
106
|
+
["Duplex majus", "duplex-majus"],
|
|
107
|
+
["Semiduplex", "semiduplex"],
|
|
108
|
+
["Duplex", "duplex"],
|
|
109
|
+
["Simplex", "simplex"],
|
|
110
|
+
["Feria major", "feria-major"],
|
|
111
|
+
["Vigilia", "vigilia"],
|
|
112
|
+
["Feria", "feria"],
|
|
113
|
+
];
|
|
114
|
+
/** Reduce a Tridentine ritus string to its canonical Grade. */
|
|
115
|
+
export function ritusToGrade(ritus) {
|
|
116
|
+
const exact = RITUS_TO_GRADE[ritus];
|
|
117
|
+
if (exact)
|
|
118
|
+
return exact;
|
|
119
|
+
for (const [pattern, grade] of RITUS_PATTERNS) {
|
|
120
|
+
if (ritus.includes(pattern))
|
|
121
|
+
return grade;
|
|
122
|
+
}
|
|
123
|
+
return "feria"; // last resort; extractor coverage is 100%, so unreached
|
|
124
|
+
}
|
|
125
|
+
// ── Privileged Sundays (per-id dignity overrides) ──
|
|
126
|
+
// DO's Tridentine ritus line under-specifies four privileged Sundays as plain
|
|
127
|
+
// "Semiduplex"; their precedence lived only in DO's numeric rank, which tonus
|
|
128
|
+
// does not use. Historically Advent I is a first-class Sunday (yields to
|
|
129
|
+
// nothing) and the Septuagesima-block Sundays are second-class (yield only to
|
|
130
|
+
// first- and second-class feasts) — the same Sunday classes DO itself encodes
|
|
131
|
+
// for Lent ("Semiduplex I classis") and late Advent ("Semiduplex II classis").
|
|
132
|
+
// `ritus` stays verbatim; only the derived grade is lifted.
|
|
133
|
+
export const PRIVILEGED_SUNDAYS = Object.freeze({
|
|
134
|
+
"Adv1-0": "semiduplex-i", // Dominica I Adventus
|
|
135
|
+
"Quadp1-0": "semiduplex-ii", // Dominica in Septuagesima
|
|
136
|
+
"Quadp2-0": "semiduplex-ii", // Dominica in Sexagesima
|
|
137
|
+
"Quadp3-0": "semiduplex-ii", // Dominica in Quinquagesima
|
|
138
|
+
});
|
|
139
|
+
/**
|
|
140
|
+
* Grade for a calendar entry: the per-id privileged-Sunday override when
|
|
141
|
+
* one exists, otherwise the ritus reduction.
|
|
142
|
+
*/
|
|
143
|
+
export function entryGrade(id, ritus) {
|
|
144
|
+
return (id !== undefined ? PRIVILEGED_SUNDAYS[id] : undefined) ?? ritusToGrade(ritus);
|
|
145
|
+
}
|
|
146
|
+
/** Precedence index (0 = highest). Use for sorting and comparison. */
|
|
147
|
+
export function gradeOrder(grade) {
|
|
148
|
+
return GRADE_ORDER.indexOf(grade);
|
|
149
|
+
}
|
|
150
|
+
/** Sort comparator: earlier in GRADE_ORDER (higher dignity) sorts first. */
|
|
151
|
+
export function compareGrade(a, b) {
|
|
152
|
+
return gradeOrder(a) - gradeOrder(b);
|
|
153
|
+
}
|
|
154
|
+
// ── BVM / Apostolic feast sets (keyed by DO stem: MM-DD for Sancti) ──
|
|
155
|
+
export const BVM_FEAST_IDS = new Set([
|
|
156
|
+
"12-08", // Immaculate Conception
|
|
157
|
+
"03-19", // St. Joseph
|
|
158
|
+
"05-31", // Queenship of Mary
|
|
159
|
+
"07-02", // Visitation
|
|
160
|
+
"07-26", // St. Anne
|
|
161
|
+
"08-05", // Our Lady of the Snows
|
|
162
|
+
"08-14", // Vigil of the Assumption
|
|
163
|
+
"08-15", // Assumption
|
|
164
|
+
"08-16", // St. Joachim
|
|
165
|
+
"08-22", // Immaculate Heart
|
|
166
|
+
"09-08", // Nativity of Mary
|
|
167
|
+
"09-12", // Most Holy Name of Mary
|
|
168
|
+
"09-15", // Seven Sorrows
|
|
169
|
+
"10-11", // Motherhood of Mary
|
|
170
|
+
]);
|
|
171
|
+
export const APOSTOLIC_FEAST_IDS = new Set([
|
|
172
|
+
"11-30", // St. Andrew
|
|
173
|
+
"12-21", // St. Thomas
|
|
174
|
+
"01-25", // Conversion of St. Paul
|
|
175
|
+
"02-24", // St. Matthias
|
|
176
|
+
"05-11", // SS. Philip and James
|
|
177
|
+
"06-11", // St. Barnabas
|
|
178
|
+
"06-28", // Vigil of SS. Peter and Paul
|
|
179
|
+
"06-29", // SS. Peter and Paul
|
|
180
|
+
"06-30", // St. Paul
|
|
181
|
+
"07-25", // St. James
|
|
182
|
+
"08-24", // St. Bartholomew
|
|
183
|
+
"09-21", // St. Matthew
|
|
184
|
+
"10-18", // St. Luke
|
|
185
|
+
"10-28", // SS. Simon and Jude
|
|
186
|
+
"04-25", // St. Mark
|
|
187
|
+
"12-27", // St. John
|
|
188
|
+
]);
|
|
189
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Chant, CantusQuery } from "./types.js";
|
|
2
|
+
export declare function resolveChant(id: string | null): Chant | null;
|
|
3
|
+
export declare function resolveChants(ids: string[]): Chant[];
|
|
4
|
+
/**
|
|
5
|
+
* Cross-corpus chant retrieval (`tonus.cantus`) over GR, LA, LH, and LU.
|
|
6
|
+
* A `gabc` field bypasses the corpus and returns a single user
|
|
7
|
+
* chant parsed from raw GABC (body or full file with headers).
|
|
8
|
+
*/
|
|
9
|
+
export declare function getChants(query?: CantusQuery): Chant[];
|
|
10
|
+
//# sourceMappingURL=chant.d.ts.map
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { OFFICE_LABELS, MODE_LABELS } from "./types.js";
|
|
2
|
+
import { GR_DATA, GR_SOURCE } from "../../data/gr.js";
|
|
3
|
+
import { LU_DATA, LU_SOURCE } from "../../data/lu.js";
|
|
4
|
+
import { LA_DATA, LA_SOURCE } from "../../data/la.js";
|
|
5
|
+
import { LH_DATA, LH_SOURCE } from "../../data/lh.js";
|
|
6
|
+
function modusOf(mode) {
|
|
7
|
+
return mode != null ? (MODE_LABELS[mode] ?? null) : null;
|
|
8
|
+
}
|
|
9
|
+
const HEADER_FIELD_REGEX = /([A-Za-z0-9_-]+)\s*:\s*([^;]*);/g;
|
|
10
|
+
function chantFromGABC(query) {
|
|
11
|
+
const raw = query.gabc ?? "";
|
|
12
|
+
const markerIndex = raw.indexOf("%%");
|
|
13
|
+
let body;
|
|
14
|
+
let name = null;
|
|
15
|
+
let headerMode = null;
|
|
16
|
+
let officePart = null;
|
|
17
|
+
if (markerIndex >= 0) {
|
|
18
|
+
const headerBlock = raw.slice(0, markerIndex);
|
|
19
|
+
body = raw.slice(markerIndex + 2).trim();
|
|
20
|
+
HEADER_FIELD_REGEX.lastIndex = 0;
|
|
21
|
+
let match;
|
|
22
|
+
while ((match = HEADER_FIELD_REGEX.exec(headerBlock)) !== null) {
|
|
23
|
+
const key = match[1].trim().toLowerCase();
|
|
24
|
+
const value = match[2].trim();
|
|
25
|
+
if (key === "name")
|
|
26
|
+
name = value;
|
|
27
|
+
else if (key === "mode")
|
|
28
|
+
headerMode = value;
|
|
29
|
+
else if (key === "office-part")
|
|
30
|
+
officePart = value;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
body = raw.trim();
|
|
35
|
+
}
|
|
36
|
+
const incipit = query.incipit ?? name ?? "";
|
|
37
|
+
const mode = query.mode != null ? String(query.mode) : headerMode;
|
|
38
|
+
const office = (query.office
|
|
39
|
+
? (Array.isArray(query.office) ? query.office[0] : query.office)
|
|
40
|
+
: officePart ?? "or");
|
|
41
|
+
return [{
|
|
42
|
+
id: `gabc:${incipit.toLowerCase().replace(/\s+/g, "_") || "untitled"}`,
|
|
43
|
+
incipit,
|
|
44
|
+
gabc: body,
|
|
45
|
+
office,
|
|
46
|
+
genus: OFFICE_LABELS[office] ?? office,
|
|
47
|
+
mode: mode ?? null,
|
|
48
|
+
modus: modusOf(mode ?? null),
|
|
49
|
+
pages: [],
|
|
50
|
+
source: { book: "User", year: null, editor: null, code: "user" },
|
|
51
|
+
}];
|
|
52
|
+
}
|
|
53
|
+
function withLabels(c, source) {
|
|
54
|
+
return {
|
|
55
|
+
...c,
|
|
56
|
+
source,
|
|
57
|
+
genus: OFFICE_LABELS[c.office] ?? c.office,
|
|
58
|
+
modus: modusOf(c.mode),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const CORPUS = [
|
|
62
|
+
...GR_DATA.map((c) => withLabels(c, GR_SOURCE)),
|
|
63
|
+
...LU_DATA.map((c) => withLabels(c, LU_SOURCE)),
|
|
64
|
+
...LA_DATA.map((c) => withLabels(c, LA_SOURCE)),
|
|
65
|
+
...LH_DATA.map((c) => withLabels(c, LH_SOURCE)),
|
|
66
|
+
];
|
|
67
|
+
let _byId = null;
|
|
68
|
+
function byId() {
|
|
69
|
+
if (!_byId)
|
|
70
|
+
_byId = new Map(CORPUS.map((c) => [c.id, c]));
|
|
71
|
+
return _byId;
|
|
72
|
+
}
|
|
73
|
+
function toArray(v) {
|
|
74
|
+
if (v === undefined)
|
|
75
|
+
return undefined;
|
|
76
|
+
return Array.isArray(v) ? v : [v];
|
|
77
|
+
}
|
|
78
|
+
export function resolveChant(id) {
|
|
79
|
+
if (!id)
|
|
80
|
+
return null;
|
|
81
|
+
return byId().get(id) ?? null;
|
|
82
|
+
}
|
|
83
|
+
export function resolveChants(ids) {
|
|
84
|
+
return ids.map(resolveChant).filter((c) => c !== null);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Cross-corpus chant retrieval (`tonus.cantus`) over GR, LA, LH, and LU.
|
|
88
|
+
* A `gabc` field bypasses the corpus and returns a single user
|
|
89
|
+
* chant parsed from raw GABC (body or full file with headers).
|
|
90
|
+
*/
|
|
91
|
+
export function getChants(query) {
|
|
92
|
+
if (!query || Object.keys(query).length === 0)
|
|
93
|
+
return [];
|
|
94
|
+
if (query.gabc)
|
|
95
|
+
return chantFromGABC(query);
|
|
96
|
+
const ids = toArray(query.id);
|
|
97
|
+
if (ids) {
|
|
98
|
+
const map = byId();
|
|
99
|
+
const found = ids.map((id) => map.get(id)).filter((c) => !!c);
|
|
100
|
+
return found;
|
|
101
|
+
}
|
|
102
|
+
let out = CORPUS;
|
|
103
|
+
const sources = toArray(query.source);
|
|
104
|
+
if (sources) {
|
|
105
|
+
const set = new Set(sources);
|
|
106
|
+
out = out.filter((c) => c.source.code != null && set.has(c.source.code));
|
|
107
|
+
}
|
|
108
|
+
const offices = toArray(query.office);
|
|
109
|
+
if (offices) {
|
|
110
|
+
const set = new Set(offices);
|
|
111
|
+
out = out.filter((c) => set.has(c.office));
|
|
112
|
+
}
|
|
113
|
+
const modes = toArray(query.mode);
|
|
114
|
+
if (modes) {
|
|
115
|
+
const set = new Set(modes.map(String));
|
|
116
|
+
out = out.filter((c) => c.mode != null && set.has(c.mode));
|
|
117
|
+
}
|
|
118
|
+
if (query.incipit) {
|
|
119
|
+
const needle = query.incipit.toLowerCase();
|
|
120
|
+
out = out.filter((c) => c.incipit.toLowerCase().includes(needle));
|
|
121
|
+
}
|
|
122
|
+
const sort = query.sort ?? "incipit";
|
|
123
|
+
const sorted = [...out].sort((a, b) => {
|
|
124
|
+
if (sort === "id")
|
|
125
|
+
return a.id.localeCompare(b.id);
|
|
126
|
+
if (sort === "mode")
|
|
127
|
+
return (String(a.mode ?? "").localeCompare(String(b.mode ?? "")) ||
|
|
128
|
+
a.incipit.localeCompare(b.incipit));
|
|
129
|
+
return a.incipit.localeCompare(b.incipit);
|
|
130
|
+
});
|
|
131
|
+
const offset = Math.max(0, query.offset ?? 0);
|
|
132
|
+
const limit = query.limit == null ? sorted.length : Math.max(0, query.limit);
|
|
133
|
+
return sorted.slice(offset, offset + limit);
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=chant.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Chant, OfficiumQuery } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Divine Office retrieval (`tonus.officium`) for a canonical hour
|
|
4
|
+
* (matutinum … completorium). Without an hour, returns chants for all
|
|
5
|
+
* available hours; a feast acts as a filter.
|
|
6
|
+
*/
|
|
7
|
+
export declare function getHour(query?: OfficiumQuery): Chant[];
|
|
8
|
+
//# sourceMappingURL=hour.d.ts.map
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// engines/chant/hour — Divine Office hour retrieval
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
import { resolveChant, resolveChants } from "./chant.js";
|
|
5
|
+
import { temporaSundayId } from "../cal/date.js";
|
|
6
|
+
import { OFFICE_ROMAN } from "../../data/office-roman.js";
|
|
7
|
+
let _roman = null;
|
|
8
|
+
function romanMap() {
|
|
9
|
+
if (!_roman)
|
|
10
|
+
_roman = new Map(OFFICE_ROMAN.map((d) => [d.feastId, d]));
|
|
11
|
+
return _roman;
|
|
12
|
+
}
|
|
13
|
+
function chantsForFeastHour(feast, hour) {
|
|
14
|
+
const map = romanMap();
|
|
15
|
+
const sunday = temporaSundayId(feast.id);
|
|
16
|
+
const day = map.get(feast.id) ?? (sunday ? (map.get(sunday) ?? null) : null);
|
|
17
|
+
if (!day)
|
|
18
|
+
return [];
|
|
19
|
+
const results = [];
|
|
20
|
+
if (hour === "matutinum") {
|
|
21
|
+
const inv = resolveChant(day.invit);
|
|
22
|
+
if (inv)
|
|
23
|
+
results.push(inv);
|
|
24
|
+
results.push(...resolveChants(day.antMatutinum));
|
|
25
|
+
const hy = resolveChant(day.hymnMatutinum);
|
|
26
|
+
if (hy)
|
|
27
|
+
results.push(hy);
|
|
28
|
+
results.push(...resolveChants(day.respMatutinum));
|
|
29
|
+
}
|
|
30
|
+
else if (hour === "laudes") {
|
|
31
|
+
results.push(...resolveChants(day.antLaudes));
|
|
32
|
+
const bc = resolveChant(day.antBenedictus);
|
|
33
|
+
if (bc)
|
|
34
|
+
results.push(bc);
|
|
35
|
+
const hy = resolveChant(day.hymnLaudes);
|
|
36
|
+
if (hy)
|
|
37
|
+
results.push(hy);
|
|
38
|
+
}
|
|
39
|
+
else if (hour === "tertia") {
|
|
40
|
+
const rb = resolveChant(day.respBreveTertia);
|
|
41
|
+
if (rb)
|
|
42
|
+
results.push(rb);
|
|
43
|
+
}
|
|
44
|
+
else if (hour === "sexta") {
|
|
45
|
+
const rb = resolveChant(day.respBreveSexta);
|
|
46
|
+
if (rb)
|
|
47
|
+
results.push(rb);
|
|
48
|
+
}
|
|
49
|
+
else if (hour === "nona") {
|
|
50
|
+
const rb = resolveChant(day.respBreveNona);
|
|
51
|
+
if (rb)
|
|
52
|
+
results.push(rb);
|
|
53
|
+
}
|
|
54
|
+
else if (hour === "vesperae") {
|
|
55
|
+
results.push(...resolveChants(day.antVespera));
|
|
56
|
+
const mc = resolveChant(day.antMagnificat);
|
|
57
|
+
if (mc)
|
|
58
|
+
results.push(mc);
|
|
59
|
+
const hy = resolveChant(day.hymnVespera);
|
|
60
|
+
if (hy)
|
|
61
|
+
results.push(hy);
|
|
62
|
+
}
|
|
63
|
+
return results;
|
|
64
|
+
}
|
|
65
|
+
function toArray(v) {
|
|
66
|
+
if (v === undefined)
|
|
67
|
+
return undefined;
|
|
68
|
+
return Array.isArray(v) ? v : [v];
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Divine Office retrieval (`tonus.officium`) for a canonical hour
|
|
72
|
+
* (matutinum … completorium). Without an hour, returns chants for all
|
|
73
|
+
* available hours; a feast acts as a filter.
|
|
74
|
+
*/
|
|
75
|
+
export function getHour(query) {
|
|
76
|
+
if (!query || Object.keys(query).length === 0)
|
|
77
|
+
return [];
|
|
78
|
+
const feasts = toArray(query.feast);
|
|
79
|
+
const hour = query.hora;
|
|
80
|
+
let results;
|
|
81
|
+
if (feasts && hour) {
|
|
82
|
+
results = feasts.flatMap((f) => chantsForFeastHour(f, hour));
|
|
83
|
+
}
|
|
84
|
+
else if (feasts) {
|
|
85
|
+
const hours = ["matutinum", "laudes", "tertia", "sexta", "nona", "vesperae"];
|
|
86
|
+
results = feasts.flatMap((f) => hours.flatMap((h) => chantsForFeastHour(f, h)));
|
|
87
|
+
}
|
|
88
|
+
else if (hour) {
|
|
89
|
+
// Hour without feast — scan all office entries
|
|
90
|
+
results = OFFICE_ROMAN.flatMap((day) => {
|
|
91
|
+
const mockFeast = { id: day.feastId };
|
|
92
|
+
return chantsForFeastHour(mockFeast, hour);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
// Apply CantusQuery filters
|
|
99
|
+
const offices = toArray(query.office);
|
|
100
|
+
if (offices) {
|
|
101
|
+
const set = new Set(offices);
|
|
102
|
+
results = results.filter((c) => set.has(c.office));
|
|
103
|
+
}
|
|
104
|
+
const modes = toArray(query.mode);
|
|
105
|
+
if (modes) {
|
|
106
|
+
const set = new Set(modes.map(String));
|
|
107
|
+
results = results.filter((c) => c.mode != null && set.has(c.mode));
|
|
108
|
+
}
|
|
109
|
+
const sources = toArray(query.source);
|
|
110
|
+
if (sources) {
|
|
111
|
+
const set = new Set(sources);
|
|
112
|
+
results = results.filter((c) => c.source.code != null && set.has(c.source.code));
|
|
113
|
+
}
|
|
114
|
+
if (query.incipit) {
|
|
115
|
+
const needle = query.incipit.toLowerCase();
|
|
116
|
+
results = results.filter((c) => c.incipit.toLowerCase().includes(needle));
|
|
117
|
+
}
|
|
118
|
+
if (query.id) {
|
|
119
|
+
const ids = new Set(toArray(query.id));
|
|
120
|
+
results = results.filter((c) => ids.has(c.id));
|
|
121
|
+
}
|
|
122
|
+
const sort = query.sort ?? "incipit";
|
|
123
|
+
results.sort((a, b) => {
|
|
124
|
+
if (sort === "id")
|
|
125
|
+
return a.id.localeCompare(b.id);
|
|
126
|
+
if (sort === "mode")
|
|
127
|
+
return (String(a.mode ?? "").localeCompare(String(b.mode ?? "")) ||
|
|
128
|
+
a.incipit.localeCompare(b.incipit));
|
|
129
|
+
return a.incipit.localeCompare(b.incipit);
|
|
130
|
+
});
|
|
131
|
+
const offset = Math.max(0, query.offset ?? 0);
|
|
132
|
+
const limit = query.limit == null ? results.length : Math.max(0, query.limit);
|
|
133
|
+
return results.slice(offset, offset + limit);
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=hour.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PsalmVerse } from "./types.js";
|
|
2
|
+
export interface IntoneOpts {
|
|
3
|
+
mode?: number;
|
|
4
|
+
differentia?: string;
|
|
5
|
+
intonation?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function intone(text: string | PsalmVerse, opts?: IntoneOpts): string;
|
|
8
|
+
//# sourceMappingURL=intone.d.ts.map
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// engines/chant/intone — GABC generation for psalm verses sung to psalm tones
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
import { syllabifyPhrase } from "./syllabify.js";
|
|
5
|
+
import { getTone, getDifferentia } from "../../data/tones.js";
|
|
6
|
+
import { midiToGabc } from "../temper/gabc.js";
|
|
7
|
+
const DEFAULT_CLEF = "c4";
|
|
8
|
+
const CLEF = `(${DEFAULT_CLEF}) `;
|
|
9
|
+
function midiListToLetters(midis, clef) {
|
|
10
|
+
return midis.map((m) => midiToGabc(m, clef));
|
|
11
|
+
}
|
|
12
|
+
function buildHalf(syllables, tenor, cadenceNotes, prefixNotes) {
|
|
13
|
+
if (syllables.length === 0)
|
|
14
|
+
return "";
|
|
15
|
+
const nonSpace = syllables.filter((s) => s !== " ");
|
|
16
|
+
let noteAssignment;
|
|
17
|
+
if (prefixNotes && prefixNotes.length > 0) {
|
|
18
|
+
noteAssignment = [];
|
|
19
|
+
const total = nonSpace.length;
|
|
20
|
+
const pfxLen = Math.min(prefixNotes.length, total);
|
|
21
|
+
const cadLen = Math.min(cadenceNotes.length, total - pfxLen);
|
|
22
|
+
const midLen = total - pfxLen - cadLen;
|
|
23
|
+
for (let i = 0; i < pfxLen; i++)
|
|
24
|
+
noteAssignment.push(prefixNotes[i]);
|
|
25
|
+
for (let i = 0; i < midLen; i++)
|
|
26
|
+
noteAssignment.push(tenor);
|
|
27
|
+
for (let i = 0; i < cadLen; i++)
|
|
28
|
+
noteAssignment.push(cadenceNotes[i]);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
noteAssignment = [];
|
|
32
|
+
const total = nonSpace.length;
|
|
33
|
+
const cadLen = Math.min(cadenceNotes.length, total);
|
|
34
|
+
const midLen = total - cadLen;
|
|
35
|
+
for (let i = 0; i < midLen; i++)
|
|
36
|
+
noteAssignment.push(tenor);
|
|
37
|
+
for (let i = 0; i < cadLen; i++)
|
|
38
|
+
noteAssignment.push(cadenceNotes[i]);
|
|
39
|
+
}
|
|
40
|
+
let noteIdx = 0;
|
|
41
|
+
const tokens = [];
|
|
42
|
+
for (const syl of syllables) {
|
|
43
|
+
if (syl === " ") {
|
|
44
|
+
tokens.push(" ");
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
const note = noteAssignment[noteIdx++] ?? tenor;
|
|
48
|
+
tokens.push(`(${note})${syl}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return tokens.join("");
|
|
52
|
+
}
|
|
53
|
+
export function intone(text, opts = {}) {
|
|
54
|
+
let rawText;
|
|
55
|
+
if (typeof text === "object") {
|
|
56
|
+
rawText = `${text.half1} * ${text.half2}`;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
rawText = text;
|
|
60
|
+
}
|
|
61
|
+
const clef = DEFAULT_CLEF;
|
|
62
|
+
const mode = opts.mode ?? 8;
|
|
63
|
+
const tone = getTone(mode);
|
|
64
|
+
const diff = getDifferentia(tone, opts.differentia);
|
|
65
|
+
const tenorLetter = midiToGabc(tone.tenor, clef);
|
|
66
|
+
const mediantLetters = midiListToLetters(tone.mediant, clef);
|
|
67
|
+
const terminationLetters = midiListToLetters(diff.termination, clef);
|
|
68
|
+
const intonationLetters = midiListToLetters(tone.intonation, clef);
|
|
69
|
+
const starIdx = rawText.indexOf(" * ");
|
|
70
|
+
if (starIdx === -1) {
|
|
71
|
+
const syllables = syllabifyPhrase(rawText.trim());
|
|
72
|
+
const body = buildHalf(syllables, tenorLetter, terminationLetters);
|
|
73
|
+
return CLEF + body + "(::)";
|
|
74
|
+
}
|
|
75
|
+
const half1Text = rawText.slice(0, starIdx).trim();
|
|
76
|
+
const half2Text = rawText.slice(starIdx + 3).trim();
|
|
77
|
+
const half1Sylls = syllabifyPhrase(half1Text);
|
|
78
|
+
const half2Sylls = syllabifyPhrase(half2Text);
|
|
79
|
+
const useIntonation = opts.intonation !== false;
|
|
80
|
+
const h1 = buildHalf(half1Sylls, tenorLetter, mediantLetters, useIntonation ? intonationLetters : undefined);
|
|
81
|
+
const h2 = buildHalf(half2Sylls, tenorLetter, terminationLetters);
|
|
82
|
+
return `${CLEF}${h1}(:) ${h2}(::)`;
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=intone.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type OrdinaryChant, type OrdinariumQuery } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Mass ordinary retrieval (`tonus.ordinarium`) from the Kyriale. A feast
|
|
4
|
+
* drives mass selection; `mass` pins a kyriale number directly.
|
|
5
|
+
*/
|
|
6
|
+
export declare function getOrdinary(query?: OrdinariumQuery): OrdinaryChant[];
|
|
7
|
+
//# sourceMappingURL=ordinary.d.ts.map
|