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.
- package/BIBLIOGRAPHY.md +99 -0
- package/LICENSE +29 -0
- package/README.md +108 -0
- package/dist/data/cal.d.ts +10 -0
- package/dist/data/cal.js +3862 -0
- package/dist/data/commune.d.ts +17 -0
- package/dist/data/commune.js +1333 -0
- package/dist/data/gr.d.ts +5 -0
- package/dist/data/gr.js +13449 -0
- package/dist/data/kyriale.d.ts +11 -0
- package/dist/data/kyriale.js +971 -0
- package/dist/data/la.d.ts +5 -0
- package/dist/data/la.js +14229 -0
- package/dist/data/lh.d.ts +5 -0
- package/dist/data/lh.js +3619 -0
- package/dist/data/lu.d.ts +5 -0
- package/dist/data/lu.js +23779 -0
- package/dist/data/masses.d.ts +18 -0
- package/dist/data/masses.js +297 -0
- package/dist/data/office-roman.d.ts +19 -0
- package/dist/data/office-roman.js +13792 -0
- package/dist/data/office.d.ts +12 -0
- package/dist/data/office.js +13052 -0
- package/dist/data/propers.d.ts +13 -0
- package/dist/data/propers.js +7584 -0
- package/dist/data/psalms.d.ts +4 -0
- package/dist/data/psalms.js +10 -0
- package/dist/data/psalms.json +22918 -0
- package/dist/data/tones.d.ts +20 -0
- package/dist/data/tones.js +153 -0
- package/dist/data/types.d.ts +3 -0
- package/dist/data/types.js +2 -0
- package/dist/engines/cal/calendar.d.ts +21 -0
- package/dist/engines/cal/calendar.js +265 -0
- package/dist/engines/cal/date.d.ts +31 -0
- package/dist/engines/cal/date.js +141 -0
- package/dist/engines/cal/types.d.ts +66 -0
- package/dist/engines/cal/types.js +189 -0
- package/dist/engines/chant/chant.d.ts +10 -0
- package/dist/engines/chant/chant.js +135 -0
- package/dist/engines/chant/hour.d.ts +8 -0
- package/dist/engines/chant/hour.js +135 -0
- package/dist/engines/chant/intone.d.ts +8 -0
- package/dist/engines/chant/intone.js +84 -0
- package/dist/engines/chant/ordinary.d.ts +7 -0
- package/dist/engines/chant/ordinary.js +232 -0
- package/dist/engines/chant/propers.d.ts +8 -0
- package/dist/engines/chant/propers.js +107 -0
- package/dist/engines/chant/psalm.d.ts +7 -0
- package/dist/engines/chant/psalm.js +60 -0
- package/dist/engines/chant/syllabify.d.ts +20 -0
- package/dist/engines/chant/syllabify.js +192 -0
- package/dist/engines/chant/types.d.ts +76 -0
- package/dist/engines/chant/types.js +34 -0
- package/dist/engines/epoch.d.ts +2 -0
- package/dist/engines/epoch.js +14 -0
- package/dist/engines/harmonia/api.d.ts +35 -0
- package/dist/engines/harmonia/api.js +90 -0
- package/dist/engines/harmonia/aspects.d.ts +8 -0
- package/dist/engines/harmonia/aspects.js +15 -0
- package/dist/engines/harmonia/data/doctrines.d.ts +16 -0
- package/dist/engines/harmonia/data/doctrines.js +154 -0
- package/dist/engines/harmonia/data/vowels.d.ts +10 -0
- package/dist/engines/harmonia/data/vowels.js +21 -0
- package/dist/engines/harmonia/presence.d.ts +13 -0
- package/dist/engines/harmonia/presence.js +48 -0
- package/dist/engines/harmonia/tabula.d.ts +28 -0
- package/dist/engines/harmonia/tabula.js +32 -0
- package/dist/engines/harmonia/voice.d.ts +19 -0
- package/dist/engines/harmonia/voice.js +51 -0
- package/dist/engines/imprint.d.ts +30 -0
- package/dist/engines/imprint.js +152 -0
- package/dist/engines/planet/appearance.d.ts +40 -0
- package/dist/engines/planet/appearance.js +84 -0
- package/dist/engines/planet/aspects.d.ts +5 -0
- package/dist/engines/planet/aspects.js +41 -0
- package/dist/engines/planet/math.d.ts +13 -0
- package/dist/engines/planet/math.js +56 -0
- package/dist/engines/planet/orbital.d.ts +25 -0
- package/dist/engines/planet/orbital.js +223 -0
- package/dist/engines/planet/planet.d.ts +13 -0
- package/dist/engines/planet/planet.js +198 -0
- package/dist/engines/planet/position.d.ts +62 -0
- package/dist/engines/planet/position.js +156 -0
- package/dist/engines/planet/types.d.ts +61 -0
- package/dist/engines/planet/types.js +14 -0
- package/dist/engines/score/api.d.ts +54 -0
- package/dist/engines/score/api.js +87 -0
- package/dist/engines/score/articulation.d.ts +6 -0
- package/dist/engines/score/articulation.js +112 -0
- package/dist/engines/score/emitters/midi.d.ts +65 -0
- package/dist/engines/score/emitters/midi.js +158 -0
- package/dist/engines/score/emitters/musicxml.d.ts +18 -0
- package/dist/engines/score/emitters/musicxml.js +166 -0
- package/dist/engines/score/infer.d.ts +4 -0
- package/dist/engines/score/infer.js +77 -0
- package/dist/engines/score/ir.d.ts +4 -0
- package/dist/engines/score/ir.js +177 -0
- package/dist/engines/score/meta.d.ts +19 -0
- package/dist/engines/score/meta.js +34 -0
- package/dist/engines/score/neume.d.ts +3 -0
- package/dist/engines/score/neume.js +26 -0
- package/dist/engines/score/parse.d.ts +3 -0
- package/dist/engines/score/parse.js +359 -0
- package/dist/engines/score/phrasing.d.ts +24 -0
- package/dist/engines/score/phrasing.js +257 -0
- package/dist/engines/score/prosody.d.ts +35 -0
- package/dist/engines/score/prosody.js +109 -0
- package/dist/engines/score/tabula.d.ts +70 -0
- package/dist/engines/score/tabula.js +109 -0
- package/dist/engines/score/types.d.ts +159 -0
- package/dist/engines/score/types.js +2 -0
- package/dist/engines/temper/api.d.ts +60 -0
- package/dist/engines/temper/api.js +130 -0
- package/dist/engines/temper/data/constants.d.ts +27 -0
- package/dist/engines/temper/data/constants.js +150 -0
- package/dist/engines/temper/data/guido.d.ts +14 -0
- package/dist/engines/temper/data/guido.js +29 -0
- package/dist/engines/temper/data/modes.d.ts +38 -0
- package/dist/engines/temper/data/modes.js +158 -0
- package/dist/engines/temper/gabc.d.ts +5 -0
- package/dist/engines/temper/gabc.js +53 -0
- package/dist/engines/temper/gamut.d.ts +9 -0
- package/dist/engines/temper/gamut.js +24 -0
- package/dist/engines/temper/guido.d.ts +16 -0
- package/dist/engines/temper/guido.js +48 -0
- package/dist/engines/temper/interval.d.ts +15 -0
- package/dist/engines/temper/interval.js +31 -0
- package/dist/engines/temper/modes.d.ts +6 -0
- package/dist/engines/temper/modes.js +13 -0
- package/dist/engines/temper/neume.d.ts +14 -0
- package/dist/engines/temper/neume.js +59 -0
- package/dist/engines/temper/pitch.d.ts +40 -0
- package/dist/engines/temper/pitch.js +129 -0
- package/dist/engines/temper/scale.d.ts +37 -0
- package/dist/engines/temper/scale.js +217 -0
- package/dist/engines/temper/step.d.ts +23 -0
- package/dist/engines/temper/step.js +53 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +27 -0
- 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
|