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,76 @@
1
+ import type { Season, Grade, Feast } from "../cal/types.js";
2
+ export type { Season, Grade, Feast };
3
+ export type OfficeCode = "an" | "al" | "ca" | "co" | "gr" | "hy" | "in" | "of" | "ps" | "re" | "rb" | "se" | "tr" | "tp" | "or";
4
+ export type OrdinaryCode = "ky" | "gl" | "cr" | "sa" | "ag" | "be" | "it" | "as" | "va";
5
+ export type ChantSource = "gr" | "lu" | "la" | "lh";
6
+ export type CanonicalHour = "matutinum" | "laudes" | "prima" | "tertia" | "sexta" | "nona" | "vesperae" | "completorium";
7
+ export declare const MODE_LABELS: Readonly<Record<string, string>>;
8
+ export declare const OFFICE_LABELS: Readonly<Record<OfficeCode, string>>;
9
+ export declare const ORDINARY_LABELS: Readonly<Record<string, string>>;
10
+ export interface Chant {
11
+ id: string;
12
+ incipit: string;
13
+ gabc: string;
14
+ office: OfficeCode;
15
+ genus: string;
16
+ mode: string | null;
17
+ modus: string | null;
18
+ pages: {
19
+ page: string;
20
+ sequence: number;
21
+ extent: number;
22
+ }[];
23
+ source: {
24
+ book: string;
25
+ year: number | null;
26
+ editor: string | null;
27
+ code?: ChantSource | "user";
28
+ };
29
+ ordinary?: OrdinaryCode;
30
+ ordinarium?: string;
31
+ mass?: number;
32
+ }
33
+ export interface OrdinaryChant extends Chant {
34
+ ordinary: OrdinaryCode;
35
+ ordinarium: string;
36
+ mass: number;
37
+ }
38
+ export interface CantusQuery {
39
+ id?: string | string[];
40
+ gabc?: string;
41
+ incipit?: string;
42
+ mode?: number | string | (number | string)[];
43
+ office?: OfficeCode | OfficeCode[];
44
+ source?: ChantSource | ChantSource[];
45
+ limit?: number;
46
+ offset?: number;
47
+ sort?: "incipit" | "mode" | "id";
48
+ }
49
+ export interface PropriumQuery extends CantusQuery {
50
+ feast?: Feast | Feast[];
51
+ }
52
+ export interface OrdinariumQuery extends CantusQuery {
53
+ feast?: Feast | Feast[];
54
+ ordinary?: OrdinaryCode;
55
+ mass?: number;
56
+ }
57
+ export interface OfficiumQuery extends CantusQuery {
58
+ feast?: Feast | Feast[];
59
+ hora?: CanonicalHour;
60
+ }
61
+ export interface PsalmusQuery {
62
+ psalm?: number | string;
63
+ verse?: string;
64
+ mode?: number;
65
+ differentia?: string;
66
+ intonatio?: boolean;
67
+ }
68
+ export interface PsalmVerse {
69
+ psalm: number;
70
+ verse: string;
71
+ half1: string;
72
+ half2: string;
73
+ type: "psalm" | "canticle";
74
+ source?: string;
75
+ }
76
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,34 @@
1
+ // ── Display labels ──
2
+ export const MODE_LABELS = Object.freeze({
3
+ "1": "Modus I", "2": "Modus II", "3": "Modus III", "4": "Modus IV",
4
+ "5": "Modus V", "6": "Modus VI", "7": "Modus VII", "8": "Modus VIII",
5
+ });
6
+ export const OFFICE_LABELS = Object.freeze({
7
+ an: "Antiphona",
8
+ al: "Alleluia",
9
+ ca: "Canticum",
10
+ co: "Communio",
11
+ gr: "Graduale",
12
+ hy: "Hymnus",
13
+ in: "Introitus",
14
+ of: "Offertorium",
15
+ ps: "Psalmus",
16
+ re: "Responsorium",
17
+ rb: "Responsorium Breve",
18
+ se: "Sequentia",
19
+ tr: "Tractus",
20
+ tp: "Tonus Peregrinus",
21
+ or: "Ordinarium",
22
+ });
23
+ export const ORDINARY_LABELS = Object.freeze({
24
+ ky: "Kyrie eleison",
25
+ gl: "Gloria",
26
+ cr: "Credo",
27
+ sa: "Sanctus",
28
+ ag: "Agnus Dei",
29
+ be: "Benedicamus",
30
+ it: "Ite missa est",
31
+ as: "Asperges",
32
+ va: "Vidi aquam",
33
+ });
34
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,2 @@
1
+ export declare const DEFAULT_EPOCH: Date;
2
+ //# sourceMappingURL=epoch.d.ts.map
@@ -0,0 +1,14 @@
1
+ // ---------------------------------------------------------------------------
2
+ // engines/epoch — the default "now" for no-argument queries
3
+ // ---------------------------------------------------------------------------
4
+ // tonus lives in the Middle Ages, so a bare festum() or caelum() resolves an
5
+ // emblematic medieval day rather than the modern date: the symbolic birthday
6
+ // of Guido d'Arezzo (c. 991), inventor of staff notation and the Guidonian
7
+ // hand. His exact birth date is unrecorded; 991-06-01 is an editorial anchor.
8
+ //
9
+ // This is a cross-cutting constant — both the calendar (cal) and the ephemeris
10
+ // (planet) default to it — so it lives at the engines root rather than inside
11
+ // either engine. (Year 991 ≥ 100, so it is safe from JS Date's 0–99 → 1900+
12
+ // remapping.)
13
+ export const DEFAULT_EPOCH = new Date(Date.UTC(991, 5, 1));
14
+ //# sourceMappingURL=epoch.js.map
@@ -0,0 +1,35 @@
1
+ import type { Cosmos } from "../planet/types.js";
2
+ import type { Temperamentum } from "../temper/api.js";
3
+ import { type VoicedBody } from "./voice.js";
4
+ import { type VoicedAspect } from "./aspects.js";
5
+ import { type Imprint } from "../imprint.js";
6
+ import { type HarmonyTabulaRow } from "./tabula.js";
7
+ import type { Author } from "./data/doctrines.js";
8
+ export type { VoicedBody, VoicedAspect, Author };
9
+ export interface HarmoniaOpts {
10
+ temperamentum?: Temperamentum;
11
+ doctrina?: Author;
12
+ }
13
+ export interface Frame {
14
+ date: Date;
15
+ bodies: VoicedBody[];
16
+ aspects: VoicedAspect[];
17
+ }
18
+ export interface Harmony {
19
+ doctrina: Author;
20
+ auctor: string;
21
+ date: Date;
22
+ bodies: VoicedBody[];
23
+ aspects: VoicedAspect[];
24
+ frames?: Frame[];
25
+ tabula: HarmonyTabulaRow[];
26
+ imprint: Imprint;
27
+ }
28
+ /**
29
+ * Harmony of the spheres builder (`tonus.harmonia`). Voices a Cosmos
30
+ * (or series) into pitches and Greek vowels under a doctrina —
31
+ * Pythagoras, Boethius (default), Pliny, or Ptolemy — returning voiced
32
+ * bodies, aspects, an imprint, and a tabula.
33
+ */
34
+ export declare function buildHarmonia(input: Cosmos | Cosmos[], opts?: HarmoniaOpts): Harmony;
35
+ //# sourceMappingURL=api.d.ts.map
@@ -0,0 +1,90 @@
1
+ import { buildTemper } from "../temper/api.js";
2
+ import { buildRatios } from "../temper/scale.js";
3
+ import { voiceBodies } from "./voice.js";
4
+ import { voiceAspects } from "./aspects.js";
5
+ import { computeImprintFromBodies } from "../imprint.js";
6
+ import { computeHarmonyTabula } from "./tabula.js";
7
+ import { DOCTRINAE } from "./data/doctrines.js";
8
+ function resolveScale(temper) {
9
+ if (temper) {
10
+ return buildRatios({
11
+ mode: temper.mode === "auto" ? 1 : temper.mode,
12
+ a4: temper.a4,
13
+ root: temper.root,
14
+ transpose: temper.transpose,
15
+ // steps carries the temperamentum's fully resolved scale (ptolemaic,
16
+ // meantone, custom, Scala) — without it only the pythagorean default
17
+ // would be rebuilt here.
18
+ steps: temper.cents,
19
+ });
20
+ }
21
+ return buildRatios();
22
+ }
23
+ function averageBodies(frames) {
24
+ if (frames.length === 0)
25
+ return [];
26
+ if (frames.length === 1)
27
+ return frames[0];
28
+ const byName = new Map();
29
+ for (const frame of frames) {
30
+ for (const body of frame) {
31
+ const list = byName.get(body.name) ?? [];
32
+ list.push(body);
33
+ byName.set(body.name, list);
34
+ }
35
+ }
36
+ const result = [];
37
+ for (const [, list] of byName) {
38
+ if (list.length === 0)
39
+ continue;
40
+ const representative = list[0];
41
+ const meanPresence = list.reduce((s, b) => s + b.presence, 0) / list.length;
42
+ const meanMotion = list.reduce((s, b) => s + b.motion, 0) / list.length;
43
+ result.push({ ...representative, presence: meanPresence, motion: meanMotion });
44
+ }
45
+ return result;
46
+ }
47
+ /**
48
+ * Harmony of the spheres builder (`tonus.harmonia`). Voices a Cosmos
49
+ * (or series) into pitches and Greek vowels under a doctrina —
50
+ * Pythagoras, Boethius (default), Pliny, or Ptolemy — returning voiced
51
+ * bodies, aspects, an imprint, and a tabula.
52
+ */
53
+ export function buildHarmonia(input, opts = {}) {
54
+ const cosmosArray = Array.isArray(input) ? input : [input];
55
+ if (cosmosArray.length === 0) {
56
+ throw new RangeError("harmonia requires at least one Cosmos");
57
+ }
58
+ const temper = opts.temperamentum ?? buildTemper();
59
+ const scale = resolveScale(temper);
60
+ const doctrinaKey = opts.doctrina ?? "boethius";
61
+ const doctrina = DOCTRINAE.get(doctrinaKey);
62
+ if (!doctrina) {
63
+ throw new RangeError(`Unknown doctrina: ${doctrinaKey}`);
64
+ }
65
+ const perCosmosBodies = [];
66
+ const frames = [];
67
+ for (const cosmos of cosmosArray) {
68
+ const vb = voiceBodies(cosmos.bodies, doctrina, scale);
69
+ const va = voiceAspects(cosmos.aspects, vb);
70
+ perCosmosBodies.push(vb);
71
+ frames.push({ date: cosmos.date, bodies: vb, aspects: va });
72
+ }
73
+ const aggregateBodies = averageBodies(perCosmosBodies);
74
+ const aggregateAspects = frames[0]?.aspects ?? [];
75
+ const imprint = computeImprintFromBodies(aggregateBodies, scale);
76
+ const harmony = {
77
+ doctrina: doctrinaKey,
78
+ auctor: doctrina.name,
79
+ date: cosmosArray[0].date,
80
+ bodies: aggregateBodies,
81
+ aspects: aggregateAspects,
82
+ tabula: computeHarmonyTabula(aggregateBodies, aggregateAspects),
83
+ imprint,
84
+ };
85
+ if (Array.isArray(input)) {
86
+ harmony.frames = frames;
87
+ }
88
+ return harmony;
89
+ }
90
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1,8 @@
1
+ import type { Aspect } from "../planet/types.js";
2
+ import type { Interval } from "../temper/interval.js";
3
+ import type { VoicedBody } from "./voice.js";
4
+ export interface VoicedAspect extends Aspect {
5
+ interval: Interval;
6
+ }
7
+ export declare function voiceAspects(aspects: Aspect[], voiced: VoicedBody[]): VoicedAspect[];
8
+ //# sourceMappingURL=aspects.d.ts.map
@@ -0,0 +1,15 @@
1
+ import { classifyInterval } from "../temper/interval.js";
2
+ export function voiceAspects(aspects, voiced) {
3
+ const byName = new Map(voiced.map((v) => [v.name, v]));
4
+ const results = [];
5
+ for (const asp of aspects) {
6
+ const a = byName.get(asp.bodies[0]);
7
+ const b = byName.get(asp.bodies[1]);
8
+ if (!a || !b)
9
+ continue;
10
+ const interval = classifyInterval(a.nota.pitch.midi, b.nota.pitch.midi);
11
+ results.push({ ...asp, interval });
12
+ }
13
+ return results;
14
+ }
15
+ //# sourceMappingURL=aspects.js.map
@@ -0,0 +1,16 @@
1
+ export type Author = "pythagoras" | "boethius" | "pliny" | "ptolemy";
2
+ export interface Voice {
3
+ body: string;
4
+ ratio: [number, number];
5
+ greekName: string;
6
+ }
7
+ export interface Doctrina {
8
+ name: string;
9
+ work: string;
10
+ date: string;
11
+ type: "A" | "B" | "C";
12
+ span: number;
13
+ voices: Voice[];
14
+ }
15
+ export declare const DOCTRINAE: Map<Author, Doctrina>;
16
+ //# sourceMappingURL=doctrines.d.ts.map
@@ -0,0 +1,154 @@
1
+ // ---------------------------------------------------------------------------
2
+ // engines/harmonia/data/doctrines — planetary harmonic systems
3
+ // ---------------------------------------------------------------------------
4
+ //
5
+ // Each doctrina defines a mapping from celestial bodies to Pythagorean ratios
6
+ // relative to the mese (structural center, the Sun). All ratios are expressed
7
+ // as [numerator, denominator] pairs.
8
+ //
9
+ // All v1 doctrinae are geocentric — Earth is the silent listener at the
10
+ // center, not a voiced body (except Pliny, where Earth is a boundary tone).
11
+ // The Sun holds the mese position in all four systems.
12
+ //
13
+ // Source: Joscelyn Godwin, "Harmonies of Heaven and Earth" (1987), Part Three;
14
+ //
15
+ // Taxonomy follows Godwin's classification:
16
+ // Type A — intervals represent distances between spheres
17
+ // Type B — intervals represent speeds/symbolic scale degrees
18
+ // Type C — intervals map to fixed tones of the Greater Perfect System
19
+ // ---------------------------------------------------------------------------
20
+ // ── Pythagoras ──
21
+ //
22
+ // Disjunct diatonic tetrachords spanning one octave. Attributed to the
23
+ // Pythagorean school via Plato (Republic X, Myth of Er) and later writers.
24
+ // Eight tones: seven planets plus the Fixed Stars.
25
+ //
26
+ // Structure (ascending from Saturn):
27
+ // [S, T, T] — tetrachord meson
28
+ // T — tone of disjunction
29
+ // [S, T, T] — tetrachord diezeugmenon
30
+ //
31
+ // The critical difference from Boethius: Venus sits on B natural (paramese),
32
+ // a whole tone above the mese, producing the disjunct system. This is
33
+ // B durum — "hard B" — the brighter, more open voicing.
34
+ const PYTHAGORAS = {
35
+ name: "Pythagoras of Samos",
36
+ work: "attributed via Plato, Republic X; Nicomachus, Manual of Harmonics",
37
+ date: "c. 530 BC / c. 100 AD",
38
+ type: "B",
39
+ span: 1200,
40
+ voices: [
41
+ { body: "FixedStars", ratio: [3, 2], greekName: "nete diezeugmenon" },
42
+ { body: "Saturn", ratio: [3, 4], greekName: "hypate meson" },
43
+ { body: "Jupiter", ratio: [64, 81], greekName: "parhypate meson" },
44
+ { body: "Mars", ratio: [8, 9], greekName: "lichanos meson" },
45
+ { body: "Sun", ratio: [1, 1], greekName: "mese" },
46
+ { body: "Venus", ratio: [9, 8], greekName: "paramese" },
47
+ { body: "Mercury", ratio: [32, 27], greekName: "trite diezeugmenon" },
48
+ { body: "Moon", ratio: [4, 3], greekName: "paranete diezeugmenon" },
49
+ ],
50
+ };
51
+ // ── Boethius ──
52
+ //
53
+ // Conjunct diatonic tetrachords spanning a seventh. Transmitted by Boethius
54
+ // (De Institutione Musica I.27) from Nicomachus of Gerasa. The medieval
55
+ // default — the system that defined musica mundana for a millennium.
56
+ //
57
+ // Structure (ascending from Saturn):
58
+ // [S, T, T] — tetrachord meson
59
+ // [S, T, T] — tetrachord synemmenon (conjunct at mese)
60
+ //
61
+ // Venus sits on Bb (trite synemmenon), a semitone above the mese.
62
+ // This is B molle — "soft B" — the darker, more introspective voicing.
63
+ // The conjunct tetrachords share the mese; no tone of disjunction.
64
+ //
65
+ // Seven planets only — Earth is silent (no motion, no sound).
66
+ const BOETHIUS = {
67
+ name: "Anicius Manlius Severinus Boethius",
68
+ work: "De Institutione Musica",
69
+ date: "c. 524",
70
+ type: "B",
71
+ span: 996,
72
+ voices: [
73
+ { body: "Saturn", ratio: [3, 4], greekName: "hypate meson" },
74
+ { body: "Jupiter", ratio: [64, 81], greekName: "parhypate meson" },
75
+ { body: "Mars", ratio: [8, 9], greekName: "lichanos meson" },
76
+ { body: "Sun", ratio: [1, 1], greekName: "mese" },
77
+ { body: "Venus", ratio: [256, 243], greekName: "trite synemmenon" },
78
+ { body: "Mercury", ratio: [32, 27], greekName: "paranete synemmenon" },
79
+ { body: "Moon", ratio: [4, 3], greekName: "nete synemmenon" },
80
+ ],
81
+ };
82
+ // ── Pliny ──
83
+ //
84
+ // Chromatic Dorian, Type A (distance-based). From Pliny the Elder,
85
+ // Naturalis Historia II.xx. The only chromatic-genus system in v1.
86
+ //
87
+ // Structure (ascending from Earth):
88
+ // T — proslambanomenos to first tetrachord
89
+ // [S, S, incomposite] — chromatic tetrachord hypaton
90
+ // [S, S, incomposite] — chromatic tetrachord meson (conjunct)
91
+ //
92
+ // The incomposite interval (19683/16384, ~318 cents) completes each
93
+ // chromatic tetrachord to a perfect fourth. Uses the corrected form
94
+ // from Censorinus / Theon of Smyrna for proper octave closure.
95
+ //
96
+ // Earth acts as a boundary tone (proslambanomenos), not a planetary voice.
97
+ // The chromatic genus produces tight semitone clusters where Boethius has
98
+ // open fourths. The sound is darker, more tense, more exotic.
99
+ const PLINY = {
100
+ name: "Gaius Plinius Secundus (Pliny the Elder)",
101
+ work: "Naturalis Historia",
102
+ date: "c. 77",
103
+ type: "A",
104
+ span: 1200,
105
+ voices: [
106
+ { body: "Saturn", ratio: [4, 3], greekName: "mese" },
107
+ { body: "Jupiter", ratio: [65536, 59049], greekName: "lichanos meson" },
108
+ { body: "Mars", ratio: [256, 243], greekName: "parhypate meson" },
109
+ { body: "Sun", ratio: [1, 1], greekName: "hypate meson" },
110
+ { body: "Venus", ratio: [16384, 19683], greekName: "lichanos hypaton" },
111
+ { body: "Mercury", ratio: [64, 81], greekName: "parhypate hypaton" },
112
+ { body: "Moon", ratio: [3, 4], greekName: "hypate hypaton" },
113
+ { body: "Earth", ratio: [2, 3], greekName: "proslambanomenos" },
114
+ ],
115
+ };
116
+ // ── Ptolemy ──
117
+ //
118
+ // Fixed tones of the Greater Perfect System, Type C. From the fragmentary
119
+ // final chapter of Ptolemy's Harmonics and the Canobus inscription.
120
+ //
121
+ // The widest span of any v1 doctrina: two full octaves. Unlike Types A
122
+ // and B, no single mode or genus is preferred — the fixed tones are the
123
+ // immovable skeleton upon which any mode can be built.
124
+ //
125
+ // The unique feature: intervals between planets carry astrological meaning.
126
+ // Saturn to Sun = octave (P8) → consonant = compatible influence
127
+ // Jupiter to Sun = fifth (P5) → consonant = compatible influence
128
+ // Mars to Sun = tone (M2) → dissonant = inimical influence
129
+ // Venus to Sun = fourth (P4) → consonant
130
+ // Mercury to Sun = seventh → dissonant
131
+ const PTOLEMY = {
132
+ name: "Claudius Ptolemaeus (Ptolemy)",
133
+ work: "Harmonics III; Canobus inscription",
134
+ date: "c. 150",
135
+ type: "C",
136
+ span: 2400,
137
+ voices: [
138
+ { body: "Saturn", ratio: [2, 1], greekName: "nete hyperbolaion" },
139
+ { body: "Jupiter", ratio: [3, 2], greekName: "nete diezeugmenon" },
140
+ { body: "Mars", ratio: [9, 8], greekName: "paramese" },
141
+ { body: "Sun", ratio: [1, 1], greekName: "mese" },
142
+ { body: "Venus", ratio: [3, 4], greekName: "hypate meson" },
143
+ { body: "Mercury", ratio: [9, 16], greekName: "hypate hypaton" },
144
+ { body: "Moon", ratio: [1, 2], greekName: "proslambanomenos" },
145
+ ],
146
+ };
147
+ // ── Export ──
148
+ export const DOCTRINAE = new Map([
149
+ ["pythagoras", PYTHAGORAS],
150
+ ["boethius", BOETHIUS],
151
+ ["pliny", PLINY],
152
+ ["ptolemy", PTOLEMY],
153
+ ]);
154
+ //# sourceMappingURL=doctrines.js.map
@@ -0,0 +1,10 @@
1
+ export interface PlanetVowel {
2
+ greek: string;
3
+ greekLower: string;
4
+ name: string;
5
+ modern: string;
6
+ phonetic: "a" | "e" | "i" | "o" | "u";
7
+ ipa: string;
8
+ }
9
+ export declare const PLANET_VOWELS: Readonly<Record<string, PlanetVowel>>;
10
+ //# sourceMappingURL=vowels.d.ts.map
@@ -0,0 +1,21 @@
1
+ // ---------------------------------------------------------------------------
2
+ // engines/harmonia/data/vowels — seven Greek planetary vowels
3
+ // ---------------------------------------------------------------------------
4
+ // Mapping of the seven classical planetary bodies to Greek vowels. The
5
+ // association is attested across late-antique magical and Pythagorean sources:
6
+ // Porphyry, Marcus Gnosticus, Demetrius of Phaleron, Nicomachus of Gerasa,
7
+ // Eusebius of Caesarea, Barthélemy of Edessa.
8
+ //
9
+ // Source: Godwin, J. *The Mystery of the Seven Vowels*. Phanes Press, 1991.
10
+ // The order Moon→Saturn → α ε η ι ο υ ω is Nicomachus's (Excerpta ex
11
+ // Nicomacho 6), matching the ascent from nearest to farthest sphere.
12
+ export const PLANET_VOWELS = {
13
+ Moon: { greek: "Α", greekLower: "α", name: "Alpha", modern: "A", phonetic: "a", ipa: "a" },
14
+ Mercury: { greek: "Ε", greekLower: "ε", name: "Epsilon", modern: "E", phonetic: "e", ipa: "e" },
15
+ Venus: { greek: "Η", greekLower: "η", name: "Eta", modern: "Ē", phonetic: "e", ipa: "ɛː" },
16
+ Sun: { greek: "Ι", greekLower: "ι", name: "Iota", modern: "I", phonetic: "i", ipa: "i" },
17
+ Mars: { greek: "Ο", greekLower: "ο", name: "Omicron", modern: "O", phonetic: "o", ipa: "o" },
18
+ Jupiter: { greek: "Υ", greekLower: "υ", name: "Upsilon", modern: "Y", phonetic: "u", ipa: "y" },
19
+ Saturn: { greek: "Ω", greekLower: "ω", name: "Omega", modern: "Ō", phonetic: "o", ipa: "ɔː" },
20
+ };
21
+ //# sourceMappingURL=vowels.js.map
@@ -0,0 +1,13 @@
1
+ import type { Body } from "../planet/types.js";
2
+ /**
3
+ * Presence: how audible a body is, 0–1.
4
+ * Combines magnitude (inverse, linear scale), elongation (low near Sun → combust),
5
+ * and phase (fraction illuminated).
6
+ */
7
+ export declare function computePresence(body: Body): number;
8
+ /**
9
+ * Motion: normalized absolute angular speed, 0–1.
10
+ * Retrograde treated as motion (sign ignored); the retrograde flag is separate.
11
+ */
12
+ export declare function computeMotion(body: Body): number;
13
+ //# sourceMappingURL=presence.d.ts.map
@@ -0,0 +1,48 @@
1
+ // Brightest to dimmest magnitude expected per body. Anything dimmer → presence 0.
2
+ // The Sun is fixed; others range within realistic visual magnitudes.
3
+ const MAX_MAGNITUDE = 6; // naked-eye limit
4
+ const MIN_MAGNITUDE = -27; // Sun
5
+ // Per-body peak geocentric speeds (deg/day) — used to normalize motion to 0–1.
6
+ // Values are approximate empirical maxima.
7
+ const SPEED_MAX = {
8
+ Moon: 15,
9
+ Mercury: 2.2,
10
+ Venus: 1.3,
11
+ Sun: 1.02,
12
+ Mars: 0.8,
13
+ Jupiter: 0.25,
14
+ Saturn: 0.14,
15
+ Earth: 1.02,
16
+ };
17
+ function clamp01(x) {
18
+ return Math.max(0, Math.min(1, x));
19
+ }
20
+ /**
21
+ * Presence: how audible a body is, 0–1.
22
+ * Combines magnitude (inverse, linear scale), elongation (low near Sun → combust),
23
+ * and phase (fraction illuminated).
24
+ */
25
+ export function computePresence(body) {
26
+ if (body.name === "Sun")
27
+ return 1;
28
+ // Earth is a listener in most doctrinae; individual harmonia may override
29
+ if (body.name === "Earth")
30
+ return 0;
31
+ // Magnitude → 0–1 (brighter = higher)
32
+ const magNorm = clamp01((MAX_MAGNITUDE - body.magnitude) / (MAX_MAGNITUDE - MIN_MAGNITUDE));
33
+ // Elongation: low → combust. Linear fall-off below 15°.
34
+ const elongFactor = clamp01(body.elongation / 15);
35
+ // Phase: already 0–1 for planets/moon
36
+ const phaseFactor = clamp01(body.phase);
37
+ // Weighted combination; magnitude dominates
38
+ return clamp01(0.6 * magNorm + 0.25 * elongFactor + 0.15 * phaseFactor);
39
+ }
40
+ /**
41
+ * Motion: normalized absolute angular speed, 0–1.
42
+ * Retrograde treated as motion (sign ignored); the retrograde flag is separate.
43
+ */
44
+ export function computeMotion(body) {
45
+ const max = SPEED_MAX[body.name] ?? 1;
46
+ return clamp01(Math.abs(body.speed) / max);
47
+ }
48
+ //# sourceMappingURL=presence.js.map
@@ -0,0 +1,28 @@
1
+ import type { BodyName } from "../planet/types.js";
2
+ import type { VoicedBody } from "./voice.js";
3
+ import type { VoicedAspect } from "./aspects.js";
4
+ export interface HarmonyTabulaRow {
5
+ bodyIndex: number;
6
+ name: BodyName;
7
+ nomen: string;
8
+ greekName: string;
9
+ midi: number;
10
+ pc: number;
11
+ oct: number;
12
+ spn: string;
13
+ hz: number;
14
+ presence: number;
15
+ motion: number;
16
+ velocity: number;
17
+ vowelGreek: string;
18
+ vowelPhonetic: string;
19
+ vowelName: string;
20
+ zodiac: number;
21
+ sign: string;
22
+ retrograde: boolean;
23
+ elongation: number;
24
+ magnitude: number;
25
+ aspectCount: number;
26
+ }
27
+ export declare function computeHarmonyTabula(bodies: VoicedBody[], aspects: VoicedAspect[]): HarmonyTabulaRow[];
28
+ //# sourceMappingURL=tabula.d.ts.map
@@ -0,0 +1,32 @@
1
+ export function computeHarmonyTabula(bodies, aspects) {
2
+ const aspectCountByName = new Map();
3
+ for (const a of aspects) {
4
+ for (const name of a.bodies) {
5
+ aspectCountByName.set(name, (aspectCountByName.get(name) ?? 0) + 1);
6
+ }
7
+ }
8
+ return bodies.map((b, i) => ({
9
+ bodyIndex: i,
10
+ name: b.name,
11
+ nomen: b.nomen,
12
+ greekName: b.greekName,
13
+ midi: b.nota.pitch.midi,
14
+ pc: b.nota.pitch.pc,
15
+ oct: b.nota.pitch.oct,
16
+ spn: b.nota.pitch.spn,
17
+ hz: b.nota.pitch.hz,
18
+ presence: b.presence,
19
+ motion: b.motion,
20
+ velocity: b.nota.performance.velocity,
21
+ vowelGreek: b.vowel.greek,
22
+ vowelPhonetic: b.vowel.phonetic,
23
+ vowelName: b.vowel.name,
24
+ zodiac: b.zodiac,
25
+ sign: b.sign,
26
+ retrograde: b.retrograde,
27
+ elongation: b.elongation,
28
+ magnitude: b.magnitude,
29
+ aspectCount: aspectCountByName.get(b.name) ?? 0,
30
+ }));
31
+ }
32
+ //# sourceMappingURL=tabula.js.map
@@ -0,0 +1,19 @@
1
+ import type { Body } from "../planet/types.js";
2
+ import type { Pitch } from "../temper/pitch.js";
3
+ import type { Scale } from "../temper/scale.js";
4
+ import type { Performance } from "../score/types.js";
5
+ import type { Doctrina } from "./data/doctrines.js";
6
+ import type { PlanetVowel } from "./data/vowels.js";
7
+ export interface VoicedPitch {
8
+ pitch: Pitch;
9
+ performance: Performance;
10
+ }
11
+ export interface VoicedBody extends Body {
12
+ nota: VoicedPitch;
13
+ presence: number;
14
+ motion: number;
15
+ greekName: string;
16
+ vowel: PlanetVowel;
17
+ }
18
+ export declare function voiceBodies(bodies: Body[], doctrina: Doctrina, scale: Scale): VoicedBody[];
19
+ //# sourceMappingURL=voice.d.ts.map