tonus 0.1.8 → 0.5.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 +132 -108
- package/CHANGELOG.md +598 -1
- package/LICENSE +133 -29
- package/README.md +106 -83
- package/dist/data/am.js +2666 -11196
- package/dist/data/ams.d.ts +5 -0
- package/dist/data/ams.js +122 -0
- package/dist/data/attestation.d.ts +19 -0
- package/dist/data/attestation.js +15716 -0
- package/dist/data/attestation.json +15711 -0
- package/dist/data/cadentiae.d.ts +43 -0
- package/dist/data/cadentiae.js +174 -0
- package/dist/data/cal.js +48 -0
- package/dist/data/census.d.ts +12 -0
- package/dist/data/census.js +36 -0
- package/dist/data/commune-office.d.ts +4 -0
- package/dist/data/commune-office.js +2371 -0
- package/dist/data/commune-office.json +2365 -0
- package/dist/data/commune.js +181 -11
- package/dist/data/corpus-overlap.d.ts +17 -0
- package/dist/data/corpus-overlap.js +299 -5
- package/dist/data/cot.d.ts +5 -0
- package/dist/data/cot.js +172 -0
- package/dist/data/cse.d.ts +5 -0
- package/dist/data/cse.js +122 -0
- package/dist/data/gabc-glyphs.d.ts +45 -0
- package/dist/data/gabc-glyphs.js +122 -0
- package/dist/data/gr.js +754 -6394
- package/dist/data/kyriale.js +116 -116
- package/dist/data/la.js +799 -13419
- package/dist/data/lh.js +113 -3473
- package/dist/data/lu.js +931 -17631
- package/dist/data/nocturnale-romanum.js +1659 -10411
- package/dist/data/office-ferial.d.ts +4 -0
- package/dist/data/office-ferial.js +396 -0
- package/dist/data/office-ferial.json +391 -0
- package/dist/data/office-monastic.d.ts +17 -1
- package/dist/data/office-monastic.js +1403 -466
- package/dist/data/office-psalms-monastic.d.ts +13 -1
- package/dist/data/office-psalms-monastic.js +9 -0
- package/dist/data/propers.js +1 -1
- package/dist/data/psalms.js +22919 -5
- package/dist/data/psm.d.ts +5 -0
- package/dist/data/psm.js +122 -0
- package/dist/data/seasonal-respbreve.d.ts +5 -0
- package/dist/data/seasonal-respbreve.js +41 -0
- package/dist/data/seasonal-respbreve.json +35 -0
- package/dist/data/smufl-glyphs.d.ts +17 -0
- package/dist/data/smufl-glyphs.js +1546 -0
- package/dist/data/smufl-glyphs.json +1530 -0
- package/dist/engines/cal/calendar.d.ts +3 -2
- package/dist/engines/cal/calendar.js +105 -29
- package/dist/engines/cal/data/eras.d.ts +35 -0
- package/dist/engines/cal/data/eras.js +128 -0
- package/dist/engines/cal/date.js +44 -0
- package/dist/engines/cal/types.d.ts +15 -3
- package/dist/engines/cal/types.js +5 -5
- package/dist/engines/census/census.d.ts +7 -0
- package/dist/engines/census/census.js +179 -0
- package/dist/engines/census/types.d.ts +55 -0
- package/dist/engines/census/types.js +8 -0
- package/dist/engines/chant/attest.d.ts +39 -0
- package/dist/engines/chant/attest.js +90 -0
- package/dist/engines/chant/chant.d.ts +16 -4
- package/dist/engines/chant/chant.js +220 -30
- package/dist/engines/chant/data/compline.js +2 -1
- package/dist/engines/chant/data/masses.d.ts +56 -4
- package/dist/engines/chant/data/masses.js +305 -80
- package/dist/engines/chant/data/prime.js +1 -1
- package/dist/engines/chant/hour.js +279 -58
- package/dist/engines/chant/ordinary.d.ts +2 -0
- package/dist/engines/chant/ordinary.js +336 -56
- package/dist/engines/chant/propers.js +55 -5
- package/dist/engines/chant/psalm.d.ts +4 -4
- package/dist/engines/chant/psalm.js +25 -11
- package/dist/engines/chant/syllabify.d.ts +1 -0
- package/dist/engines/chant/syllabify.js +90 -17
- package/dist/engines/chant/types.d.ts +115 -12
- package/dist/engines/chant/types.js +38 -3
- package/dist/engines/harmonia/api.js +4 -0
- package/dist/engines/harmonia/data/doctrines.js +3 -1
- package/dist/engines/harmonia/tabula.d.ts +3 -0
- package/dist/engines/harmonia/tabula.js +1 -0
- package/dist/engines/harmonia/voice.d.ts +4 -0
- package/dist/engines/harmonia/voice.js +8 -4
- package/dist/engines/imprint.js +14 -1
- package/dist/engines/planet/orbital.js +4 -4
- package/dist/engines/planet/planet.d.ts +10 -0
- package/dist/engines/planet/planet.js +30 -3
- package/dist/engines/planet/position.js +13 -10
- package/dist/engines/planet/types.d.ts +1 -0
- package/dist/engines/score/api.d.ts +2 -13
- package/dist/engines/score/api.js +21 -8
- package/dist/engines/score/articulation.js +2 -2
- package/dist/engines/score/cadence.d.ts +76 -0
- package/dist/engines/score/cadence.js +96 -0
- package/dist/engines/score/emitters/accidentals.d.ts +21 -0
- package/dist/engines/score/emitters/accidentals.js +88 -0
- package/dist/engines/score/emitters/atramentum.d.ts +107 -0
- package/dist/engines/score/emitters/atramentum.js +239 -0
- package/dist/engines/score/emitters/breaking.d.ts +62 -0
- package/dist/engines/score/emitters/breaking.js +80 -0
- package/dist/engines/score/emitters/moderna.d.ts +38 -0
- package/dist/engines/score/emitters/moderna.js +612 -0
- package/dist/engines/score/emitters/svg.d.ts +143 -0
- package/dist/engines/score/emitters/svg.js +1328 -0
- package/dist/engines/score/emitters/tracks.d.ts +104 -0
- package/dist/engines/score/emitters/tracks.js +728 -0
- package/dist/engines/score/infer.d.ts +3 -3
- package/dist/engines/score/infer.js +2 -2
- package/dist/engines/score/inscriptio.d.ts +69 -0
- package/dist/engines/score/inscriptio.js +138 -0
- package/dist/engines/score/ir.d.ts +2 -2
- package/dist/engines/score/ir.js +59 -12
- package/dist/engines/score/lyric.d.ts +23 -0
- package/dist/engines/score/lyric.js +234 -0
- package/dist/engines/score/meta.d.ts +2 -2
- package/dist/engines/score/modulation.d.ts +12 -0
- package/dist/engines/score/modulation.js +49 -0
- package/dist/engines/score/neume.js +35 -4
- package/dist/engines/score/parse.js +150 -9
- package/dist/engines/score/phrasing.js +4 -3
- package/dist/engines/score/prosody.d.ts +38 -0
- package/dist/engines/score/prosody.js +70 -6
- package/dist/engines/score/tabula.d.ts +37 -5
- package/dist/engines/score/tabula.js +18 -0
- package/dist/engines/score/types.d.ts +88 -1
- package/dist/engines/temper/api.d.ts +4 -1
- package/dist/engines/temper/api.js +28 -5
- package/dist/engines/temper/data/guido.js +6 -2
- package/dist/engines/temper/data/modes.d.ts +6 -0
- package/dist/engines/temper/data/modes.js +42 -0
- package/dist/engines/temper/data/tones.d.ts +1 -1
- package/dist/engines/temper/data/tones.js +20 -11
- package/dist/engines/temper/interval.js +4 -3
- package/dist/engines/temper/modality.d.ts +11 -2
- package/dist/engines/temper/modality.js +74 -2
- package/dist/engines/temper/modes.d.ts +1 -1
- package/dist/engines/temper/pitch.d.ts +1 -1
- package/dist/engines/temper/pitch.js +12 -2
- package/dist/engines/temper/scale.d.ts +53 -0
- package/dist/engines/temper/scale.js +107 -8
- package/dist/index.d.ts +26 -8
- package/dist/index.js +37 -4
- package/docs/api/calendar.md +279 -0
- package/docs/api/census.md +288 -0
- package/docs/api/chant.md +657 -0
- package/docs/api/heavens.md +346 -0
- package/docs/api/index.md +263 -0
- package/docs/api/score.md +873 -0
- package/docs/api/tuning.md +619 -0
- package/package.json +11 -5
- package/dist/data/office-matins-roman.d.ts +0 -19
- package/dist/data/office-matins-roman.js +0 -4383
- package/dist/data/office-psalms-roman.d.ts +0 -15
- package/dist/data/office-psalms-roman.js +0 -28
- package/dist/data/office-roman.d.ts +0 -19
- package/dist/data/office-roman.js +0 -13792
- package/dist/engines/chant/matutinum.d.ts +0 -33
- package/dist/engines/chant/matutinum.js +0 -81
- package/dist/engines/score/emitters/midi.d.ts +0 -65
- package/dist/engines/score/emitters/midi.js +0 -162
- package/dist/engines/score/emitters/musicxml.d.ts +0 -18
- package/dist/engines/score/emitters/musicxml.js +0 -166
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChantType,
|
|
2
|
-
export declare function inferChantType(ir:
|
|
3
|
-
export declare function inferMode(ir:
|
|
1
|
+
import type { ChantType, ScoreIR } from "./types.js";
|
|
2
|
+
export declare function inferChantType(ir: ScoreIR): ChantType | undefined;
|
|
3
|
+
export declare function inferMode(ir: ScoreIR): number | undefined;
|
|
4
4
|
//# sourceMappingURL=infer.d.ts.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { OFFICIA } from "../chant/types.js";
|
|
2
2
|
import { MODES } from "../temper/modes.js";
|
|
3
3
|
// Identify an ordinary chant by its opening words. Two movements have a second,
|
|
4
4
|
// interior incipit because they are conventionally intoned from their second
|
|
@@ -15,7 +15,7 @@ const ORDINARY_INCIPITS = [
|
|
|
15
15
|
[/^benedica/i, "be"],
|
|
16
16
|
[/^ite/i, "it"],
|
|
17
17
|
];
|
|
18
|
-
const OFFICE_CODES = new Set(Object.keys(
|
|
18
|
+
const OFFICE_CODES = new Set(Object.keys(OFFICIA));
|
|
19
19
|
export function inferChantType(ir) {
|
|
20
20
|
const officePart = ir.chant.office?.toLowerCase().trim();
|
|
21
21
|
if (officePart && OFFICE_CODES.has(officePart))
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type NoteGeometry, type Theme } from "./emitters/svg.js";
|
|
2
|
+
import type { TrackName } from "./emitters/tracks.js";
|
|
3
|
+
import type { Score } from "./api.js";
|
|
4
|
+
export interface InscriptioOpts {
|
|
5
|
+
/** Notation species. Square-note by default; "moderna" is the modern transcription. */
|
|
6
|
+
notation?: "quadrata" | "moderna";
|
|
7
|
+
/** Accidental channel. "standard" performance accidentals by default. */
|
|
8
|
+
accidentals?: "standard" | "heji" | "cents";
|
|
9
|
+
/** Baseline for the cents channel; the chant's home intonation by default. */
|
|
10
|
+
centsBaseline?: "pythagorean" | "et";
|
|
11
|
+
/**
|
|
12
|
+
* Analysis tracks drawn beneath each system: "chironomia" — the rhythmic
|
|
13
|
+
* band (wave, Pierik letters) — and "tonarium" — the melodic band (maneriae
|
|
14
|
+
* rails, pressure sparkline, mode line, cadence nodes). Either rides either
|
|
15
|
+
* species, and both may ride one score; they stack in a fixed order, the
|
|
16
|
+
* chironomia above the tonarium, whichever order they are asked for.
|
|
17
|
+
*
|
|
18
|
+
* The two-register principle is the house pairing — the rhythmic band under
|
|
19
|
+
* the square notation (the body), the melodic band under the transcription
|
|
20
|
+
* (the mind). It is a default worth keeping, not a constraint enforced here.
|
|
21
|
+
*/
|
|
22
|
+
tracks?: readonly TrackName[];
|
|
23
|
+
/** Wrap systems to this px width. Absent = a single system (current behaviour). */
|
|
24
|
+
width?: number;
|
|
25
|
+
/**
|
|
26
|
+
* How big the chant is drawn: `"small"`, `"normal"` (default), `"large"`, or
|
|
27
|
+
* a staff height in px for fitting a known column. Everything scales from it
|
|
28
|
+
* — notes, lyrics, the gap between systems — and it reflows the music, so a
|
|
29
|
+
* larger scale means fewer notes per line.
|
|
30
|
+
*/
|
|
31
|
+
scale?: "small" | "normal" | "large" | number;
|
|
32
|
+
title?: string;
|
|
33
|
+
rubric?: string;
|
|
34
|
+
/** Derive the rubric block from chant meta (feast / genus / modus / source). */
|
|
35
|
+
annotation?: "auto";
|
|
36
|
+
/** Draw a rubricated initial from the first lyric; the first system indents. */
|
|
37
|
+
dropcap?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Faces, ink, and measurements — the whole look in one object.
|
|
40
|
+
*
|
|
41
|
+
* `fonts` carries four roles (`dropcap`, `title`, `annotation`, `lyric`), each
|
|
42
|
+
* a family string or `{ family, weight?, scale?, embed? }`. Without `embed`
|
|
43
|
+
* the SVG carries a font-family REFERENCE and the host page supplies the face;
|
|
44
|
+
* with it the CALLER's bytes ride inside the SVG's own `<style>` and the file
|
|
45
|
+
* is self-contained. tonus bundles no font files — with `embed` it is a
|
|
46
|
+
* conduit for data the consumer supplies, and the consumer carries the face's
|
|
47
|
+
* license terms.
|
|
48
|
+
*
|
|
49
|
+
* `colors` reaches the SVG as CSS custom properties with the theme's value as
|
|
50
|
+
* the fallback, so a host stylesheet can retheme a drawn chant without
|
|
51
|
+
* re-rendering it. `metrics` cannot work that way: staff height and note scale
|
|
52
|
+
* are consumed by line breaking long before a stylesheet sees the output.
|
|
53
|
+
*/
|
|
54
|
+
theme?: Theme;
|
|
55
|
+
}
|
|
56
|
+
export interface Inscriptio {
|
|
57
|
+
svg: string;
|
|
58
|
+
geometry: NoteGeometry[];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Render a `Score` as SVG. Returns the markup plus a geometry array (one entry
|
|
62
|
+
* per note, tabula order) for downstream analysis tracks.
|
|
63
|
+
* @throws Error when `score` is not a valid Score (a builder-function contract).
|
|
64
|
+
*/
|
|
65
|
+
export declare function inscriptio(score: Score, opts?: InscriptioOpts): Inscriptio;
|
|
66
|
+
export type { NoteGeometry } from "./emitters/svg.js";
|
|
67
|
+
export type { FontSpec, FontSlot, FontEmbed, Theme, ThemeColors } from "./emitters/svg.js";
|
|
68
|
+
export type { TrackName, TrackData } from "./emitters/tracks.js";
|
|
69
|
+
//# sourceMappingURL=inscriptio.d.ts.map
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// engines/score/inscriptio — the score renderer (SVG + geometry contract)
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// `inscriptio` (the inscribing) draws a Score. Per the rendering boundary
|
|
5
|
+
// (CODE-STANDARDS → Boundaries), rendering is a standalone function that TAKES a
|
|
6
|
+
// score — not a method on one. It inks the score itself; the analysis tracks
|
|
7
|
+
// (chironomia, the rhythmic band; tonarium, the melodic one) ship with it as
|
|
8
|
+
// opt-in bands (`tracks`), either riding either species, drawn from the same
|
|
9
|
+
// placements the geometry contract exports — never by scraping the notation.
|
|
10
|
+
// Custom tracks remain downstream consumers of that contract. One emitter
|
|
11
|
+
// format: SVG.
|
|
12
|
+
//
|
|
13
|
+
// The result is `{ svg, geometry }`. The geometry array — one entry per note in
|
|
14
|
+
// tabula order — is the TRACK CONTRACT: downstream consumers place marks against
|
|
15
|
+
// notes by index and coordinate, never by scraping the SVG.
|
|
16
|
+
//
|
|
17
|
+
// This is the standalone shell over the species emitters: the square-note
|
|
18
|
+
// renderer (emitters/svg.ts) and the modern transcription (emitters/moderna.ts).
|
|
19
|
+
// The multi-system layout engine, front matter, and the accidental channels
|
|
20
|
+
// are all wired in through InscriptioOpts.
|
|
21
|
+
import { toSvg, } from "./emitters/svg.js";
|
|
22
|
+
import { toModerna } from "./emitters/moderna.js";
|
|
23
|
+
// Options handed through to the species emitters — everything but the species
|
|
24
|
+
// selector itself.
|
|
25
|
+
const EMITTER_KEYS = [
|
|
26
|
+
"width",
|
|
27
|
+
"title", "rubric", "annotation", "dropcap",
|
|
28
|
+
"accidentals", "centsBaseline", "tracks",
|
|
29
|
+
];
|
|
30
|
+
/**
|
|
31
|
+
* Flatten a `theme` into the keys the emitters consume.
|
|
32
|
+
*
|
|
33
|
+
* The emitters take a flat option bag — one field per measurement, one per
|
|
34
|
+
* colour — because that is the shape a render loop wants. The theme is the
|
|
35
|
+
* shape a CALLER wants: faces, ink, and metrics travel together, and a house
|
|
36
|
+
* style is worth naming once. Resolving between them is this function's whole
|
|
37
|
+
* job, and it is the only place that knows both shapes.
|
|
38
|
+
*/
|
|
39
|
+
function flattenTheme(theme) {
|
|
40
|
+
if (!theme)
|
|
41
|
+
return {};
|
|
42
|
+
const out = {};
|
|
43
|
+
if (theme.fonts)
|
|
44
|
+
out.fonts = theme.fonts;
|
|
45
|
+
const c = theme.colors;
|
|
46
|
+
if (c?.note !== undefined)
|
|
47
|
+
out.noteColor = c.note;
|
|
48
|
+
if (c?.staffLine !== undefined)
|
|
49
|
+
out.staffLineColor = c.staffLine;
|
|
50
|
+
if (c?.rubrica !== undefined)
|
|
51
|
+
out.rubricaColor = c.rubrica;
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
/** The named sizes, as staff heights in px. `normal` is the default 40. */
|
|
55
|
+
const SCALES = {
|
|
56
|
+
small: 30,
|
|
57
|
+
normal: 40,
|
|
58
|
+
large: 56,
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Resolve `scale` to the emitter's `staffHeight`.
|
|
62
|
+
*
|
|
63
|
+
* A caller decides how big the chant should be, not how tall its staff is in
|
|
64
|
+
* pixels — so the public option is a size and the staff height is what it
|
|
65
|
+
* resolves to. A raw number still works for the caller who is fitting a known
|
|
66
|
+
* column, and means exactly what `staffHeight` meant.
|
|
67
|
+
*/
|
|
68
|
+
function resolveScale(scale) {
|
|
69
|
+
if (scale === undefined)
|
|
70
|
+
return undefined;
|
|
71
|
+
if (typeof scale === "number")
|
|
72
|
+
return scale;
|
|
73
|
+
const px = SCALES[scale];
|
|
74
|
+
if (px === undefined) {
|
|
75
|
+
throw new Error(`inscriptio: unknown scale "${scale}" ` +
|
|
76
|
+
`(expected ${Object.keys(SCALES).join(", ")}, or a staff height in px)`);
|
|
77
|
+
}
|
|
78
|
+
return px;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Render a `Score` as SVG. Returns the markup plus a geometry array (one entry
|
|
82
|
+
* per note, tabula order) for downstream analysis tracks.
|
|
83
|
+
* @throws Error when `score` is not a valid Score (a builder-function contract).
|
|
84
|
+
*/
|
|
85
|
+
export function inscriptio(score, opts = {}) {
|
|
86
|
+
if (!score || !Array.isArray(score.tabula)) {
|
|
87
|
+
throw new Error("inscriptio: expected a Score (from notatio)");
|
|
88
|
+
}
|
|
89
|
+
if (opts.notation && opts.notation !== "quadrata" && opts.notation !== "moderna") {
|
|
90
|
+
throw new Error(`inscriptio: unknown notation "${opts.notation}"`);
|
|
91
|
+
}
|
|
92
|
+
// The HEJI and cents channels are modern analytical overlays; they belong on
|
|
93
|
+
// the modern staff, not on historical square notation. Quadrata carries only
|
|
94
|
+
// the accidentals GABC itself expresses (flat/natural/sharp).
|
|
95
|
+
if (opts.notation !== "moderna" &&
|
|
96
|
+
(opts.accidentals === "heji" || opts.accidentals === "cents")) {
|
|
97
|
+
throw new Error(`inscriptio: accidentals "${opts.accidentals}" is a moderna-only intonation ` +
|
|
98
|
+
`overlay; square notation (quadrata) carries only GABC accidentals. ` +
|
|
99
|
+
`Use notation: "moderna".`);
|
|
100
|
+
}
|
|
101
|
+
// Any track rides either species, and all may ride one score — the
|
|
102
|
+
// selection is independent of the notation, as the notation itself is. An
|
|
103
|
+
// unknown name is a caller bug and throws.
|
|
104
|
+
for (const track of opts.tracks ?? []) {
|
|
105
|
+
if (track !== "prosodia" && track !== "chironomia" && track !== "tonarium") {
|
|
106
|
+
throw new Error(`inscriptio: unknown track "${track}" — tracks are "prosodia" ` +
|
|
107
|
+
`(the word band), "chironomia" (the rhythmic band) and "tonarium" ` +
|
|
108
|
+
`(the melodic band)`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Pass through the options the emitters consume; the species selector stays
|
|
112
|
+
// here. A species ignores, not errors on, options that do not apply to it.
|
|
113
|
+
// Both species honour the front matter (title, rubric/annotation) — the
|
|
114
|
+
// official opening is `title` + `annotation: "auto"`, no dropcap.
|
|
115
|
+
const emitterOpts = flattenTheme(opts.theme);
|
|
116
|
+
const staffHeight = resolveScale(opts.scale);
|
|
117
|
+
if (staffHeight !== undefined)
|
|
118
|
+
emitterOpts.staffHeight = staffHeight;
|
|
119
|
+
for (const k of EMITTER_KEYS)
|
|
120
|
+
if (opts[k] !== undefined)
|
|
121
|
+
emitterOpts[k] = opts[k];
|
|
122
|
+
// The tracks consume score-level analysis the flat tabula does not carry;
|
|
123
|
+
// hand it through only when a track asked for it.
|
|
124
|
+
if (opts.tracks && opts.tracks.length > 0) {
|
|
125
|
+
const modeDigit = score.chant.mode ? parseInt(score.chant.mode) : NaN;
|
|
126
|
+
const trackData = {
|
|
127
|
+
cadences: score.cadences,
|
|
128
|
+
modulations: score.modulations,
|
|
129
|
+
mode: Number.isFinite(modeDigit) ? modeDigit : undefined,
|
|
130
|
+
};
|
|
131
|
+
emitterOpts["trackData"] = trackData;
|
|
132
|
+
}
|
|
133
|
+
// Dispatch to the species' own renderer — each owns its spacing pass.
|
|
134
|
+
const render = opts.notation === "moderna" ? toModerna : toSvg;
|
|
135
|
+
const result = render(score.tabula, score.chant, emitterOpts);
|
|
136
|
+
return { svg: result.svg, geometry: result.geometry };
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=inscriptio.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { CompoundBeat, RhythmicType,
|
|
1
|
+
import type { CompoundBeat, RhythmicType, ScoreIR, ParseResult } from "./types.js";
|
|
2
2
|
import type { Scale } from "../temper/scale.js";
|
|
3
3
|
export declare function classifyRhythmicType(beats: CompoundBeat[]): RhythmicType;
|
|
4
|
-
export declare function buildIR(parsed: ParseResult, chant:
|
|
4
|
+
export declare function buildIR(parsed: ParseResult, chant: ScoreIR["chant"], scale: Scale): ScoreIR;
|
|
5
5
|
//# sourceMappingURL=ir.d.ts.map
|
package/dist/engines/score/ir.js
CHANGED
|
@@ -4,8 +4,14 @@ import { selectVowel } from "../chant/syllabify.js";
|
|
|
4
4
|
import { classifyNeume } from "./neume.js";
|
|
5
5
|
function rawToNote(raw, scale) {
|
|
6
6
|
const midi = raw.step;
|
|
7
|
+
// One read of the lyric: the nucleus and the diphthong it belongs to.
|
|
8
|
+
const sung = selectVowel(raw.lyric);
|
|
7
9
|
return {
|
|
8
|
-
|
|
10
|
+
// Spell the note as the SOURCE wrote it: a GABC `x` is a flat, `#` a sharp.
|
|
11
|
+
// Without the hint the speller reads the pitch class alone and calls a
|
|
12
|
+
// written D-flat "C#" — the same note, the opposite spelling, and `acc`
|
|
13
|
+
// reported +1 for a source that wrote a flat.
|
|
14
|
+
pitch: toPitch(midi, scale, raw.accidental === -1 ? "flat" : raw.accidental === 1 ? "sharp" : undefined),
|
|
9
15
|
step: toStep(midi, scale),
|
|
10
16
|
performance: {
|
|
11
17
|
velocity: 0,
|
|
@@ -15,11 +21,22 @@ function rawToNote(raw, scale) {
|
|
|
15
21
|
},
|
|
16
22
|
context: {
|
|
17
23
|
lyric: raw.lyric,
|
|
18
|
-
|
|
24
|
+
runs: raw.runs,
|
|
25
|
+
vowel: sung.vowel,
|
|
26
|
+
diphthong: sung.diphthong,
|
|
19
27
|
syllableIndex: raw.syllableIndex,
|
|
28
|
+
accent: raw.accent,
|
|
20
29
|
neumeGroup: raw.neumeGroup,
|
|
30
|
+
lineBreak: raw.lineBreak,
|
|
31
|
+
keepWithPrev: raw.keepWithPrev,
|
|
32
|
+
staffLetter: raw.staffLetter,
|
|
33
|
+
clef: raw.clef,
|
|
34
|
+
shape: raw.shape,
|
|
21
35
|
ictus: raw.ictus,
|
|
36
|
+
ictusSign: raw.ictusSign,
|
|
37
|
+
episema: raw.episema,
|
|
22
38
|
accidentalSource: raw.accidentalSource,
|
|
39
|
+
accidentalSign: raw.accidentalSign,
|
|
23
40
|
quilisma: raw.quilisma,
|
|
24
41
|
liquescent: raw.liquescent,
|
|
25
42
|
strophicus: raw.strophicus,
|
|
@@ -29,16 +46,33 @@ function rawToNote(raw, scale) {
|
|
|
29
46
|
},
|
|
30
47
|
};
|
|
31
48
|
}
|
|
32
|
-
// The salicus
|
|
33
|
-
//
|
|
49
|
+
// The salicus's principal note is prolonged. Modest, in the spirit of an
|
|
50
|
+
// episema.
|
|
51
|
+
//
|
|
52
|
+
// WHICH note is Cardine's correction [biblio: cardine-semiology, ch. 16 §II].
|
|
53
|
+
// The printed editions mark the note bearing the oriscus and the current
|
|
54
|
+
// practice lengthens it, treating it as principal "because of its special
|
|
55
|
+
// sign"; the manuscripts show the principal note is the one IMMEDIATELY
|
|
56
|
+
// FOLLOWING the oriscus — the summit — with §II.A proving it from the St Gall
|
|
57
|
+
// signs and the melodic tension toward that summit. The oriscus is a note of
|
|
58
|
+
// passage, not of rest. This is the one point where the rhythmic layer
|
|
59
|
+
// departs from Mocquereau/Suñol, and it departs deliberately.
|
|
60
|
+
//
|
|
61
|
+
// The summit is the LAST note of the ascent, because a salicus is ascending
|
|
62
|
+
// by definition and the oriscus is its next-to-last note — so the note after
|
|
63
|
+
// the oriscus is the final one, at any length. The 4- and 5-note forms need no
|
|
64
|
+
// separate rule.
|
|
34
65
|
const SALICUS_PROLONGATION = 1.3;
|
|
35
66
|
function makeSyllable(lyric, notes) {
|
|
36
67
|
const neume = classifyNeume(notes);
|
|
37
68
|
if (neume.type === "salicus" && notes.length >= 2) {
|
|
38
|
-
const
|
|
39
|
-
|
|
69
|
+
const summit = notes[notes.length - 1];
|
|
70
|
+
summit.performance.duration *= SALICUS_PROLONGATION;
|
|
40
71
|
}
|
|
41
|
-
|
|
72
|
+
const runs = notes[0]?.context.runs;
|
|
73
|
+
return runs
|
|
74
|
+
? { lyric, runs, notes, neume, melisma: notes.length }
|
|
75
|
+
: { lyric, notes, neume, melisma: notes.length };
|
|
42
76
|
}
|
|
43
77
|
function partitionByIctus(annotated) {
|
|
44
78
|
const groups = [];
|
|
@@ -140,7 +174,8 @@ function classifyCompoundBeats(annotated) {
|
|
|
140
174
|
// only when the sequence genuinely fits; ambiguous shapes stay null (a wrong
|
|
141
175
|
// analytic label is worse than none). Contraction (Type VIII) is Suñol's local
|
|
142
176
|
// reading [biblio: sunol-textbook]: a thesis immediately followed by an arsis
|
|
143
|
-
// mid-incise means two simple rhythms overlap at a shared ictus
|
|
177
|
+
// mid-incise means two simple rhythms overlap at a shared ictus — and, being two
|
|
178
|
+
// *complete* rhythms, the incise still resolves thetic; an unresolved seam is null.
|
|
144
179
|
export function classifyRhythmicType(beats) {
|
|
145
180
|
if (beats.length < 2)
|
|
146
181
|
return null; // a lone beat has no chaining; needs ≥ A–T
|
|
@@ -163,8 +198,16 @@ export function classifyRhythmicType(beats) {
|
|
|
163
198
|
return null;
|
|
164
199
|
}
|
|
165
200
|
// There is at least one thesis→arsis seam, so the incise chains multiple simple
|
|
166
|
-
// rhythms
|
|
167
|
-
//
|
|
201
|
+
// rhythms — and a chain of *complete* rhythms must still resolve (end thetic).
|
|
202
|
+
// In juxtaposition "each thesis marks the end of one and each arsis the
|
|
203
|
+
// beginning of another" [biblio: sunol-textbook], so a chain left hanging arsic
|
|
204
|
+
// (A–T–A, A–T–A–T–A) has opened a rhythm it never closes: null, whatever the
|
|
205
|
+
// seam pattern. This gate applies to VII exactly as to VIII.
|
|
206
|
+
if (seq[seq.length - 1] !== "thetic")
|
|
207
|
+
return null;
|
|
208
|
+
// Strict alternation A–T–A–T(…) is Carroll's Type VII; any other seam
|
|
209
|
+
// pattern is a contraction (Suñol) — Carroll's Type VIII, two (or more)
|
|
210
|
+
// complete rhythms overlapping at a shared ictus.
|
|
168
211
|
const alternating = seq.every((s, i) => s === (i % 2 === 0 ? "arsic" : "thetic"));
|
|
169
212
|
if (alternating && seq.length >= 4)
|
|
170
213
|
return "VII";
|
|
@@ -180,11 +223,15 @@ function applyCompoundBeats(phrases) {
|
|
|
180
223
|
}
|
|
181
224
|
phrase.beats = classifyCompoundBeats(annotated);
|
|
182
225
|
phrase.rhythmicType = classifyRhythmicType(phrase.beats);
|
|
226
|
+
// Phrase-level conveniences: a sung syllable carries at least one note.
|
|
227
|
+
const sung = phrase.syllables.filter((s) => s.notes.length > 0);
|
|
228
|
+
phrase.syllableCount = sung.length;
|
|
229
|
+
phrase.noteCount = sung.reduce((n, s) => n + s.notes.length, 0);
|
|
183
230
|
}
|
|
184
231
|
}
|
|
185
232
|
export function buildIR(parsed, chant, scale) {
|
|
186
233
|
const phrases = [];
|
|
187
|
-
let currentPhrase = { syllables: [], beats: [], rhythmicType: null };
|
|
234
|
+
let currentPhrase = { syllables: [], noteCount: 0, syllableCount: 0, beats: [], rhythmicType: null };
|
|
188
235
|
let currentNotes = [];
|
|
189
236
|
let currentLyric = null;
|
|
190
237
|
for (const event of parsed.events) {
|
|
@@ -209,7 +256,7 @@ export function buildIR(parsed, chant, scale) {
|
|
|
209
256
|
}
|
|
210
257
|
currentPhrase.divisio = event;
|
|
211
258
|
phrases.push(currentPhrase);
|
|
212
|
-
currentPhrase = { syllables: [], beats: [], rhythmicType: null };
|
|
259
|
+
currentPhrase = { syllables: [], noteCount: 0, syllableCount: 0, beats: [], rhythmicType: null };
|
|
213
260
|
}
|
|
214
261
|
}
|
|
215
262
|
if (currentLyric !== null && currentNotes.length > 0) {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { LyricRun } from "./types.js";
|
|
2
|
+
export interface DecodedLyric {
|
|
3
|
+
/** Clean display text — tags decoded, specials as real Unicode. */
|
|
4
|
+
text: string;
|
|
5
|
+
/** Styled spans covering `text`, present only when some style rides. */
|
|
6
|
+
runs?: LyricRun[];
|
|
7
|
+
/** Inside an open `<nlba>` — do not break a line before this syllable. */
|
|
8
|
+
keepWithPrev?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* One decoder per GABC source. Feed it each syllable's raw text in document
|
|
12
|
+
* order; open styles carry across calls.
|
|
13
|
+
*/
|
|
14
|
+
export declare function createLyricDecoder(): {
|
|
15
|
+
decode(raw: string): DecodedLyric;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Trim a syllable's edge hyphens/whitespace from its runs, mirroring the
|
|
19
|
+
* emitters' display trim of the plain text, so the runs' concatenation stays
|
|
20
|
+
* equal to what is drawn.
|
|
21
|
+
*/
|
|
22
|
+
export declare function trimRuns(runs: LyricRun[]): LyricRun[];
|
|
23
|
+
//# sourceMappingURL=lyric.d.ts.map
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// Gregorio's <sp> shortcuts, as they occur in the corpus. The barred letters
|
|
2
|
+
// ℟/℣ have codepoints; the antiphon A takes the combining solidus. Unmapped
|
|
3
|
+
// content (‡, ¶, …) passes through literally — <sp> is "insert this symbol".
|
|
4
|
+
const SP_MAP = {
|
|
5
|
+
"R/": "℟", // ℟ response
|
|
6
|
+
"V/": "℣", // ℣ versicle
|
|
7
|
+
"A/": "A̸", // barred A (antiphon)
|
|
8
|
+
"*": "*", // the raised star (schola entry)
|
|
9
|
+
"+": "†", // † the flex cross
|
|
10
|
+
"ae": "æ", // æ
|
|
11
|
+
"'ae": "ǽ", // ǽ
|
|
12
|
+
"'æ": "ǽ", // 'æ → ǽ
|
|
13
|
+
"oe": "œ", // œ
|
|
14
|
+
"'oe": "œ́", // œ́
|
|
15
|
+
"oe'": "œ́", // corpus typo for the same
|
|
16
|
+
"'œ": "œ́",
|
|
17
|
+
};
|
|
18
|
+
// Editorial markers, each normalized to ONE style however the source wrote
|
|
19
|
+
// them (<i>, <c>, or plain text): the psalm incipit "Ps." rubric red, as the
|
|
20
|
+
// books print it; the iterum "ij."/"iij." (almost) always italic.
|
|
21
|
+
const PS_MARK = /^(\s*)(Ps\.)(?=\s|$)/;
|
|
22
|
+
const IJ_MARK = /(^|\s)(i{1,3}j\.)(?=\s|$)/;
|
|
23
|
+
/** Decode a <v> verbatim-TeX payload to display text (page refs vanish). */
|
|
24
|
+
function decodeTex(buf) {
|
|
25
|
+
const italic = /\\itshape\b/.test(buf);
|
|
26
|
+
const text = buf
|
|
27
|
+
.replace(/\\pageref\{[^}]*\}/g, "") // cross-refs to the paper book
|
|
28
|
+
.replace(/\\greheightstar\b/g, "*")
|
|
29
|
+
.replace(/\\ddag\b/g, "‡")
|
|
30
|
+
.replace(/\\P\b/g, "¶")
|
|
31
|
+
.replace(/\\textup\{([^}]*)\}/g, "$1")
|
|
32
|
+
.replace(/\\ /g, " ") // control space
|
|
33
|
+
.replace(/\\[a-zA-Z]+\*?/g, "") // any remaining command
|
|
34
|
+
.replace(/[{}]/g, "");
|
|
35
|
+
return { text, italic };
|
|
36
|
+
}
|
|
37
|
+
/** Plain lyric characters: centering braces drop, ~ is a TeX placeholder. */
|
|
38
|
+
function cleanText(s) {
|
|
39
|
+
return s.replace(/[{}~]/g, "");
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* One decoder per GABC source. Feed it each syllable's raw text in document
|
|
43
|
+
* order; open styles carry across calls.
|
|
44
|
+
*/
|
|
45
|
+
export function createLyricDecoder() {
|
|
46
|
+
const st = {
|
|
47
|
+
italic: 0, bold: 0, smallCaps: 0, rubric: 0, alt: 0, nlba: 0, capture: null,
|
|
48
|
+
pendingSpace: false,
|
|
49
|
+
};
|
|
50
|
+
return {
|
|
51
|
+
decode(raw) {
|
|
52
|
+
const runs = [];
|
|
53
|
+
let styled = false;
|
|
54
|
+
const nlbaAtEntry = st.nlba;
|
|
55
|
+
let openedHere = false;
|
|
56
|
+
const push = (text, extraItalic = false) => {
|
|
57
|
+
if (!text || st.alt > 0)
|
|
58
|
+
return;
|
|
59
|
+
if (st.pendingSpace) {
|
|
60
|
+
// "℣Jubilate" → "℣ Jubilate"; "℣." keeps its period tight.
|
|
61
|
+
if (/^[A-Za-zÀ-ÿĀ-ž]/.test(text))
|
|
62
|
+
text = " " + text;
|
|
63
|
+
st.pendingSpace = false;
|
|
64
|
+
}
|
|
65
|
+
const run = { text };
|
|
66
|
+
if (st.italic > 0 || extraItalic)
|
|
67
|
+
run.italic = true;
|
|
68
|
+
if (st.bold > 0)
|
|
69
|
+
run.bold = true;
|
|
70
|
+
if (st.smallCaps > 0)
|
|
71
|
+
run.smallCaps = true;
|
|
72
|
+
if (st.rubric > 0)
|
|
73
|
+
run.rubric = true;
|
|
74
|
+
if (run.italic || run.bold || run.smallCaps || run.rubric)
|
|
75
|
+
styled = true;
|
|
76
|
+
const prev = runs[runs.length - 1];
|
|
77
|
+
if (prev &&
|
|
78
|
+
!prev.italic === !run.italic && !prev.bold === !run.bold &&
|
|
79
|
+
!prev.smallCaps === !run.smallCaps && !prev.rubric === !run.rubric) {
|
|
80
|
+
prev.text += text;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
runs.push(run);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
for (const part of raw.split(/(<[^<>]*>)/)) {
|
|
87
|
+
if (!part)
|
|
88
|
+
continue;
|
|
89
|
+
if (part.startsWith("<") && part.endsWith(">")) {
|
|
90
|
+
const tag = part.slice(1, -1).toLowerCase();
|
|
91
|
+
switch (tag) {
|
|
92
|
+
case "i":
|
|
93
|
+
case "e":
|
|
94
|
+
st.italic++;
|
|
95
|
+
break; // elision renders italic
|
|
96
|
+
case "/i":
|
|
97
|
+
case "/e":
|
|
98
|
+
st.italic = Math.max(0, st.italic - 1);
|
|
99
|
+
break;
|
|
100
|
+
case "b":
|
|
101
|
+
st.bold++;
|
|
102
|
+
break;
|
|
103
|
+
case "/b":
|
|
104
|
+
st.bold = Math.max(0, st.bold - 1);
|
|
105
|
+
break;
|
|
106
|
+
case "sc":
|
|
107
|
+
st.smallCaps++;
|
|
108
|
+
break;
|
|
109
|
+
case "/sc":
|
|
110
|
+
st.smallCaps = Math.max(0, st.smallCaps - 1);
|
|
111
|
+
break;
|
|
112
|
+
case "c":
|
|
113
|
+
st.rubric++;
|
|
114
|
+
break;
|
|
115
|
+
case "/c":
|
|
116
|
+
st.rubric = Math.max(0, st.rubric - 1);
|
|
117
|
+
break;
|
|
118
|
+
case "alt":
|
|
119
|
+
st.alt++;
|
|
120
|
+
break;
|
|
121
|
+
// The one layout hint that survives decoding. <nlba> spans a group
|
|
122
|
+
// the editor will not let a line break inside — "T. P. Allelúia"
|
|
123
|
+
// and its verse, kept whole. It is a span like <i>, opening in one
|
|
124
|
+
// syllable and closing several later, so it counts depth here and
|
|
125
|
+
// is read per-syllable rather than resolved into the text.
|
|
126
|
+
case "nlba":
|
|
127
|
+
if (nlbaAtEntry === 0)
|
|
128
|
+
openedHere = true;
|
|
129
|
+
st.nlba++;
|
|
130
|
+
break;
|
|
131
|
+
case "/nlba":
|
|
132
|
+
st.nlba = Math.max(0, st.nlba - 1);
|
|
133
|
+
break;
|
|
134
|
+
case "/alt":
|
|
135
|
+
st.alt = Math.max(0, st.alt - 1);
|
|
136
|
+
break;
|
|
137
|
+
case "sp":
|
|
138
|
+
st.capture = { kind: "sp", buf: "" };
|
|
139
|
+
break;
|
|
140
|
+
case "/sp": {
|
|
141
|
+
const buf = (st.capture?.buf ?? "").trim();
|
|
142
|
+
st.capture = null;
|
|
143
|
+
if (buf) {
|
|
144
|
+
const mapped = SP_MAP[buf] ?? buf;
|
|
145
|
+
push(mapped);
|
|
146
|
+
if (mapped === "℣" || mapped === "℟")
|
|
147
|
+
st.pendingSpace = true;
|
|
148
|
+
}
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
case "v":
|
|
152
|
+
st.capture = { kind: "v", buf: "" };
|
|
153
|
+
break;
|
|
154
|
+
case "/v": {
|
|
155
|
+
const tex = decodeTex(st.capture?.buf ?? "");
|
|
156
|
+
st.capture = null;
|
|
157
|
+
if (tex.text)
|
|
158
|
+
push(tex.text, tex.italic);
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
// Structural / layout markup with no display form: the euouae
|
|
162
|
+
// marker (its text prints plain), no-line-break areas, above-lines
|
|
163
|
+
// clears — and anything unrecognized drops its brackets, keeps its
|
|
164
|
+
// content (the pre-decode behavior, minus the visible tag soup).
|
|
165
|
+
default: break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else if (st.capture) {
|
|
169
|
+
st.capture.buf += part;
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
push(cleanText(part));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const text = runs.map((r) => r.text).join("");
|
|
176
|
+
// Normalize the editorial markers: the marker's span takes EXACTLY its
|
|
177
|
+
// one style, clearing whatever markup the source happened to wrap it in.
|
|
178
|
+
let out = styled ? runs : null;
|
|
179
|
+
const sliceRuns = (from, a, b) => {
|
|
180
|
+
const acc = [];
|
|
181
|
+
let pos = 0;
|
|
182
|
+
for (const run of from) {
|
|
183
|
+
const s = Math.max(a, pos);
|
|
184
|
+
const e = Math.min(b, pos + run.text.length);
|
|
185
|
+
if (e > s)
|
|
186
|
+
acc.push({ ...run, text: run.text.slice(s - pos, e - pos) });
|
|
187
|
+
pos += run.text.length;
|
|
188
|
+
}
|
|
189
|
+
return acc;
|
|
190
|
+
};
|
|
191
|
+
const restyle = (start, end, style) => {
|
|
192
|
+
const base = out ?? [{ text }];
|
|
193
|
+
out = [
|
|
194
|
+
...sliceRuns(base, 0, start),
|
|
195
|
+
{ text: text.slice(start, end), ...style },
|
|
196
|
+
...sliceRuns(base, end, text.length),
|
|
197
|
+
];
|
|
198
|
+
};
|
|
199
|
+
const ps = PS_MARK.exec(text);
|
|
200
|
+
if (ps)
|
|
201
|
+
restyle(ps[1].length, ps[1].length + ps[2].length, { rubric: true });
|
|
202
|
+
const ij = IJ_MARK.exec(text);
|
|
203
|
+
if (ij) {
|
|
204
|
+
const start = ij.index + ij[1].length;
|
|
205
|
+
restyle(start, start + ij[2].length, { italic: true });
|
|
206
|
+
}
|
|
207
|
+
// Depth is read AFTER the syllable's own tags are applied. A group opens
|
|
208
|
+
// with `<nlba>` at the head of its first syllable, so that syllable is
|
|
209
|
+
// already inside the span and every later one is too — which is what the
|
|
210
|
+
// flag has to mean: no break BEFORE me. The first syllable of the group
|
|
211
|
+
// may still start a line; it is the seam inside the group that is sealed.
|
|
212
|
+
const keep = st.nlba > 0 && !openedHere;
|
|
213
|
+
const res = out ? { text, runs: out } : { text };
|
|
214
|
+
if (keep)
|
|
215
|
+
res.keepWithPrev = true;
|
|
216
|
+
return res;
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Trim a syllable's edge hyphens/whitespace from its runs, mirroring the
|
|
222
|
+
* emitters' display trim of the plain text, so the runs' concatenation stays
|
|
223
|
+
* equal to what is drawn.
|
|
224
|
+
*/
|
|
225
|
+
export function trimRuns(runs) {
|
|
226
|
+
const out = runs.map((r) => ({ ...r }));
|
|
227
|
+
if (out.length > 0)
|
|
228
|
+
out[0].text = out[0].text.replace(/^-+/, "").replace(/^\s+/, "");
|
|
229
|
+
const last = out[out.length - 1];
|
|
230
|
+
if (last)
|
|
231
|
+
last.text = last.text.replace(/-+$/, "").replace(/\s+$/, "");
|
|
232
|
+
return out.filter((r) => r.text.length > 0);
|
|
233
|
+
}
|
|
234
|
+
//# sourceMappingURL=lyric.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChantType,
|
|
1
|
+
import type { ChantType, ScoreIR } from "./types.js";
|
|
2
2
|
export interface ChantMeta {
|
|
3
3
|
mode: number | null;
|
|
4
4
|
modeAlias: string | null;
|
|
@@ -15,5 +15,5 @@ export interface ChantMetaOptions {
|
|
|
15
15
|
mode?: number;
|
|
16
16
|
office?: ChantType;
|
|
17
17
|
}
|
|
18
|
-
export declare function computeMeta(ir:
|
|
18
|
+
export declare function computeMeta(ir: ScoreIR, options?: ChantMetaOptions): ChantMeta;
|
|
19
19
|
//# sourceMappingURL=meta.d.ts.map
|
|
@@ -8,6 +8,18 @@ export interface Modulation {
|
|
|
8
8
|
toMode: number;
|
|
9
9
|
/** 0–1: how strongly the foreign mode outscored the home mode, averaged. */
|
|
10
10
|
confidence: number;
|
|
11
|
+
/**
|
|
12
|
+
* What the span is evidence of. "inflection" — a single phrase leaning away
|
|
13
|
+
* and back: passing colour, not a shift. "modulation" — a SUSTAINED internal
|
|
14
|
+
* excursion, two phrases or more, that returns. "transposition" — the whole
|
|
15
|
+
* chant sits in a foreign mode's frame
|
|
16
|
+
* (it does not close on its labeled final, and one foreign mode dominates
|
|
17
|
+
* most of its phrases), i.e. the melody is notated at a transposed position
|
|
18
|
+
* (the affinal) or the label disagrees with the notation. A transposed chant
|
|
19
|
+
* is not modulating: the displacement is global, and callers displaying
|
|
20
|
+
* "modulations" should treat these spans as a re-reading of the whole chant.
|
|
21
|
+
*/
|
|
22
|
+
kind: "inflection" | "modulation" | "transposition";
|
|
11
23
|
}
|
|
12
24
|
/**
|
|
13
25
|
* Detect tonal-centre shifts. For each phrase, score it against every mode; a
|