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,33 +0,0 @@
|
|
|
1
|
-
import type { Chant, OfficiumQuery } from "./types.js";
|
|
2
|
-
/** One nocturn of a resolved Matins: its responsories (and any antiphons). */
|
|
3
|
-
export interface Nocturn {
|
|
4
|
-
/** 1–3. */
|
|
5
|
-
n: number;
|
|
6
|
-
/** The nocturn's great responsories, in order, as resolved chants. */
|
|
7
|
-
responsories: Chant[];
|
|
8
|
-
/** The nocturn's antiphons, where the Nocturnale carries them (else empty). */
|
|
9
|
-
antiphons: Chant[];
|
|
10
|
-
}
|
|
11
|
-
/** A feast's structured Roman Matins. */
|
|
12
|
-
export interface Matins {
|
|
13
|
-
/** The tonus feast id this Matins was resolved for. */
|
|
14
|
-
feastId: string;
|
|
15
|
-
/** The feast's Latin name from the Nocturnale. */
|
|
16
|
-
name: string;
|
|
17
|
-
/** The rank/class, e.g. "I. classis", "Feria". */
|
|
18
|
-
rank: string;
|
|
19
|
-
/** One nocturn (simple) or three (festal). */
|
|
20
|
-
nocturns: Nocturn[];
|
|
21
|
-
/** The feast whose chants were borrowed by rubric, if any (provenance). */
|
|
22
|
-
redirectedFrom: string | null;
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Structured Roman Matins (`tonus.matutinum`) for a feast: the nocturns with
|
|
26
|
-
* their responsories (and antiphons where present), assembled from the
|
|
27
|
-
* Nocturnale Romanum. Without a feast, resolves the default epoch's feast (as
|
|
28
|
-
* `officium` does). Only the Roman rite is served today; other rites return
|
|
29
|
-
* null (the monastic night office is not yet modelled — see office-matins-roman).
|
|
30
|
-
* @returns the Matins structure, or null when no Nocturnale match exists.
|
|
31
|
-
*/
|
|
32
|
-
export declare function getMatins(query?: OfficiumQuery): Matins | null;
|
|
33
|
-
//# sourceMappingURL=matutinum.d.ts.map
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// engines/chant/matutinum — structured Roman Matins (nocturns)
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
// The flat `officium({ hora: "matutinum" })` returns Matins chants as an
|
|
5
|
-
// undifferentiated Chant[] (the best-effort flat view, unchanged). This module
|
|
6
|
-
// adds the STRUCTURED night office for the Roman rite: the 3-nocturn assembly
|
|
7
|
-
// from the Nocturnale Romanum, joined to the tonus calendar by feast id.
|
|
8
|
-
//
|
|
9
|
-
// It is additive and separate — a distinct accessor (`tonus.matutinum`), not a
|
|
10
|
-
// reshape of getHour — so the flat path and every other rite stay as they are.
|
|
11
|
-
// Coverage is what the Nocturnale + the calendar bridge give: the sanctorale and
|
|
12
|
-
// Advent today (see office-matins-roman.ts). A feast with no Nocturnale match
|
|
13
|
-
// returns null, the office's graceful-degradation convention.
|
|
14
|
-
import { resolveChant } from "./chant.js";
|
|
15
|
-
import { getFeast } from "../cal/calendar.js";
|
|
16
|
-
import { temporaSundayId } from "../cal/date.js";
|
|
17
|
-
import { MATINS_ROMAN } from "../../data/office-matins-roman.js";
|
|
18
|
-
let _byFeast = null;
|
|
19
|
-
function byFeast() {
|
|
20
|
-
if (!_byFeast) {
|
|
21
|
-
_byFeast = new Map();
|
|
22
|
-
for (const d of MATINS_ROMAN)
|
|
23
|
-
if (d.tonusFeastId)
|
|
24
|
-
_byFeast.set(d.tonusFeastId, d);
|
|
25
|
-
}
|
|
26
|
-
return _byFeast;
|
|
27
|
-
}
|
|
28
|
-
function resolveDay(day) {
|
|
29
|
-
const nocturns = day.nocturns.map((noc) => {
|
|
30
|
-
const responsories = [];
|
|
31
|
-
const antiphons = [];
|
|
32
|
-
for (const c of noc.chants) {
|
|
33
|
-
const chant = resolveChant(c.id);
|
|
34
|
-
if (!chant)
|
|
35
|
-
continue;
|
|
36
|
-
if (c.type === "A")
|
|
37
|
-
antiphons.push(chant);
|
|
38
|
-
else
|
|
39
|
-
responsories.push(chant); // R (and I invitatory) resolve as responsories
|
|
40
|
-
}
|
|
41
|
-
return { n: noc.n, responsories, antiphons };
|
|
42
|
-
});
|
|
43
|
-
return {
|
|
44
|
-
feastId: day.tonusFeastId,
|
|
45
|
-
name: day.name,
|
|
46
|
-
rank: day.rank,
|
|
47
|
-
nocturns,
|
|
48
|
-
redirectedFrom: day.redirectedFrom,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
/** The Nocturnale Matins for a feast id, or null when the calendar has no match. */
|
|
52
|
-
function matinsForFeastId(feastId) {
|
|
53
|
-
const map = byFeast();
|
|
54
|
-
const day = map.get(feastId) ?? (() => {
|
|
55
|
-
// Temporal feasts resolve their Sunday where a weekday has no proper.
|
|
56
|
-
const sunday = temporaSundayId(feastId);
|
|
57
|
-
return sunday ? map.get(sunday) : undefined;
|
|
58
|
-
})();
|
|
59
|
-
return day ? resolveDay(day) : null;
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Structured Roman Matins (`tonus.matutinum`) for a feast: the nocturns with
|
|
63
|
-
* their responsories (and antiphons where present), assembled from the
|
|
64
|
-
* Nocturnale Romanum. Without a feast, resolves the default epoch's feast (as
|
|
65
|
-
* `officium` does). Only the Roman rite is served today; other rites return
|
|
66
|
-
* null (the monastic night office is not yet modelled — see office-matins-roman).
|
|
67
|
-
* @returns the Matins structure, or null when no Nocturnale match exists.
|
|
68
|
-
*/
|
|
69
|
-
export function getMatins(query) {
|
|
70
|
-
const rite = query?.rite ?? "romanum";
|
|
71
|
-
if (rite !== "romanum")
|
|
72
|
-
return null;
|
|
73
|
-
const feasts = query?.feast
|
|
74
|
-
? Array.isArray(query.feast) ? query.feast : [query.feast]
|
|
75
|
-
: null;
|
|
76
|
-
const feast = feasts ? feasts[0] : getFeast()[0];
|
|
77
|
-
if (!feast)
|
|
78
|
-
return null;
|
|
79
|
-
return matinsForFeastId(feast.id);
|
|
80
|
-
}
|
|
81
|
-
//# sourceMappingURL=matutinum.js.map
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import type { ChantTabulaRow } from "../tabula.js";
|
|
2
|
-
/** Lightweight diagnostic record. */
|
|
3
|
-
interface Diagnostic {
|
|
4
|
-
code?: string;
|
|
5
|
-
message: string;
|
|
6
|
-
}
|
|
7
|
-
export interface MidiOpts {
|
|
8
|
-
/** "file" (default) → Uint8Array; "json" → event structure; "both" → object. */
|
|
9
|
-
format?: "file" | "json" | "both";
|
|
10
|
-
ppq?: number;
|
|
11
|
-
tempoBpm?: number;
|
|
12
|
-
channel?: number;
|
|
13
|
-
/** Fallback 0–127 velocity when a row has no phrasing velocity. */
|
|
14
|
-
velocity?: number;
|
|
15
|
-
includeMeta?: boolean;
|
|
16
|
-
trackName?: string;
|
|
17
|
-
emitPitchBend?: boolean;
|
|
18
|
-
transpose?: number;
|
|
19
|
-
}
|
|
20
|
-
export type MidiJsonEvent = {
|
|
21
|
-
type: "noteOn";
|
|
22
|
-
tick: number;
|
|
23
|
-
pitch: number;
|
|
24
|
-
velocity: number;
|
|
25
|
-
channel: number;
|
|
26
|
-
hz?: number;
|
|
27
|
-
} | {
|
|
28
|
-
type: "noteOff";
|
|
29
|
-
tick: number;
|
|
30
|
-
pitch: number;
|
|
31
|
-
velocity: number;
|
|
32
|
-
channel: number;
|
|
33
|
-
} | {
|
|
34
|
-
type: "pitchBend";
|
|
35
|
-
tick: number;
|
|
36
|
-
channel: number;
|
|
37
|
-
value14: number;
|
|
38
|
-
} | {
|
|
39
|
-
type: "meta";
|
|
40
|
-
tick: number;
|
|
41
|
-
metaType: "tempo" | "trackName";
|
|
42
|
-
value: number | string;
|
|
43
|
-
};
|
|
44
|
-
export interface MidiJsonTrack {
|
|
45
|
-
name?: string;
|
|
46
|
-
events: MidiJsonEvent[];
|
|
47
|
-
}
|
|
48
|
-
export interface MidiJsonResult {
|
|
49
|
-
ppq: number;
|
|
50
|
-
tempoBpm: number;
|
|
51
|
-
tracks: MidiJsonTrack[];
|
|
52
|
-
diagnostics: Diagnostic[];
|
|
53
|
-
}
|
|
54
|
-
export interface MidiEmitResult {
|
|
55
|
-
json?: MidiJsonResult;
|
|
56
|
-
bytes?: Uint8Array;
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Emit a Standard MIDI File from a score's tabula rows. Returns the file bytes
|
|
60
|
-
* (`Uint8Array`) by default; `format: "json"` returns the event structure and
|
|
61
|
-
* `"both"` returns `{ json, bytes }`.
|
|
62
|
-
*/
|
|
63
|
-
export declare function toMidi(rows: ChantTabulaRow[], options?: MidiOpts): Uint8Array | MidiEmitResult;
|
|
64
|
-
export {};
|
|
65
|
-
//# sourceMappingURL=midi.d.ts.map
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
// Divisio → rest duration (in the same beat units as note durations); a phrase's
|
|
2
|
-
// terminal divisio becomes a rest of this length. This is the *durational*
|
|
3
|
-
// reading of the divisio hierarchy (canonical table in docs/score.md), distinct
|
|
4
|
-
// from prosody.ts's analytic weights and phrasing.ts's shaping strengths — three
|
|
5
|
-
// readings of the same bar-lines, each for its own purpose. Values mirror
|
|
6
|
-
// DIVISIO_DURATIONS in parse.ts (the source of the fractional beat lengths).
|
|
7
|
-
const DIVISIO_REST = {
|
|
8
|
-
",": 0.54, "`": 0.33, ";": 0.8, ":": 1.1, "::": 1.8,
|
|
9
|
-
};
|
|
10
|
-
const DEFAULT_PPQ = 480;
|
|
11
|
-
const DEFAULT_TEMPO = 120;
|
|
12
|
-
const DEFAULT_CHANNEL = 0;
|
|
13
|
-
const DEFAULT_VELOCITY = 80;
|
|
14
|
-
const PITCH_BEND_CENTER = 8192;
|
|
15
|
-
function resolveOpts(options) {
|
|
16
|
-
return {
|
|
17
|
-
format: options.format ?? "file",
|
|
18
|
-
ppq: options.ppq ?? DEFAULT_PPQ,
|
|
19
|
-
tempoBpm: options.tempoBpm ?? DEFAULT_TEMPO,
|
|
20
|
-
channel: options.channel ?? DEFAULT_CHANNEL,
|
|
21
|
-
velocity: options.velocity ?? DEFAULT_VELOCITY,
|
|
22
|
-
includeMeta: options.includeMeta ?? true,
|
|
23
|
-
trackName: options.trackName ?? "tonus",
|
|
24
|
-
emitPitchBend: options.emitPitchBend ?? true,
|
|
25
|
-
transpose: options.transpose ?? 0,
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
function encodeVarLen(value) {
|
|
29
|
-
let val = Math.max(0, value | 0);
|
|
30
|
-
const bytes = [val & 0x7f];
|
|
31
|
-
while ((val >>= 7) > 0)
|
|
32
|
-
bytes.unshift((val & 0x7f) | 0x80);
|
|
33
|
-
return bytes;
|
|
34
|
-
}
|
|
35
|
-
function writeUInt32BE(value) {
|
|
36
|
-
return [(value >>> 24) & 0xff, (value >>> 16) & 0xff, (value >>> 8) & 0xff, value & 0xff];
|
|
37
|
-
}
|
|
38
|
-
function writeUInt16BE(value) {
|
|
39
|
-
return [(value >>> 8) & 0xff, value & 0xff];
|
|
40
|
-
}
|
|
41
|
-
function eventPriority(event) {
|
|
42
|
-
if (event.type === "meta")
|
|
43
|
-
return 0;
|
|
44
|
-
if (event.type === "pitchBend")
|
|
45
|
-
return 1;
|
|
46
|
-
if (event.type === "noteOff")
|
|
47
|
-
return 2;
|
|
48
|
-
return 3;
|
|
49
|
-
}
|
|
50
|
-
function clampToMidiPitch(step, diagnostics) {
|
|
51
|
-
if (step < 0 || step > 127) {
|
|
52
|
-
diagnostics.push({
|
|
53
|
-
code: "MIDI_PITCH_CLAMPED",
|
|
54
|
-
message: `Pitch ${step} clamped to MIDI range 0..127`,
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
return Math.min(127, Math.max(0, step));
|
|
58
|
-
}
|
|
59
|
-
function buildJson(rows, opts, diagnostics) {
|
|
60
|
-
const events = [];
|
|
61
|
-
let tick = 0;
|
|
62
|
-
if (opts.includeMeta) {
|
|
63
|
-
events.push({ type: "meta", tick: 0, metaType: "tempo", value: opts.tempoBpm });
|
|
64
|
-
events.push({ type: "meta", tick: 0, metaType: "trackName", value: opts.trackName });
|
|
65
|
-
}
|
|
66
|
-
let prevPhrase = null;
|
|
67
|
-
for (const row of rows) {
|
|
68
|
-
// A phrase boundary inserts the previous phrase's terminal divisio as a rest.
|
|
69
|
-
if (prevPhrase !== null && row.phraseIndex !== prevPhrase) {
|
|
70
|
-
const restDivisio = rows.find((r) => r.phraseIndex === prevPhrase)?.divisio ?? null;
|
|
71
|
-
const restBeats = restDivisio ? (DIVISIO_REST[restDivisio] ?? 0) : 0;
|
|
72
|
-
tick += Math.max(0, Math.round(restBeats * opts.ppq));
|
|
73
|
-
}
|
|
74
|
-
prevPhrase = row.phraseIndex;
|
|
75
|
-
const pitch = clampToMidiPitch(row.midi + opts.transpose, diagnostics);
|
|
76
|
-
const durationTicks = Math.max(1, Math.round(row.shapedDuration * opts.ppq));
|
|
77
|
-
const velocity = row.velocity != null && row.velocity > 0
|
|
78
|
-
? Math.round(row.velocity * 127)
|
|
79
|
-
: opts.velocity;
|
|
80
|
-
const bends = opts.emitPitchBend && Math.abs(row.bend - PITCH_BEND_CENTER) > 1;
|
|
81
|
-
if (bends) {
|
|
82
|
-
events.push({ type: "pitchBend", tick, channel: opts.channel, value14: row.bend });
|
|
83
|
-
}
|
|
84
|
-
events.push({ type: "noteOn", tick, pitch, velocity, channel: opts.channel, hz: row.hz });
|
|
85
|
-
events.push({ type: "noteOff", tick: tick + durationTicks, pitch, velocity: 0, channel: opts.channel });
|
|
86
|
-
if (bends) {
|
|
87
|
-
events.push({ type: "pitchBend", tick: tick + durationTicks, channel: opts.channel, value14: PITCH_BEND_CENTER });
|
|
88
|
-
}
|
|
89
|
-
tick += durationTicks;
|
|
90
|
-
}
|
|
91
|
-
events.sort((a, b) => a.tick - b.tick || eventPriority(a) - eventPriority(b));
|
|
92
|
-
return {
|
|
93
|
-
ppq: opts.ppq,
|
|
94
|
-
tempoBpm: opts.tempoBpm,
|
|
95
|
-
tracks: [{ name: opts.trackName, events }],
|
|
96
|
-
diagnostics,
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
function buildMidiBytes(json) {
|
|
100
|
-
const trackEvents = json.tracks[0]?.events ?? [];
|
|
101
|
-
const trackData = [];
|
|
102
|
-
let lastTick = 0;
|
|
103
|
-
for (const event of trackEvents) {
|
|
104
|
-
const delta = Math.max(0, event.tick - lastTick);
|
|
105
|
-
lastTick = event.tick;
|
|
106
|
-
trackData.push(...encodeVarLen(delta));
|
|
107
|
-
if (event.type === "meta") {
|
|
108
|
-
if (event.metaType === "tempo") {
|
|
109
|
-
const mpqn = Math.max(1, Math.round(60000000 / Number(event.value)));
|
|
110
|
-
trackData.push(0xff, 0x51, 0x03, (mpqn >>> 16) & 0xff, (mpqn >>> 8) & 0xff, mpqn & 0xff);
|
|
111
|
-
}
|
|
112
|
-
else if (event.metaType === "trackName") {
|
|
113
|
-
const textBytes = Array.from(new TextEncoder().encode(String(event.value)));
|
|
114
|
-
trackData.push(0xff, 0x03, ...encodeVarLen(textBytes.length), ...textBytes);
|
|
115
|
-
}
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
if (event.type === "pitchBend") {
|
|
119
|
-
const lsb = event.value14 & 0x7f;
|
|
120
|
-
const msb = (event.value14 >> 7) & 0x7f;
|
|
121
|
-
trackData.push(0xe0 | (event.channel & 0x0f), lsb, msb);
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
if (event.type === "noteOn") {
|
|
125
|
-
trackData.push(0x90 | (event.channel & 0x0f), event.pitch & 0x7f, event.velocity & 0x7f);
|
|
126
|
-
}
|
|
127
|
-
else {
|
|
128
|
-
trackData.push(0x80 | (event.channel & 0x0f), event.pitch & 0x7f, event.velocity & 0x7f);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
// End-of-track meta event
|
|
132
|
-
trackData.push(0x00, 0xff, 0x2f, 0x00);
|
|
133
|
-
const headerChunk = [
|
|
134
|
-
0x4d, 0x54, 0x68, 0x64, // "MThd"
|
|
135
|
-
...writeUInt32BE(6), // chunk length
|
|
136
|
-
...writeUInt16BE(0), // format 0
|
|
137
|
-
...writeUInt16BE(1), // 1 track
|
|
138
|
-
...writeUInt16BE(json.ppq),
|
|
139
|
-
];
|
|
140
|
-
const trackChunk = [
|
|
141
|
-
0x4d, 0x54, 0x72, 0x6b, // "MTrk"
|
|
142
|
-
...writeUInt32BE(trackData.length),
|
|
143
|
-
...trackData,
|
|
144
|
-
];
|
|
145
|
-
return Uint8Array.from([...headerChunk, ...trackChunk]);
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* Emit a Standard MIDI File from a score's tabula rows. Returns the file bytes
|
|
149
|
-
* (`Uint8Array`) by default; `format: "json"` returns the event structure and
|
|
150
|
-
* `"both"` returns `{ json, bytes }`.
|
|
151
|
-
*/
|
|
152
|
-
export function toMidi(rows, options = {}) {
|
|
153
|
-
const opts = resolveOpts(options);
|
|
154
|
-
const diagnostics = [];
|
|
155
|
-
const json = buildJson(rows, opts, diagnostics);
|
|
156
|
-
if (opts.format === "file")
|
|
157
|
-
return buildMidiBytes(json);
|
|
158
|
-
if (opts.format === "json")
|
|
159
|
-
return { json };
|
|
160
|
-
return { json, bytes: buildMidiBytes(json) };
|
|
161
|
-
}
|
|
162
|
-
//# sourceMappingURL=midi.js.map
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import type { ChantTabulaRow } from "../tabula.js";
|
|
2
|
-
import type { Chant } from "../../chant/types.js";
|
|
3
|
-
export interface MusicXmlOpts {
|
|
4
|
-
/** Emit the arsis/thesis shape + index as an other-notation annotation. */
|
|
5
|
-
emitWeights?: boolean;
|
|
6
|
-
}
|
|
7
|
-
export interface MusicXmlEmitResult {
|
|
8
|
-
xml: string;
|
|
9
|
-
diagnostics: {
|
|
10
|
-
message: string;
|
|
11
|
-
}[];
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Emit a MusicXML 4.0 partwise document from a score's tabula rows plus the
|
|
15
|
-
* source chant (for titling). Returns `{ xml, diagnostics }`.
|
|
16
|
-
*/
|
|
17
|
-
export declare function toMusicXML(rows: ChantTabulaRow[], chant: Chant, options?: MusicXmlOpts): MusicXmlEmitResult;
|
|
18
|
-
//# sourceMappingURL=musicxml.d.ts.map
|
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
// ── XML helpers ──
|
|
2
|
-
function xmlEscape(s) {
|
|
3
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
|
4
|
-
.replace(/"/g, """).replace(/'/g, "'");
|
|
5
|
-
}
|
|
6
|
-
function elem(tag, content) {
|
|
7
|
-
return `<${tag}>${content}</${tag}>`;
|
|
8
|
-
}
|
|
9
|
-
// ── Pitch / accidental from a tabula row ──
|
|
10
|
-
function xmlStep(row) {
|
|
11
|
-
// spn is e.g. "D4", "Bb3" — the first character is the step letter.
|
|
12
|
-
return row.spn[0] ?? "C";
|
|
13
|
-
}
|
|
14
|
-
function xmlAccidentalGlyph(row) {
|
|
15
|
-
// Only an explicitly-marked accidental prints a glyph.
|
|
16
|
-
if (row.accidentalSource !== "explicit")
|
|
17
|
-
return null;
|
|
18
|
-
if (row.accidental === -1)
|
|
19
|
-
return "flat";
|
|
20
|
-
if (row.accidental === 1)
|
|
21
|
-
return "sharp";
|
|
22
|
-
return "natural";
|
|
23
|
-
}
|
|
24
|
-
const BARLINE_STYLE = {
|
|
25
|
-
",": "dashed", "`": "dotted", ";": "normal", ":": "light-light", "::": "light-heavy",
|
|
26
|
-
};
|
|
27
|
-
function computeSyllabic(lyric) {
|
|
28
|
-
const s = lyric.startsWith("-"), e = lyric.endsWith("-");
|
|
29
|
-
if (!s && !e)
|
|
30
|
-
return "single";
|
|
31
|
-
if (!s && e)
|
|
32
|
-
return "begin";
|
|
33
|
-
if (s && e)
|
|
34
|
-
return "middle";
|
|
35
|
-
return "end";
|
|
36
|
-
}
|
|
37
|
-
function renderNote(row, flags, ctx) {
|
|
38
|
-
const lines = [` <note>`];
|
|
39
|
-
lines.push(` <pitch>`);
|
|
40
|
-
lines.push(` ${elem("step", xmlStep(row))}`);
|
|
41
|
-
if (row.accidental !== 0)
|
|
42
|
-
lines.push(` ${elem("alter", String(row.accidental))}`);
|
|
43
|
-
lines.push(` ${elem("octave", String(row.octave))}`);
|
|
44
|
-
lines.push(` </pitch>`);
|
|
45
|
-
lines.push(` ${elem("duration", "4")}`);
|
|
46
|
-
lines.push(` ${elem("type", "eighth")}`);
|
|
47
|
-
const glyph = xmlAccidentalGlyph(row);
|
|
48
|
-
if (glyph)
|
|
49
|
-
lines.push(` ${elem("accidental", glyph)}`);
|
|
50
|
-
if (row.quilisma)
|
|
51
|
-
lines.push(` ${elem("notehead", "diamond")}`);
|
|
52
|
-
const notations = [];
|
|
53
|
-
// Slurs bind a single neume figure (pes, clivis, torculus …), so a syllable
|
|
54
|
-
// built of several figures shows one arc per figure.
|
|
55
|
-
if (flags.slurStart)
|
|
56
|
-
notations.push(` <slur type="start" number="1"/>`);
|
|
57
|
-
if (flags.slurStop)
|
|
58
|
-
notations.push(` <slur type="stop" number="1"/>`);
|
|
59
|
-
if (row.ictus) {
|
|
60
|
-
notations.push(` <articulations>`, ` <accent/>`, ` </articulations>`);
|
|
61
|
-
}
|
|
62
|
-
const technical = [];
|
|
63
|
-
if (row.quilisma)
|
|
64
|
-
technical.push(` <other-technical>quilisma</other-technical>`);
|
|
65
|
-
if (row.liquescent)
|
|
66
|
-
technical.push(` <other-technical>liquescent</other-technical>`);
|
|
67
|
-
if (row.strophicus)
|
|
68
|
-
technical.push(` <other-technical>strophicus</other-technical>`);
|
|
69
|
-
if (technical.length) {
|
|
70
|
-
notations.push(` <technical>`, ...technical, ` </technical>`);
|
|
71
|
-
}
|
|
72
|
-
if (flags.syllableStart && row.neume.type !== "punctum") {
|
|
73
|
-
notations.push(` <other-notation type="start">${xmlEscape(row.neume.type)}</other-notation>`);
|
|
74
|
-
}
|
|
75
|
-
if (ctx.emitWeights) {
|
|
76
|
-
notations.push(` <other-notation type="start">shape:${row.rhythmicShape} index:${row.rhythmicIndex}</other-notation>`);
|
|
77
|
-
}
|
|
78
|
-
if (notations.length) {
|
|
79
|
-
lines.push(` <notations>`, ...notations, ` </notations>`);
|
|
80
|
-
}
|
|
81
|
-
if (flags.syllableStart) {
|
|
82
|
-
const syllabic = computeSyllabic(row.lyric);
|
|
83
|
-
const clean = row.lyric.replace(/^-+/, "").replace(/-+$/, "").trim();
|
|
84
|
-
lines.push(` <lyric number="1">`, ` ${elem("syllabic", syllabic)}`, ` ${elem("text", xmlEscape(clean))}`, ` </lyric>`);
|
|
85
|
-
}
|
|
86
|
-
lines.push(` </note>`);
|
|
87
|
-
return lines.join("\n");
|
|
88
|
-
}
|
|
89
|
-
function renderMeasure(rows, measureNumber, isFirst, ctx) {
|
|
90
|
-
const lines = [` <measure number="${measureNumber}">`];
|
|
91
|
-
if (isFirst) {
|
|
92
|
-
lines.push(` <attributes>`, ` ${elem("divisions", "8")}`, ` <key>`, ` ${elem("fifths", "0")}`, ` </key>`, ` <time symbol="cut">`, ` ${elem("beats", "4")}`, ` ${elem("beat-type", "4")}`, ` </time>`, ` <clef>`, ` ${elem("sign", "G")}`, ` ${elem("line", "2")}`, ` </clef>`, ` </attributes>`);
|
|
93
|
-
}
|
|
94
|
-
// Walk the phrase's rows. The lyric attaches at each syllable start; slurs
|
|
95
|
-
// bind each neume figure (a syllable may hold several), so they are grouped
|
|
96
|
-
// by (syllableIndex, neumeGroup) and only drawn when a figure has >1 note.
|
|
97
|
-
const sameFigure = (a, b) => a.syllableIndex === b.syllableIndex && a.neumeGroup === b.neumeGroup;
|
|
98
|
-
for (let k = 0; k < rows.length; k++) {
|
|
99
|
-
const row = rows[k];
|
|
100
|
-
const prev = rows[k - 1];
|
|
101
|
-
const next = rows[k + 1];
|
|
102
|
-
const figureStart = !prev || !sameFigure(prev, row);
|
|
103
|
-
const figureEnd = !next || !sameFigure(next, row);
|
|
104
|
-
const figureIsMulti = !(figureStart && figureEnd); // >1 note in this figure
|
|
105
|
-
const syllableStart = !prev || prev.syllableIndex !== row.syllableIndex;
|
|
106
|
-
lines.push(renderNote(row, {
|
|
107
|
-
slurStart: figureIsMulti && figureStart,
|
|
108
|
-
slurStop: figureIsMulti && figureEnd,
|
|
109
|
-
syllableStart,
|
|
110
|
-
}, ctx));
|
|
111
|
-
}
|
|
112
|
-
const divisio = rows[rows.length - 1]?.divisio ?? "::";
|
|
113
|
-
const style = BARLINE_STYLE[divisio] ?? "normal";
|
|
114
|
-
lines.push(` <barline location="right">`, ` <bar-style>${style}</bar-style>`, ` </barline>`);
|
|
115
|
-
lines.push(` </measure>`);
|
|
116
|
-
return lines.join("\n");
|
|
117
|
-
}
|
|
118
|
-
function renderScore(rows, chant, ctx) {
|
|
119
|
-
const parts = [
|
|
120
|
-
`<?xml version="1.0" encoding="UTF-8"?>`,
|
|
121
|
-
`<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 4.0 Partwise//EN"`,
|
|
122
|
-
` "http://www.musicxml.org/dtds/partwise.dtd">`,
|
|
123
|
-
`<score-partwise version="4.0">`,
|
|
124
|
-
];
|
|
125
|
-
if (chant.incipit) {
|
|
126
|
-
parts.push(` <work>`, ` ${elem("work-title", xmlEscape(chant.incipit))}`, ` </work>`);
|
|
127
|
-
}
|
|
128
|
-
parts.push(` <identification>`);
|
|
129
|
-
parts.push(` <encoding>`);
|
|
130
|
-
parts.push(` ${elem("software", "tonus")}`, ` </encoding>`, ` </identification>`);
|
|
131
|
-
for (const text of [
|
|
132
|
-
chant.mode ? `Mode ${chant.mode}` : null,
|
|
133
|
-
chant.office,
|
|
134
|
-
]) {
|
|
135
|
-
if (text)
|
|
136
|
-
parts.push(` <credit page="1">`, ` ${elem("credit-words", xmlEscape(text))}`, ` </credit>`);
|
|
137
|
-
}
|
|
138
|
-
// Group rows into measures by phrase index.
|
|
139
|
-
const measures = [];
|
|
140
|
-
let i = 0;
|
|
141
|
-
let measureNumber = 1;
|
|
142
|
-
while (i < rows.length) {
|
|
143
|
-
const phraseIndex = rows[i].phraseIndex;
|
|
144
|
-
let j = i;
|
|
145
|
-
while (j < rows.length && rows[j].phraseIndex === phraseIndex)
|
|
146
|
-
j++;
|
|
147
|
-
measures.push(renderMeasure(rows.slice(i, j), measureNumber, measureNumber === 1, ctx));
|
|
148
|
-
measureNumber++;
|
|
149
|
-
i = j;
|
|
150
|
-
}
|
|
151
|
-
parts.push(` <part-list>`, ` <score-part id="P1">`, ` ${elem("part-name", "Chant")}`, ` </score-part>`, ` </part-list>`, ` <part id="P1">`, ...measures, ` </part>`, `</score-partwise>`);
|
|
152
|
-
return parts.join("\n");
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Emit a MusicXML 4.0 partwise document from a score's tabula rows plus the
|
|
156
|
-
* source chant (for titling). Returns `{ xml, diagnostics }`.
|
|
157
|
-
*/
|
|
158
|
-
export function toMusicXML(rows, chant, options = {}) {
|
|
159
|
-
const diagnostics = [];
|
|
160
|
-
const ctx = { emitWeights: options.emitWeights ?? false };
|
|
161
|
-
const xml = rows.length
|
|
162
|
-
? renderScore(rows, chant, ctx)
|
|
163
|
-
: renderScore([], chant, ctx);
|
|
164
|
-
return { xml, diagnostics };
|
|
165
|
-
}
|
|
166
|
-
//# sourceMappingURL=musicxml.js.map
|