tonus 0.1.3 → 0.1.4

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.
@@ -1,7 +1,9 @@
1
1
  import type { Phrase } from "./score/types.js";
2
2
  import type { Pitch } from "./temper/pitch.js";
3
3
  import type { Scale } from "./temper/scale.js";
4
+ import { type ModalAffinity } from "./temper/modality.js";
4
5
  import type { VoicedBody } from "./harmonia/voice.js";
6
+ export type { ModalAffinity };
5
7
  export interface Attractor {
6
8
  pc: number;
7
9
  weight: number;
@@ -12,19 +14,18 @@ export interface VowelAttractor {
12
14
  weight: number;
13
15
  pitch: Pitch;
14
16
  }
15
- export interface ModalAffinity {
16
- mode: number;
17
- alias: string;
18
- score: number;
19
- }
20
17
  export interface Imprint {
21
18
  pcDistribution: Record<number, number>;
22
19
  attractors: Attractor[];
23
20
  vowelAttractors: VowelAttractor[];
24
21
  modalAffinity: ModalAffinity[];
25
22
  }
26
- /** Build an Imprint from chant phrases (unweighted pc counts). */
27
- export declare function computeImprint(phrases: Phrase[], scale: Scale): Imprint;
23
+ export interface ImprintOptions {
24
+ /** Positions "phrase:syllable:note" of cadence resolution notes, weighted up. */
25
+ cadenceNotes?: Set<string>;
26
+ }
27
+ /** Build an Imprint from chant phrases, weighting structural notes more. */
28
+ export declare function computeImprint(phrases: Phrase[], scale: Scale, opts?: ImprintOptions): Imprint;
28
29
  /** Build an Imprint from voiced planetary bodies (presence-weighted pc counts). */
29
30
  export declare function computeImprintFromBodies(bodies: VoicedBody[], scale: Scale): Imprint;
30
31
  //# sourceMappingURL=imprint.d.ts.map
@@ -1,5 +1,5 @@
1
1
  import { toPitch } from "./temper/pitch.js";
2
- import { MODES } from "./temper/modes.js";
2
+ import { computeModalAffinity } from "./temper/modality.js";
3
3
  const DEFAULT_TOP = 5;
4
4
  const DEFAULT_MIDI_OCTAVE = 4;
5
5
  const VOWELS = ["a", "e", "i", "o", "u"];
@@ -22,24 +22,6 @@ function computeAttractors(pcDistribution, scale, topN = DEFAULT_TOP) {
22
22
  pitch: pitchForPc(pc, scale),
23
23
  }));
24
24
  }
25
- function computeModalAffinity(pcDistribution) {
26
- const results = [];
27
- for (let m = 1; m <= 8; m++) {
28
- const data = MODES.get(m);
29
- if (!data)
30
- continue;
31
- const structural = new Set([
32
- data.final,
33
- data.tenor,
34
- ...data.modulations.regular,
35
- ]);
36
- let score = 0;
37
- for (const pc of structural)
38
- score += pcDistribution[pc] ?? 0;
39
- results.push({ mode: m, alias: data.alias, score });
40
- }
41
- return results.sort((a, b) => b.score - a.score);
42
- }
43
25
  function computeVowelAttractors(phrases, scale) {
44
26
  const vowelPcMap = new Map();
45
27
  for (const v of VOWELS)
@@ -85,15 +67,30 @@ function computeVowelAttractors(phrases, scale) {
85
67
  }
86
68
  return results.sort((a, b) => b.weight - a.weight);
87
69
  }
