tonus 0.1.6 → 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 +610 -1
- package/LICENSE +133 -29
- package/README.md +106 -83
- package/dist/data/am.js +2669 -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 +24 -0
- package/dist/data/corpus-overlap.js +347 -0
- 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 +757 -6394
- package/dist/data/kyriale.js +116 -116
- package/dist/data/la.js +802 -13419
- package/dist/data/lh.js +116 -3473
- package/dist/data/lu.js +935 -17632
- package/dist/data/nocturnale-romanum.d.ts +5 -0
- package/dist/data/nocturnale-romanum.js +3772 -0
- 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 +22 -4
- package/dist/engines/chant/chant.js +274 -14
- 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 +145 -10
- 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 +28 -6
- package/dist/index.js +39 -3
- 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-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/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,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
|