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.
Files changed (164) hide show
  1. package/BIBLIOGRAPHY.md +132 -108
  2. package/CHANGELOG.md +598 -1
  3. package/LICENSE +133 -29
  4. package/README.md +106 -83
  5. package/dist/data/am.js +2666 -11196
  6. package/dist/data/ams.d.ts +5 -0
  7. package/dist/data/ams.js +122 -0
  8. package/dist/data/attestation.d.ts +19 -0
  9. package/dist/data/attestation.js +15716 -0
  10. package/dist/data/attestation.json +15711 -0
  11. package/dist/data/cadentiae.d.ts +43 -0
  12. package/dist/data/cadentiae.js +174 -0
  13. package/dist/data/cal.js +48 -0
  14. package/dist/data/census.d.ts +12 -0
  15. package/dist/data/census.js +36 -0
  16. package/dist/data/commune-office.d.ts +4 -0
  17. package/dist/data/commune-office.js +2371 -0
  18. package/dist/data/commune-office.json +2365 -0
  19. package/dist/data/commune.js +181 -11
  20. package/dist/data/corpus-overlap.d.ts +17 -0
  21. package/dist/data/corpus-overlap.js +299 -5
  22. package/dist/data/cot.d.ts +5 -0
  23. package/dist/data/cot.js +172 -0
  24. package/dist/data/cse.d.ts +5 -0
  25. package/dist/data/cse.js +122 -0
  26. package/dist/data/gabc-glyphs.d.ts +45 -0
  27. package/dist/data/gabc-glyphs.js +122 -0
  28. package/dist/data/gr.js +754 -6394
  29. package/dist/data/kyriale.js +116 -116
  30. package/dist/data/la.js +799 -13419
  31. package/dist/data/lh.js +113 -3473
  32. package/dist/data/lu.js +931 -17631
  33. package/dist/data/nocturnale-romanum.js +1659 -10411
  34. package/dist/data/office-ferial.d.ts +4 -0
  35. package/dist/data/office-ferial.js +396 -0
  36. package/dist/data/office-ferial.json +391 -0
  37. package/dist/data/office-monastic.d.ts +17 -1
  38. package/dist/data/office-monastic.js +1403 -466
  39. package/dist/data/office-psalms-monastic.d.ts +13 -1
  40. package/dist/data/office-psalms-monastic.js +9 -0
  41. package/dist/data/propers.js +1 -1
  42. package/dist/data/psalms.js +22919 -5
  43. package/dist/data/psm.d.ts +5 -0
  44. package/dist/data/psm.js +122 -0
  45. package/dist/data/seasonal-respbreve.d.ts +5 -0
  46. package/dist/data/seasonal-respbreve.js +41 -0
  47. package/dist/data/seasonal-respbreve.json +35 -0
  48. package/dist/data/smufl-glyphs.d.ts +17 -0
  49. package/dist/data/smufl-glyphs.js +1546 -0
  50. package/dist/data/smufl-glyphs.json +1530 -0
  51. package/dist/engines/cal/calendar.d.ts +3 -2
  52. package/dist/engines/cal/calendar.js +105 -29
  53. package/dist/engines/cal/data/eras.d.ts +35 -0
  54. package/dist/engines/cal/data/eras.js +128 -0
  55. package/dist/engines/cal/date.js +44 -0
  56. package/dist/engines/cal/types.d.ts +15 -3
  57. package/dist/engines/cal/types.js +5 -5
  58. package/dist/engines/census/census.d.ts +7 -0
  59. package/dist/engines/census/census.js +179 -0
  60. package/dist/engines/census/types.d.ts +55 -0
  61. package/dist/engines/census/types.js +8 -0
  62. package/dist/engines/chant/attest.d.ts +39 -0
  63. package/dist/engines/chant/attest.js +90 -0
  64. package/dist/engines/chant/chant.d.ts +16 -4
  65. package/dist/engines/chant/chant.js +220 -30
  66. package/dist/engines/chant/data/compline.js +2 -1
  67. package/dist/engines/chant/data/masses.d.ts +56 -4
  68. package/dist/engines/chant/data/masses.js +305 -80
  69. package/dist/engines/chant/data/prime.js +1 -1
  70. package/dist/engines/chant/hour.js +279 -58
  71. package/dist/engines/chant/ordinary.d.ts +2 -0
  72. package/dist/engines/chant/ordinary.js +336 -56
  73. package/dist/engines/chant/propers.js +55 -5
  74. package/dist/engines/chant/psalm.d.ts +4 -4
  75. package/dist/engines/chant/psalm.js +25 -11
  76. package/dist/engines/chant/syllabify.d.ts +1 -0
  77. package/dist/engines/chant/syllabify.js +90 -17
  78. package/dist/engines/chant/types.d.ts +115 -12
  79. package/dist/engines/chant/types.js +38 -3
  80. package/dist/engines/harmonia/api.js +4 -0
  81. package/dist/engines/harmonia/data/doctrines.js +3 -1
  82. package/dist/engines/harmonia/tabula.d.ts +3 -0
  83. package/dist/engines/harmonia/tabula.js +1 -0
  84. package/dist/engines/harmonia/voice.d.ts +4 -0
  85. package/dist/engines/harmonia/voice.js +8 -4
  86. package/dist/engines/imprint.js +14 -1
  87. package/dist/engines/planet/orbital.js +4 -4
  88. package/dist/engines/planet/planet.d.ts +10 -0
  89. package/dist/engines/planet/planet.js +30 -3
  90. package/dist/engines/planet/position.js +13 -10
  91. package/dist/engines/planet/types.d.ts +1 -0
  92. package/dist/engines/score/api.d.ts +2 -13
  93. package/dist/engines/score/api.js +21 -8
  94. package/dist/engines/score/articulation.js +2 -2
  95. package/dist/engines/score/cadence.d.ts +76 -0
  96. package/dist/engines/score/cadence.js +96 -0
  97. package/dist/engines/score/emitters/accidentals.d.ts +21 -0
  98. package/dist/engines/score/emitters/accidentals.js +88 -0
  99. package/dist/engines/score/emitters/atramentum.d.ts +107 -0
  100. package/dist/engines/score/emitters/atramentum.js +239 -0
  101. package/dist/engines/score/emitters/breaking.d.ts +62 -0
  102. package/dist/engines/score/emitters/breaking.js +80 -0
  103. package/dist/engines/score/emitters/moderna.d.ts +38 -0
  104. package/dist/engines/score/emitters/moderna.js +612 -0
  105. package/dist/engines/score/emitters/svg.d.ts +143 -0
  106. package/dist/engines/score/emitters/svg.js +1328 -0
  107. package/dist/engines/score/emitters/tracks.d.ts +104 -0
  108. package/dist/engines/score/emitters/tracks.js +728 -0
  109. package/dist/engines/score/infer.d.ts +3 -3
  110. package/dist/engines/score/infer.js +2 -2
  111. package/dist/engines/score/inscriptio.d.ts +69 -0
  112. package/dist/engines/score/inscriptio.js +138 -0
  113. package/dist/engines/score/ir.d.ts +2 -2
  114. package/dist/engines/score/ir.js +59 -12
  115. package/dist/engines/score/lyric.d.ts +23 -0
  116. package/dist/engines/score/lyric.js +234 -0
  117. package/dist/engines/score/meta.d.ts +2 -2
  118. package/dist/engines/score/modulation.d.ts +12 -0
  119. package/dist/engines/score/modulation.js +49 -0
  120. package/dist/engines/score/neume.js +35 -4
  121. package/dist/engines/score/parse.js +150 -9
  122. package/dist/engines/score/phrasing.js +4 -3
  123. package/dist/engines/score/prosody.d.ts +38 -0
  124. package/dist/engines/score/prosody.js +70 -6
  125. package/dist/engines/score/tabula.d.ts +37 -5
  126. package/dist/engines/score/tabula.js +18 -0
  127. package/dist/engines/score/types.d.ts +88 -1
  128. package/dist/engines/temper/api.d.ts +4 -1
  129. package/dist/engines/temper/api.js +28 -5
  130. package/dist/engines/temper/data/guido.js +6 -2
  131. package/dist/engines/temper/data/modes.d.ts +6 -0
  132. package/dist/engines/temper/data/modes.js +42 -0
  133. package/dist/engines/temper/data/tones.d.ts +1 -1
  134. package/dist/engines/temper/data/tones.js +20 -11
  135. package/dist/engines/temper/interval.js +4 -3
  136. package/dist/engines/temper/modality.d.ts +11 -2
  137. package/dist/engines/temper/modality.js +74 -2
  138. package/dist/engines/temper/modes.d.ts +1 -1
  139. package/dist/engines/temper/pitch.d.ts +1 -1
  140. package/dist/engines/temper/pitch.js +12 -2
  141. package/dist/engines/temper/scale.d.ts +53 -0
  142. package/dist/engines/temper/scale.js +107 -8
  143. package/dist/index.d.ts +26 -8
  144. package/dist/index.js +37 -4
  145. package/docs/api/calendar.md +279 -0
  146. package/docs/api/census.md +288 -0
  147. package/docs/api/chant.md +657 -0
  148. package/docs/api/heavens.md +346 -0
  149. package/docs/api/index.md +263 -0
  150. package/docs/api/score.md +873 -0
  151. package/docs/api/tuning.md +619 -0
  152. package/package.json +11 -5
  153. package/dist/data/office-matins-roman.d.ts +0 -19
  154. package/dist/data/office-matins-roman.js +0 -4383
  155. package/dist/data/office-psalms-roman.d.ts +0 -15
  156. package/dist/data/office-psalms-roman.js +0 -28
  157. package/dist/data/office-roman.d.ts +0 -19
  158. package/dist/data/office-roman.js +0 -13792
  159. package/dist/engines/chant/matutinum.d.ts +0 -33
  160. package/dist/engines/chant/matutinum.js +0 -81
  161. package/dist/engines/score/emitters/midi.d.ts +0 -65
  162. package/dist/engines/score/emitters/midi.js +0 -162
  163. package/dist/engines/score/emitters/musicxml.d.ts +0 -18
  164. 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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
4
- .replace(/"/g, "&quot;").replace(/'/g, "&apos;");
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