tonus 0.1.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 (141) hide show
  1. package/BIBLIOGRAPHY.md +99 -0
  2. package/LICENSE +29 -0
  3. package/README.md +108 -0
  4. package/dist/data/cal.d.ts +10 -0
  5. package/dist/data/cal.js +3862 -0
  6. package/dist/data/commune.d.ts +17 -0
  7. package/dist/data/commune.js +1333 -0
  8. package/dist/data/gr.d.ts +5 -0
  9. package/dist/data/gr.js +13449 -0
  10. package/dist/data/kyriale.d.ts +11 -0
  11. package/dist/data/kyriale.js +971 -0
  12. package/dist/data/la.d.ts +5 -0
  13. package/dist/data/la.js +14229 -0
  14. package/dist/data/lh.d.ts +5 -0
  15. package/dist/data/lh.js +3619 -0
  16. package/dist/data/lu.d.ts +5 -0
  17. package/dist/data/lu.js +23779 -0
  18. package/dist/data/masses.d.ts +18 -0
  19. package/dist/data/masses.js +297 -0
  20. package/dist/data/office-roman.d.ts +19 -0
  21. package/dist/data/office-roman.js +13792 -0
  22. package/dist/data/office.d.ts +12 -0
  23. package/dist/data/office.js +13052 -0
  24. package/dist/data/propers.d.ts +13 -0
  25. package/dist/data/propers.js +7584 -0
  26. package/dist/data/psalms.d.ts +4 -0
  27. package/dist/data/psalms.js +10 -0
  28. package/dist/data/psalms.json +22918 -0
  29. package/dist/data/tones.d.ts +20 -0
  30. package/dist/data/tones.js +153 -0
  31. package/dist/data/types.d.ts +3 -0
  32. package/dist/data/types.js +2 -0
  33. package/dist/engines/cal/calendar.d.ts +21 -0
  34. package/dist/engines/cal/calendar.js +265 -0
  35. package/dist/engines/cal/date.d.ts +31 -0
  36. package/dist/engines/cal/date.js +141 -0
  37. package/dist/engines/cal/types.d.ts +66 -0
  38. package/dist/engines/cal/types.js +189 -0
  39. package/dist/engines/chant/chant.d.ts +10 -0
  40. package/dist/engines/chant/chant.js +135 -0
  41. package/dist/engines/chant/hour.d.ts +8 -0
  42. package/dist/engines/chant/hour.js +135 -0
  43. package/dist/engines/chant/intone.d.ts +8 -0
  44. package/dist/engines/chant/intone.js +84 -0
  45. package/dist/engines/chant/ordinary.d.ts +7 -0
  46. package/dist/engines/chant/ordinary.js +232 -0
  47. package/dist/engines/chant/propers.d.ts +8 -0
  48. package/dist/engines/chant/propers.js +107 -0
  49. package/dist/engines/chant/psalm.d.ts +7 -0
  50. package/dist/engines/chant/psalm.js +60 -0
  51. package/dist/engines/chant/syllabify.d.ts +20 -0
  52. package/dist/engines/chant/syllabify.js +192 -0
  53. package/dist/engines/chant/types.d.ts +76 -0
  54. package/dist/engines/chant/types.js +34 -0
  55. package/dist/engines/epoch.d.ts +2 -0
  56. package/dist/engines/epoch.js +14 -0
  57. package/dist/engines/harmonia/api.d.ts +35 -0
  58. package/dist/engines/harmonia/api.js +90 -0
  59. package/dist/engines/harmonia/aspects.d.ts +8 -0
  60. package/dist/engines/harmonia/aspects.js +15 -0
  61. package/dist/engines/harmonia/data/doctrines.d.ts +16 -0
  62. package/dist/engines/harmonia/data/doctrines.js +154 -0
  63. package/dist/engines/harmonia/data/vowels.d.ts +10 -0
  64. package/dist/engines/harmonia/data/vowels.js +21 -0
  65. package/dist/engines/harmonia/presence.d.ts +13 -0
  66. package/dist/engines/harmonia/presence.js +48 -0
  67. package/dist/engines/harmonia/tabula.d.ts +28 -0
  68. package/dist/engines/harmonia/tabula.js +32 -0
  69. package/dist/engines/harmonia/voice.d.ts +19 -0
  70. package/dist/engines/harmonia/voice.js +51 -0
  71. package/dist/engines/imprint.d.ts +30 -0
  72. package/dist/engines/imprint.js +152 -0
  73. package/dist/engines/planet/appearance.d.ts +40 -0
  74. package/dist/engines/planet/appearance.js +84 -0
  75. package/dist/engines/planet/aspects.d.ts +5 -0
  76. package/dist/engines/planet/aspects.js +41 -0
  77. package/dist/engines/planet/math.d.ts +13 -0
  78. package/dist/engines/planet/math.js +56 -0
  79. package/dist/engines/planet/orbital.d.ts +25 -0
  80. package/dist/engines/planet/orbital.js +223 -0
  81. package/dist/engines/planet/planet.d.ts +13 -0
  82. package/dist/engines/planet/planet.js +198 -0
  83. package/dist/engines/planet/position.d.ts +62 -0
  84. package/dist/engines/planet/position.js +156 -0
  85. package/dist/engines/planet/types.d.ts +61 -0
  86. package/dist/engines/planet/types.js +14 -0
  87. package/dist/engines/score/api.d.ts +54 -0
  88. package/dist/engines/score/api.js +87 -0
  89. package/dist/engines/score/articulation.d.ts +6 -0
  90. package/dist/engines/score/articulation.js +112 -0
  91. package/dist/engines/score/emitters/midi.d.ts +65 -0
  92. package/dist/engines/score/emitters/midi.js +158 -0
  93. package/dist/engines/score/emitters/musicxml.d.ts +18 -0
  94. package/dist/engines/score/emitters/musicxml.js +166 -0
  95. package/dist/engines/score/infer.d.ts +4 -0
  96. package/dist/engines/score/infer.js +77 -0
  97. package/dist/engines/score/ir.d.ts +4 -0
  98. package/dist/engines/score/ir.js +177 -0
  99. package/dist/engines/score/meta.d.ts +19 -0
  100. package/dist/engines/score/meta.js +34 -0
  101. package/dist/engines/score/neume.d.ts +3 -0
  102. package/dist/engines/score/neume.js +26 -0
  103. package/dist/engines/score/parse.d.ts +3 -0
  104. package/dist/engines/score/parse.js +359 -0
  105. package/dist/engines/score/phrasing.d.ts +24 -0
  106. package/dist/engines/score/phrasing.js +257 -0
  107. package/dist/engines/score/prosody.d.ts +35 -0
  108. package/dist/engines/score/prosody.js +109 -0
  109. package/dist/engines/score/tabula.d.ts +70 -0
  110. package/dist/engines/score/tabula.js +109 -0
  111. package/dist/engines/score/types.d.ts +159 -0
  112. package/dist/engines/score/types.js +2 -0
  113. package/dist/engines/temper/api.d.ts +60 -0
  114. package/dist/engines/temper/api.js +130 -0
  115. package/dist/engines/temper/data/constants.d.ts +27 -0
  116. package/dist/engines/temper/data/constants.js +150 -0
  117. package/dist/engines/temper/data/guido.d.ts +14 -0
  118. package/dist/engines/temper/data/guido.js +29 -0
  119. package/dist/engines/temper/data/modes.d.ts +38 -0
  120. package/dist/engines/temper/data/modes.js +158 -0
  121. package/dist/engines/temper/gabc.d.ts +5 -0
  122. package/dist/engines/temper/gabc.js +53 -0
  123. package/dist/engines/temper/gamut.d.ts +9 -0
  124. package/dist/engines/temper/gamut.js +24 -0
  125. package/dist/engines/temper/guido.d.ts +16 -0
  126. package/dist/engines/temper/guido.js +48 -0
  127. package/dist/engines/temper/interval.d.ts +15 -0
  128. package/dist/engines/temper/interval.js +31 -0
  129. package/dist/engines/temper/modes.d.ts +6 -0
  130. package/dist/engines/temper/modes.js +13 -0
  131. package/dist/engines/temper/neume.d.ts +14 -0
  132. package/dist/engines/temper/neume.js +59 -0
  133. package/dist/engines/temper/pitch.d.ts +40 -0
  134. package/dist/engines/temper/pitch.js +129 -0
  135. package/dist/engines/temper/scale.d.ts +37 -0
  136. package/dist/engines/temper/scale.js +217 -0
  137. package/dist/engines/temper/step.d.ts +23 -0
  138. package/dist/engines/temper/step.js +53 -0
  139. package/dist/index.d.ts +40 -0
  140. package/dist/index.js +27 -0
  141. package/package.json +60 -0
