tonus 0.11.1 → 0.13.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/CHANGELOG.md +44 -0
- package/dist/data/gabc-glyphs.d.ts +34 -2
- package/dist/data/gabc-glyphs.js +62 -5
- package/dist/data/kyriale.d.ts +4 -0
- package/dist/data/kyriale.js +147 -26
- package/dist/engines/chant/ordinary.js +39 -18
- package/dist/engines/chant/types.d.ts +7 -0
- package/dist/engines/score/emitters/svg.js +49 -6
- package/dist/engines/score/ir.js +4 -0
- package/dist/engines/score/parse.js +53 -5
- package/dist/engines/score/tabula.d.ts +9 -1
- package/dist/engines/score/tabula.js +4 -0
- package/dist/engines/score/types.d.ts +24 -1
- package/docs/api/chant.md +13 -6
- package/docs/api/score.md +12 -2
- package/package.json +1 -1
|
@@ -21,16 +21,20 @@ function resolveMasses(feast) {
|
|
|
21
21
|
return resolved;
|
|
22
22
|
return [feast.marian ? AD_LIB.bvm : AD_LIB.standard];
|
|
23
23
|
}
|
|
24
|
-
//
|
|
25
|
-
// the
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
|
|
24
|
+
// Where the Kyriale prints an entry. Only the "mass" section carries a mass
|
|
25
|
+
// number; the others are addressed by section. The appendix used to carry its
|
|
26
|
+
// Graduale page as a "mass" (Clemens Rector was mass 75, the Requiem 94, 101
|
|
27
|
+
// and 102), and the two ad libitum Kyries whose incipits carry a numeral
|
|
28
|
+
// collided with Masses VI and X.
|
|
29
|
+
const KYRIALE_SECTIONS = new Set([
|
|
30
|
+
"mass", "credo", "sprinkling", "ad-libitum", "requiem",
|
|
31
|
+
]);
|
|
32
|
+
const SPRINKLING_OFFICES = new Set(["as", "va"]);
|
|
33
|
+
// The Cantus ad libitum, which no feast's `masses` list ever names: reachable
|
|
34
|
+
// on the appendix's solemnity turn, as a last-resort fallback here, or by a
|
|
35
|
+
// direct `ordinarium({ section: "ad-libitum" })` query.
|
|
29
36
|
function isAdLibitum(entry) {
|
|
30
|
-
|
|
31
|
-
// Kyries are numbered 6 and 10 in the source data, colliding with the
|
|
32
|
-
// numbered masses — for those the incipit is the only discriminator.
|
|
33
|
-
return (entry.mass ?? 0) > NUMBERED_MASS_MAX || /\(ad lib\./i.test(entry.incipit);
|
|
37
|
+
return entry.section === "ad-libitum";
|
|
34
38
|
}
|
|
35
39
|
/** Prefer a proper numbered setting over an appendix one at equal standing. */
|
|
36
40
|
function adLibLast(a, b) {
|
|
@@ -38,12 +42,13 @@ function adLibLast(a, b) {
|
|
|
38
42
|
}
|
|
39
43
|
// The Missa pro defunctis settings belong to the Requiem, not to the temporal or
|
|
40
44
|
// sanctoral day. They stay out of every calendar-driven pick and remain
|
|
41
|
-
// reachable only by direct `ordinarium({
|
|
42
|
-
// dismissal carries the bare incipit "Requiescant"
|
|
43
|
-
//
|
|
44
|
-
// last-resort appendix onto every feria whose mass prints no
|
|
45
|
+
// reachable only by direct `ordinarium({ section: "requiem" })` query. The
|
|
46
|
+
// Requiem's own dismissal carries the bare incipit "Requiescant", which is why
|
|
47
|
+
// the section is data and not an incipit test: left to the incipit it leaked
|
|
48
|
+
// through the last-resort appendix onto every feria whose mass prints no
|
|
49
|
+
// dismissal.
|
|
45
50
|
function isRequiem(entry) {
|
|
46
|
-
return
|
|
51
|
+
return entry.section === "requiem";
|
|
47
52
|
}
|
|
48
53
|
/**
|
|
49
54
|
* Deterministic rotation of a preference list by year.
|
|
@@ -104,8 +109,14 @@ function appendixLeadsThisYear(rubric, poolSize, year) {
|
|
|
104
109
|
const n = poolSize + 1;
|
|
105
110
|
return ((year % n) + n) % n === poolSize;
|
|
106
111
|
}
|
|
112
|
+
// The appendix in the book's own order. Its settings carry no mass number, so
|
|
113
|
+
// the Graduale's page orders them: Kyrie I to XI, then the Glorias, and so on.
|
|
114
|
+
function grPage(entry) {
|
|
115
|
+
const page = /\d+/.exec(entry.pages?.gr?.[0]?.page ?? "")?.[0];
|
|
116
|
+
return page == null ? Number.POSITIVE_INFINITY : Number(page);
|
|
117
|
+
}
|
|
107
118
|
function appendixFor(office, pool) {
|
|
108
|
-
return pool.filter((e) => e.office === office && isAdLibitum(e) && !isRequiem(e)).sort((a, b) => (a
|
|
119
|
+
return pool.filter((e) => e.office === office && isAdLibitum(e) && !isRequiem(e)).sort((a, b) => grPage(a) - grPage(b) || (a.id < b.id ? -1 : 1));
|
|
109
120
|
}
|
|
110
121
|
/**
|
|
111
122
|
* Candidate settings for one ordinary slot, most preferred first.
|
|
@@ -183,7 +194,7 @@ function entriesForOffice(office, massNumbers, year, appendixLeads, wholeMass =
|
|
|
183
194
|
// is the standing answer — its ad libitum variants must NOT rotate against it
|
|
184
195
|
// as equals, or the appendix would be sung more often than the rite itself.
|
|
185
196
|
// They arrive on the appendix turn like every other appendix setting.
|
|
186
|
-
const unnumbered = pool.filter((e) => e.office === office &&
|
|
197
|
+
const unnumbered = pool.filter((e) => e.office === office && SPRINKLING_OFFICES.has(office) && !isRequiem(e));
|
|
187
198
|
const plain = unnumbered.filter((e) => !isAdLibitum(e));
|
|
188
199
|
if (plain.length)
|
|
189
200
|
return rotate(plain, year);
|
|
@@ -310,6 +321,7 @@ export function entryToOrdinaryChant(entry) {
|
|
|
310
321
|
ordinary,
|
|
311
322
|
ordinarium: ORDINARIA[ordinary] ?? entry.incipit,
|
|
312
323
|
mass: entry.mass ?? 0,
|
|
324
|
+
section: entry.section,
|
|
313
325
|
};
|
|
314
326
|
}
|
|
315
327
|
// Maundy Thursday (In Cena Domini) is a Triduum exception: it retains a full
|
|
@@ -454,7 +466,7 @@ function assertFeasts(feasts, method) {
|
|
|
454
466
|
throw new Error(`${method}: feast must be a Feast (from tonus.festum) — got ${typeof f}`);
|
|
455
467
|
}
|
|
456
468
|
}
|
|
457
|
-
const ORDINARIUM_QUERY_KEYS = new Set([...CANTUS_QUERY_KEYS, "feast", "ordinary", "mass"]);
|
|
469
|
+
const ORDINARIUM_QUERY_KEYS = new Set([...CANTUS_QUERY_KEYS, "feast", "ordinary", "mass", "section"]);
|
|
458
470
|
/**
|
|
459
471
|
* Mass ordinary retrieval (`tonus.ordinarium`) from the Kyriale. A feast
|
|
460
472
|
* drives mass selection; `mass` pins a kyriale number directly.
|
|
@@ -472,6 +484,10 @@ export function getOrdinary(query) {
|
|
|
472
484
|
if (query.ordinary) {
|
|
473
485
|
assertOrdinaryCodes(Array.isArray(query.ordinary) ? query.ordinary : [query.ordinary], "ordinarium");
|
|
474
486
|
}
|
|
487
|
+
if (query.section != null && !KYRIALE_SECTIONS.has(query.section)) {
|
|
488
|
+
throw new Error(`ordinarium: unknown section "${query.section}" ` +
|
|
489
|
+
`(expected ${[...KYRIALE_SECTIONS].join(", ")}).`);
|
|
490
|
+
}
|
|
475
491
|
const feasts = toArray(query.feast);
|
|
476
492
|
assertFeasts(feasts, "ordinarium");
|
|
477
493
|
// `mode` accepts a scalar or an array, with cantus's semantics: match any.
|
|
@@ -493,7 +509,7 @@ export function getOrdinary(query) {
|
|
|
493
509
|
return ordinaryForFeast(f, query.mass, filterModes, adm);
|
|
494
510
|
});
|
|
495
511
|
}
|
|
496
|
-
else if (query.mass != null || query.ordinary) {
|
|
512
|
+
else if (query.mass != null || query.ordinary || query.section) {
|
|
497
513
|
// Direct kyriale query without feast context — same admissibility rule as
|
|
498
514
|
// cantus, so the two doors cannot disagree.
|
|
499
515
|
let entries = KYRIALE.slice();
|
|
@@ -503,6 +519,8 @@ export function getOrdinary(query) {
|
|
|
503
519
|
}
|
|
504
520
|
if (query.mass != null)
|
|
505
521
|
entries = entries.filter((e) => e.mass === query.mass);
|
|
522
|
+
if (query.section)
|
|
523
|
+
entries = entries.filter((e) => e.section === query.section);
|
|
506
524
|
if (query.ordinary)
|
|
507
525
|
entries = entries.filter((e) => e.office === query.ordinary);
|
|
508
526
|
if (filterModes)
|
|
@@ -515,6 +533,9 @@ export function getOrdinary(query) {
|
|
|
515
533
|
return [];
|
|
516
534
|
}
|
|
517
535
|
// Apply remaining CantusQuery filters
|
|
536
|
+
if (feasts && query.section) {
|
|
537
|
+
results = results.filter((c) => c.section === query.section);
|
|
538
|
+
}
|
|
518
539
|
if (query.incipit) {
|
|
519
540
|
const needle = query.incipit.toLowerCase();
|
|
520
541
|
results = results.filter((c) => c.incipit.toLowerCase().includes(needle));
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Season, Grade, Feast } from "../cal/types.js";
|
|
2
|
+
import type { KyrialeSection } from "../../data/kyriale.js";
|
|
2
3
|
export type { Season, Grade, Feast };
|
|
3
4
|
export type OfficeCode = "an" | "al" | "ca" | "co" | "gr" | "hy" | "in" | "of" | "ps" | "re" | "rb" | "se" | "tr" | "tp" | "or" | "ky" | "va" | "im" | "pa" | "su";
|
|
4
5
|
export type OrdinaryCode = "ke" | "gl" | "cr" | "sa" | "ag" | "be" | "it" | "as" | "va";
|
|
@@ -92,11 +93,14 @@ export interface Chant {
|
|
|
92
93
|
ordinary?: OrdinaryCode;
|
|
93
94
|
ordinarium?: string;
|
|
94
95
|
mass?: number;
|
|
96
|
+
section?: KyrialeSection;
|
|
95
97
|
}
|
|
96
98
|
export interface OrdinaryChant extends Chant {
|
|
97
99
|
ordinary: OrdinaryCode;
|
|
98
100
|
ordinarium: string;
|
|
101
|
+
/** One of Masses I–XVIII; 0 outside the numbered Masses (see `section`). */
|
|
99
102
|
mass: number;
|
|
103
|
+
section: KyrialeSection;
|
|
100
104
|
}
|
|
101
105
|
export interface GenusCount {
|
|
102
106
|
office: OfficeCode;
|
|
@@ -219,7 +223,10 @@ export interface PropriumQuery extends CantusQuery {
|
|
|
219
223
|
export interface OrdinariumQuery extends CantusQuery {
|
|
220
224
|
feast?: Feast | Feast[];
|
|
221
225
|
ordinary?: OrdinaryCode;
|
|
226
|
+
/** One of Masses I–XVIII. */
|
|
222
227
|
mass?: number;
|
|
228
|
+
/** A section of the Kyriale: "mass", "credo", "sprinkling", "ad-libitum", "requiem". */
|
|
229
|
+
section?: KyrialeSection;
|
|
223
230
|
}
|
|
224
231
|
export interface OfficiumQuery extends CantusQuery {
|
|
225
232
|
feast?: Feast | Feast[];
|
|
@@ -21,7 +21,7 @@ import { decideBreak } from "./breaking.js";
|
|
|
21
21
|
import { trimRuns } from "../lyric.js";
|
|
22
22
|
import { GLYPHS, GLYPH_UPM } from "../../../data/smufl-glyphs.js";
|
|
23
23
|
import { computeAccidentals, } from "./accidentals.js";
|
|
24
|
-
import { GLYPH, SHAPE_GLYPH, DIVISIO_GLYPH, ligaturaDesc, } from "../../../data/gabc-glyphs.js";
|
|
24
|
+
import { GLYPH, SHAPE_GLYPH, headGlyph, tracedHollow, DIVISIO_GLYPH, ligaturaDesc, } from "../../../data/gabc-glyphs.js";
|
|
25
25
|
import { buildChironomia, buildProsodia, buildTonarium, trackBands, } from "./tracks.js";
|
|
26
26
|
function resolveFont(slot, fallback) {
|
|
27
27
|
if (!slot)
|
|
@@ -352,14 +352,19 @@ function yAt(pos, systemY, L, r) {
|
|
|
352
352
|
// SMuFL nominal (noteheads use r.noteScale; clefs/divisiones use 1). dyFont
|
|
353
353
|
// shifts the glyph in font units (y-up) before the flip — used to re-register
|
|
354
354
|
// base-registered components.
|
|
355
|
-
|
|
355
|
+
// `traced` draws the outline alone, at the staff line's weight whatever the
|
|
356
|
+
// scale — the hollow for a head Bravura carries only full.
|
|
357
|
+
function placeGlyph(code, x, y, r, cls, data = "", factor = 1, dyFont = 0, traced = false) {
|
|
356
358
|
const g = GLYPHS[code];
|
|
357
359
|
if (!g)
|
|
358
360
|
return null;
|
|
359
361
|
const s = r.glyphScale * factor;
|
|
360
362
|
const yy = y - dyFont * s;
|
|
363
|
+
const paint = traced
|
|
364
|
+
? `fill="none" stroke="${r.noteColor}" stroke-width="${r.lineWeight.toFixed(2)}" vector-effect="non-scaling-stroke"`
|
|
365
|
+
: `fill="${r.noteColor}"`;
|
|
361
366
|
const svg = `<g class="${cls}"${data} transform="translate(${x.toFixed(2)} ${yy.toFixed(2)}) scale(${s.toFixed(5)} ${(-s).toFixed(5)})">` +
|
|
362
|
-
`<path d="${g.path}"
|
|
367
|
+
`<path d="${g.path}" ${paint}/></g>`;
|
|
363
368
|
return {
|
|
364
369
|
svg,
|
|
365
370
|
advance: g.advance * s,
|
|
@@ -484,10 +489,14 @@ export function toSvg(rows, chant, options = {}) {
|
|
|
484
489
|
};
|
|
485
490
|
// Place a notehead glyph for a row at x; returns the placement.
|
|
486
491
|
const placeNote = (row, atX, code, dyFont = 0) => {
|
|
487
|
-
|
|
492
|
+
// A liquescent with a head of its own draws it at full size; one Bravura
|
|
493
|
+
// does not carry (a liquescent virga, a figure component) keeps the small
|
|
494
|
+
// head that stood for every liquescence before the heads were routed.
|
|
495
|
+
const head = code ? null : headGlyph(row);
|
|
496
|
+
const glyphCode = code ?? head ?? SHAPE_GLYPH[row.shape] ?? GLYPH.punctum;
|
|
488
497
|
const y = yFor(row.staffPosition, L, r);
|
|
489
|
-
const sc = row.liquescent ? r.noteScale * 0.66 : r.noteScale;
|
|
490
|
-
const p = placeGlyph(glyphCode, atX, y, r, "note", dataAttrs(row), sc, dyFont);
|
|
498
|
+
const sc = row.liquescent && !head ? r.noteScale * 0.66 : r.noteScale;
|
|
499
|
+
const p = placeGlyph(glyphCode, atX, y, r, "note", dataAttrs(row), sc, dyFont, !code && tracedHollow(row));
|
|
491
500
|
if (!p)
|
|
492
501
|
return null;
|
|
493
502
|
ledger(row.staffPosition, p.inkLeft, p.inkRight);
|
|
@@ -1126,6 +1135,40 @@ export function toSvg(rows, chant, options = {}) {
|
|
|
1126
1135
|
body.push(p.svg);
|
|
1127
1136
|
}
|
|
1128
1137
|
}
|
|
1138
|
+
// ── Signa over the note (GABC r1–r5) ──
|
|
1139
|
+
// Bravura carries the accentus and the semicirculus one way round only, so
|
|
1140
|
+
// one of each pair is the glyph mirrored about its own ink. For the accentus
|
|
1141
|
+
// that is the reversus; for the semicirculus it is the plain one — gregorio's
|
|
1142
|
+
// r4 is the cup, r5 the cap, and Bravura's "semicirculus above" is the cap.
|
|
1143
|
+
const SIGNUM = {
|
|
1144
|
+
accentus: { code: GLYPH.accentus },
|
|
1145
|
+
accentusReversus: { code: GLYPH.accentus, flip: "x" },
|
|
1146
|
+
circulus: { code: GLYPH.circulus },
|
|
1147
|
+
semicirculus: { code: GLYPH.semicirculus, flip: "y" },
|
|
1148
|
+
semicirculusReversus: { code: GLYPH.semicirculus },
|
|
1149
|
+
};
|
|
1150
|
+
for (const pl of placements) {
|
|
1151
|
+
const sign = pl.row.signum ? SIGNUM[pl.row.signum] : undefined;
|
|
1152
|
+
const g = sign ? GLYPHS[sign.code] : undefined;
|
|
1153
|
+
if (!sign || !g)
|
|
1154
|
+
continue;
|
|
1155
|
+
const s = r.glyphScale * r.noteScale;
|
|
1156
|
+
const w = (g.bbox[2] - g.bbox[0]) * s;
|
|
1157
|
+
const midX = (pl.inkLeft + pl.inkRight) / 2;
|
|
1158
|
+
const y = yAt(pl.row.staffPosition, pl.systemY, L, r) - r.noteheadH * 0.45;
|
|
1159
|
+
const p = placeGlyph(sign.code, midX - w / 2, y, r, "signum", "", r.noteScale);
|
|
1160
|
+
if (!p)
|
|
1161
|
+
continue;
|
|
1162
|
+
if (!sign.flip) {
|
|
1163
|
+
body.push(p.svg);
|
|
1164
|
+
continue;
|
|
1165
|
+
}
|
|
1166
|
+
const cy = y - ((g.bbox[1] + g.bbox[3]) / 2) * s;
|
|
1167
|
+
const m = sign.flip === "x"
|
|
1168
|
+
? `translate(${(2 * midX).toFixed(2)} 0) scale(-1 1)`
|
|
1169
|
+
: `translate(0 ${(2 * cy).toFixed(2)}) scale(1 -1)`;
|
|
1170
|
+
body.push(`<g transform="${m}">${p.svg}</g>`);
|
|
1171
|
+
}
|
|
1129
1172
|
// Close the final system; height reaches the last.
|
|
1130
1173
|
systemMaxX.push(Math.max(x, prevLyricRight) + r.padding);
|
|
1131
1174
|
// The canvas is what the CALLER asked for, not what the content happened to
|
package/dist/engines/score/ir.js
CHANGED
|
@@ -41,6 +41,10 @@ function rawToNote(raw, scale) {
|
|
|
41
41
|
liquescent: raw.liquescent,
|
|
42
42
|
strophicus: raw.strophicus,
|
|
43
43
|
oriscus: raw.oriscus,
|
|
44
|
+
liquescence: raw.liquescence,
|
|
45
|
+
oriscusDirection: raw.oriscusDirection,
|
|
46
|
+
hollow: raw.hollow,
|
|
47
|
+
signum: raw.signum,
|
|
44
48
|
mora: raw.mora,
|
|
45
49
|
weight: raw.weight,
|
|
46
50
|
},
|
|
@@ -42,6 +42,27 @@ const DIVISIO_DURATIONS = new Map([
|
|
|
42
42
|
const STAFF_STEPS = [0, 2, 4, 5, 7, 9, 11];
|
|
43
43
|
const SYLLABLES_REGEX = /(?=.)((?:[^(])*)(?:\(?([^)]*)\)?)?/g;
|
|
44
44
|
const NOTATIONS_REGEX = /z0|z|Z|::|:|[,;][1-6]?|`|[cf][1-4]|cb[1-4]|fb[1-4]|\/+| |\!|-?[a-mA-M][oOwWvVrRsxy#~\+><_\.'012345]*(?:\[[^\]]*\]?)*|\{([^}]+)\}?/g;
|
|
45
|
+
// GABC r1–r5, the signs gregorio prints over a note.
|
|
46
|
+
const SIGNA = Object.freeze({
|
|
47
|
+
"1": "accentus",
|
|
48
|
+
"2": "accentusReversus",
|
|
49
|
+
"3": "circulus",
|
|
50
|
+
"4": "semicirculus",
|
|
51
|
+
"5": "semicirculusReversus",
|
|
52
|
+
});
|
|
53
|
+
// An oriscus written without o0/o1 points at the next pitch that differs from
|
|
54
|
+
// its own: up is ascending, down (or nothing after it) descending. Gregorio's
|
|
55
|
+
// rule (gabc_determine_oriscus_orientation), run over the whole score because
|
|
56
|
+
// the next pitch is often in the next syllable.
|
|
57
|
+
function orientOrisci(events) {
|
|
58
|
+
const notes = events.filter((e) => e.type === "note");
|
|
59
|
+
notes.forEach((n, i) => {
|
|
60
|
+
if (!n.oriscus || n.oriscusDirection)
|
|
61
|
+
return;
|
|
62
|
+
const next = notes.slice(i + 1).find((m) => m.step !== n.step);
|
|
63
|
+
n.oriscusDirection = next && next.step > n.step ? "ascending" : "descending";
|
|
64
|
+
});
|
|
65
|
+
}
|
|
45
66
|
// Helpers
|
|
46
67
|
function initialAccidentalState(clef) {
|
|
47
68
|
const state = new Map();
|
|
@@ -279,12 +300,22 @@ function parseNeume(notation, context) {
|
|
|
279
300
|
// Written note shape (gregorio's S_* vocabulary, simplified) — what the
|
|
280
301
|
// renderer draws. Priority: the inclinatum diamond and the ornamental
|
|
281
302
|
// shapes win over the plain punctum/virga forms.
|
|
303
|
+
//
|
|
304
|
+
// The r family is read the way gregorio's lexer reads it: r alone empties
|
|
305
|
+
// the note (cavum), R draws it between two vertical lines, r0 does both,
|
|
306
|
+
// and r1–r5 are signs printed over a FULL note. Testing for a bare "r"
|
|
307
|
+
// made gr1 a hollow head, which is the one thing its accent does not say.
|
|
308
|
+
const marks = modifiers.replace(/\[[^\]]*\]?/g, "");
|
|
309
|
+
const hollow = /r(?![1-8])/.test(marks);
|
|
310
|
+
const signum = SIGNA[/r([1-5])/.exec(marks)?.[1] ?? ""] ?? null;
|
|
282
311
|
let shape = "punctum";
|
|
283
312
|
if (isInclinatum)
|
|
284
313
|
shape = "inclinatum";
|
|
285
314
|
else if (isQuilisma)
|
|
286
315
|
shape = "quilisma";
|
|
287
|
-
|
|
316
|
+
// A lone s is gregorio's stropha too; the analytical `strophicus` flag
|
|
317
|
+
// above stays with the repeated ss/sss, whose weighting it drives.
|
|
318
|
+
else if (isStrophicus || marks.includes("s"))
|
|
288
319
|
shape = "strophicus";
|
|
289
320
|
else if (modifiers.includes("V"))
|
|
290
321
|
shape = "virgaReversa";
|
|
@@ -292,10 +323,18 @@ function parseNeume(notation, context) {
|
|
|
292
323
|
shape = "virga";
|
|
293
324
|
else if (isOriscus)
|
|
294
325
|
shape = "oriscus";
|
|
295
|
-
else if (
|
|
296
|
-
shape = "
|
|
297
|
-
|
|
298
|
-
|
|
326
|
+
else if (marks.includes("R") || marks.includes("r0"))
|
|
327
|
+
shape = "lineaPunctum";
|
|
328
|
+
const liquescence = !isLiquescent ? null
|
|
329
|
+
: marks.includes("~") ? "deminutive"
|
|
330
|
+
: marks.includes("<") ? "ascending"
|
|
331
|
+
: "descending";
|
|
332
|
+
// o0 / o1 write the orientation; a bare o leaves it to the next pitch,
|
|
333
|
+
// resolved after the syllable's notes are known (below).
|
|
334
|
+
const oriscusDirection = !isOriscus ? null
|
|
335
|
+
: marks.includes("o1") ? "ascending"
|
|
336
|
+
: marks.includes("o0") ? "descending"
|
|
337
|
+
: null;
|
|
299
338
|
intermed.push({
|
|
300
339
|
step,
|
|
301
340
|
degree,
|
|
@@ -318,6 +357,10 @@ function parseNeume(notation, context) {
|
|
|
318
357
|
liquescent: isLiquescent,
|
|
319
358
|
strophicus: isStrophicus,
|
|
320
359
|
oriscus: isOriscus,
|
|
360
|
+
liquescence,
|
|
361
|
+
oriscusDirection,
|
|
362
|
+
hollow,
|
|
363
|
+
signum,
|
|
321
364
|
mora,
|
|
322
365
|
_weight: w,
|
|
323
366
|
_durWeight: durWeight,
|
|
@@ -385,6 +428,10 @@ function parseNeume(notation, context) {
|
|
|
385
428
|
liquescent: note.liquescent,
|
|
386
429
|
strophicus: note.strophicus,
|
|
387
430
|
oriscus: note.oriscus,
|
|
431
|
+
liquescence: note.liquescence,
|
|
432
|
+
oriscusDirection: note.oriscusDirection,
|
|
433
|
+
hollow: note.hollow,
|
|
434
|
+
signum: note.signum,
|
|
388
435
|
mora: note.mora,
|
|
389
436
|
};
|
|
390
437
|
});
|
|
@@ -519,6 +566,7 @@ export function parseGABC(gabc, options = {}) {
|
|
|
519
566
|
if (events.length === 0) {
|
|
520
567
|
errors.push({ message: "No parseable notation found" });
|
|
521
568
|
}
|
|
569
|
+
orientOrisci(events);
|
|
522
570
|
return { events, errors };
|
|
523
571
|
}
|
|
524
572
|
//# sourceMappingURL=parse.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ScoreIR, Neume, LyricRun } from "./types.js";
|
|
2
2
|
import type { Cadence } from "./cadence.js";
|
|
3
|
-
import type { ChantType, InterpretationOptions, WrittenShape } from "./types.js";
|
|
3
|
+
import type { ChantType, InterpretationOptions, Liquescence, Signum, WrittenShape } from "./types.js";
|
|
4
4
|
export type NoteRole = "finalis" | "tenor" | "alia" | null;
|
|
5
5
|
export interface ChantTabulaRow {
|
|
6
6
|
phraseIndex: number;
|
|
@@ -74,6 +74,14 @@ export interface ChantTabulaRow {
|
|
|
74
74
|
liquescent: boolean;
|
|
75
75
|
strophicus: boolean;
|
|
76
76
|
oriscus: boolean;
|
|
77
|
+
/** Which way a liquescent bends (see `Liquescence`); null when not liquescent. */
|
|
78
|
+
liquescence: Liquescence | null;
|
|
79
|
+
/** An oriscus's orientation — written (o0/o1) or read off the next pitch; null otherwise. */
|
|
80
|
+
oriscusDirection: "ascending" | "descending" | null;
|
|
81
|
+
/** Written hollow (GABC r, cavum). */
|
|
82
|
+
hollow: boolean;
|
|
83
|
+
/** Editorial sign over the note (GABC r1–r5); null when none. */
|
|
84
|
+
signum: Signum | null;
|
|
77
85
|
/** Mora vocis count: 0 none, 1 dot '.', 2 double dot '..' (a stronger cadential hold). */
|
|
78
86
|
mora: 0 | 1 | 2;
|
|
79
87
|
divisio: string | null;
|
|
@@ -122,6 +122,10 @@ export function computeTabula(ir, options = {}) {
|
|
|
122
122
|
liquescent: n.context.liquescent,
|
|
123
123
|
strophicus: n.context.strophicus,
|
|
124
124
|
oriscus: n.context.oriscus,
|
|
125
|
+
liquescence: n.context.liquescence,
|
|
126
|
+
oriscusDirection: n.context.oriscusDirection,
|
|
127
|
+
hollow: n.context.hollow,
|
|
128
|
+
signum: n.context.signum,
|
|
125
129
|
mora: n.context.mora,
|
|
126
130
|
divisio: a.divisio,
|
|
127
131
|
cadenceRef: cadenceRefByPos.get(`${a.phraseIndex}:${a.syllableIndex}:${a.noteIndex}`) ?? null,
|
|
@@ -14,7 +14,14 @@ export interface Performance {
|
|
|
14
14
|
rhythmicShape: ArsisThesis;
|
|
15
15
|
rhythmicIndex: number;
|
|
16
16
|
}
|
|
17
|
-
export type WrittenShape = "punctum" | "inclinatum" | "virga" | "virgaReversa" | "quilisma" | "oriscus" | "strophicus" | "
|
|
17
|
+
export type WrittenShape = "punctum" | "inclinatum" | "virga" | "virgaReversa" | "quilisma" | "oriscus" | "strophicus" | "lineaPunctum";
|
|
18
|
+
/**
|
|
19
|
+
* Which way a liquescent note bends, from its GABC mark: `~` deminutive (the
|
|
20
|
+
* small head), `<` augmented ascending, `>` augmented descending.
|
|
21
|
+
*/
|
|
22
|
+
export type Liquescence = "deminutive" | "ascending" | "descending";
|
|
23
|
+
/** An editorial sign printed over the note, GABC r1–r5 (gregorio's names). */
|
|
24
|
+
export type Signum = "accentus" | "accentusReversus" | "circulus" | "semicirculus" | "semicirculusReversus";
|
|
18
25
|
/**
|
|
19
26
|
* One styled span of a syllable's lyric — the decoded form of GABC's text
|
|
20
27
|
* markup (<i>, <b>, <sc>, <c>, elisions). The spans concatenate to the
|
|
@@ -71,6 +78,14 @@ export interface Context {
|
|
|
71
78
|
liquescent: boolean;
|
|
72
79
|
strophicus: boolean;
|
|
73
80
|
oriscus: boolean;
|
|
81
|
+
/** Which way a liquescent bends (see `Liquescence`); null when not liquescent. */
|
|
82
|
+
liquescence: Liquescence | null;
|
|
83
|
+
/** An oriscus's orientation — written (o0/o1) or read off the next pitch; null otherwise. */
|
|
84
|
+
oriscusDirection: "ascending" | "descending" | null;
|
|
85
|
+
/** Written hollow (GABC r, cavum). */
|
|
86
|
+
hollow: boolean;
|
|
87
|
+
/** Editorial sign over the note (GABC r1–r5); null when none. */
|
|
88
|
+
signum: Signum | null;
|
|
74
89
|
mora: 0 | 1 | 2;
|
|
75
90
|
weight: number;
|
|
76
91
|
}
|
|
@@ -204,6 +219,14 @@ export interface ParsedNote {
|
|
|
204
219
|
liquescent: boolean;
|
|
205
220
|
strophicus: boolean;
|
|
206
221
|
oriscus: boolean;
|
|
222
|
+
/** Which way a liquescent bends (see `Liquescence`); null when not liquescent. */
|
|
223
|
+
liquescence: Liquescence | null;
|
|
224
|
+
/** An oriscus's orientation — written (o0/o1) or read off the next pitch; null otherwise. */
|
|
225
|
+
oriscusDirection: "ascending" | "descending" | null;
|
|
226
|
+
/** Written hollow (GABC r, cavum). */
|
|
227
|
+
hollow: boolean;
|
|
228
|
+
/** Editorial sign over the note (GABC r1–r5); null when none. */
|
|
229
|
+
signum: Signum | null;
|
|
207
230
|
mora: 0 | 1 | 2;
|
|
208
231
|
}
|
|
209
232
|
export interface RestEvent {
|
package/docs/api/chant.md
CHANGED
|
@@ -405,8 +405,12 @@ interface PropriumQuery extends CantusQuery {
|
|
|
405
405
|
`ordinarium(query?)` retrieves the fixed chants of the Mass from the
|
|
406
406
|
Kyriale. A feast drives mass selection through its `masses` list, the
|
|
407
407
|
masses the day's Kyriale RUBRIC appoints, derived as described in
|
|
408
|
-
[calendar.md](calendar.md#the-days-feasts--festum); `mass` pins
|
|
409
|
-
|
|
408
|
+
[calendar.md](calendar.md#the-days-feasts--festum); `mass` pins one of the
|
|
409
|
+
eighteen numbered Masses directly, and `section` asks for a part of the book:
|
|
410
|
+
`mass` (Masses I–XVIII), `credo` (Credo I–VI), `sprinkling` (Asperges me,
|
|
411
|
+
Vidi aquam), `ad-libitum` (the Cantus ad libitum) or `requiem` (the Missa pro
|
|
412
|
+
defunctis). Only the `mass` section carries a mass number; every other entry
|
|
413
|
+
reads `mass: 0`. Where the rubric names several masses, the year rotates
|
|
410
414
|
through them (same feast, same year → same answer, every time), and sibling
|
|
411
415
|
printings under one number (Mass I prints two dismissals; Mass XVII prints
|
|
412
416
|
Kyrie A/B/C) rotate with it. Slots resolve independently, which the book
|
|
@@ -420,7 +424,7 @@ const [easter] = tonus.festum({ date: new Date("2026-04-05") });
|
|
|
420
424
|
tonus.ordinarium({ feast: easter });
|
|
421
425
|
// ky Kyrie I (mass 1) — Lux et origo, Paschal time, every year
|
|
422
426
|
// gl Gloria I (mass 1)
|
|
423
|
-
// cr Credo III (mass
|
|
427
|
+
// cr Credo III (mass 0) — the credo rotates on its own cycle
|
|
424
428
|
// sa Sanctus I (mass 1)
|
|
425
429
|
// ag Agnus Dei I (mass 1)
|
|
426
430
|
// it Ite Ia (mass 1)
|
|
@@ -437,7 +441,7 @@ sings Mass XVI whole with the Mass II Benedicamus. The ad libitum appendix
|
|
|
437
441
|
is a **solemnity boost**, reachable only under the festal rubrics (it takes
|
|
438
442
|
its turn in the rotation once every _n + 1_ years); it never reaches a
|
|
439
443
|
penitential day or a feria, and the Requiem settings stay out of every
|
|
440
|
-
calendar-driven pick (reachable by `ordinarium({
|
|
444
|
+
calendar-driven pick (reachable by `ordinarium({ section: "requiem" })` only). The
|
|
441
445
|
Triduum returns no ordinary at all: Good Friday has no Mass, and the
|
|
442
446
|
Vigil's ordinary belongs to Easter. A pinned `mass` overrides the Triduum
|
|
443
447
|
rule and the rotation both.
|
|
@@ -452,6 +456,7 @@ me** (`as`) otherwise.
|
|
|
452
456
|
```js
|
|
453
457
|
tonus.ordinarium({ ordinary: "ke" }); // every Kyrie
|
|
454
458
|
tonus.ordinarium({ mass: 9, ordinary: "gl" }); // Gloria of Cum jubilo
|
|
459
|
+
tonus.ordinarium({ section: "ad-libitum", ordinary: "ke" }); // the eleven ad libitum Kyries
|
|
455
460
|
```
|
|
456
461
|
|
|
457
462
|
| `ordinary` | `ordinarium` |
|
|
@@ -470,13 +475,15 @@ tonus.ordinarium({ mass: 9, ordinary: "gl" }); // Gloria of Cum jubilo
|
|
|
470
475
|
interface OrdinaryChant extends Chant {
|
|
471
476
|
ordinary: OrdinaryCode; // movement code
|
|
472
477
|
ordinarium: string; // Latin movement name, "Kyrie eleison" …
|
|
473
|
-
mass: number;
|
|
478
|
+
mass: number; // 1–18, or 0 outside the numbered Masses
|
|
479
|
+
section: "mass" | "credo" | "sprinkling" | "ad-libitum" | "requiem";
|
|
474
480
|
}
|
|
475
481
|
|
|
476
482
|
interface OrdinariumQuery extends CantusQuery {
|
|
477
483
|
feast?: Feast | Feast[];
|
|
478
484
|
ordinary?: OrdinaryCode;
|
|
479
|
-
mass?: number;
|
|
485
|
+
mass?: number; // 1–18
|
|
486
|
+
section?: "mass" | "credo" | "sprinkling" | "ad-libitum" | "requiem";
|
|
480
487
|
}
|
|
481
488
|
```
|
|
482
489
|
|
package/docs/api/score.md
CHANGED
|
@@ -271,10 +271,15 @@ interface Context {
|
|
|
271
271
|
liquescent: boolean;
|
|
272
272
|
strophicus: boolean;
|
|
273
273
|
oriscus: boolean;
|
|
274
|
+
liquescence: "deminutive" | "ascending" | "descending" | null; // ~ < >
|
|
275
|
+
oriscusDirection: "ascending" | "descending" | null; // o1/o0, else toward the next pitch
|
|
276
|
+
hollow: boolean; // cavum, gabc r
|
|
277
|
+
signum: "accentus" | "accentusReversus" | "circulus"
|
|
278
|
+
| "semicirculus" | "semicirculusReversus" | null; // gabc r1–r5
|
|
274
279
|
mora: 0 | 1 | 2; // mora vocis: 0 none, 1 dot, 2 double dot
|
|
275
280
|
staffLetter: string; // the GABC staff letter as written
|
|
276
281
|
clef: string; // the clef in force at this note ("c3", "f4", …)
|
|
277
|
-
shape: string; // the notehead shape (punctum, inclinatum, quilisma, …)
|
|
282
|
+
shape: string; // the notehead shape (punctum, inclinatum, quilisma, lineaPunctum, …)
|
|
278
283
|
weight: number; // articulation weight
|
|
279
284
|
}
|
|
280
285
|
```
|
|
@@ -326,6 +331,11 @@ interface ChantTabulaRow {
|
|
|
326
331
|
liquescent: boolean;
|
|
327
332
|
strophicus: boolean;
|
|
328
333
|
oriscus: boolean;
|
|
334
|
+
liquescence: "deminutive" | "ascending" | "descending" | null; // ~ < >
|
|
335
|
+
oriscusDirection: "ascending" | "descending" | null; // o1/o0, else toward the next pitch
|
|
336
|
+
hollow: boolean; // cavum, gabc r
|
|
337
|
+
signum: "accentus" | "accentusReversus" | "circulus"
|
|
338
|
+
| "semicirculus" | "semicirculusReversus" | null; // gabc r1–r5
|
|
329
339
|
mora: 0 | 1 | 2; // mora vocis: 0 none, 1 dot, 2 double dot
|
|
330
340
|
hz: number;
|
|
331
341
|
offset: number;
|
|
@@ -333,7 +343,7 @@ interface ChantTabulaRow {
|
|
|
333
343
|
staffLetter: string; // the GABC staff letter as written
|
|
334
344
|
staffPosition: number; // vertical staff position (line/space index)
|
|
335
345
|
clef: string; // the clef in force at this note ("c3", "f4", …)
|
|
336
|
-
shape: string; // the notehead shape (punctum, inclinatum, quilisma, …)
|
|
346
|
+
shape: string; // the notehead shape (punctum, inclinatum, quilisma, lineaPunctum, …)
|
|
337
347
|
bend: number; // 14-bit MIDI pitch bend (8192 = center)
|
|
338
348
|
velocity: number | null;
|
|
339
349
|
duration: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tonus",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Medieval music analysis: GABC plainchant exports, liturgical calendar, tuning systems, ephemeris, and the harmony of the spheres",
|
|
6
6
|
"author": "Jeffrey Pierce <jeffrey@jeffreypierce.net>",
|