88
- /** Build an Imprint from chant phrases (unweighted pc counts). */
89
- export function computeImprint(phrases, scale) {
70
+ // A note's contribution to the pc-distribution is raised where it carries more
71
+ // structural weight: on an ictus (the rhythmic footfall) and, above all, when it
72
+ // is a cadence's resolution. Cadence notes are passed in — the imprint sits below
73
+ // the score engine, so it cannot detect them itself.
74
+ const ICTUS_WEIGHT = 1.5;
75
+ const CADENCE_WEIGHT = 2;
76
+ /** Build an Imprint from chant phrases, weighting structural notes more. */
77
+ export function computeImprint(phrases, scale, opts = {}) {
78
+ const cadenceNotes = opts.cadenceNotes;
90
79
  const pcCounts = new Array(12).fill(0);
91
80
  let total = 0;
92
- for (const phrase of phrases) {
93
- for (const syl of phrase.syllables) {
94
- for (const note of syl.notes) {
95
- pcCounts[note.pitch.pc]++;
96
- total++;
81
+ for (let pi = 0; pi < phrases.length; pi++) {
82
+ const phrase = phrases[pi];
83
+ for (let si = 0; si < phrase.syllables.length; si++) {
84
+ const notes = phrase.syllables[si].notes;
85
+ for (let ni = 0; ni < notes.length; ni++) {
86
+ const note = notes[ni];
87
+ let w = 1;
88
+ if (note.context.ictus)
89
+ w *= ICTUS_WEIGHT;
90
+ if (cadenceNotes?.has(`${pi}:${si}:${ni}`))
91
+ w *= CADENCE_WEIGHT;
92
+ pcCounts[note.pitch.pc] += w;
93
+ total += w;
97
94
  }
98
95
  }
99
96
  }
@@ -101,11 +98,12 @@ export function computeImprint(phrases, scale) {
101
98
  for (let pc = 0; pc < 12; pc++) {
102
99
  pcDistribution[pc] = total > 0 ? pcCounts[pc] / total : 0;
103
100
  }
101
+ const firstNotePc = phrases[0]?.syllables[0]?.notes[0]?.pitch.pc;
104
102
  return {
105
103
  pcDistribution,
106
104
  attractors: computeAttractors(pcDistribution, scale),
107
105
  vowelAttractors: computeVowelAttractors(phrases, scale),
108
- modalAffinity: computeModalAffinity(pcDistribution),
106
+ modalAffinity: computeModalAffinity(pcDistribution, firstNotePc),
109
107
  };
110
108
  }
111
109
  /** Build an Imprint from voiced planetary bodies (presence-weighted pc counts). */
@@ -1,6 +1,7 @@
1
1
  import { type Imprint } from "../imprint.js";
2
2
  import { type Prosody } from "./prosody.js";
3
3
  import { type Cadence } from "./cadence.js";
4
+ import { type Modulation } from "./modulation.js";
4
5
  import { type ChantTabulaRow } from "./tabula.js";
5
6
  import { type MidiOpts, type MidiEmitResult } from "./emitters/midi.js";
6
7
  import { type MusicXmlOpts, type MusicXmlEmitResult } from "./emitters/musicxml.js";
@@ -32,6 +33,8 @@ export interface Score {
32
33
  prosody: Prosody;
33
34
  /** Mode-specific cadence at each phrase-ending divisio. */
34
35
  cadences: Cadence[];
36
+ /** Passages where the tonal centre leans away from the home mode. */
37
+ modulations: Modulation[];
35
38
  imprint: Imprint;
36
39
  /**
37
40
  * Emit a Standard MIDI File from the score's tabula. Returns the file bytes
@@ -53,6 +56,7 @@ export interface Score {
53
56
  export declare function buildScore(chant: Chant, opts?: ScoreOpts): Score;
54
57
  export type { ParseError };
55
58
  export type { Cadence, CadenceTarget, CadenceApproach } from "./cadence.js";
59
+ export type { Modulation } from "./modulation.js";
56
60
  export type { MidiOpts, MidiEmitResult, MidiJsonResult, MidiJsonEvent } from "./emitters/midi.js";
57
61
  export type { MusicXmlOpts, MusicXmlEmitResult } from "./emitters/musicxml.js";
58
62
  //# sourceMappingURL=api.d.ts.map
@@ -8,6 +8,7 @@ import { computeMeta } from "./meta.js";
8
8
  import { computeImprint } from "../imprint.js";
9
9
  import { computeProsody } from "./prosody.js";
10
10
  import { detectCadences } from "./cadence.js";
11
+ import { detectModulations } from "./modulation.js";
11
12
  import { computeTabula } from "./tabula.js";
12
13
  import { MODES } from "../temper/modes.js";
13
14
  import { toMidi } from "./emitters/midi.js";
@@ -61,6 +62,8 @@ export function buildScore(chant, opts) {
61
62
  // Cadence detection runs here, where the resolved mode (and its cadence
62
63
  // figures) is in hand. Pure data — mirrors the arsis/thesis pass in ir.ts.
63
64
  const cadences = detectCadences(ir.phrases, meta.mode != null ? MODES.get(meta.mode) : undefined);
65
+ // Modulation: where the tonal centre leans away from the home mode.
66
+ const modulations = detectModulations(ir.phrases, meta.mode ?? undefined);
64
67
  const tabula = computeTabula(ir, {
65
68
  mode: meta.mode ?? undefined,
66
69
  a4Hz: opts?.temperamentum?.a4,
@@ -82,7 +85,14 @@ export function buildScore(chant, opts) {
82
85
  tabula,
83
86
  prosody: computeProsody(ir.phrases),
84
87
  cadences,
85
- imprint: computeImprint(ir.phrases, scale),
88
+ modulations,
89
+ imprint: computeImprint(ir.phrases, scale, {
90
+ // Each cadence's resolution note (its last) is the strongest modal anchor.
91
+ cadenceNotes: new Set(cadences.map((c) => {
92
+ const [pi, si, ni] = c.notes[c.notes.length - 1];
93
+ return `${pi}:${si}:${ni}`;
94
+ })),
95
+ }),
86
96
  midi(emitOpts) {
87
97
  return toMidi(tabula, emitOpts);
88
98
  },
@@ -0,0 +1,20 @@
1
+ import type { Phrase } from "./types.js";
2
+ export interface Modulation {
3
+ /** Phrase index where the modulation begins (inclusive). */
4
+ startPhrase: number;
5
+ /** Phrase index where it ends (inclusive). */
6
+ endPhrase: number;
7
+ /** The mode the passage leans toward (1–8). */
8
+ toMode: number;
9
+ /** 0–1: how strongly the foreign mode outscored the home mode, averaged. */
10
+ confidence: number;
11
+ }
12
+ /**
13
+ * Detect tonal-centre shifts. For each phrase, score it against every mode; a
14
+ * phrase whose top mode is not the home mode, and beats the home mode by MARGIN,
15
+ * "leans" toward that foreign mode. Consecutive phrases leaning to the same mode
16
+ * merge into one modulation span. `homeMode` is the chant's own mode (1–8); with
17
+ * no mode, nothing is detected.
18
+ */
19
+ export declare function detectModulations(phrases: Phrase[], homeMode: number | undefined): Modulation[];
20
+ //# sourceMappingURL=modulation.d.ts.map
@@ -0,0 +1,74 @@
1
+ import { computeModalAffinity } from "../temper/modality.js";
2
+ // How much a foreign mode must outscore the home mode (in normalised affinity)
3
+ // before a phrase counts as leaning away. Calibrated against Suñol's worked
4
+ // examples: at 0.25 the modulations he names in Christus resurgens (to mode 3)
5
+ // register, while incidental modal colouring below that does not.
6
+ const MARGIN = 0.25;
7
+ /** The pitch-class distribution of one phrase's notes (fractions summing to 1). */
8
+ function phrasePcDistribution(phrase) {
9
+ const counts = new Array(12).fill(0);
10
+ let total = 0;
11
+ for (const syl of phrase.syllables) {
12
+ for (const note of syl.notes) {
13
+ counts[note.pitch.pc]++;
14
+ total++;
15
+ }
16
+ }
17
+ const dist = {};
18
+ for (let pc = 0; pc < 12; pc++)
19
+ dist[pc] = total > 0 ? counts[pc] / total : 0;
20
+ return dist;
21
+ }
22
+ /**
23
+ * Detect tonal-centre shifts. For each phrase, score it against every mode; a
24
+ * phrase whose top mode is not the home mode, and beats the home mode by MARGIN,
25
+ * "leans" toward that foreign mode. Consecutive phrases leaning to the same mode
26
+ * merge into one modulation span. `homeMode` is the chant's own mode (1–8); with
27
+ * no mode, nothing is detected.
28
+ */
29
+ export function detectModulations(phrases, homeMode) {
30
+ if (homeMode == null)
31
+ return [];
32
+ // Per-phrase lean: the foreign mode a phrase favours, with its margin over
33
+ // the home mode — or null if the phrase stays home.
34
+ const leans = phrases.map((phrase) => {
35
+ if (phrase.syllables.every((s) => s.notes.length === 0))
36
+ return null;
37
+ const affinity = computeModalAffinity(phrasePcDistribution(phrase));
38
+ const top = affinity[0];
39
+ if (!top || top.mode === homeMode)
40
+ return null;
41
+ const home = affinity.find((a) => a.mode === homeMode);
42
+ const margin = top.score - (home?.score ?? 0);
43
+ return margin >= MARGIN ? { mode: top.mode, margin } : null;
44
+ });
45
+ // Merge consecutive phrases leaning to the same foreign mode into spans.
46
+ const modulations = [];
47
+ let run = null;
48
+ const flush = () => {
49
+ if (!run)
50
+ return;
51
+ const avg = run.margins.reduce((s, m) => s + m, 0) / run.margins.length;
52
+ modulations.push({
53
+ startPhrase: run.start,
54
+ endPhrase: run.start + run.margins.length - 1,
55
+ toMode: run.mode,
56
+ confidence: Math.min(1, Math.round(avg * 100) / 100),
57
+ });
58
+ run = null;
59
+ };
60
+ for (let i = 0; i < leans.length; i++) {
61
+ const lean = leans[i];
62
+ if (lean && run && lean.mode === run.mode) {
63
+ run.margins.push(lean.margin);
64
+ }
65
+ else {
66
+ flush();
67
+ if (lean)
68
+ run = { mode: lean.mode, start: i, margins: [lean.margin] };
69
+ }
70
+ }
71
+ flush();
72
+ return modulations;
73
+ }
74
+ //# sourceMappingURL=modulation.js.map
@@ -28,6 +28,22 @@ export interface Tonus {
28
28
  mediatio: Pitch[];
29
29
  terminatio: Pitch[];
30
30
  }
31
+ /** A pitch resolved through the tuning, with its Guidonian annotation. */
32
+ export interface TunedNote {
33
+ pitch: Pitch;
34
+ step: Step;
35
+ }
36
+ /**
37
+ * A mode's reference data (ModeData), enriched with its structural pitches
38
+ * tuned through the temperamentum that returned it. `modus()`. Cadence figures
39
+ * stay in their diatonic-step form on `cadences` — they are transposition-
40
+ * relative by design.
41
+ */
42
+ export interface Modus extends ModeData {
43
+ finalis: TunedNote;
44
+ reciting: TunedNote;
45
+ ambitusNotes: TunedNote[];
46
+ }
31
47
  export interface Temperamentum {
32
48
  tuning: Tuning;
33
49
  mode: number | "auto";
@@ -45,7 +61,7 @@ export interface Temperamentum {
45
61
  step: Step | null;
46
62
  };
47
63
  gamut(opts?: GamutOptions): Pitch[];
48
- modus(mode: number): ModeData;
64
+ modus(mode: number): Modus;
49
65
  tonus(opts?: TonusOpts): Tonus;
50
66
  }
51
67
  /**
@@ -110,7 +110,25 @@ export function buildTemper(input) {
110
110
  return buildGamut(scala, gamutOpts);
111
111
  },
112
112
  modus(mode) {
113
- return getMode(mode);
113
+ const data = getMode(mode);
114
+ // The mode's degrees are stored as semitone offsets from C (pc 0), with
115
+ // values past 12 in the upper octave. Anchor them at C4 (MIDI 60).
116
+ const tuned = (offset) => ({
117
+ pitch: toPitch(60 + offset, scala),
118
+ step: toStep(60 + offset, scala),
119
+ });
120
+ const scaleSet = new Set(data.scalePcs);
121
+ const ambitusNotes = [];
122
+ for (let off = data.ambitus.lowest; off <= data.ambitus.highest; off++) {
123
+ if (scaleSet.has(((off % 12) + 12) % 12))
124
+ ambitusNotes.push(tuned(off));
125
+ }
126
+ return {
127
+ ...data,
128
+ finalis: tuned(data.final),
129
+ reciting: tuned(data.tenor),
130
+ ambitusNotes,
131
+ };
114
132
  },
115
133
  tonus(tonusOpts) {
116
134
  if (modeVal === "auto")
@@ -0,0 +1,11 @@
1
+ export interface ModalAffinity {
2
+ mode: number;
3
+ alias: string;
4
+ score: number;
5
+ }
6
+ /**
7
+ * Rank a pitch-class distribution against the eight modes, best fit first.
8
+ * `firstNotePc`, when given, applies the rank-weighted initials bonus.
9
+ */
10
+ export declare function computeModalAffinity(pcDistribution: Record<number, number>, firstNotePc?: number): ModalAffinity[];
11
+ //# sourceMappingURL=modality.d.ts.map
@@ -0,0 +1,57 @@
1
+ // ---------------------------------------------------------------------------
2
+ // engines/temper/modality — how well pitch content fits each church mode
3
+ // ---------------------------------------------------------------------------
4
+ // Modal theory, not tied to any one caller: the imprint uses it to fingerprint a
5
+ // whole chant, modulation detection to read each phrase. A pure function of a
6
+ // pitch-class distribution (and, optionally, the chant's opening note).
7
+ import { MODES } from "./modes.js";
8
+ // A mode's structural degrees are not equal: the finalis defines it, the tenor
9
+ // anchors its recitation, and modulation degrees are only secondary colour. Time
10
+ // spent on each pitch counts toward the mode in that proportion.
11
+ const FINALIS_WEIGHT = 3;
12
+ const TENOR_WEIGHT = 2;
13
+ const REGULAR_MOD_WEIGHT = 1;
14
+ const CONCEDED_MOD_WEIGHT = 0.5;
15
+ // A chant's opening note is a modal signal: each mode lists its valid initials
16
+ // in rank order (the first is the most characteristic). Opening on a mode's
17
+ // primary initial boosts it more than opening on a lower-ranked one — which is
18
+ // what separates an authentic mode from its plagal partner, since the two share
19
+ // a finalis but rank the same opening pitch differently.
20
+ const INITIAL_BONUS = 0.3;
21
+ /**
22
+ * Rank a pitch-class distribution against the eight modes, best fit first.
23
+ * `firstNotePc`, when given, applies the rank-weighted initials bonus.
24
+ */
25
+ export function computeModalAffinity(pcDistribution, firstNotePc) {
26
+ const results = [];
27
+ for (let m = 1; m <= 8; m++) {
28
+ const data = MODES.get(m);
29
+ if (!data)
30
+ continue;
31
+ // Weight each degree by its modal role; a pc that fills more than one role
32
+ // (e.g. a modulation degree that is also the tenor) takes the strongest.
33
+ const degreeWeight = new Map();
34
+ const set = (pc, w) => {
35
+ degreeWeight.set(pc, Math.max(degreeWeight.get(pc) ?? 0, w));
36
+ };
37
+ for (const pc of data.modulations.conceded)
38
+ set(pc % 12, CONCEDED_MOD_WEIGHT);
39
+ for (const pc of data.modulations.regular)
40
+ set(pc % 12, REGULAR_MOD_WEIGHT);
41
+ set(data.tenor, TENOR_WEIGHT);
42
+ set(data.final, FINALIS_WEIGHT);
43
+ let score = 0;
44
+ for (const [pc, w] of degreeWeight)
45
+ score += (pcDistribution[pc] ?? 0) * w;
46
+ // Initials bonus, scaled by how highly the mode ranks the opening pitch.
47
+ if (firstNotePc != null) {
48
+ const initials = data.modulations.initials;
49
+ const rank = initials.findIndex((pc) => pc % 12 === firstNotePc);
50
+ if (rank !== -1)
51
+ score += (INITIAL_BONUS * (initials.length - rank)) / initials.length;
52
+ }
53
+ results.push({ mode: m, alias: data.alias, score });
54
+ }
55
+ return results.sort((a, b) => b.score - a.score);
56
+ }
57
+ //# sourceMappingURL=modality.js.map
package/dist/index.d.ts CHANGED
@@ -10,8 +10,8 @@ import { getCosmos } from "./engines/planet/planet.js";
10
10
  import { buildHarmonia } from "./engines/harmonia/api.js";
11
11
  import type { FeastQuery, Feast, Pascha, Season, Grade } from "./engines/cal/types.js";
12
12
  import type { CantusQuery, Chant, OrdinaryChant, PropriumQuery, OrdinariumQuery, OfficiumQuery, PsalmusQuery } from "./engines/chant/types.js";
13
- import type { TemperamentumInput, Temperamentum, Tuning, TemperamentumOpts, Pitch, PitchInput, Step, Neume, NeumeShape, Interval, ModeData, CadenceFigure, GamutOptions, Tonus, TonusOpts } from "./engines/temper/api.js";
14
- import type { Score, ScoreOpts, PondusInput, PondusOpts, AccentusInput, AccentusOpts, Cadence, CadenceTarget, CadenceApproach, MidiOpts, MidiEmitResult, MidiJsonResult, MidiJsonEvent, MusicXmlOpts, MusicXmlEmitResult } from "./engines/score/api.js";
13
+ import type { TemperamentumInput, Temperamentum, Tuning, TemperamentumOpts, Pitch, PitchInput, Step, Neume, NeumeShape, Interval, ModeData, CadenceFigure, Modus, TunedNote, GamutOptions, Tonus, TonusOpts } from "./engines/temper/api.js";
14
+ import type { Score, ScoreOpts, PondusInput, PondusOpts, AccentusInput, AccentusOpts, Cadence, CadenceTarget, CadenceApproach, Modulation, MidiOpts, MidiEmitResult, MidiJsonResult, MidiJsonEvent, MusicXmlOpts, MusicXmlEmitResult } from "./engines/score/api.js";
15
15
  import type { ChantTabulaRow } from "./engines/score/tabula.js";
16
16
  import type { Imprint, Attractor, VowelAttractor, ModalAffinity } from "./engines/imprint.js";
17
17
  import type { Prosody, RhythmicProfile, NoteRange, CadenceDistribution } from "./engines/score/prosody.js";
@@ -36,5 +36,5 @@ declare const tonus: {
36
36
  };
37
37
  export default tonus;
38
38
  export { SEASON_LABELS, TEMPUS_NAMES, GRADE_ORDER, GRADE_NAMES, gradeOrder, compareGrade, ritusToGrade, } from "./engines/cal/types.js";
39
- export type { Feast, FeastQuery, Pascha, Season, Grade, Chant, CantusQuery, OrdinaryChant, PropriumQuery, OrdinariumQuery, OfficiumQuery, PsalmusQuery, Temperamentum, TemperamentumInput, TemperamentumOpts, Tuning, Pitch, PitchInput, Step, Neume, NeumeShape, Interval, ModeData, CadenceFigure, GamutOptions, Tonus, TonusOpts, Score, ScoreOpts, PondusInput, PondusOpts, AccentusInput, AccentusOpts, Cadence, CadenceTarget, CadenceApproach, MidiOpts, MidiEmitResult, MidiJsonResult, MidiJsonEvent, MusicXmlOpts, MusicXmlEmitResult, ChantTabulaRow, Note, Performance, Phrase, Syllable, RestEvent, ParseError, ArsisThesis, VoicedPitch, Cosmos, CosmosQuery, Body, BodyName, Aspect, Imprint, Attractor, VowelAttractor, ModalAffinity, Prosody, RhythmicProfile, NoteRange, CadenceDistribution, Harmony, HarmoniaOpts, VoicedBody, VoicedAspect, Frame, Author, HarmonyTabulaRow, PlanetVowel, };
39
+ export type { Feast, FeastQuery, Pascha, Season, Grade, Chant, CantusQuery, OrdinaryChant, PropriumQuery, OrdinariumQuery, OfficiumQuery, PsalmusQuery, Temperamentum, TemperamentumInput, TemperamentumOpts, Tuning, Pitch, PitchInput, Step, Neume, NeumeShape, Interval, ModeData, CadenceFigure, Modus, TunedNote, GamutOptions, Tonus, TonusOpts, Score, ScoreOpts, PondusInput, PondusOpts, AccentusInput, AccentusOpts, Cadence, CadenceTarget, CadenceApproach, Modulation, MidiOpts, MidiEmitResult, MidiJsonResult, MidiJsonEvent, MusicXmlOpts, MusicXmlEmitResult, ChantTabulaRow, Note, Performance, Phrase, Syllable, RestEvent, ParseError, ArsisThesis, VoicedPitch, Cosmos, CosmosQuery, Body, BodyName, Aspect, Imprint, Attractor, VowelAttractor, ModalAffinity, Prosody, RhythmicProfile, NoteRange, CadenceDistribution, Harmony, HarmoniaOpts, VoicedBody, VoicedAspect, Frame, Author, HarmonyTabulaRow, PlanetVowel, };
40
40
  //# sourceMappingURL=index.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tonus",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Medieval music analysis and performance: GABC plainchant exports, liturgical calendar, tuning systems, ephemeris, and the harmony of the spheres",