tonus 0.12.0 → 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 CHANGED
@@ -2,6 +2,33 @@
2
2
 
3
3
  All notable changes to tonus. Newest first.
4
4
 
5
+ ## 0.13.0 — 2026-10-01
6
+
7
+ Every written head reaches its glyph.
8
+
9
+ ### Changed
10
+
11
+ - **Breaking: the hollow is a flag, not a shape.** `WrittenShape` loses
12
+ `cavum` and `linea` and gains `lineaPunctum` (gabc `R`, the punctum between
13
+ two vertical lines). A hollow note keeps its shape and sets `hollow`, since
14
+ gregorio empties a punctum, an inclinatum or a lineaPunctum alike. `linea`
15
+ could never be parsed; its glyph was the lineaPunctum's under a wrong label.
16
+ - `gr1`–`gr5` were read as hollow notes. They are full notes carrying a sign.
17
+
18
+ ### Added
19
+
20
+ - Rows carry `liquescence` (`~` deminutive, `<` ascending, `>` descending),
21
+ `oriscusDirection` (written `o0`/`o1`, or toward the next pitch that differs,
22
+ as gregorio resolves it), `hollow`, and `signum` (gabc `r1`–`r5`).
23
+ - The quadrata emitter draws the liquescent puncta and inclinata, the orisci
24
+ both ways and liquescent, the stropha and its auctus, the cavum and the
25
+ lineaPunctum (full and hollow) from their own Bravura glyphs, where it drew a
26
+ shrunk punctum for every liquescence. The signa print over the note; the
27
+ reversed accentus and the cup semicircle are Bravura's mirrored. A hollow
28
+ inclinatum, which Bravura does not carry, is the diamond's outline traced at
29
+ the staff line's weight — the way the score app already hollows it.
30
+ - A lone `s` is a stropha, as in gregorio.
31
+
5
32
  ## 0.12.0 — 2026-09-24
6
33
 
7
34
  A mass number is a Mass.
