tonus 0.7.0 → 0.9.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 +213 -1
- package/README.md +1 -1
- package/dist/engines/chant/attest.js +8 -8
- package/dist/engines/chant/hour.js +3 -3
- package/dist/engines/chant/intone.js +7 -1
- package/dist/engines/score/api.d.ts +3 -3
- package/dist/engines/score/api.js +3 -3
- package/dist/engines/score/cadence.d.ts +10 -4
- package/dist/engines/score/cadence.js +10 -4
- package/dist/engines/score/emitters/accidentals.d.ts +9 -0
- package/dist/engines/score/emitters/accidentals.js +76 -13
- package/dist/engines/score/emitters/atramentum.js +1 -1
- package/dist/engines/score/emitters/breaking.d.ts +2 -2
- package/dist/engines/score/emitters/breaking.js +2 -2
- package/dist/engines/score/emitters/moderna.js +16 -6
- package/dist/engines/score/emitters/svg.d.ts +16 -0
- package/dist/engines/score/emitters/svg.js +19 -12
- package/dist/engines/score/emitters/tracks.js +29 -10
- package/dist/engines/score/ir.js +24 -4
- package/dist/engines/score/{prosody.d.ts → metrics.d.ts} +3 -3
- package/dist/engines/score/{prosody.js → metrics.js} +2 -2
- package/dist/engines/score/neume.d.ts +19 -0
- package/dist/engines/score/neume.js +35 -0
- package/dist/engines/score/parse.js +4 -1
- package/dist/engines/score/phrasing.js +1 -1
- package/dist/engines/score/types.d.ts +6 -0
- package/dist/engines/temper/data/guido.js +4 -2
- package/dist/engines/temper/neume.d.ts +1 -1
- package/dist/engines/temper/neume.js +24 -3
- package/dist/index.d.ts +2 -2
- package/docs/api/chant.md +7 -7
- package/docs/api/index.md +2 -2
- package/docs/api/score.md +159 -106
- package/docs/api/tuning.md +17 -9
- package/package.json +2 -2
|
@@ -492,7 +492,9 @@ export function toSvg(rows, chant, options = {}) {
|
|
|
492
492
|
`fill="${r.noteColor}">${esc(mark.label ?? "")}</text>`);
|
|
493
493
|
return 0;
|
|
494
494
|
}
|
|
495
|
-
|
|
495
|
+
// The sign sits on the line of the pitch it alters, which is not this
|
|
496
|
+
// row's own line where the sign was written before the figure it governs.
|
|
497
|
+
const p = placeGlyph(mark.glyph, atX, yFor(mark.degree ?? row.staffPosition, L, r), r, "accidental", "", r.noteScale * 0.62);
|
|
496
498
|
if (!p)
|
|
497
499
|
return 0;
|
|
498
500
|
body.push(p.svg);
|
|
@@ -736,9 +738,9 @@ export function toSvg(rows, chant, options = {}) {
|
|
|
736
738
|
// GABC's `z` says "start a new line here", and it is not a hint: an
|
|
737
739
|
// editor who set a chant chose where its lines end, and that choice
|
|
738
740
|
// carries a reading of the piece a width cannot infer. tonus SKIPPED the
|
|
739
|
-
// token at parse
|
|
740
|
-
// being thrown away, which is why the automatic breaks looked
|
|
741
|
-
// against a printed copy.
|
|
741
|
+
// token at parse, so every break the Graduale chants that carry one had
|
|
742
|
+
// set was being thrown away, which is why the automatic breaks looked
|
|
743
|
+
// arbitrary against a printed copy.
|
|
742
744
|
//
|
|
743
745
|
// It wins over the fit test. Where it is absent the layout still decides.
|
|
744
746
|
if (r.width != null && figure[0].lineBreak && prevSyllable !== -1) {
|
|
@@ -831,8 +833,9 @@ export function toSvg(rows, chant, options = {}) {
|
|
|
831
833
|
// Break when the NEXT phrase will not fit, rather than once this one has
|
|
832
834
|
// already overrun. The check was `x > width - padding`, which only fires
|
|
833
835
|
// AFTER the boundary is crossed — and since a system may break only at a
|
|
834
|
-
// divisio, the overrun was a whole phrase wide. Measured over
|
|
835
|
-
// graduals, every one of them overran
|
|
836
|
+
// divisio, the overrun was a whole phrase wide. Measured over a sweep of
|
|
837
|
+
// graduals, every one of them overran the requested width, some by a
|
|
838
|
+
// wide margin.
|
|
836
839
|
// That is what made a render wider than the column it was drawn for, and
|
|
837
840
|
// why "sometimes bigger, sometimes smaller" varied by chant: the overrun
|
|
838
841
|
// depends on where the phrases happen to fall.
|
|
@@ -984,9 +987,9 @@ export function toSvg(rows, chant, options = {}) {
|
|
|
984
987
|
// above. But it cannot be the only one: quadrata's break test used to live
|
|
985
988
|
// entirely inside `if (div && phraseEnds)`, so a system could end nowhere
|
|
986
989
|
// else, and a phrase wider than the line simply ran until its next barline.
|
|
987
|
-
// Measured over
|
|
988
|
-
//
|
|
989
|
-
// was the asymmetry, not a spacing difference.
|
|
990
|
+
// Measured over a sweep of graduals, a QUARTER of quadrata's lines came out
|
|
991
|
+
// barely more than half full, against a handful in moderna — which breaks
|
|
992
|
+
// between syllables. That gap was the asymmetry, not a spacing difference.
|
|
990
993
|
//
|
|
991
994
|
// The books break mid-phrase freely; the unit is the word, never a syllable
|
|
992
995
|
// mid-word (which would split a lyric) and never mid-neume. So: at a word
|
|
@@ -1217,8 +1220,9 @@ export function toSvg(rows, chant, options = {}) {
|
|
|
1217
1220
|
// ...and a word carried to the NEXT system takes a hyphen at the line's
|
|
1218
1221
|
// end, which is what the books set. The gap-centred rule above cannot
|
|
1219
1222
|
// reach this case — the two syllables have no gap between them, they have
|
|
1220
|
-
// a line break — so the hyphen was simply dropped: measured,
|
|
1221
|
-
//
|
|
1223
|
+
// a line break — so the hyphen was simply dropped: measured, the great
|
|
1224
|
+
// majority of a sweep of graduals rendered a split word with nothing
|
|
1225
|
+
// joining the halves.
|
|
1222
1226
|
// "Sanc" ended a line and "tus" opened the next, reading as two words.
|
|
1223
1227
|
const thisRight = ly.cx + estLyricW(ly.text) / 2;
|
|
1224
1228
|
lyricSvgs.push(lyricText(thisRight + r.lyricSize * 0.42, ly.systemY, "-"));
|
|
@@ -1324,10 +1328,13 @@ export function toSvg(rows, chant, options = {}) {
|
|
|
1324
1328
|
phraseIndex: pl.row.phraseIndex,
|
|
1325
1329
|
syllableIndex: pl.row.syllableIndex,
|
|
1326
1330
|
neumeGroup: pl.row.neumeGroup,
|
|
1327
|
-
noteIndex: pl.row.
|
|
1331
|
+
noteIndex: pl.row.noteIndex,
|
|
1332
|
+
neumeIndex: pl.row.neumeIndex,
|
|
1328
1333
|
system: pl.system,
|
|
1329
1334
|
x: Number(pl.x.toFixed(2)),
|
|
1330
1335
|
y: Number(pl.y.toFixed(2)),
|
|
1336
|
+
inkLeft: Number(pl.inkLeft.toFixed(2)),
|
|
1337
|
+
inkRight: Number(pl.inkRight.toFixed(2)),
|
|
1331
1338
|
systemY: Number(pl.systemY.toFixed(2)),
|
|
1332
1339
|
}));
|
|
1333
1340
|
return { svg, geometry };
|
|
@@ -513,7 +513,7 @@ const SHARE_FLOOR = 10;
|
|
|
513
513
|
*/
|
|
514
514
|
function cadenceLabel(fam, mode) {
|
|
515
515
|
// NO CATALOGUE FAMILY IS ITSELF A MEASUREMENT. CADENTIAE holds the families
|
|
516
|
-
// above a floor of fifty corpus occurrences —
|
|
516
|
+
// above a floor of fifty corpus occurrences — 110 of them — so a close that
|
|
517
517
|
// fails to join is not unknown, it is RARER than anything the catalogue
|
|
518
518
|
// records. A third of inked cadences land here, and leaving them bare made
|
|
519
519
|
// the rarest closes look like the ones the analysis had nothing to say
|
|
@@ -570,7 +570,7 @@ export function buildTonarium(notes, data, cfg) {
|
|
|
570
570
|
const governing = (p) => {
|
|
571
571
|
let best = null;
|
|
572
572
|
for (const m of data.modulations) {
|
|
573
|
-
if (m.confidence >=
|
|
573
|
+
if (m.confidence >= CONF_FLOOR && m.startPhrase <= p && p <= m.endPhrase &&
|
|
574
574
|
(!best || m.confidence > best.confidence))
|
|
575
575
|
best = m;
|
|
576
576
|
}
|
|
@@ -579,6 +579,18 @@ export function buildTonarium(notes, data, cfg) {
|
|
|
579
579
|
return home != null ? { mode: home, conf: 1, kind: "home" } : null;
|
|
580
580
|
};
|
|
581
581
|
const systems = [...new Set(notes.map((n) => n.system))].sort((a, b) => a - b);
|
|
582
|
+
// WHERE EACH CADENCE LANDS. `notes` is in tabula order, so the last note
|
|
583
|
+
// carrying a cadenceRef is that cadence's closing note, and the system it
|
|
584
|
+
// sits in is the only one that may draw the closing dot and the label. A
|
|
585
|
+
// figure that wraps is re-inked in every system it crosses — the claim spans
|
|
586
|
+
// them — but it CLOSES once. Drawn per-system, a wrapped cadence printed its
|
|
587
|
+
// landing dot on the earlier fragment's last sample (a landing mid-figure)
|
|
588
|
+
// and repeated its label, so one close read as two.
|
|
589
|
+
const landingSystem = new Map();
|
|
590
|
+
for (const n of notes) {
|
|
591
|
+
if (n.row.cadenceRef != null)
|
|
592
|
+
landingSystem.set(n.row.cadenceRef, n.system);
|
|
593
|
+
}
|
|
582
594
|
for (const s of systems) {
|
|
583
595
|
const sysNotes = notes.filter((n) => n.system === s);
|
|
584
596
|
const sysY = sysNotes[0].systemY;
|
|
@@ -676,17 +688,22 @@ export function buildTonarium(notes, data, cfg) {
|
|
|
676
688
|
const samples = (samplesByPhrase.get(fig[0].row.phraseIndex) ?? [])
|
|
677
689
|
.filter(([px]) => px >= x0 && px <= x1 + 2 * k);
|
|
678
690
|
// Where the cadence LANDS — the closing dot, which the label centres on.
|
|
691
|
+
// Only the landing system draws it; an earlier fragment re-inks the
|
|
692
|
+
// ribbon and stops there.
|
|
693
|
+
const lands = landingSystem.get(ci) === s;
|
|
679
694
|
let dot;
|
|
680
695
|
if (samples.length >= 2) {
|
|
681
696
|
const d = ribbonPath(samples, vat, vmax, 1);
|
|
682
697
|
g.push(`<path d="${d}" fill="${INK}" fill-opacity="${(STRATUM.cadence * op).toFixed(2)}"/>`);
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
698
|
+
if (lands) {
|
|
699
|
+
const [nx, ny] = samples[samples.length - 1];
|
|
700
|
+
dot = nx;
|
|
701
|
+
const r = sc(1.8 * k);
|
|
702
|
+
g.push(closes
|
|
703
|
+
? `<circle cx="${nx.toFixed(1)}" cy="${ny.toFixed(1)}" r="${r}" fill="${INK}" opacity="${op.toFixed(2)}"/>`
|
|
704
|
+
: `<circle cx="${nx.toFixed(1)}" cy="${ny.toFixed(1)}" r="${r}" fill="none" stroke="${INK}" ` +
|
|
705
|
+
`stroke-width="${sc(0.9 * k)}" opacity="${op.toFixed(2)}"/>`);
|
|
706
|
+
}
|
|
690
707
|
}
|
|
691
708
|
// The label: how characteristic this close is OF THIS CHANT'S MODE —
|
|
692
709
|
// the family's in-mode share over its corpus share ("×2.1"). The raw
|
|
@@ -696,7 +713,7 @@ export function buildTonarium(notes, data, cfg) {
|
|
|
696
713
|
// system's edge it clamps to the margin rather than jumping to the
|
|
697
714
|
// figure's other side. A light end-ticked bracket ties it to the span.
|
|
698
715
|
const lab = cadenceLabel(fam, data.mode);
|
|
699
|
-
if (lab) {
|
|
716
|
+
if (lab && lands) {
|
|
700
717
|
// The label sits UNDER THE CLOSING DOT, centred on it. The dot is
|
|
701
718
|
// where the cadence lands — the one point the measure is about — so
|
|
702
719
|
// the number belongs beneath it rather than trailing the figure at
|
|
@@ -704,6 +721,8 @@ export function buildTonarium(notes, data, cfg) {
|
|
|
704
721
|
// figure's span, which the re-inked sparkline above already draws,
|
|
705
722
|
// and two marks for one extent read as two claims.
|
|
706
723
|
const estW = lab.length * 5.6 * k; // 9px mono advance, measured
|
|
724
|
+
// With fewer than two samples in the landing slice no dot was drawn;
|
|
725
|
+
// the label centres on the figure's closing ink instead of orphaning.
|
|
707
726
|
const dotX = dot ?? x1;
|
|
708
727
|
const left = xL;
|
|
709
728
|
const right = cfg.rightFor(s) - 2 * k;
|
package/dist/engines/score/ir.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { toPitch } from "../temper/pitch.js";
|
|
2
2
|
import { toStep } from "../temper/step.js";
|
|
3
3
|
import { selectVowel } from "../chant/syllabify.js";
|
|
4
|
-
import { classifyNeume } from "./neume.js";
|
|
4
|
+
import { classifyNeume, classifyFigures } from "./neume.js";
|
|
5
5
|
function rawToNote(raw, scale) {
|
|
6
6
|
const midi = raw.step;
|
|
7
7
|
// One read of the lyric: the nucleus and the diphthong it belongs to.
|
|
@@ -65,14 +65,15 @@ function rawToNote(raw, scale) {
|
|
|
65
65
|
const SALICUS_PROLONGATION = 1.3;
|
|
66
66
|
function makeSyllable(lyric, notes) {
|
|
67
67
|
const neume = classifyNeume(notes);
|
|
68
|
+
const neumes = classifyFigures(notes);
|
|
68
69
|
if (neume.type === "salicus" && notes.length >= 2) {
|
|
69
70
|
const summit = notes[notes.length - 1];
|
|
70
71
|
summit.performance.duration *= SALICUS_PROLONGATION;
|
|
71
72
|
}
|
|
72
73
|
const runs = notes[0]?.context.runs;
|
|
73
74
|
return runs
|
|
74
|
-
? { lyric, runs, notes, neume, melisma: notes.length }
|
|
75
|
-
: { lyric, notes, neume, melisma: notes.length };
|
|
75
|
+
? { lyric, runs, notes, neume, neumes, melisma: notes.length }
|
|
76
|
+
: { lyric, notes, neume, neumes, melisma: notes.length };
|
|
76
77
|
}
|
|
77
78
|
function partitionByIctus(annotated) {
|
|
78
79
|
const groups = [];
|
|
@@ -217,8 +218,27 @@ function applyCompoundBeats(phrases) {
|
|
|
217
218
|
for (const phrase of phrases) {
|
|
218
219
|
const annotated = [];
|
|
219
220
|
for (const syl of phrase.syllables) {
|
|
221
|
+
// A note is named by ITS OWN figure. Annotating every note of a melisma
|
|
222
|
+
// with the whole syllable's name told the two conventional overrides
|
|
223
|
+
// below (salicus → arsic, doubly-dotted clivis → thetic) that a
|
|
224
|
+
// three-figure syllable was one "compound" neume, so a clivis inside it
|
|
225
|
+
// was invisible to the rule that names it. The salicus is classified at
|
|
226
|
+
// syllable scope and so survives the split — see `classifyFigures`.
|
|
227
|
+
const byGroup = new Map();
|
|
228
|
+
let figure = -1;
|
|
229
|
+
let cursor = -1;
|
|
220
230
|
for (const note of syl.notes) {
|
|
221
|
-
|
|
231
|
+
if (note.context.neumeGroup !== figure) {
|
|
232
|
+
figure = note.context.neumeGroup;
|
|
233
|
+
cursor++;
|
|
234
|
+
}
|
|
235
|
+
byGroup.set(note.context.neumeGroup, syl.neumes[cursor]?.type ?? syl.neume.type);
|
|
236
|
+
}
|
|
237
|
+
for (const note of syl.notes) {
|
|
238
|
+
annotated.push({
|
|
239
|
+
note,
|
|
240
|
+
neumeType: byGroup.get(note.context.neumeGroup) ?? syl.neume.type,
|
|
241
|
+
});
|
|
222
242
|
}
|
|
223
243
|
}
|
|
224
244
|
phrase.beats = classifyCompoundBeats(annotated);
|
|
@@ -48,7 +48,7 @@ export interface Arcus {
|
|
|
48
48
|
*/
|
|
49
49
|
archIndex: number;
|
|
50
50
|
}
|
|
51
|
-
export interface
|
|
51
|
+
export interface Metrics {
|
|
52
52
|
noteCount: number;
|
|
53
53
|
syllableCount: number;
|
|
54
54
|
phraseCount: number;
|
|
@@ -69,5 +69,5 @@ export interface Prosody {
|
|
|
69
69
|
cadenceWeight: number;
|
|
70
70
|
cadenceDistribution: CadenceDistribution;
|
|
71
71
|
}
|
|
72
|
-
export declare function
|
|
73
|
-
//# sourceMappingURL=
|
|
72
|
+
export declare function computeMetrics(phrases: Phrase[]): Metrics;
|
|
73
|
+
//# sourceMappingURL=metrics.d.ts.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export function
|
|
1
|
+
export function computeMetrics(phrases) {
|
|
2
2
|
let phraseCount = 0;
|
|
3
3
|
let noteCount = 0;
|
|
4
4
|
let syllableCount = 0;
|
|
@@ -175,4 +175,4 @@ export function computeProsody(phrases) {
|
|
|
175
175
|
cadenceDistribution: cadDist,
|
|
176
176
|
};
|
|
177
177
|
}
|
|
178
|
-
//# sourceMappingURL=
|
|
178
|
+
//# sourceMappingURL=metrics.js.map
|
|
@@ -1,3 +1,22 @@
|
|
|
1
1
|
import type { Neume, Note } from "./types.js";
|
|
2
2
|
export declare function classifyNeume(notes: Note[]): Neume;
|
|
3
|
+
/**
|
|
4
|
+
* The syllable's figures, each classified in its own right.
|
|
5
|
+
*
|
|
6
|
+
* GABC marks figure boundaries (`!`, `/`, `//`) and the parser records them as
|
|
7
|
+
* `context.neumeGroup`, but classification read the whole syllable, so a
|
|
8
|
+
* three-figure melisma was named once and the name was almost always
|
|
9
|
+
* "compound" — two of every three compounds the corpus reported. A syllable is
|
|
10
|
+
* not a neume; it carries neumes.
|
|
11
|
+
*
|
|
12
|
+
* THE SALICUS IS CLASSIFIED AT SYLLABLE SCOPE, DELIBERATELY, and the exception
|
|
13
|
+
* is the point rather than an oversight. Its rule reads the oriscus on the
|
|
14
|
+
* next-to-last note of an ascent (see `classifyNeume` above), and 41 of the
|
|
15
|
+
* corpus's 255 salici are written across a figure boundary — the oriscus in
|
|
16
|
+
* one figure, the summit in the next. Splitting first severed them and the
|
|
17
|
+
* count fell to 251, which would have narrowed Cardine's definition by
|
|
18
|
+
* refactoring rather than by ruling. The ascent is the neume; where the
|
|
19
|
+
* scribe broke the figure is a separate fact.
|
|
20
|
+
*/
|
|
21
|
+
export declare function classifyFigures(notes: Note[]): Neume[];
|
|
3
22
|
//# sourceMappingURL=neume.d.ts.map
|
|
@@ -56,4 +56,39 @@ export function classifyNeume(notes) {
|
|
|
56
56
|
}
|
|
57
57
|
return { type, intervals, hasQuilisma, hasLiquescent, hasStrophicus };
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* The syllable's figures, each classified in its own right.
|
|
61
|
+
*
|
|
62
|
+
* GABC marks figure boundaries (`!`, `/`, `//`) and the parser records them as
|
|
63
|
+
* `context.neumeGroup`, but classification read the whole syllable, so a
|
|
64
|
+
* three-figure melisma was named once and the name was almost always
|
|
65
|
+
* "compound" — two of every three compounds the corpus reported. A syllable is
|
|
66
|
+
* not a neume; it carries neumes.
|
|
67
|
+
*
|
|
68
|
+
* THE SALICUS IS CLASSIFIED AT SYLLABLE SCOPE, DELIBERATELY, and the exception
|
|
69
|
+
* is the point rather than an oversight. Its rule reads the oriscus on the
|
|
70
|
+
* next-to-last note of an ascent (see `classifyNeume` above), and 41 of the
|
|
71
|
+
* corpus's 255 salici are written across a figure boundary — the oriscus in
|
|
72
|
+
* one figure, the summit in the next. Splitting first severed them and the
|
|
73
|
+
* count fell to 251, which would have narrowed Cardine's definition by
|
|
74
|
+
* refactoring rather than by ruling. The ascent is the neume; where the
|
|
75
|
+
* scribe broke the figure is a separate fact.
|
|
76
|
+
*/
|
|
77
|
+
export function classifyFigures(notes) {
|
|
78
|
+
if (notes.length === 0)
|
|
79
|
+
return [];
|
|
80
|
+
const whole = classifyNeume(notes);
|
|
81
|
+
if (whole.type === "salicus")
|
|
82
|
+
return [whole];
|
|
83
|
+
const figures = [];
|
|
84
|
+
let group = -1;
|
|
85
|
+
for (const note of notes) {
|
|
86
|
+
if (note.context.neumeGroup !== group) {
|
|
87
|
+
figures.push([]);
|
|
88
|
+
group = note.context.neumeGroup;
|
|
89
|
+
}
|
|
90
|
+
figures[figures.length - 1].push(note);
|
|
91
|
+
}
|
|
92
|
+
return figures.map(classifyNeume);
|
|
93
|
+
}
|
|
59
94
|
//# sourceMappingURL=neume.js.map
|
|
@@ -2,8 +2,11 @@ import { buildArticulation } from "./articulation.js";
|
|
|
2
2
|
import { createLyricDecoder } from "./lyric.js";
|
|
3
3
|
import { detectVowelAccent } from "../chant/syllabify.js";
|
|
4
4
|
// Constants
|
|
5
|
+
// `oct` anchors the staff onto the gamut, which is an absolute frame: guido.ts
|
|
6
|
+
// fixes Γ at midi 43. At oct 3 a chant read an octave below that anchor, so
|
|
7
|
+
// most of the corpus sat at or under the gamut's floor.
|
|
5
8
|
const DEFAULT_OPTIONS = {
|
|
6
|
-
oct:
|
|
9
|
+
oct: 4,
|
|
7
10
|
useVowelAccent: true,
|
|
8
11
|
};
|
|
9
12
|
// Per-clef diatonic-step offset. A GABC clef names the staff line it sits on
|
|
@@ -163,7 +163,7 @@ const CADENCE_VELOCITY_FACTOR = 0.5;
|
|
|
163
163
|
const CADENCE_DURATION_FACTOR = 0.6;
|
|
164
164
|
// The phrasing-side reading of the divisio (bar-line) hierarchy. This is the
|
|
165
165
|
// same bar hierarchy tabulated canonically in ../../../docs/api/score.md and
|
|
166
|
-
// weighted for analysis by
|
|
166
|
+
// weighted for analysis by metrics.ts's cadenceWeight ladder — but here the
|
|
167
167
|
// weights differ on purpose:
|
|
168
168
|
// this is a *shaping* factor, so the virgula (`) gets 0 (a breath, no cadential
|
|
169
169
|
// stress), where the analytic ladder still counts it. Do not unify the three.
|
|
@@ -221,7 +221,13 @@ export interface Syllable {
|
|
|
221
221
|
/** Styled lyric spans; present only when GABC markup styled this syllable. */
|
|
222
222
|
runs?: LyricRun[];
|
|
223
223
|
notes: Note[];
|
|
224
|
+
/** The syllable read as ONE figure. A syllable carrying several neumes
|
|
225
|
+
* classifies as "compound" here; `neumes` names them individually. */
|
|
224
226
|
neume: Neume;
|
|
227
|
+
/** The syllable's figures, as GABC groups them (`!`, `/`, `//`), each
|
|
228
|
+
* classified in its own right. One entry for a syllable of one figure —
|
|
229
|
+
* then `neumes[0]` and `neume` agree. */
|
|
230
|
+
neumes: Neume[];
|
|
225
231
|
/** Notes sung on this syllable — its melisma. 1 = syllabic, >1 = melismatic. */
|
|
226
232
|
melisma: number;
|
|
227
233
|
}
|
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
// finger to cc before crossing to the middle for dd and ee. Reading it with
|
|
8
8
|
// cc on the middle and dd on the ring makes the line cross three times where
|
|
9
9
|
// the hand turns once. The figure that DRAWS the turn lives in the site
|
|
10
|
-
// (orreliquum
|
|
11
|
-
//
|
|
10
|
+
// (orreliquum), and its LOCUS table must agree with the fingers named here;
|
|
11
|
+
// the test that holds the two together lives there beside it.
|
|
12
|
+
//
|
|
13
|
+
// The two b/♮ pairs (58/59, 70/71) share a
|
|
12
14
|
// locus by design — one joint, two readings (fa in molle, mi in durum), the
|
|
13
15
|
// tradition, not a duplication to fix. They are one distinction, named for the
|
|
14
16
|
// two shapes of the letter: b rotundum (the round b) against b quadratum (the
|
|
@@ -2,7 +2,7 @@ import type { Interval } from "./interval.js";
|
|
|
2
2
|
import type { Pitch, PitchInput } from "./pitch.js";
|
|
3
3
|
import type { Scale } from "./scale.js";
|
|
4
4
|
export type { Interval };
|
|
5
|
-
export type NeumeShape = "punctum" | "pes" | "clivis" | "torculus" | "porrectus" | "scandicus" | "salicus" | "climacus" | "torculus resupinus" | "porrectus flexus" | "scandicus flexus" | "climacus resupinus" | "pes subpunctis" | "compound";
|
|
5
|
+
export type NeumeShape = "punctum" | "pes" | "clivis" | "torculus" | "porrectus" | "scandicus" | "salicus" | "climacus" | "torculus resupinus" | "porrectus flexus" | "scandicus flexus" | "climacus resupinus" | "pes subpunctis" | "distropha" | "tristropha" | "tristropha flexa" | "pressus" | "pressus maior" | "scandicus subpunctis" | "compound";
|
|
6
6
|
export interface Neume {
|
|
7
7
|
pitches: Pitch[];
|
|
8
8
|
intervals: Interval[];
|
|
@@ -9,9 +9,12 @@ export function classifyShape(dirs) {
|
|
|
9
9
|
return "punctum";
|
|
10
10
|
const up = (d) => d === "up";
|
|
11
11
|
const dn = (d) => d === "down";
|
|
12
|
+
const un = (d) => d === "unison";
|
|
12
13
|
switch (n) {
|
|
13
14
|
case 1:
|
|
14
|
-
|
|
15
|
+
// A unison here is two notes on one pitch — a distropha, not a punctum.
|
|
16
|
+
// Reporting "punctum" said a two-note figure was one note.
|
|
17
|
+
return up(dirs[0]) ? "pes" : dn(dirs[0]) ? "clivis" : "distropha";
|
|
15
18
|
case 2: {
|
|
16
19
|
const [d0, d1] = dirs;
|
|
17
20
|
if (up(d0) && dn(d1))
|
|
@@ -22,6 +25,10 @@ export function classifyShape(dirs) {
|
|
|
22
25
|
return "scandicus";
|
|
23
26
|
if (dn(d0) && dn(d1))
|
|
24
27
|
return "climacus";
|
|
28
|
+
if (un(d0) && un(d1))
|
|
29
|
+
return "tristropha";
|
|
30
|
+
if (un(d0) && dn(d1))
|
|
31
|
+
return "pressus";
|
|
25
32
|
return "compound";
|
|
26
33
|
}
|
|
27
34
|
case 3: {
|
|
@@ -36,12 +43,26 @@ export function classifyShape(dirs) {
|
|
|
36
43
|
return "climacus resupinus";
|
|
37
44
|
if (up(d0) && dn(d1) && dn(d2))
|
|
38
45
|
return "pes subpunctis";
|
|
46
|
+
if (un(d0) && un(d1) && dn(d2))
|
|
47
|
+
return "tristropha flexa";
|
|
48
|
+
if (dn(d0) && un(d1) && dn(d2))
|
|
49
|
+
return "pressus maior";
|
|
39
50
|
return "compound";
|
|
40
51
|
}
|
|
41
|
-
default:
|
|
42
|
-
|
|
52
|
+
default: {
|
|
53
|
+
// The long forms: a head, then an unbroken descent. Only the head
|
|
54
|
+
// distinguishes them, and the descent must be total — a figure that
|
|
55
|
+
// turns again is a compound melisma, which at this length most are.
|
|
56
|
+
if (up(dirs[0]) && dirs.slice(1).every(dn))
|
|
43
57
|
return "pes subpunctis";
|
|
58
|
+
if (up(dirs[0]) && up(dirs[1]) && dirs.slice(2).every(dn)) {
|
|
59
|
+
return "scandicus subpunctis";
|
|
60
|
+
}
|
|
61
|
+
if (dirs.slice(0, -1).every(un) && dn(dirs[dirs.length - 1])) {
|
|
62
|
+
return "tristropha flexa";
|
|
63
|
+
}
|
|
44
64
|
return "compound";
|
|
65
|
+
}
|
|
45
66
|
}
|
|
46
67
|
}
|
|
47
68
|
export function buildNeume(inputs, scala) {
|
package/dist/index.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ import type { Score, ScoreOpts, PondusInput, PondusOpts, AccentusInput, Accentus
|
|
|
17
17
|
import type { InscriptioOpts, Inscriptio, NoteGeometry, FontSpec, FontSlot, FontEmbed } from "./engines/score/inscriptio.js";
|
|
18
18
|
import type { ChantTabulaRow } from "./engines/score/tabula.js";
|
|
19
19
|
import type { Imprint, Attractor, VowelAttractor, ModalAffinity } from "./engines/imprint.js";
|
|
20
|
-
import type {
|
|
20
|
+
import type { Metrics, RhythmicProfile, NoteRange, CadenceDistribution } from "./engines/score/metrics.js";
|
|
21
21
|
import type { Harmony, HarmoniaOpts, VoicedBody, VoicedAspect, Frame, Author } from "./engines/harmonia/api.js";
|
|
22
22
|
import type { HarmonyTabulaRow } from "./engines/harmonia/tabula.js";
|
|
23
23
|
import type { PlanetVowel } from "./engines/harmonia/data/vowels.js";
|
|
@@ -58,5 +58,5 @@ export { CADENTIAE, CADENTIAE_POPULATION } from "./data/cadentiae.js";
|
|
|
58
58
|
export type { CadentiaFamilia } from "./data/cadentiae.js";
|
|
59
59
|
export { ZODIACA } from "./engines/harmonia/data/zodiac.js";
|
|
60
60
|
export { CENSUS_GROUPS, CENSUS_ORDER } from "./data/census.js";
|
|
61
|
-
export type { Feast, FeastQuery, Pascha, Season, Grade, Chant, CantusQuery, OrdinaryChant, PropriumQuery, OrdinariumQuery, OfficiumQuery, PsalmusQuery, Corpus, GenusCount, ModeCount, SharedCount, CorpusLedger, CorpusFullCount, CorpusQuery, Census, CensusQuery, CensusBy, CensusGroup, CensusGroupProfile, CensusNeighbor, Temperamentum, TemperamentumInput, TemperamentumOpts, Tuning, Pitch, PitchInput, Step, Neume, NeumeShape, Interval, ModeData, CadenceFigure, Modus, TunedNote, GamutOptions, Tonus, TonusOpts, Score, ScoreOpts, PondusInput, PondusOpts, AccentusInput, AccentusOpts, Cadence, CadenceTarget, CadenceApproach, Modulation, InscriptioOpts, Inscriptio, NoteGeometry, FontSpec, FontSlot, FontEmbed, ChantTabulaRow, Note, Performance, Phrase, Syllable, LyricRun, RestEvent, ParseError, ArsisThesis, RhythmicType, CompoundBeat, VoicedPitch, Cosmos, CosmosQuery, Body, BodyName, Aspect, Imprint, Attractor, VowelAttractor, ModalAffinity,
|
|
61
|
+
export type { Feast, FeastQuery, Pascha, Season, Grade, Chant, CantusQuery, OrdinaryChant, PropriumQuery, OrdinariumQuery, OfficiumQuery, PsalmusQuery, Corpus, GenusCount, ModeCount, SharedCount, CorpusLedger, CorpusFullCount, CorpusQuery, Census, CensusQuery, CensusBy, CensusGroup, CensusGroupProfile, CensusNeighbor, Temperamentum, TemperamentumInput, TemperamentumOpts, Tuning, Pitch, PitchInput, Step, Neume, NeumeShape, Interval, ModeData, CadenceFigure, Modus, TunedNote, GamutOptions, Tonus, TonusOpts, Score, ScoreOpts, PondusInput, PondusOpts, AccentusInput, AccentusOpts, Cadence, CadenceTarget, CadenceApproach, Modulation, InscriptioOpts, Inscriptio, NoteGeometry, FontSpec, FontSlot, FontEmbed, ChantTabulaRow, Note, Performance, Phrase, Syllable, LyricRun, RestEvent, ParseError, ArsisThesis, RhythmicType, CompoundBeat, VoicedPitch, Cosmos, CosmosQuery, Body, BodyName, Aspect, Imprint, Attractor, VowelAttractor, ModalAffinity, Metrics, RhythmicProfile, NoteRange, CadenceDistribution, Harmony, HarmoniaOpts, VoicedBody, VoicedAspect, Frame, Author, HarmonyTabulaRow, PlanetVowel, };
|
|
62
62
|
//# sourceMappingURL=index.d.ts.map
|
package/docs/api/chant.md
CHANGED
|
@@ -95,9 +95,9 @@ tonus.corpus();
|
|
|
95
95
|
```
|
|
96
96
|
|
|
97
97
|
**`count` is the number of chants** — the one to quote. `listings` is how long
|
|
98
|
-
the shelf is
|
|
99
|
-
|
|
100
|
-
`count` does, so `genera` and `modes` sum to it.
|
|
98
|
+
the shelf is: a melody printed in several books is stored once and listed under
|
|
99
|
+
each, so the shelf runs longer than the repertory. The breakdowns describe the
|
|
100
|
+
same population `count` does, so `genera` and `modes` sum to it.
|
|
101
101
|
|
|
102
102
|
```js
|
|
103
103
|
tonus.corpus("am");
|
|
@@ -313,9 +313,9 @@ for that chant. The two prefixes in the shipped corpus are `gregobase:` (1,717)
|
|
|
313
313
|
and `nocturnale:` (470), the latter carrying the Nocturnale's own alphanumeric
|
|
314
314
|
keys rather than numbers.
|
|
315
315
|
|
|
316
|
-
Within tonus an id is exactly one chant. A melody printed in several books
|
|
317
|
-
|
|
318
|
-
|
|
316
|
+
Within tonus an id is exactly one chant. A melody printed in several books is
|
|
317
|
+
stored once, under the record `cantus({ id })` returns, so `id` is a stable key
|
|
318
|
+
to a chant rather than to a printing.
|
|
319
319
|
|
|
320
320
|
## The repertoire as of a date — the era view
|
|
321
321
|
|
|
@@ -601,7 +601,7 @@ phrase punctuation ([score.md](score.md#the-tabula)).
|
|
|
601
601
|
|
|
602
602
|
Because the encoding is textual, lyrics and neumes stay aligned syllable
|
|
603
603
|
by syllable, which is what lets `tonus.notatio` reconstruct syllables,
|
|
604
|
-
neumes, and
|
|
604
|
+
neumes, and metrics without images.
|
|
605
605
|
|
|
606
606
|
### The Mass: proper and ordinary
|
|
607
607
|
|
package/docs/api/index.md
CHANGED
|
@@ -4,7 +4,7 @@ The technical center of tonus: the full public API, the conventions every method
|
|
|
4
4
|
obeys, and the error contract. The API is **fourteen methods on the `tonus`
|
|
5
5
|
namespace**, no sub-namespaces.
|
|
6
6
|
|
|
7
|
-
**[
|
|
7
|
+
**[Orreliquum — the library at work →](https://jeffreypierce.github.io/orreliquum/)**
|
|
8
8
|
|
|
9
9
|
```js
|
|
10
10
|
import tonus from "tonus";
|
|
@@ -173,7 +173,7 @@ the list resolve their pitches through the ones before.
|
|
|
173
173
|
- [The tabula](score.md#the-tabula)
|
|
174
174
|
- [Rendering — `inscriptio`](score.md#rendering) · [theme](score.md#theme--faces-and-ink) · [The analysis tracks](score.md#the-analysis-tracks)
|
|
175
175
|
- [The imprint](score.md#the-imprint)
|
|
176
|
-
- [
|
|
176
|
+
- [Metrics](score.md#metrics)
|
|
177
177
|
- [Cadences](score.md#cadences) · [One spine, two annotations](score.md#one-spine-two-annotations)
|
|
178
178
|
- [Modulations](score.md#modulations)
|
|
179
179
|
- [Theory & Context](score.md#theory--context) · [Sources](score.md#sources)
|