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.
Files changed (141) hide show
  1. package/BIBLIOGRAPHY.md +99 -0
  2. package/LICENSE +29 -0
  3. package/README.md +108 -0
  4. package/dist/data/cal.d.ts +10 -0
  5. package/dist/data/cal.js +3862 -0
  6. package/dist/data/commune.d.ts +17 -0
  7. package/dist/data/commune.js +1333 -0
  8. package/dist/data/gr.d.ts +5 -0
  9. package/dist/data/gr.js +13449 -0
  10. package/dist/data/kyriale.d.ts +11 -0
  11. package/dist/data/kyriale.js +971 -0
  12. package/dist/data/la.d.ts +5 -0
  13. package/dist/data/la.js +14229 -0
  14. package/dist/data/lh.d.ts +5 -0
  15. package/dist/data/lh.js +3619 -0
  16. package/dist/data/lu.d.ts +5 -0
  17. package/dist/data/lu.js +23779 -0
  18. package/dist/data/masses.d.ts +18 -0
  19. package/dist/data/masses.js +297 -0
  20. package/dist/data/office-roman.d.ts +19 -0
  21. package/dist/data/office-roman.js +13792 -0
  22. package/dist/data/office.d.ts +12 -0
  23. package/dist/data/office.js +13052 -0
  24. package/dist/data/propers.d.ts +13 -0
  25. package/dist/data/propers.js +7584 -0
  26. package/dist/data/psalms.d.ts +4 -0
  27. package/dist/data/psalms.js +10 -0
  28. package/dist/data/psalms.json +22918 -0
  29. package/dist/data/tones.d.ts +20 -0
  30. package/dist/data/tones.js +153 -0
  31. package/dist/data/types.d.ts +3 -0
  32. package/dist/data/types.js +2 -0
  33. package/dist/engines/cal/calendar.d.ts +21 -0
  34. package/dist/engines/cal/calendar.js +265 -0
  35. package/dist/engines/cal/date.d.ts +31 -0
  36. package/dist/engines/cal/date.js +141 -0
  37. package/dist/engines/cal/types.d.ts +66 -0
  38. package/dist/engines/cal/types.js +189 -0
  39. package/dist/engines/chant/chant.d.ts +10 -0
  40. package/dist/engines/chant/chant.js +135 -0
  41. package/dist/engines/chant/hour.d.ts +8 -0
  42. package/dist/engines/chant/hour.js +135 -0
  43. package/dist/engines/chant/intone.d.ts +8 -0
  44. package/dist/engines/chant/intone.js +84 -0
  45. package/dist/engines/chant/ordinary.d.ts +7 -0
  46. package/dist/engines/chant/ordinary.js +232 -0
  47. package/dist/engines/chant/propers.d.ts +8 -0
  48. package/dist/engines/chant/propers.js +107 -0
  49. package/dist/engines/chant/psalm.d.ts +7 -0
  50. package/dist/engines/chant/psalm.js +60 -0
  51. package/dist/engines/chant/syllabify.d.ts +20 -0
  52. package/dist/engines/chant/syllabify.js +192 -0
  53. package/dist/engines/chant/types.d.ts +76 -0
  54. package/dist/engines/chant/types.js +34 -0
  55. package/dist/engines/epoch.d.ts +2 -0
  56. package/dist/engines/epoch.js +14 -0
  57. package/dist/engines/harmonia/api.d.ts +35 -0
  58. package/dist/engines/harmonia/api.js +90 -0
  59. package/dist/engines/harmonia/aspects.d.ts +8 -0
  60. package/dist/engines/harmonia/aspects.js +15 -0
  61. package/dist/engines/harmonia/data/doctrines.d.ts +16 -0
  62. package/dist/engines/harmonia/data/doctrines.js +154 -0
  63. package/dist/engines/harmonia/data/vowels.d.ts +10 -0
  64. package/dist/engines/harmonia/data/vowels.js +21 -0
  65. package/dist/engines/harmonia/presence.d.ts +13 -0
  66. package/dist/engines/harmonia/presence.js +48 -0
  67. package/dist/engines/harmonia/tabula.d.ts +28 -0
  68. package/dist/engines/harmonia/tabula.js +32 -0
  69. package/dist/engines/harmonia/voice.d.ts +19 -0
  70. package/dist/engines/harmonia/voice.js +51 -0
  71. package/dist/engines/imprint.d.ts +30 -0
  72. package/dist/engines/imprint.js +152 -0
  73. package/dist/engines/planet/appearance.d.ts +40 -0
  74. package/dist/engines/planet/appearance.js +84 -0
  75. package/dist/engines/planet/aspects.d.ts +5 -0
  76. package/dist/engines/planet/aspects.js +41 -0
  77. package/dist/engines/planet/math.d.ts +13 -0
  78. package/dist/engines/planet/math.js +56 -0
  79. package/dist/engines/planet/orbital.d.ts +25 -0
  80. package/dist/engines/planet/orbital.js +223 -0
  81. package/dist/engines/planet/planet.d.ts +13 -0
  82. package/dist/engines/planet/planet.js +198 -0
  83. package/dist/engines/planet/position.d.ts +62 -0
  84. package/dist/engines/planet/position.js +156 -0
  85. package/dist/engines/planet/types.d.ts +61 -0
  86. package/dist/engines/planet/types.js +14 -0
  87. package/dist/engines/score/api.d.ts +54 -0
  88. package/dist/engines/score/api.js +87 -0
  89. package/dist/engines/score/articulation.d.ts +6 -0
  90. package/dist/engines/score/articulation.js +112 -0
  91. package/dist/engines/score/emitters/midi.d.ts +65 -0
  92. package/dist/engines/score/emitters/midi.js +158 -0
  93. package/dist/engines/score/emitters/musicxml.d.ts +18 -0
  94. package/dist/engines/score/emitters/musicxml.js +166 -0
  95. package/dist/engines/score/infer.d.ts +4 -0
  96. package/dist/engines/score/infer.js +77 -0
  97. package/dist/engines/score/ir.d.ts +4 -0
  98. package/dist/engines/score/ir.js +177 -0
  99. package/dist/engines/score/meta.d.ts +19 -0
  100. package/dist/engines/score/meta.js +34 -0
  101. package/dist/engines/score/neume.d.ts +3 -0
  102. package/dist/engines/score/neume.js +26 -0
  103. package/dist/engines/score/parse.d.ts +3 -0
  104. package/dist/engines/score/parse.js +359 -0
  105. package/dist/engines/score/phrasing.d.ts +24 -0
  106. package/dist/engines/score/phrasing.js +257 -0
  107. package/dist/engines/score/prosody.d.ts +35 -0
  108. package/dist/engines/score/prosody.js +109 -0
  109. package/dist/engines/score/tabula.d.ts +70 -0
  110. package/dist/engines/score/tabula.js +109 -0
  111. package/dist/engines/score/types.d.ts +159 -0
  112. package/dist/engines/score/types.js +2 -0
  113. package/dist/engines/temper/api.d.ts +60 -0
  114. package/dist/engines/temper/api.js +130 -0
  115. package/dist/engines/temper/data/constants.d.ts +27 -0
  116. package/dist/engines/temper/data/constants.js +150 -0
  117. package/dist/engines/temper/data/guido.d.ts +14 -0
  118. package/dist/engines/temper/data/guido.js +29 -0
  119. package/dist/engines/temper/data/modes.d.ts +38 -0
  120. package/dist/engines/temper/data/modes.js +158 -0
  121. package/dist/engines/temper/gabc.d.ts +5 -0
  122. package/dist/engines/temper/gabc.js +53 -0
  123. package/dist/engines/temper/gamut.d.ts +9 -0
  124. package/dist/engines/temper/gamut.js +24 -0
  125. package/dist/engines/temper/guido.d.ts +16 -0
  126. package/dist/engines/temper/guido.js +48 -0
  127. package/dist/engines/temper/interval.d.ts +15 -0
  128. package/dist/engines/temper/interval.js +31 -0
  129. package/dist/engines/temper/modes.d.ts +6 -0
  130. package/dist/engines/temper/modes.js +13 -0
  131. package/dist/engines/temper/neume.d.ts +14 -0
  132. package/dist/engines/temper/neume.js +59 -0
  133. package/dist/engines/temper/pitch.d.ts +40 -0
  134. package/dist/engines/temper/pitch.js +129 -0
  135. package/dist/engines/temper/scale.d.ts +37 -0
  136. package/dist/engines/temper/scale.js +217 -0
  137. package/dist/engines/temper/step.d.ts +23 -0
  138. package/dist/engines/temper/step.js +53 -0
  139. package/dist/index.d.ts +40 -0
  140. package/dist/index.js +27 -0
  141. 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