@@ -13,14 +13,21 @@ export declare const GLYPH: {
13
13
  readonly cClef: "E906";
14
14
  readonly punctum: "E990";
15
15
  readonly punctumInclinatum: "E991";
16
+ readonly punctumInclinatumAuctum: "E992";
17
+ readonly punctumInclinatumDeminutum: "E993";
18
+ readonly auctumAsc: "E994";
19
+ readonly auctumDesc: "E995";
16
20
  readonly virga: "E996";
17
21
  readonly virgaReversa: "E997";
18
22
  readonly cavum: "E998";
19
- readonly linea: "E999";
23
+ readonly lineaPunctum: "E999";
24
+ readonly lineaPunctumCavum: "E99A";
20
25
  readonly quilisma: "E99B";
21
26
  readonly oriscusAsc: "E99C";
22
27
  readonly oriscusDesc: "E99D";
28
+ readonly oriscusLiquescens: "E99E";
23
29
  readonly strophicus: "E99F";
30
+ readonly strophicusAuctus: "E9A0";
24
31
  readonly punctumDeminutum: "E9A1";
25
32
  readonly custosUp: readonly ["EA04", "EA05", "EA06"];
26
33
  readonly custosDown: readonly ["EA07", "EA08", "EA09"];
@@ -28,14 +35,39 @@ export declare const GLYPH: {
28
35
  readonly podatusUpper: "E9B1";
29
36
  readonly ictusAbove: "E9D0";
30
37
  readonly ictusBelow: "E9D1";
38
+ readonly circulus: "E9D2";
39
+ readonly semicirculus: "E9D4";
40
+ readonly accentus: "E9D6";
31
41
  readonly episema: "E9D8";
32
42
  readonly mora: "E9D9";
33
43
  readonly flat: "E9E0";
34
44
  readonly natural: "E9E1";
35
45
  readonly sharp: "E262";
36
46
  };
37
- /** Written note shape → notehead glyph codepoint. */
47
+ /** Written note shape → notehead glyph codepoint, before liquescence and the hollow. */
38
48
  export declare const SHAPE_GLYPH: Readonly<Record<string, string>>;
49
+ /** The fields of a note that choose its head. */
50
+ export interface HeadSpec {
51
+ shape: string;
52
+ liquescence: "deminutive" | "ascending" | "descending" | null;
53
+ oriscusDirection: "ascending" | "descending" | null;
54
+ hollow: boolean;
55
+ }
56
+ /**
57
+ * The notehead for a note, or null when Bravura draws no head for that
58
+ * combination (a liquescent virga or quilisma) and the caller keeps its
59
+ * fallback. Gregorio's rules: an oriscus takes only the deminutive
60
+ * liquescence; a strophicus either augmentation as its auctus. A hollow
61
+ * inclinatum returns the full diamond: Bravura has no cavum of it, so the
62
+ * emitter traces that outline instead (see `tracedHollow`).
63
+ */
64
+ export declare function headGlyph(n: HeadSpec): string | null;
65
+ /**
66
+ * A hollow head Bravura does not carry, drawn as its full glyph's outline:
67
+ * the inclinatum (gabc Gr, G<r). The score app hollows its diamond the same
68
+ * way, so the two agree.
69
+ */
70
+ export declare function tracedHollow(n: HeadSpec): boolean;
39
71
  /** Divisio mark → glyph codepoint. */
40
72
  export declare const DIVISIO_GLYPH: Readonly<Record<string, string>>;
41
73
  /** EntryLineAsc component for a rising interval of `steps` diatonic degrees. */
@@ -30,17 +30,27 @@ export const GLYPH = {
30
30
  // clefs
31
31
  fClef: "E902",
32
32
  cClef: "E906",
33
- // single notes
33
+ // single notes (SMuFL plainchant, E990–E9A1)
34
34
  punctum: "E990",
35
35
  punctumInclinatum: "E991",
36
+ punctumInclinatumAuctum: "E992",
37
+ punctumInclinatumDeminutum: "E993",
38
+ auctumAsc: "E994",
39
+ auctumDesc: "E995",
36
40
  virga: "E996",
37
41
  virgaReversa: "E997",
38
42
  cavum: "E998",
39
- linea: "E999",
43
+ // chantPunctumLinea: a punctum BETWEEN two vertical lines, gabc R (and, hollow,
44
+ // r0). Not the linea — gabc's `=` bar-shaped note, which SMuFL does not carry.
45
+ // gabc-smufl's map labels it "bar note, no notehead"; the outline says otherwise.
46
+ lineaPunctum: "E999",
47
+ lineaPunctumCavum: "E99A",
40
48
  quilisma: "E99B",
41
49
  oriscusAsc: "E99C",
42
50
  oriscusDesc: "E99D",
51
+ oriscusLiquescens: "E99E",
43
52
  strophicus: "E99F",
53
+ strophicusAuctus: "E9A0",
44
54
  punctumDeminutum: "E9A1",
45
55
  // THE CUSTOS, added to the bake 2026-08-12. Bravura carries it at EA00-EA09
46
56
  // and the subset did not, so the emitter drew a plain punctum at a line's
@@ -75,6 +85,9 @@ export const GLYPH = {
75
85
  // rhythmic signs
76
86
  ictusAbove: "E9D0",
77
87
  ictusBelow: "E9D1",
88
+ circulus: "E9D2",
89
+ semicirculus: "E9D4",
90
+ accentus: "E9D6",
78
91
  episema: "E9D8",
79
92
  mora: "E9D9",
80
93
  // accidentals (medieval soft-b flat / natural; standard sharp fallback).
@@ -85,7 +98,7 @@ export const GLYPH = {
85
98
  natural: "E9E1",
86
99
  sharp: "E262",
87
100
  };
88
- /** Written note shape → notehead glyph codepoint. */
101
+ /** Written note shape → notehead glyph codepoint, before liquescence and the hollow. */
89
102
  export const SHAPE_GLYPH = Object.freeze({
90
103
  punctum: GLYPH.punctum,
91
104
  inclinatum: GLYPH.punctumInclinatum,
@@ -94,9 +107,53 @@ export const SHAPE_GLYPH = Object.freeze({
94
107
  quilisma: GLYPH.quilisma,
95
108
  oriscus: GLYPH.oriscusAsc,
96
109
  strophicus: GLYPH.strophicus,
97
- cavum: GLYPH.cavum,
98
- linea: GLYPH.linea,
110
+ lineaPunctum: GLYPH.lineaPunctum,
99
111
  });
112
+ /**
113
+ * The notehead for a note, or null when Bravura draws no head for that
114
+ * combination (a liquescent virga or quilisma) and the caller keeps its
115
+ * fallback. Gregorio's rules: an oriscus takes only the deminutive
116
+ * liquescence; a strophicus either augmentation as its auctus. A hollow
117
+ * inclinatum returns the full diamond: Bravura has no cavum of it, so the
118
+ * emitter traces that outline instead (see `tracedHollow`).
119
+ */
120
+ export function headGlyph(n) {
121
+ const liq = n.liquescence;
122
+ switch (n.shape) {
123
+ case "punctum":
124
+ if (liq === "deminutive")
125
+ return GLYPH.punctumDeminutum;
126
+ if (liq === "ascending")
127
+ return GLYPH.auctumAsc;
128
+ if (liq === "descending")
129
+ return GLYPH.auctumDesc;
130
+ return n.hollow ? GLYPH.cavum : GLYPH.punctum;
131
+ case "inclinatum":
132
+ if (liq === "deminutive")
133
+ return GLYPH.punctumInclinatumDeminutum;
134
+ if (liq)
135
+ return GLYPH.punctumInclinatumAuctum;
136
+ return GLYPH.punctumInclinatum;
137
+ case "oriscus":
138
+ if (liq === "deminutive")
139
+ return GLYPH.oriscusLiquescens;
140
+ return n.oriscusDirection === "descending" ? GLYPH.oriscusDesc : GLYPH.oriscusAsc;
141
+ case "strophicus":
142
+ return liq ? GLYPH.strophicusAuctus : GLYPH.strophicus;
143
+ case "lineaPunctum":
144
+ return n.hollow ? GLYPH.lineaPunctumCavum : GLYPH.lineaPunctum;
145
+ default:
146
+ return liq ? null : SHAPE_GLYPH[n.shape] ?? null;
147
+ }
148
+ }
149
+ /**
150
+ * A hollow head Bravura does not carry, drawn as its full glyph's outline:
151
+ * the inclinatum (gabc Gr, G<r). The score app hollows its diamond the same
152
+ * way, so the two agree.
153
+ */
154
+ export function tracedHollow(n) {
155
+ return n.hollow && n.shape === "inclinatum";
156
+ }
100
157
  /** Divisio mark → glyph codepoint. */
101
158
  export const DIVISIO_GLYPH = Object.freeze({
102
159
  ",": GLYPH.divisioMinima,
@@ -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
- function placeGlyph(code, x, y, r, cls, data = "", factor = 1, dyFont = 0) {
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}" fill="${r.noteColor}"/></g>`;
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
- const glyphCode = code ?? SHAPE_GLYPH[row.shape] ?? GLYPH.punctum;
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
@@ -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
- else if (isStrophicus)
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 (modifiers.includes("r"))
296
- shape = "cavum";
297
- else if (modifiers.includes("="))
298
- shape = "linea";
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" | "cavum" | "linea";
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/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.12.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>",