@@ -0,0 +1,61 @@
1
+ export interface HelioPos {
2
+ lon: number;
3
+ lat: number;
4
+ dist: number;
5
+ }
6
+ export interface GeoPos {
7
+ lon: number;
8
+ lat: number;
9
+ dist: number;
10
+ equatorial: {
11
+ ra: number;
12
+ dec: number;
13
+ dist: number;
14
+ };
15
+ }
16
+ export type BodyName = "Sun" | "Moon" | "Mercury" | "Venus" | "Earth" | "Mars" | "Jupiter" | "Saturn";
17
+ export type PlanetName = "Mercury" | "Venus" | "Mars" | "Jupiter" | "Saturn";
18
+ export declare function latinName(name: BodyName): string;
19
+ export interface Body {
20
+ name: BodyName;
21
+ nomen: string;
22
+ symbol: string;
23
+ helio: HelioPos;
24
+ geo: GeoPos;
25
+ speed: number;
26
+ retrograde: boolean;
27
+ magnitude: number;
28
+ elongation: number;
29
+ phase: number;
30
+ apparentDiameter: number | {
31
+ equ: number;
32
+ pol: number;
33
+ };
34
+ zodiac: number;
35
+ sign: string;
36
+ distEarthRadii?: number;
37
+ }
38
+ export interface Aspect {
39
+ type: "conjunction" | "opposition" | "trine" | "square" | "sextile";
40
+ bodies: [string, string];
41
+ angle: number;
42
+ orb: number;
43
+ strength: number;
44
+ }
45
+ export interface Cosmos {
46
+ date: Date;
47
+ bodies: Body[];
48
+ aspects: Aspect[];
49
+ }
50
+ export interface CosmosQuery {
51
+ date?: Date;
52
+ feast?: {
53
+ date: Date;
54
+ };
55
+ from?: Date;
56
+ to?: Date;
57
+ step?: number;
58
+ bodies?: BodyName[];
59
+ orbLimit?: number;
60
+ }
61
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,14 @@
1
+ const LATIN_NAMES = {
2
+ Sun: "Sol",
3
+ Moon: "Luna",
4
+ Mercury: "Mercurius",
5
+ Venus: "Venus",
6
+ Earth: "Terra",
7
+ Mars: "Mars",
8
+ Jupiter: "Iuppiter",
9
+ Saturn: "Saturnus",
10
+ };
11
+ export function latinName(name) {
12
+ return LATIN_NAMES[name];
13
+ }
14
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,54 @@
1
+ import { type Imprint } from "../imprint.js";
2
+ import { type Prosody } from "./prosody.js";
3
+ import { type ChantTabulaRow } from "./tabula.js";
4
+ import { type MidiOpts, type MidiEmitResult } from "./emitters/midi.js";
5
+ import { type MusicXmlOpts, type MusicXmlEmitResult } from "./emitters/musicxml.js";
6
+ import type { Chant } from "../chant/types.js";
7
+ import type { Temperamentum } from "../temper/api.js";
8
+ import type { ArticulationProfile, PhrasingProfile, ParseError, Phrase as IRPhrase } from "./types.js";
9
+ export type PondusStyle = "restrained" | "balanced" | "expressive" | "strict";
10
+ export type AccentusStyle = "recitative" | "lyrical" | "hymnic" | "solemn";
11
+ export interface PondusOpts {
12
+ style?: PondusStyle;
13
+ overrides?: Partial<ArticulationProfile>;
14
+ }
15
+ export interface AccentusOpts {
16
+ style?: AccentusStyle;
17
+ overrides?: Partial<PhrasingProfile>;
18
+ }
19
+ export type PondusInput = PondusStyle | PondusOpts;
20
+ export type AccentusInput = AccentusStyle | AccentusOpts;
21
+ export interface ScoreOpts {
22
+ temperamentum?: Temperamentum;
23
+ pondus?: PondusInput;
24
+ accentus?: AccentusInput;
25
+ }
26
+ export interface Score {
27
+ chant: Chant;
28
+ phrases: IRPhrase[];
29
+ errors: ParseError[];
30
+ tabula: ChantTabulaRow[];
31
+ prosody: Prosody;
32
+ imprint: Imprint;
33
+ /**
34
+ * Emit a Standard MIDI File from the score's tabula. Returns the file bytes
35
+ * by default; `format: "json"` returns the event structure, `"both"` an
36
+ * object. The score's pondus/accentus reach the output via the tabula.
37
+ */
38
+ midi(opts?: MidiOpts): Uint8Array | MidiEmitResult;
39
+ /** Emit a MusicXML 4.0 partwise document from the score's tabula. */
40
+ musicxml(opts?: MusicXmlOpts): MusicXmlEmitResult;
41
+ }
42
+ /**
43
+ * Score builder (`tonus.notatio`). Parses a chant's GABC into a musical
44
+ * IR — phrases, syllables, notes with tuned pitches, arsis/thesis
45
+ * rhythm, prosody, imprint, and a tabula. Options: a temperamentum
46
+ * (tuning), a pondus (articulation weight, style name or opts), and an
47
+ * accentus (phrasing, style name or opts).
48
+ * @throws Error on invalid Chant input or unparseable GABC.
49
+ */
50
+ export declare function buildScore(chant: Chant, opts?: ScoreOpts): Score;
51
+ export type { ParseError };
52
+ export type { MidiOpts, MidiEmitResult, MidiJsonResult, MidiJsonEvent } from "./emitters/midi.js";
53
+ export type { MusicXmlOpts, MusicXmlEmitResult } from "./emitters/musicxml.js";
54
+ //# sourceMappingURL=api.d.ts.map
@@ -0,0 +1,87 @@
1
+ // ---------------------------------------------------------------------------
2
+ // engines/score/api — score builder and interpretation profiles
3
+ // ---------------------------------------------------------------------------
4
+ import { parseGABC } from "./parse.js";
5
+ import { buildIR } from "./ir.js";
6
+ import { buildRatios } from "../temper/scale.js";
7
+ import { computeMeta } from "./meta.js";
8
+ import { computeImprint } from "../imprint.js";
9
+ import { computeProsody } from "./prosody.js";
10
+ import { computeTabula } from "./tabula.js";
11
+ import { toMidi } from "./emitters/midi.js";
12
+ import { toMusicXML } from "./emitters/musicxml.js";
13
+ const PONDUS_TO_ARTICULATION = {
14
+ restrained: "restrained",
15
+ balanced: "balanced",
16
+ expressive: "expressive",
17
+ strict: "strict",
18
+ };
19
+ const ACCENTUS_TO_PHRASING = {
20
+ recitative: "recitative",
21
+ lyrical: "lyrical",
22
+ hymnic: "hymnic",
23
+ solemn: "solemn",
24
+ };
25
+ function resolvePondus(input) {
26
+ return typeof input === "string" ? { style: input } : (input ?? {});
27
+ }
28
+ function resolveAccentus(input) {
29
+ return typeof input === "string" ? { style: input } : (input ?? {});
30
+ }
31
+ /**
32
+ * Score builder (`tonus.notatio`). Parses a chant's GABC into a musical
33
+ * IR — phrases, syllables, notes with tuned pitches, arsis/thesis
34
+ * rhythm, prosody, imprint, and a tabula. Options: a temperamentum
35
+ * (tuning), a pondus (articulation weight, style name or opts), and an
36
+ * accentus (phrasing, style name or opts).
37
+ * @throws Error on invalid Chant input or unparseable GABC.
38
+ */
39
+ export function buildScore(chant, opts) {
40
+ const pondus = resolvePondus(opts?.pondus);
41
+ const accentus = resolveAccentus(opts?.accentus);
42
+ const parsed = parseGABC(chant.gabc, {
43
+ interpretation: {
44
+ articulation: PONDUS_TO_ARTICULATION[pondus.style ?? "balanced"],
45
+ articulationOverrides: pondus.overrides,
46
+ },
47
+ });
48
+ const modeNum = chant.mode ? parseInt(chant.mode) || undefined : undefined;
49
+ const scale = buildRatios({
50
+ mode: opts?.temperamentum?.mode === "auto" ? (modeNum ?? 1) : (opts?.temperamentum?.mode ?? modeNum ?? 1),
51
+ a4: opts?.temperamentum?.a4 ?? 440,
52
+ transpose: opts?.temperamentum?.transpose ?? 0,
53
+ // Carry the temperamentum's fully resolved scale — otherwise custom
54
+ // and non-pythagorean tunings would be silently rebuilt as default.
55
+ steps: opts?.temperamentum?.cents,
56
+ });
57
+ const ir = buildIR(parsed, chant, scale);
58
+ const meta = computeMeta(ir, { mode: modeNum });
59
+ const tabula = computeTabula(ir, {
60
+ mode: meta.mode ?? undefined,
61
+ a4Hz: opts?.temperamentum?.a4,
62
+ transpose: opts?.temperamentum?.transpose,
63
+ // Only pass phrasing when the caller asked for it, so the default
64
+ // tabula shaping (mode-gated) is unchanged.
65
+ interpretation: opts?.accentus
66
+ ? {
67
+ phrasing: ACCENTUS_TO_PHRASING[accentus.style ?? "lyrical"],
68
+ phrasingOverrides: accentus.overrides,
69
+ }
70
+ : undefined,
71
+ });
72
+ return {
73
+ chant,
74
+ phrases: ir.phrases,
75
+ errors: ir.errors,
76
+ tabula,
77
+ prosody: computeProsody(ir.phrases),
78
+ imprint: computeImprint(ir.phrases, scale),
79
+ midi(emitOpts) {
80
+ return toMidi(tabula, emitOpts);
81
+ },
82
+ musicxml(emitOpts) {
83
+ return toMusicXML(tabula, chant, emitOpts);
84
+ },
85
+ };
86
+ }
87
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1,6 @@
1
+ import type { ArticulationProfile, ArticulationType } from "./types.js";
2
+ export interface BuildArticulationOptions {
3
+ overrides?: Partial<ArticulationProfile>;
4
+ }
5
+ export declare function buildArticulation(type?: ArticulationType, options?: BuildArticulationOptions): ArticulationProfile;
6
+ //# sourceMappingURL=articulation.d.ts.map
@@ -0,0 +1,112 @@
1
+ // ---------------------------------------------------------------------------
2
+ // engines/score/articulation — note-level articulation profiles
3
+ // ---------------------------------------------------------------------------
4
+ const BASE_WEIGHTS = {
5
+ ictusWeight: 0.9,
6
+ ictusDuration: 0.35,
7
+ episemaWeight: 0.6,
8
+ episemaDuration: 0.9,
9
+ episemaDoubleDuration: 0.6,
10
+ strophicusWeight: 0.4,
11
+ strophicusDuration: 0.8,
12
+ strophicusTripleDuration: 0.6,
13
+ quilismaPrevWeight: 0.6,
14
+ quilismaWeight: -0.3,
15
+ liquescentWeight: -0.4,
16
+ liquescentDuration: -0.35,
17
+ initioWeight: 0.6,
18
+ initioMelismaWeight: 0.3,
19
+ initioMelismaDuration: 0.1,
20
+ accentWeight: 0.4,
21
+ uppercaseWeight: -0.7,
22
+ uppercaseDuration: -0.2,
23
+ repercussionPrevWeight: 0.5,
24
+ repercussionPrevDuration: 0.4,
25
+ repercussionOriscusWeight: -0.5,
26
+ breakWeight: 0.6,
27
+ dashWeight: -0.8,
28
+ dashDuration: -0.2,
29
+ };
30
+ const ARTICULATION_PROFILES = {
31
+ restrained: {
32
+ weights: { ...BASE_WEIGHTS, accentWeight: 0.25, ictusDuration: 0.2 },
33
+ weightBase: 5,
34
+ weightGain: 2.3,
35
+ weightSaturation: 3,
36
+ durationBase: 1,
37
+ durationGain: 0.45,
38
+ durationMin: 0.25,
39
+ durationMax: 3.6,
40
+ ruleGain: 0.8,
41
+ contourScale: 0.12,
42
+ neumeArch: 0.3,
43
+ durArch: 0.05,
44
+ ictusBoost: 1.04,
45
+ },
46
+ balanced: {
47
+ weights: { ...BASE_WEIGHTS },
48
+ weightBase: 5,
49
+ weightGain: 3,
50
+ weightSaturation: 3,
51
+ durationBase: 1,
52
+ durationGain: 0.6,
53
+ durationMin: 0.2,
54
+ durationMax: 4,
55
+ ruleGain: 1,
56
+ contourScale: 0.2,
57
+ neumeArch: 0.5,
58
+ durArch: 0.08,
59
+ ictusBoost: 1.08,
60
+ },
61
+ expressive: {
62
+ weights: { ...BASE_WEIGHTS, ictusWeight: 1.1, accentWeight: 0.55 },
63
+ weightBase: 5,
64
+ weightGain: 3.5,
65
+ weightSaturation: 2.8,
66
+ durationBase: 1,
67
+ durationGain: 0.72,
68
+ durationMin: 0.2,
69
+ durationMax: 4,
70
+ ruleGain: 1.2,
71
+ contourScale: 0.28,
72
+ neumeArch: 0.62,
73
+ durArch: 0.1,
74
+ ictusBoost: 1.12,
75
+ },
76
+ strict: {
77
+ weights: {
78
+ ...BASE_WEIGHTS,
79
+ episemaDuration: 1.0,
80
+ quilismaPrevWeight: 0.7,
81
+ quilismaWeight: -0.4,
82
+ liquescentDuration: -0.4,
83
+ },
84
+ weightBase: 5,
85
+ weightGain: 3.2,
86
+ weightSaturation: 3,
87
+ durationBase: 1,
88
+ durationGain: 0.65,
89
+ durationMin: 0.2,
90
+ durationMax: 4,
91
+ ruleGain: 1.08,
92
+ contourScale: 0.2,
93
+ neumeArch: 0.56,
94
+ durArch: 0.1,
95
+ ictusBoost: 1.1,
96
+ },
97
+ };
98
+ export function buildArticulation(type = "balanced", options = {}) {
99
+ const base = ARTICULATION_PROFILES[type] ?? ARTICULATION_PROFILES.balanced;
100
+ const overrides = options.overrides ?? {};
101
+ const baseWeights = base.weights;
102
+ const overrideWeights = overrides.weights ?? {};
103
+ return {
104
+ ...base,
105
+ ...overrides,
106
+ weights: {
107
+ ...baseWeights,
108
+ ...overrideWeights,
109
+ },
110
+ };
111
+ }
112
+ //# sourceMappingURL=articulation.js.map
@@ -0,0 +1,65 @@
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
@@ -0,0 +1,158 @@
1
+ // Divisio → rest duration (in the same beat units as note durations). Mirrors
2
+ // DIVISIO_DURATIONS in parse.ts; a phrase's terminal divisio becomes a rest.
3
+ const DIVISIO_REST = {
4
+ ",": 0.54, "`": 0.33, ";": 0.8, ":": 1.1, "::": 1.8,
5
+ };
6
+ const DEFAULT_PPQ = 480;
7
+ const DEFAULT_TEMPO = 120;
8
+ const DEFAULT_CHANNEL = 0;
9
+ const DEFAULT_VELOCITY = 80;
10
+ const PITCH_BEND_CENTER = 8192;
11
+ function resolveOpts(options) {
12
+ return {
13
+ format: options.format ?? "file",
14
+ ppq: options.ppq ?? DEFAULT_PPQ,
15
+ tempoBpm: options.tempoBpm ?? DEFAULT_TEMPO,
16
+ channel: options.channel ?? DEFAULT_CHANNEL,
17
+ velocity: options.velocity ?? DEFAULT_VELOCITY,
18
+ includeMeta: options.includeMeta ?? true,
19
+ trackName: options.trackName ?? "tonus",
20
+ emitPitchBend: options.emitPitchBend ?? true,
21
+ transpose: options.transpose ?? 0,
22
+ };
23
+ }
24
+ function encodeVarLen(value) {
25
+ let val = Math.max(0, value | 0);
26
+ const bytes = [val & 0x7f];
27
+ while ((val >>= 7) > 0)
28
+ bytes.unshift((val & 0x7f) | 0x80);
29
+ return bytes;
30
+ }
31
+ function writeUInt32BE(value) {
32
+ return [(value >>> 24) & 0xff, (value >>> 16) & 0xff, (value >>> 8) & 0xff, value & 0xff];
33
+ }
34
+ function writeUInt16BE(value) {
35
+ return [(value >>> 8) & 0xff, value & 0xff];
36
+ }
37
+ function eventPriority(event) {
38
+ if (event.type === "meta")
39
+ return 0;
40
+ if (event.type === "pitchBend")
41
+ return 1;
42
+ if (event.type === "noteOff")
43
+ return 2;
44
+ return 3;
45
+ }
46
+ function clampToMidiPitch(step, diagnostics) {
47
+ if (step < 0 || step > 127) {
48
+ diagnostics.push({
49
+ code: "MIDI_PITCH_CLAMPED",
50
+ message: `Pitch ${step} clamped to MIDI range 0..127`,
51
+ });
52
+ }
53
+ return Math.min(127, Math.max(0, step));
54
+ }
55
+ function buildJson(rows, opts, diagnostics) {
56
+ const events = [];
57
+ let tick = 0;
58
+ if (opts.includeMeta) {
59
+ events.push({ type: "meta", tick: 0, metaType: "tempo", value: opts.tempoBpm });
60
+ events.push({ type: "meta", tick: 0, metaType: "trackName", value: opts.trackName });
61
+ }
62
+ let prevPhrase = null;
63
+ for (const row of rows) {
64
+ // A phrase boundary inserts the previous phrase's terminal divisio as a rest.
65
+ if (prevPhrase !== null && row.phraseIndex !== prevPhrase) {
66
+ const restDivisio = rows.find((r) => r.phraseIndex === prevPhrase)?.divisio ?? null;
67
+ const restBeats = restDivisio ? (DIVISIO_REST[restDivisio] ?? 0) : 0;
68
+ tick += Math.max(0, Math.round(restBeats * opts.ppq));
69
+ }
70
+ prevPhrase = row.phraseIndex;
71
+ const pitch = clampToMidiPitch(row.midi + opts.transpose, diagnostics);
72
+ const durationTicks = Math.max(1, Math.round(row.shapedDuration * opts.ppq));
73
+ const velocity = row.velocity != null && row.velocity > 0
74
+ ? Math.round(row.velocity * 127)
75
+ : opts.velocity;
76
+ const bends = opts.emitPitchBend && Math.abs(row.bend - PITCH_BEND_CENTER) > 1;
77
+ if (bends) {
78
+ events.push({ type: "pitchBend", tick, channel: opts.channel, value14: row.bend });
79
+ }
80
+ events.push({ type: "noteOn", tick, pitch, velocity, channel: opts.channel, hz: row.hz });
81
+ events.push({ type: "noteOff", tick: tick + durationTicks, pitch, velocity: 0, channel: opts.channel });
82
+ if (bends) {
83
+ events.push({ type: "pitchBend", tick: tick + durationTicks, channel: opts.channel, value14: PITCH_BEND_CENTER });
84
+ }
85
+ tick += durationTicks;
86
+ }
87
+ events.sort((a, b) => a.tick - b.tick || eventPriority(a) - eventPriority(b));
88
+ return {
89
+ ppq: opts.ppq,
90
+ tempoBpm: opts.tempoBpm,
91
+ tracks: [{ name: opts.trackName, events }],
92
+ diagnostics,
93
+ };
94
+ }
95
+ function buildMidiBytes(json) {
96
+ const trackEvents = json.tracks[0]?.events ?? [];
97
+ const trackData = [];
98
+ let lastTick = 0;
99
+ for (const event of trackEvents) {
100
+ const delta = Math.max(0, event.tick - lastTick);
101
+ lastTick = event.tick;
102
+ trackData.push(...encodeVarLen(delta));
103
+ if (event.type === "meta") {
104
+ if (event.metaType === "tempo") {
105
+ const mpqn = Math.max(1, Math.round(60000000 / Number(event.value)));
106
+ trackData.push(0xff, 0x51, 0x03, (mpqn >>> 16) & 0xff, (mpqn >>> 8) & 0xff, mpqn & 0xff);
107
+ }
108
+ else if (event.metaType === "trackName") {
109
+ const textBytes = Array.from(new TextEncoder().encode(String(event.value)));
110
+ trackData.push(0xff, 0x03, ...encodeVarLen(textBytes.length), ...textBytes);
111
+ }
112
+ continue;
113
+ }
114
+ if (event.type === "pitchBend") {
115
+ const lsb = event.value14 & 0x7f;
116
+ const msb = (event.value14 >> 7) & 0x7f;
117
+ trackData.push(0xe0 | (event.channel & 0x0f), lsb, msb);
118
+ continue;
119
+ }
120
+ if (event.type === "noteOn") {
121
+ trackData.push(0x90 | (event.channel & 0x0f), event.pitch & 0x7f, event.velocity & 0x7f);
122
+ }
123
+ else {
124
+ trackData.push(0x80 | (event.channel & 0x0f), event.pitch & 0x7f, event.velocity & 0x7f);
125
+ }
126
+ }
127
+ // End-of-track meta event
128
+ trackData.push(0x00, 0xff, 0x2f, 0x00);
129
+ const headerChunk = [
130
+ 0x4d, 0x54, 0x68, 0x64, // "MThd"
131
+ ...writeUInt32BE(6), // chunk length
132
+ ...writeUInt16BE(0), // format 0
133
+ ...writeUInt16BE(1), // 1 track
134
+ ...writeUInt16BE(json.ppq),
135
+ ];
136
+ const trackChunk = [
137
+ 0x4d, 0x54, 0x72, 0x6b, // "MTrk"
138
+ ...writeUInt32BE(trackData.length),
139
+ ...trackData,
140
+ ];
141
+ return Uint8Array.from([...headerChunk, ...trackChunk]);
142
+ }
143
+ /**
144
+ * Emit a Standard MIDI File from a score's tabula rows. Returns the file bytes
145
+ * (`Uint8Array`) by default; `format: "json"` returns the event structure and
146
+ * `"both"` returns `{ json, bytes }`.
147
+ */
148
+ export function toMidi(rows, options = {}) {
149
+ const opts = resolveOpts(options);
150
+ const diagnostics = [];
151
+ const json = buildJson(rows, opts, diagnostics);
152
+ if (opts.format === "file")
153
+ return buildMidiBytes(json);
154
+ if (opts.format === "json")
155
+ return { json };
156
+ return { json, bytes: buildMidiBytes(json) };
157
+ }
158
+ //# sourceMappingURL=midi.js.map
@@ -0,0 +1,18 @@
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