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,109 @@
1
+ export function computeProsody(phrases) {
2
+ let phraseCount = 0;
3
+ let noteCount = 0;
4
+ let syllableCount = 0;
5
+ let ictusCount = 0;
6
+ let minMidi = Infinity;
7
+ let maxMidi = -Infinity;
8
+ let arsicCount = 0;
9
+ let theticCount = 0;
10
+ // Group-size tracking: a new group starts at every rhythmicIndex === 1.
11
+ let groupSizes = [];
12
+ let currentGroupSize = 0;
13
+ let maxGroupSize = 0;
14
+ const cadDist = {
15
+ comma: 0, tick: 0, semicolon: 0, colon: 0, doubleBar: 0,
16
+ };
17
+ let cadenceWeight = 0;
18
+ const melismaByPhrase = [];
19
+ const closeGroup = () => {
20
+ if (currentGroupSize > 0) {
21
+ groupSizes.push(currentGroupSize);
22
+ if (currentGroupSize > maxGroupSize)
23
+ maxGroupSize = currentGroupSize;
24
+ }
25
+ };
26
+ for (const phrase of phrases) {
27
+ phraseCount++;
28
+ let phraseNotes = 0;
29
+ let phraseSyllables = 0;
30
+ for (const syl of phrase.syllables) {
31
+ if (syl.notes.length === 0)
32
+ continue;
33
+ phraseSyllables++;
34
+ syllableCount++;
35
+ for (const note of syl.notes) {
36
+ noteCount++;
37
+ phraseNotes++;
38
+ if (note.pitch.midi < minMidi)
39
+ minMidi = note.pitch.midi;
40
+ if (note.pitch.midi > maxMidi)
41
+ maxMidi = note.pitch.midi;
42
+ if (note.context.ictus)
43
+ ictusCount++;
44
+ if (note.performance.rhythmicShape === "arsic")
45
+ arsicCount++;
46
+ else
47
+ theticCount++;
48
+ if (note.performance.rhythmicIndex === 1) {
49
+ closeGroup();
50
+ currentGroupSize = 1;
51
+ }
52
+ else {
53
+ currentGroupSize++;
54
+ }
55
+ }
56
+ }
57
+ if (phrase.divisio) {
58
+ const d = phrase.divisio.divisio;
59
+ if (d === "::") {
60
+ cadDist.doubleBar++;
61
+ cadenceWeight += 1.5;
62
+ }
63
+ else if (d === ":") {
64
+ cadDist.colon++;
65
+ cadenceWeight += 1.0;
66
+ }
67
+ else if (d === ";") {
68
+ cadDist.semicolon++;
69
+ cadenceWeight += 0.75;
70
+ }
71
+ else if (d === ",") {
72
+ cadDist.comma++;
73
+ cadenceWeight += 0.5;
74
+ }
75
+ else if (d === "`") {
76
+ cadDist.tick++;
77
+ cadenceWeight += 0.25;
78
+ }
79
+ }
80
+ if (phraseSyllables > 0)
81
+ melismaByPhrase.push(phraseNotes / phraseSyllables);
82
+ }
83
+ closeGroup();
84
+ const noteRange = noteCount > 0
85
+ ? { min: minMidi, max: maxMidi, span: maxMidi - minMidi }
86
+ : null;
87
+ const avgGroupSize = groupSizes.length > 0
88
+ ? groupSizes.reduce((s, n) => s + n, 0) / groupSizes.length
89
+ : 0;
90
+ return {
91
+ phraseCount,
92
+ noteCount,
93
+ syllableCount,
94
+ noteRange,
95
+ ambitus: noteRange?.span ?? null,
96
+ melismaRatio: syllableCount > 0 ? noteCount / syllableCount : 0,
97
+ melismaByPhrase,
98
+ ictusRate: noteCount > 0 ? ictusCount / noteCount : 0,
99
+ rhythmicProfile: {
100
+ arsic: arsicCount,
101
+ thetic: theticCount,
102
+ avgGroupSize,
103
+ maxGroupSize,
104
+ },
105
+ cadenceWeight,
106
+ cadenceDistribution: cadDist,
107
+ };
108
+ }
109
+ //# sourceMappingURL=prosody.js.map
@@ -0,0 +1,70 @@
1
+ import type { Score, Neume } from "./types.js";
2
+ import type { ChantType, InterpretationOptions } from "./types.js";
3
+ export type NoteRole = "finalis" | "tenor" | "other" | null;
4
+ export interface ChantTabulaRow {
5
+ phraseIndex: number;
6
+ syllableIndex: number;
7
+ noteIndex: number;
8
+ /** 0-based index of the neume figure within the syllable (GABC break markers). */
9
+ neumeGroup: number;
10
+ /** 0-based position of this note within its neume figure. */
11
+ neumeIndex: number;
12
+ lyric: string;
13
+ vowel: string;
14
+ /** MIDI pitch number (after transpose, clamped 0–127) */
15
+ midi: number;
16
+ /** Pitch class 0–11 (C=0) */
17
+ pc: number;
18
+ /** Octave (MIDI convention: C4 = octave 4) */
19
+ octave: number;
20
+ /** Diatonic scale degree 1–7 relative to mode finalis; null for non-diatonic pitches or no mode */
21
+ degree: number | null;
22
+ hz: number;
23
+ offset: number;
24
+ /** Scientific pitch name, e.g. "D4", "Bb3" — the MusicXML step + octave. */
25
+ spn: string;
26
+ /** 14-bit MIDI pitch bend for this note's microtuning (8192 = center). */
27
+ bend: number;
28
+ /** Solesmes quality of this note's compound beat (shared across group). */
29
+ rhythmicShape: "arsic" | "thetic";
30
+ /** 1-based position within the compound beat. */
31
+ rhythmicIndex: number;
32
+ duration: number;
33
+ /** 0–1 from phrasing; null if phrasing inactive */
34
+ velocity: number | null;
35
+ shapedDuration: number;
36
+ ictus: boolean;
37
+ accidental: -1 | 0 | 1;
38
+ /** How this note's accidental arose — only "explicit" prints a glyph. */
39
+ accidentalSource: "none" | "state" | "explicit";
40
+ quilisma: boolean;
41
+ liquescent: boolean;
42
+ strophicus: boolean;
43
+ divisio: string | null;
44
+ /** Modal role: "final" | "tenor" | "mod" (modulation) | null if no mode or no match */
45
+ role: NoteRole;
46
+ /** Guidonian short name (e.g. "g", "aa") — null for chromatic pitches with no gamut entry */
47
+ name: string | null;
48
+ /** Guidonian full compound name (e.g. "Gesolreut") — null for chromatic pitches */
49
+ nomen: string | null;
50
+ /** Guidonian hand position */
51
+ hand: {
52
+ finger: string;
53
+ region: string;
54
+ } | null;
55
+ /** Hexachord context for this pitch */
56
+ hexachord: "durum" | "naturale" | "molle" | null;
57
+ /** Solfege syllable — Guidonian variant when in gamut, chromatic fallback otherwise */
58
+ solfege: string | null;
59
+ /** Neume classification for the syllable this note belongs to */
60
+ neume: Neume;
61
+ }
62
+ export interface TabulaOptions {
63
+ mode?: number;
64
+ office?: ChantType;
65
+ interpretation?: InterpretationOptions;
66
+ a4Hz?: number;
67
+ transpose?: number;
68
+ }
69
+ export declare function computeTabula(ir: Score, options?: TabulaOptions): ChantTabulaRow[];
70
+ //# sourceMappingURL=tabula.d.ts.map
@@ -0,0 +1,109 @@
1
+ import { MODES } from "../temper/modes.js";
2
+ import { buildPhrasing, shapePhrasingForMode, applyPhrasing, } from "./phrasing.js";
3
+ import { inferMode } from "./infer.js";
4
+ import { CHROMA_TO_SOLFEGE as SOLFEGE_BY_PC } from "../temper/data/constants.js";
5
+ export function computeTabula(ir, options = {}) {
6
+ const modeNum = options.mode ?? inferMode(ir);
7
+ const modeData = modeNum !== undefined ? MODES.get(modeNum) : undefined;
8
+ const interpretation = options.interpretation ?? {};
9
+ const usePhrasing = options.mode !== undefined ||
10
+ interpretation.phrasing !== undefined ||
11
+ options.office !== undefined;
12
+ const annotated = [];
13
+ const flatForPhrasing = [];
14
+ for (let pi = 0; pi < ir.phrases.length; pi++) {
15
+ const phrase = ir.phrases[pi];
16
+ const divStr = phrase.divisio?.divisio ?? null;
17
+ let si = 0;
18
+ for (const syl of phrase.syllables) {
19
+ for (let ni = 0; ni < syl.notes.length; ni++) {
20
+ const note = syl.notes[ni];
21
+ annotated.push({
22
+ note,
23
+ phraseIndex: pi,
24
+ syllableIndex: si,
25
+ noteIndex: ni,
26
+ divisio: divStr,
27
+ neume: syl.neume,
28
+ });
29
+ flatForPhrasing.push(note);
30
+ }
31
+ si++;
32
+ }
33
+ if (phrase.divisio) {
34
+ flatForPhrasing.push({
35
+ type: "rest",
36
+ divisio: phrase.divisio.divisio,
37
+ duration: phrase.divisio.duration,
38
+ });
39
+ }
40
+ }
41
+ let velocities = annotated.map(() => null);
42
+ let shapedDurations = annotated.map((a) => a.note.performance.duration);
43
+ if (usePhrasing && annotated.length > 0) {
44
+ const profile = buildPhrasing(interpretation.phrasing ?? "lyrical", {
45
+ overrides: interpretation.phrasingOverrides,
46
+ });
47
+ const modeProfile = shapePhrasingForMode(profile, modeData, {
48
+ strength: interpretation.modalInfluence,
49
+ });
50
+ const tenorPc = modeData?.tenor;
51
+ const shaped = applyPhrasing(flatForPhrasing, modeProfile, tenorPc);
52
+ for (let i = 0; i < shaped.length && i < annotated.length; i++) {
53
+ velocities[i] = shaped[i].performance.velocity;
54
+ shapedDurations[i] = shaped[i].shapedDuration;
55
+ }
56
+ }
57
+ // Position of each note within its neume figure — resets when the
58
+ // (syllableIndex, neumeGroup) pair changes.
59
+ const neumeIndices = [];
60
+ let prevKey = null;
61
+ let withinGroup = 0;
62
+ for (const a of annotated) {
63
+ const key = `${a.phraseIndex}:${a.syllableIndex}:${a.note.context.neumeGroup}`;
64
+ withinGroup = key === prevKey ? withinGroup + 1 : 0;
65
+ neumeIndices.push(withinGroup);
66
+ prevKey = key;
67
+ }
68
+ const rows = annotated.map((a, i) => {
69
+ const n = a.note;
70
+ return {
71
+ phraseIndex: a.phraseIndex,
72
+ syllableIndex: a.syllableIndex,
73
+ noteIndex: a.noteIndex,
74
+ neumeGroup: n.context.neumeGroup,
75
+ neumeIndex: neumeIndices[i],
76
+ lyric: n.context.lyric,
77
+ vowel: n.context.vowel,
78
+ midi: n.pitch.midi,
79
+ pc: n.pitch.pc,
80
+ octave: n.pitch.oct,
81
+ degree: n.step.degree,
82
+ hz: n.pitch.hz,
83
+ offset: n.pitch.offset,
84
+ spn: n.pitch.spn,
85
+ bend: n.pitch.bend,
86
+ rhythmicShape: n.performance.rhythmicShape,
87
+ rhythmicIndex: n.performance.rhythmicIndex,
88
+ duration: n.performance.duration,
89
+ velocity: velocities[i],
90
+ shapedDuration: shapedDurations[i],
91
+ ictus: n.context.ictus,
92
+ accidental: n.pitch.acc,
93
+ accidentalSource: n.context.accidentalSource,
94
+ quilisma: n.context.quilisma,
95
+ liquescent: n.context.liquescent,
96
+ strophicus: n.context.strophicus,
97
+ divisio: a.divisio,
98
+ role: n.step.role,
99
+ name: n.step.name,
100
+ nomen: n.step.nomen,
101
+ hand: n.step.hand,
102
+ hexachord: n.step.hexachord,
103
+ solfege: n.step.solmization ?? SOLFEGE_BY_PC.get(n.pitch.pc) ?? null,
104
+ neume: a.neume,
105
+ };
106
+ });
107
+ return rows;
108
+ }
109
+ //# sourceMappingURL=tabula.js.map
@@ -0,0 +1,159 @@
1
+ import type { Pitch } from "../temper/pitch.js";
2
+ import type { Step } from "../temper/step.js";
3
+ import type { NeumeShape } from "../temper/neume.js";
4
+ import type { OfficeCode, OrdinaryCode } from "../chant/types.js";
5
+ export type ArsisThesis = "arsic" | "thetic";
6
+ export interface Performance {
7
+ velocity: number;
8
+ duration: number;
9
+ rhythmicShape: ArsisThesis;
10
+ rhythmicIndex: number;
11
+ }
12
+ export interface Context {
13
+ lyric: string;
14
+ vowel: string;
15
+ syllableIndex: number;
16
+ /** 0-based index of the neume figure within the syllable (GABC break markers). */
17
+ neumeGroup: number;
18
+ ictus: boolean;
19
+ accidentalSource: "none" | "state" | "explicit";
20
+ quilisma: boolean;
21
+ liquescent: boolean;
22
+ strophicus: boolean;
23
+ doubleEpisema: boolean;
24
+ weight: number;
25
+ }
26
+ export interface Note {
27
+ pitch: Pitch;
28
+ step: Step;
29
+ performance: Performance;
30
+ context: Context;
31
+ }
32
+ export type { Pitch, Step, NeumeShape, OfficeCode, OrdinaryCode };
33
+ export type Clef = "c1" | "c2" | "c3" | "c4" | "f1" | "f2" | "f3" | "f4" | `cb${1 | 2 | 3 | 4}` | `fb${1 | 2 | 3 | 4}`;
34
+ export type PhrasingType = "recitative" | "lyrical" | "hymnic" | "solemn";
35
+ export type ArticulationType = "restrained" | "balanced" | "expressive" | "strict";
36
+ export interface ArticulationWeights {
37
+ ictusWeight: number;
38
+ ictusDuration: number;
39
+ episemaWeight: number;
40
+ episemaDuration: number;
41
+ episemaDoubleDuration: number;
42
+ strophicusWeight: number;
43
+ strophicusDuration: number;
44
+ strophicusTripleDuration: number;
45
+ quilismaPrevWeight: number;
46
+ quilismaWeight: number;
47
+ liquescentWeight: number;
48
+ liquescentDuration: number;
49
+ initioWeight: number;
50
+ initioMelismaWeight: number;
51
+ initioMelismaDuration: number;
52
+ accentWeight: number;
53
+ uppercaseWeight: number;
54
+ uppercaseDuration: number;
55
+ repercussionPrevWeight: number;
56
+ repercussionPrevDuration: number;
57
+ repercussionOriscusWeight: number;
58
+ breakWeight: number;
59
+ dashWeight: number;
60
+ dashDuration: number;
61
+ }
62
+ export interface ArticulationProfile {
63
+ weights: ArticulationWeights;
64
+ weightBase: number;
65
+ weightGain: number;
66
+ weightSaturation: number;
67
+ durationBase: number;
68
+ durationGain: number;
69
+ durationMin: number;
70
+ durationMax: number;
71
+ neumeArch?: number;
72
+ durArch?: number;
73
+ ictusBoost?: number;
74
+ ruleGain?: number;
75
+ contourScale?: number;
76
+ }
77
+ export interface PhrasingProfile {
78
+ curve: number;
79
+ accent: number;
80
+ cadence: number;
81
+ tenor: number;
82
+ baseVelocity: number;
83
+ contourVel: number;
84
+ contourDur: number;
85
+ velSpread: number;
86
+ ictusBoost: number;
87
+ neumeArch: number;
88
+ durArch: number;
89
+ }
90
+ export interface InterpretationOptions {
91
+ articulation?: ArticulationType;
92
+ articulationOverrides?: Partial<ArticulationProfile>;
93
+ phrasing?: PhrasingType;
94
+ phrasingOverrides?: Partial<PhrasingProfile>;
95
+ modalInfluence?: number;
96
+ }
97
+ export interface ParseOptions {
98
+ oct?: number;
99
+ useVowelAccent?: boolean;
100
+ interpretation?: InterpretationOptions;
101
+ }
102
+ export interface ParseError {
103
+ message: string;
104
+ index?: number;
105
+ }
106
+ export interface ParsedNote {
107
+ type: "note";
108
+ step: number;
109
+ lyric: string;
110
+ syllableIndex: number;
111
+ /** 0-based index of the neume figure within the syllable (GABC break markers). */
112
+ neumeGroup: number;
113
+ ictus: boolean;
114
+ weight: number;
115
+ duration: number;
116
+ accidental: -1 | 0 | 1;
117
+ accidentalSource: "none" | "state" | "explicit";
118
+ quilisma: boolean;
119
+ liquescent: boolean;
120
+ strophicus: boolean;
121
+ doubleEpisema: boolean;
122
+ }
123
+ export interface RestEvent {
124
+ type: "rest";
125
+ divisio: "," | "`" | ";" | ":" | "::";
126
+ duration: number;
127
+ }
128
+ export type ChantEvent = ParsedNote | RestEvent;
129
+ export interface ParseResult {
130
+ events: ChantEvent[];
131
+ errors: ParseError[];
132
+ }
133
+ export type ChantType = OrdinaryCode | OfficeCode;
134
+ export interface Neume {
135
+ type: NeumeShape;
136
+ intervals: number[];
137
+ hasQuilisma: boolean;
138
+ hasLiquescent: boolean;
139
+ hasStrophicus: boolean;
140
+ }
141
+ export interface Syllable {
142
+ lyric: string;
143
+ notes: Note[];
144
+ neume: Neume;
145
+ }
146
+ export interface Phrase {
147
+ syllables: Syllable[];
148
+ divisio?: RestEvent;
149
+ }
150
+ export interface Score {
151
+ chant: {
152
+ incipit: string;
153
+ mode: string | null;
154
+ office: string;
155
+ };
156
+ phrases: Phrase[];
157
+ errors: ParseError[];
158
+ }
159
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,60 @@
1
+ import type { Scale, ScaleOpts, ScalaFile, RatioResult } from "./scale.js";
2
+ import type { Pitch, PitchInput } from "./pitch.js";
3
+ import type { Step, StepVariant, Finger, Region } from "./step.js";
4
+ import type { Interval, IntervalDirection, IntervalQuality } from "./interval.js";
5
+ import type { Neume, NeumeShape } from "./neume.js";
6
+ import type { GamutOptions } from "./gamut.js";
7
+ import type { ModeData, ModeProfile } from "./modes.js";
8
+ import type { GuidonianEntry, GuidonianVariant } from "./guido.js";
9
+ export type BuiltinTuning = "pythagorean" | "meantone" | "equal" | "ptolemy-intense" | "ptolemy-soft" | "ptolemy-equable";
10
+ export type Tuning = BuiltinTuning | string;
11
+ export interface TemperamentumOpts {
12
+ tuning?: Tuning;
13
+ mode?: number | "auto";
14
+ a4?: number;
15
+ root?: number;
16
+ transpose?: number;
17
+ comma?: number | string;
18
+ scale?: string | string[];
19
+ }
20
+ export type TemperamentumInput = BuiltinTuning | TemperamentumOpts;
21
+ export interface TonusOpts {
22
+ differentia?: string;
23
+ }
24
+ export interface Tonus {
25
+ mode: number;
26
+ differentia: string;
27
+ intonatio: Pitch[];
28
+ mediatio: Pitch[];
29
+ terminatio: Pitch[];
30
+ }
31
+ export interface Temperamentum {
32
+ tuning: Tuning;
33
+ mode: number | "auto";
34
+ a4: number;
35
+ root: number;
36
+ transpose: number;
37
+ comma: number;
38
+ ratios: number[];
39
+ cents: number[];
40
+ nota(input: PitchInput): Pitch;
41
+ gradus(input: PitchInput): Step;
42
+ intervallum(a: PitchInput, b: PitchInput): Interval;
43
+ neuma(inputs: PitchInput[]): Neume;
44
+ ratio(input: string): RatioResult & {
45
+ step: Step | null;
46
+ };
47
+ gamut(opts?: GamutOptions): Pitch[];
48
+ modus(mode: number): ModeData;
49
+ tonus(opts?: TonusOpts): Tonus;
50
+ }
51
+ /**
52
+ * Tuning context builder (`tonus.temperamentum`). Accepts a built-in
53
+ * tuning name, options, a custom scale array, or a Scala file, and
54
+ * returns a Temperamentum whose methods resolve pitches, intervals,
55
+ * neumes, the Guidonian gamut, and psalm tones under that tuning.
56
+ * @throws Error on invalid tuning, scale, or mode input.
57
+ */
58
+ export declare function buildTemper(input?: TemperamentumInput): Temperamentum;
59
+ export type { Scale, ScaleOpts, ScalaFile, RatioResult, Pitch, PitchInput, Step, StepVariant, Finger, Region, Neume, NeumeShape, Interval, IntervalDirection, IntervalQuality, ModeData, ModeProfile, GamutOptions, GuidonianEntry, GuidonianVariant, };
60
+ //# sourceMappingURL=api.d.ts.map
@@ -0,0 +1,130 @@
1
+ // ---------------------------------------------------------------------------
2
+ // engines/temper/api — Temperamentum context builder
3
+ // ---------------------------------------------------------------------------
4
+ import { buildRatios, parseScala, getPtolemaicRatios, toRatio } from "./scale.js";
5
+ import { parsePitch, toPitch } from "./pitch.js";
6
+ import { toStep } from "./step.js";
7
+ import { classifyInterval } from "./interval.js";
8
+ import { buildNeume } from "./neume.js";
9
+ import { buildGamut } from "./gamut.js";
10
+ import { getMode } from "./modes.js";
11
+ import { getTone, getDifferentia } from "../../data/tones.js";
12
+ function resolveOpts(input) {
13
+ if (!input)
14
+ return {};
15
+ if (typeof input === "string")
16
+ return { tuning: input };
17
+ return input;
18
+ }
19
+ function resolveScale(opts) {
20
+ if (!opts.scale)
21
+ return { tuning: opts.tuning ?? "pythagorean" };
22
+ if (Array.isArray(opts.scale)) {
23
+ return { tuning: opts.tuning ?? "custom", scaleSteps: opts.scale };
24
+ }
25
+ const parsed = parseScala(opts.scale);
26
+ return { tuning: opts.tuning ?? (parsed.name || "custom"), scaleSteps: parsed.steps };
27
+ }
28
+ function tuningToScaleOpts(opts) {
29
+ const { tuning, scaleSteps } = resolveScale(opts);
30
+ const scalaOpts = {
31
+ mode: opts.mode === "auto" ? 1 : (opts.mode ?? 1),
32
+ a4: opts.a4 ?? 440,
33
+ root: opts.root,
34
+ transpose: opts.transpose ?? 0,
35
+ };
36
+ if (scaleSteps) {
37
+ scalaOpts.steps = scaleSteps;
38
+ }
39
+ else {
40
+ switch (tuning) {
41
+ case "pythagorean":
42
+ scalaOpts.comma = 0;
43
+ break;
44
+ case "meantone":
45
+ scalaOpts.comma = opts.comma ?? "1/4";
46
+ break;
47
+ case "equal":
48
+ scalaOpts.steps = Array.from({ length: 12 }, (_, i) => i * 100);
49
+ break;
50
+ default: {
51
+ const ptolemaic = getPtolemaicRatios(tuning);
52
+ if (ptolemaic)
53
+ scalaOpts.steps = ptolemaic;
54
+ break;
55
+ }
56
+ }
57
+ }
58
+ return { scalaOpts, tuning };
59
+ }
60
+ /**
61
+ * Tuning context builder (`tonus.temperamentum`). Accepts a built-in
62
+ * tuning name, options, a custom scale array, or a Scala file, and
63
+ * returns a Temperamentum whose methods resolve pitches, intervals,
64
+ * neumes, the Guidonian gamut, and psalm tones under that tuning.
65
+ * @throws Error on invalid tuning, scale, or mode input.
66
+ */
67
+ export function buildTemper(input) {
68
+ const opts = resolveOpts(input);
69
+ const modeVal = opts.mode ?? "auto";
70
+ const { scalaOpts, tuning } = tuningToScaleOpts(opts);
71
+ const scala = buildRatios(scalaOpts);
72
+ return {
73
+ tuning,
74
+ mode: modeVal,
75
+ a4: scala.a4,
76
+ root: scala.root,
77
+ transpose: scala.transpose,
78
+ comma: scala.comma,
79
+ ratios: scala.ratios,
80
+ cents: scala.cents,
81
+ nota(pitchInput) {
82
+ return toPitch(pitchInput, scala);
83
+ },
84
+ gradus(pitchInput) {
85
+ const midi = parsePitch(pitchInput, { mode: scala.mode, a4: scala.a4 });
86
+ return toStep(midi, scala);
87
+ },
88
+ intervallum(a, b) {
89
+ const midiA = parsePitch(a, { mode: scala.mode, a4: scala.a4 });
90
+ const midiB = parsePitch(b, { mode: scala.mode, a4: scala.a4 });
91
+ return classifyInterval(midiA, midiB);
92
+ },
93
+ neuma(inputs) {
94
+ return buildNeume(inputs, scala);
95
+ },
96
+ ratio(input) {
97
+ const result = toRatio(input);
98
+ const folded = result.ratio >= 2 ? result.ratio / Math.pow(2, Math.floor(Math.log2(result.ratio))) : result.ratio;
99
+ let step = null;
100
+ for (let pc = 0; pc < 12; pc++) {
101
+ if (Math.abs((scala.ratios[pc] ?? 0) - folded) < 1e-6) {
102
+ const baseMidi = 60 + pc;
103
+ step = toStep(baseMidi, scala);
104
+ break;
105
+ }
106
+ }
107
+ return { ...result, step };
108
+ },
109
+ gamut(gamutOpts) {
110
+ return buildGamut(scala, gamutOpts);
111
+ },
112
+ modus(mode) {
113
+ return getMode(mode);
114
+ },
115
+ tonus(tonusOpts) {
116
+ if (modeVal === "auto")
117
+ throw new Error("tonus() requires an explicit mode — set mode in temperamentum()");
118
+ const tone = getTone(modeVal);
119
+ const diff = getDifferentia(tone, tonusOpts?.differentia);
120
+ return {
121
+ mode: modeVal,
122
+ differentia: diff.code,
123
+ intonatio: tone.intonation.map((m) => toPitch(m, scala)),
124
+ mediatio: tone.mediant.map((m) => toPitch(m, scala)),
125
+ terminatio: diff.termination.map((m) => toPitch(m, scala)),
126
+ };
127
+ },
128
+ };
129
+ }
130
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1,27 @@
1
+ /** 12 pitch-class names, index = pitch class 0–11. Sharp-biased except Eb/Ab/Bb. */
2
+ export declare const PITCH_CLASS: readonly ["C", "C#", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B"];
3
+ /** Diatonic step letters in ascending order. */
4
+ export declare const DIATONIC_STEPS: readonly ["C", "D", "E", "F", "G", "A", "B"];
5
+ export type DiatonicStep = (typeof DIATONIC_STEPS)[number];
6
+ /** Map any note name (including enharmonics) to its pitch class 0–11. */
7
+ export declare const NAME_TO_CHROMA: Map<string, number>;
8
+ export declare const SOLFEGE_TO_CHROMA: Map<string, number>;
9
+ export declare const CHROMA_TO_SOLFEGE: Map<number, string>;
10
+ export interface IntervalEntry {
11
+ latin: string;
12
+ alias?: string;
13
+ degree: number;
14
+ quality: "perfect" | "major" | "minor" | "augmented" | "diminished";
15
+ class: string;
16
+ }
17
+ export declare const INTERVAL: IntervalEntry[];
18
+ export declare const UNISONUS: IntervalEntry;
19
+ export interface StepAlter {
20
+ step: DiatonicStep;
21
+ acc: -1 | 0 | 1;
22
+ }
23
+ export declare const SHARP_SPELLING: StepAlter[];
24
+ export declare const FLAT_SPELLING: StepAlter[];
25
+ export declare const PREFER_FLAT_PCS: Set<number>;
26
+ export declare const GUIDO_TO_PC: Record<string, Record<string, number>>;
27
+ //# sourceMappingURL=constants.d.ts.map