tonus 0.1.2 → 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.
- package/BIBLIOGRAPHY.md +11 -0
- package/README.md +5 -0
- package/dist/data/tones.d.ts +1 -0
- package/dist/engines/chant/intone.d.ts +4 -0
- package/dist/engines/chant/intone.js +7 -3
- package/dist/engines/chant/psalm.js +3 -3
- package/dist/engines/chant/types.d.ts +4 -0
- package/dist/engines/imprint.d.ts +8 -7
- package/dist/engines/imprint.js +25 -27
- package/dist/engines/score/api.d.ts +8 -0
- package/dist/engines/score/api.js +18 -1
- package/dist/engines/score/articulation.js +3 -0
- package/dist/engines/score/cadence.d.ts +38 -0
- package/dist/engines/score/cadence.js +179 -0
- package/dist/engines/score/ir.js +10 -1
- package/dist/engines/score/modulation.d.ts +20 -0
- package/dist/engines/score/modulation.js +74 -0
- package/dist/engines/score/neume.js +7 -5
- package/dist/engines/score/parse.js +13 -0
- package/dist/engines/score/tabula.d.ts +6 -0
- package/dist/engines/score/tabula.js +11 -0
- package/dist/engines/score/types.d.ts +5 -0
- package/dist/engines/temper/api.d.ts +19 -3
- package/dist/engines/temper/api.js +19 -1
- package/dist/engines/temper/data/modes.d.ts +6 -4
- package/dist/engines/temper/data/modes.js +52 -16
- package/dist/engines/temper/modality.d.ts +11 -0
- package/dist/engines/temper/modality.js +57 -0
- package/dist/engines/temper/modes.d.ts +1 -1
- package/dist/index.d.ts +3 -3
- package/package.json +1 -1
package/BIBLIOGRAPHY.md
CHANGED
|
@@ -35,6 +35,17 @@ section of the relevant page.
|
|
|
35
35
|
semiological approach → [docs/score.md](docs/score.md#sources)
|
|
36
36
|
- **Desrocquettes, "Gregorian Musical Values"** — Solesmes rhythmic
|
|
37
37
|
values → [docs/score.md](docs/score.md#sources)
|
|
38
|
+
- **Niedermeyer & d'Ortigue, _Gregorian Accompaniment_ (trans. Goodrich)** —
|
|
39
|
+
the per-mode cadence figures → [docs/tuning.md](docs/tuning.md#sources)
|
|
40
|
+
- **Bragers, _A Short Treatise on Gregorian Accompaniment_ (1934)** —
|
|
41
|
+
cadence figures, cross-check → [docs/tuning.md](docs/tuning.md#sources)
|
|
42
|
+
- **Homan, _Cadence in Gregorian Chant_ (1961)** — analytic study of
|
|
43
|
+
cadences → [docs/score.md](docs/score.md#sources)
|
|
44
|
+
- **Murray, "Accentual Cadences in Gregorian Chant" (1958)** — spondaic and
|
|
45
|
+
dactylic verbal cadences → [docs/score.md](docs/score.md#sources)
|
|
46
|
+
- **Suñol, _Textbook of Gregorian Chant According to the Solesmes Method_
|
|
47
|
+
(1930)** — modes, dominants, psalmody, the Solesmes rhythm doctrine →
|
|
48
|
+
[docs/tuning.md](docs/tuning.md#sources)
|
|
38
49
|
- **Apel, _Gregorian Chant_ (1958)** — analytic study of the repertoire →
|
|
39
50
|
[docs/chant.md](docs/chant.md#sources)
|
|
40
51
|
- **Hiley, _Western Plainchant_ (1993)** — the standard reference →
|
package/README.md
CHANGED
|
@@ -93,6 +93,11 @@ const harmony = tonus.harmonia(tonus.caelum({ date: feast.date }));
|
|
|
93
93
|
// each visible planet voiced as pitch and Greek vowel, after Boethius
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
+
## Roadmap
|
|
97
|
+
|
|
98
|
+
SVG chant engraving, an interactive documentation site,
|
|
99
|
+
and cadence detection is laid out in [ROADMAP.md](ROADMAP.md).
|
|
100
|
+
|
|
96
101
|
## Development
|
|
97
102
|
|
|
98
103
|
```sh
|
package/dist/data/tones.d.ts
CHANGED
|
@@ -3,6 +3,10 @@ export interface IntoneOpts {
|
|
|
3
3
|
mode?: number;
|
|
4
4
|
differentia?: string;
|
|
5
5
|
intonation?: boolean;
|
|
6
|
+
/** Sing in directum: recite straight through to the termination, no mediant. */
|
|
7
|
+
inDirectum?: boolean;
|
|
8
|
+
/** Use the tone's ornamented mediant for solemn occasions, where it has one. */
|
|
9
|
+
solemn?: boolean;
|
|
6
10
|
}
|
|
7
11
|
export declare function intone(text: string | PsalmVerse, opts?: IntoneOpts): string;
|
|
8
12
|
//# sourceMappingURL=intone.d.ts.map
|
|
@@ -63,12 +63,16 @@ export function intone(text, opts = {}) {
|
|
|
63
63
|
const tone = getTone(mode);
|
|
64
64
|
const diff = getDifferentia(tone, opts.differentia);
|
|
65
65
|
const tenorLetter = midiToGabc(tone.tenor, clef);
|
|
66
|
-
const
|
|
66
|
+
const mediant = opts.solemn && tone.solemnMediant ? tone.solemnMediant : tone.mediant;
|
|
67
|
+
const mediantLetters = midiListToLetters(mediant, clef);
|
|
67
68
|
const terminationLetters = midiListToLetters(diff.termination, clef);
|
|
68
69
|
const intonationLetters = midiListToLetters(tone.intonation, clef);
|
|
69
|
-
|
|
70
|
+
// In directum ignores the verse's mediant split: the whole verse recites to
|
|
71
|
+
// the termination as one phrase (a psalm sung with no antiphon framing).
|
|
72
|
+
const starIdx = opts.inDirectum ? -1 : rawText.indexOf(" * ");
|
|
70
73
|
if (starIdx === -1) {
|
|
71
|
-
const
|
|
74
|
+
const text = opts.inDirectum ? rawText.replace(" * ", " ") : rawText;
|
|
75
|
+
const syllables = syllabifyPhrase(text.trim());
|
|
72
76
|
const body = buildHalf(syllables, tenorLetter, terminationLetters);
|
|
73
77
|
return CLEF + body + "(::)";
|
|
74
78
|
}
|
|
@@ -27,8 +27,8 @@ function lookupVerses(psalm, verse) {
|
|
|
27
27
|
results = results.filter((v) => v.verse === verse);
|
|
28
28
|
return results;
|
|
29
29
|
}
|
|
30
|
-
function verseToChant(v, mode, differentia, intonation) {
|
|
31
|
-
const gabc = intone(v, { mode, differentia, intonation });
|
|
30
|
+
function verseToChant(v, mode, differentia, intonation, inDirectum, solemn) {
|
|
31
|
+
const gabc = intone(v, { mode, differentia, intonation, inDirectum, solemn });
|
|
32
32
|
return {
|
|
33
33
|
id: `psalm:${v.psalm}:${v.verse}`,
|
|
34
34
|
incipit: v.half1.slice(0, 40),
|
|
@@ -56,7 +56,7 @@ export function getPsalm(query) {
|
|
|
56
56
|
if (!verses.length)
|
|
57
57
|
return [];
|
|
58
58
|
const mode = query.mode ?? 8;
|
|
59
|
-
return verses.map((v) => verseToChant(v, mode, query.differentia, query.intonatio));
|
|
59
|
+
return verses.map((v) => verseToChant(v, mode, query.differentia, query.intonatio, query.inDirectum, query.solemn));
|
|
60
60
|
}
|
|
61
61
|
/**
|
|
62
62
|
* A contiguous verse range of one psalm, intoned — e.g. `getPsalmRange(30, 2, 6)`
|
|
@@ -64,6 +64,10 @@ export interface PsalmusQuery {
|
|
|
64
64
|
mode?: number;
|
|
65
65
|
differentia?: string;
|
|
66
66
|
intonatio?: boolean;
|
|
67
|
+
/** Sing in directum: straight through to the termination, no mediant. */
|
|
68
|
+
inDirectum?: boolean;
|
|
69
|
+
/** Use the ornamented solemn mediant, where the tone has one. */
|
|
70
|
+
solemn?: boolean;
|
|
67
71
|
}
|
|
68
72
|
export interface PsalmVerse {
|
|
69
73
|
psalm: number;
|
|
@@ -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
|
-
|
|
27
|
-
|
|
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
|
package/dist/engines/imprint.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { toPitch } from "./temper/pitch.js";
|
|
2
|
-
import {
|
|
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
|
-
|
|
89
|
-
|
|
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 (
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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,5 +1,7 @@
|
|
|
1
1
|
import { type Imprint } from "../imprint.js";
|
|
2
2
|
import { type Prosody } from "./prosody.js";
|
|
3
|
+
import { type Cadence } from "./cadence.js";
|
|
4
|
+
import { type Modulation } from "./modulation.js";
|
|
3
5
|
import { type ChantTabulaRow } from "./tabula.js";
|
|
4
6
|
import { type MidiOpts, type MidiEmitResult } from "./emitters/midi.js";
|
|
5
7
|
import { type MusicXmlOpts, type MusicXmlEmitResult } from "./emitters/musicxml.js";
|
|
@@ -29,6 +31,10 @@ export interface Score {
|
|
|
29
31
|
errors: ParseError[];
|
|
30
32
|
tabula: ChantTabulaRow[];
|
|
31
33
|
prosody: Prosody;
|
|
34
|
+
/** Mode-specific cadence at each phrase-ending divisio. */
|
|
35
|
+
cadences: Cadence[];
|
|
36
|
+
/** Passages where the tonal centre leans away from the home mode. */
|
|
37
|
+
modulations: Modulation[];
|
|
32
38
|
imprint: Imprint;
|
|
33
39
|
/**
|
|
34
40
|
* Emit a Standard MIDI File from the score's tabula. Returns the file bytes
|
|
@@ -49,6 +55,8 @@ export interface Score {
|
|
|
49
55
|
*/
|
|
50
56
|
export declare function buildScore(chant: Chant, opts?: ScoreOpts): Score;
|
|
51
57
|
export type { ParseError };
|
|
58
|
+
export type { Cadence, CadenceTarget, CadenceApproach } from "./cadence.js";
|
|
59
|
+
export type { Modulation } from "./modulation.js";
|
|
52
60
|
export type { MidiOpts, MidiEmitResult, MidiJsonResult, MidiJsonEvent } from "./emitters/midi.js";
|
|
53
61
|
export type { MusicXmlOpts, MusicXmlEmitResult } from "./emitters/musicxml.js";
|
|
54
62
|
//# sourceMappingURL=api.d.ts.map
|
|
@@ -7,7 +7,10 @@ import { buildRatios } from "../temper/scale.js";
|
|
|
7
7
|
import { computeMeta } from "./meta.js";
|
|
8
8
|
import { computeImprint } from "../imprint.js";
|
|
9
9
|
import { computeProsody } from "./prosody.js";
|
|
10
|
+
import { detectCadences } from "./cadence.js";
|
|
11
|
+
import { detectModulations } from "./modulation.js";
|
|
10
12
|
import { computeTabula } from "./tabula.js";
|
|
13
|
+
import { MODES } from "../temper/modes.js";
|
|
11
14
|
import { toMidi } from "./emitters/midi.js";
|
|
12
15
|
import { toMusicXML } from "./emitters/musicxml.js";
|
|
13
16
|
const PONDUS_TO_ARTICULATION = {
|
|
@@ -56,10 +59,16 @@ export function buildScore(chant, opts) {
|
|
|
56
59
|
});
|
|
57
60
|
const ir = buildIR(parsed, chant, scale);
|
|
58
61
|
const meta = computeMeta(ir, { mode: modeNum });
|
|
62
|
+
// Cadence detection runs here, where the resolved mode (and its cadence
|
|
63
|
+
// figures) is in hand. Pure data — mirrors the arsis/thesis pass in ir.ts.
|
|
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);
|
|
59
67
|
const tabula = computeTabula(ir, {
|
|
60
68
|
mode: meta.mode ?? undefined,
|
|
61
69
|
a4Hz: opts?.temperamentum?.a4,
|
|
62
70
|
transpose: opts?.temperamentum?.transpose,
|
|
71
|
+
cadences,
|
|
63
72
|
// Only pass phrasing when the caller asked for it, so the default
|
|
64
73
|
// tabula shaping (mode-gated) is unchanged.
|
|
65
74
|
interpretation: opts?.accentus
|
|
@@ -75,7 +84,15 @@ export function buildScore(chant, opts) {
|
|
|
75
84
|
errors: ir.errors,
|
|
76
85
|
tabula,
|
|
77
86
|
prosody: computeProsody(ir.phrases),
|
|
78
|
-
|
|
87
|
+
cadences,
|
|
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
|
+
}),
|
|
79
96
|
midi(emitOpts) {
|
|
80
97
|
return toMidi(tabula, emitOpts);
|
|
81
98
|
},
|
|
@@ -23,6 +23,9 @@ const BASE_WEIGHTS = {
|
|
|
23
23
|
repercussionPrevWeight: 0.5,
|
|
24
24
|
repercussionPrevDuration: 0.4,
|
|
25
25
|
repercussionOriscusWeight: -0.5,
|
|
26
|
+
oriscusWeight: -0.3, // soft, light note
|
|
27
|
+
oriscusDuration: -0.15, // taken slightly faster (accelerando)
|
|
28
|
+
oriscusPrevWeight: 0.3, // rhythmic support on the preceding note
|
|
26
29
|
breakWeight: 0.6,
|
|
27
30
|
dashWeight: -0.8,
|
|
28
31
|
dashDuration: -0.2,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Phrase } from "./types.js";
|
|
2
|
+
import type { ModeData } from "../temper/data/modes.js";
|
|
3
|
+
export type CadenceTarget = "finalis" | "tenor" | "other";
|
|
4
|
+
export type CadenceApproach = "descending" | "ascending" | "unison";
|
|
5
|
+
export interface Cadence {
|
|
6
|
+
/** Index of the phrase this cadence closes. */
|
|
7
|
+
phraseIndex: number;
|
|
8
|
+
/** The divisio bar that ends the phrase: "," "`" ";" ":" "::". */
|
|
9
|
+
divisio: string;
|
|
10
|
+
/**
|
|
11
|
+
* Where the phrase came to rest, from the final note's modal role. Medial
|
|
12
|
+
* cadences often rest on the tenor or elsewhere
|
|
13
|
+
*/
|
|
14
|
+
target: CadenceTarget;
|
|
15
|
+
/** Melodic contour into the resolution, across the observed window. */
|
|
16
|
+
approach: CadenceApproach;
|
|
17
|
+
/** Matched finalis-cadence figure id (e.g. "mi-re"), or null. */
|
|
18
|
+
formula: string | null;
|
|
19
|
+
/** The observed final pitch-class run — the evidence, resolution note last. */
|
|
20
|
+
pcs: number[];
|
|
21
|
+
/**
|
|
22
|
+
* The window as diatonic steps relative to the resolution target (0 = target,
|
|
23
|
+
* -1 = the note below), resolution last — the surface the catalog matches on.
|
|
24
|
+
* Empty when there is no mode/target; null entries are notes off the scale.
|
|
25
|
+
*/
|
|
26
|
+
steps: Array<number | null>;
|
|
27
|
+
/** 0–1: how cleanly the ending lands, raised by a catalog match. */
|
|
28
|
+
confidence: number;
|
|
29
|
+
/** Note positions forming this cadence: [phraseIndex, syllableIndex, noteIndex]. */
|
|
30
|
+
notes: Array<[number, number, number]>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Detect the cadence closing each phrase. One Cadence per phrase that carries a
|
|
34
|
+
* divisio. With no mode, targets/approach are still classified but no figure is
|
|
35
|
+
* named (formula: null), matching the score's graceful-degradation convention.
|
|
36
|
+
*/
|
|
37
|
+
export declare function detectCadences(phrases: Phrase[], modeData: ModeData | undefined): Cadence[];
|
|
38
|
+
//# sourceMappingURL=cadence.d.ts.map
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// Cadence formulae run four to ten notes (Homan, Cadence in Gregorian Chant,
|
|
2
|
+
// 1961, p. xiii). Take a window at the upper end so the longest figures fit,
|
|
3
|
+
// with room for the approach; tail-matching ignores the extra leading notes.
|
|
4
|
+
const WINDOW = 8;
|
|
5
|
+
/** Last up-to-WINDOW notes of a phrase, resolution note last. */
|
|
6
|
+
function phraseFinalWindow(phrase) {
|
|
7
|
+
const window = [];
|
|
8
|
+
outer: for (let si = phrase.syllables.length - 1; si >= 0; si--) {
|
|
9
|
+
const notes = phrase.syllables[si].notes;
|
|
10
|
+
for (let ni = notes.length - 1; ni >= 0; ni--) {
|
|
11
|
+
const note = notes[ni];
|
|
12
|
+
window.push({
|
|
13
|
+
pc: note.step.pc,
|
|
14
|
+
midi: note.pitch.midi,
|
|
15
|
+
role: note.step.role,
|
|
16
|
+
syllableIndex: si,
|
|
17
|
+
noteIndex: ni,
|
|
18
|
+
});
|
|
19
|
+
if (window.length >= WINDOW)
|
|
20
|
+
break outer;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
window.reverse();
|
|
24
|
+
return window;
|
|
25
|
+
}
|
|
26
|
+
function classifyTarget(final) {
|
|
27
|
+
if (!final)
|
|
28
|
+
return "other";
|
|
29
|
+
if (final.role === "finalis")
|
|
30
|
+
return "finalis";
|
|
31
|
+
if (final.role === "tenor")
|
|
32
|
+
return "tenor";
|
|
33
|
+
return "other";
|
|
34
|
+
}
|
|
35
|
+
function classifyApproach(window) {
|
|
36
|
+
if (window.length < 2)
|
|
37
|
+
return "unison";
|
|
38
|
+
const slope = window[window.length - 1].midi - window[0].midi;
|
|
39
|
+
if (slope < 0)
|
|
40
|
+
return "descending";
|
|
41
|
+
if (slope > 0)
|
|
42
|
+
return "ascending";
|
|
43
|
+
return "unison";
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Signed diatonic step of pitch class `pc` relative to target pc `on`, within
|
|
47
|
+
* the mode's 7-note scale: 0 = the target, +1 = one scale step above, -1 = the
|
|
48
|
+
* note below, wrapping by octave so it stays in a small signed range. Returns
|
|
49
|
+
* null for a pc outside the mode's scale (e.g. a chromatic inflection).
|
|
50
|
+
*/
|
|
51
|
+
function diatonicStep(pc, on, scalePcs) {
|
|
52
|
+
const iPc = scalePcs.indexOf(((pc % 12) + 12) % 12);
|
|
53
|
+
const iOn = scalePcs.indexOf(((on % 12) + 12) % 12);
|
|
54
|
+
if (iPc === -1 || iOn === -1)
|
|
55
|
+
return null;
|
|
56
|
+
const n = scalePcs.length;
|
|
57
|
+
let d = iPc - iOn;
|
|
58
|
+
// Fold to the nearest octave so a cadence's small leaps read as small steps.
|
|
59
|
+
while (d > n / 2)
|
|
60
|
+
d -= n;
|
|
61
|
+
while (d < -n / 2)
|
|
62
|
+
d += n;
|
|
63
|
+
return d;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Collapse consecutive equal steps to one. A chant that lands on the final and
|
|
67
|
+
* repeats it (a final distropha, say) should still match the plain figure the
|
|
68
|
+
* treatises write with one note per pitch. Figures have no adjacent repeats, so
|
|
69
|
+
* collapsing is a no-op on them.
|
|
70
|
+
*/
|
|
71
|
+
function collapseRepeats(steps) {
|
|
72
|
+
const out = [];
|
|
73
|
+
for (const s of steps) {
|
|
74
|
+
if (out.length === 0 || out[out.length - 1] !== s)
|
|
75
|
+
out.push(s);
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Match a collapsed step-run against a catalog figure. Both end on the
|
|
81
|
+
* resolution (0), so compare from the tail backward. Returns a fraction in
|
|
82
|
+
* 0..1: how much of the figure the ending realises (0 = no tail match).
|
|
83
|
+
*/
|
|
84
|
+
function figureMatch(observed, figure) {
|
|
85
|
+
if (figure.length === 0 || observed.length === 0)
|
|
86
|
+
return 0;
|
|
87
|
+
let matched = 0;
|
|
88
|
+
for (let k = 1; k <= figure.length && k <= observed.length; k++) {
|
|
89
|
+
if (observed[observed.length - k] === figure[figure.length - k])
|
|
90
|
+
matched++;
|
|
91
|
+
else
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
return matched / figure.length;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The best catalog figure for an ending. A figure the ending realises in full
|
|
98
|
+
* (frac === 1) is preferred, and among those the longest — the most specific
|
|
99
|
+
* description — wins, so e.g. sol-fa-mi beats its own fa-mi suffix. Failing a
|
|
100
|
+
* full match, the highest partial fraction is kept (a weaker signal).
|
|
101
|
+
*/
|
|
102
|
+
function bestFigure(observed, figures) {
|
|
103
|
+
const collapsed = collapseRepeats(observed);
|
|
104
|
+
let best = null;
|
|
105
|
+
for (const figure of figures) {
|
|
106
|
+
const frac = figureMatch(collapsed, figure.steps);
|
|
107
|
+
if (frac === 0)
|
|
108
|
+
continue;
|
|
109
|
+
if (!best) {
|
|
110
|
+
best = { figure, frac };
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
// Rank: a full match outranks any partial; among full matches the longer
|
|
114
|
+
// figure wins; otherwise the higher fraction.
|
|
115
|
+
const bestFull = best.frac === 1;
|
|
116
|
+
const thisFull = frac === 1;
|
|
117
|
+
if (thisFull && !bestFull)
|
|
118
|
+
best = { figure, frac };
|
|
119
|
+
else if (thisFull &&
|
|
120
|
+
bestFull &&
|
|
121
|
+
figure.steps.length > best.figure.steps.length)
|
|
122
|
+
best = { figure, frac };
|
|
123
|
+
else if (!thisFull && !bestFull && frac > best.frac)
|
|
124
|
+
best = { figure, frac };
|
|
125
|
+
}
|
|
126
|
+
return best;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Detect the cadence closing each phrase. One Cadence per phrase that carries a
|
|
130
|
+
* divisio. With no mode, targets/approach are still classified but no figure is
|
|
131
|
+
* named (formula: null), matching the score's graceful-degradation convention.
|
|
132
|
+
*/
|
|
133
|
+
export function detectCadences(phrases, modeData) {
|
|
134
|
+
const cadences = [];
|
|
135
|
+
for (let pi = 0; pi < phrases.length; pi++) {
|
|
136
|
+
const phrase = phrases[pi];
|
|
137
|
+
if (!phrase.divisio)
|
|
138
|
+
continue;
|
|
139
|
+
const window = phraseFinalWindow(phrase);
|
|
140
|
+
if (window.length === 0)
|
|
141
|
+
continue;
|
|
142
|
+
const divisio = phrase.divisio.divisio;
|
|
143
|
+
const finalNote = window[window.length - 1];
|
|
144
|
+
const target = classifyTarget(finalNote);
|
|
145
|
+
const approach = classifyApproach(window);
|
|
146
|
+
const pcs = window.map((w) => w.pc);
|
|
147
|
+
// A clean landing on finalis/tenor is confident on its own; a catalog match
|
|
148
|
+
// raises it further. No modal role → a weak baseline.
|
|
149
|
+
let confidence = target === "other" ? 0.3 : 0.6;
|
|
150
|
+
let formula = null;
|
|
151
|
+
let steps = [];
|
|
152
|
+
if (modeData && (target === "finalis" || target === "tenor")) {
|
|
153
|
+
// Express the window as diatonic steps relative to the note it resolved
|
|
154
|
+
// onto. The catalogue holds final cadences, so only match on the finalis.
|
|
155
|
+
const onPc = target === "finalis" ? modeData.final : modeData.tenor;
|
|
156
|
+
steps = window.map((w) => diatonicStep(w.pc, onPc, modeData.scalePcs));
|
|
157
|
+
if (target === "finalis") {
|
|
158
|
+
const match = bestFigure(steps, modeData.cadences);
|
|
159
|
+
if (match) {
|
|
160
|
+
formula = match.figure.id;
|
|
161
|
+
confidence = Math.min(1, confidence + 0.4 * match.frac);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
cadences.push({
|
|
166
|
+
phraseIndex: pi,
|
|
167
|
+
divisio,
|
|
168
|
+
target,
|
|
169
|
+
approach,
|
|
170
|
+
formula,
|
|
171
|
+
pcs,
|
|
172
|
+
steps,
|
|
173
|
+
confidence: Math.round(confidence * 100) / 100,
|
|
174
|
+
notes: window.map((w) => [pi, w.syllableIndex, w.noteIndex]),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return cadences;
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=cadence.js.map
|
package/dist/engines/score/ir.js
CHANGED
|
@@ -23,13 +23,22 @@ function rawToNote(raw, scale) {
|
|
|
23
23
|
quilisma: raw.quilisma,
|
|
24
24
|
liquescent: raw.liquescent,
|
|
25
25
|
strophicus: raw.strophicus,
|
|
26
|
+
oriscus: raw.oriscus,
|
|
26
27
|
doubleEpisema: raw.doubleEpisema,
|
|
27
28
|
weight: raw.weight,
|
|
28
29
|
},
|
|
29
30
|
};
|
|
30
31
|
}
|
|
32
|
+
// The salicus ictus note (its second-to-last ascending note) is prolonged —
|
|
33
|
+
// Suñol, Textbook Ch. V. Modest, in the spirit of an episema lengthening.
|
|
34
|
+
const SALICUS_PROLONGATION = 1.3;
|
|
31
35
|
function makeSyllable(lyric, notes) {
|
|
32
|
-
|
|
36
|
+
const neume = classifyNeume(notes);
|
|
37
|
+
if (neume.type === "salicus" && notes.length >= 2) {
|
|
38
|
+
const ictic = notes[notes.length - 2];
|
|
39
|
+
ictic.performance.duration *= SALICUS_PROLONGATION;
|
|
40
|
+
}
|
|
41
|
+
return { lyric, notes, neume };
|
|
33
42
|
}
|
|
34
43
|
function partitionByIctus(annotated) {
|
|
35
44
|
const groups = [];
|
|
@@ -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
|
|
@@ -14,11 +14,13 @@ export function classifyNeume(notes) {
|
|
|
14
14
|
for (let i = 1; i < notes.length; i++) {
|
|
15
15
|
intervals.push(notes[i].pitch.midi - notes[i - 1].pitch.midi);
|
|
16
16
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
|
|
17
|
+
const dirs = intervals.map(toDirection);
|
|
18
|
+
let type = classifyShape(dirs);
|
|
19
|
+
// Salicus: an ascending run whose ictus (GABC `'`) marks it apart from a plain
|
|
20
|
+
// scandicus. The ictus sits on the second-to-last ascending note — the middle
|
|
21
|
+
// note of a three-note salicus, the penultimate of a longer one (Suñol).
|
|
22
|
+
const allAscending = dirs.length >= 2 && dirs.every((d) => d === "up");
|
|
23
|
+
if (allAscending && notes[notes.length - 2].context.ictus) {
|
|
22
24
|
type = "salicus";
|
|
23
25
|
}
|
|
24
26
|
return { type, intervals, hasQuilisma, hasLiquescent, hasStrophicus };
|
|
@@ -83,6 +83,7 @@ function parseNeume(notation, context) {
|
|
|
83
83
|
let isQuilisma = false;
|
|
84
84
|
let isLiquescent = false;
|
|
85
85
|
let isStrophicus = false;
|
|
86
|
+
let isOriscus = false;
|
|
86
87
|
let isDoubleEpisema = false;
|
|
87
88
|
// Dash prefix (weak note)
|
|
88
89
|
if (token[0] === "-") {
|
|
@@ -200,6 +201,16 @@ function parseNeume(notation, context) {
|
|
|
200
201
|
w += weights.uppercaseWeight;
|
|
201
202
|
durWeight += weights.uppercaseDuration;
|
|
202
203
|
}
|
|
204
|
+
// Oriscus (o = soft, light note taken slightly faster; the rhythmic
|
|
205
|
+
// support falls on the note before it). Suñol, Textbook Ch. V.
|
|
206
|
+
if (modifiers.includes("o")) {
|
|
207
|
+
w += weights.oriscusWeight;
|
|
208
|
+
durWeight += weights.oriscusDuration;
|
|
209
|
+
isOriscus = true;
|
|
210
|
+
const before = intermed.length > 0 ? intermed[intermed.length - 1] : null;
|
|
211
|
+
if (before)
|
|
212
|
+
before._weight += weights.oriscusPrevWeight;
|
|
213
|
+
}
|
|
203
214
|
// Repercussion (same pitch as previous note)
|
|
204
215
|
const prev = intermed.length > 0 ? intermed[intermed.length - 1] : null;
|
|
205
216
|
if (prev && step === prev.step) {
|
|
@@ -221,6 +232,7 @@ function parseNeume(notation, context) {
|
|
|
221
232
|
quilisma: isQuilisma,
|
|
222
233
|
liquescent: isLiquescent,
|
|
223
234
|
strophicus: isStrophicus,
|
|
235
|
+
oriscus: isOriscus,
|
|
224
236
|
doubleEpisema: isDoubleEpisema,
|
|
225
237
|
_weight: w,
|
|
226
238
|
_durWeight: durWeight,
|
|
@@ -275,6 +287,7 @@ function parseNeume(notation, context) {
|
|
|
275
287
|
quilisma: note.quilisma,
|
|
276
288
|
liquescent: note.liquescent,
|
|
277
289
|
strophicus: note.strophicus,
|
|
290
|
+
oriscus: note.oriscus,
|
|
278
291
|
doubleEpisema: note.doubleEpisema,
|
|
279
292
|
};
|
|
280
293
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Score, Neume } from "./types.js";
|
|
2
|
+
import type { Cadence } from "./cadence.js";
|
|
2
3
|
import type { ChantType, InterpretationOptions } from "./types.js";
|
|
3
4
|
export type NoteRole = "finalis" | "tenor" | "other" | null;
|
|
4
5
|
export interface ChantTabulaRow {
|
|
@@ -40,7 +41,10 @@ export interface ChantTabulaRow {
|
|
|
40
41
|
quilisma: boolean;
|
|
41
42
|
liquescent: boolean;
|
|
42
43
|
strophicus: boolean;
|
|
44
|
+
oriscus: boolean;
|
|
43
45
|
divisio: string | null;
|
|
46
|
+
/** Index into score.cadences[] when this note forms a cadence; null otherwise. */
|
|
47
|
+
cadenceRef: number | null;
|
|
44
48
|
/** Modal role: "final" | "tenor" | "mod" (modulation) | null if no mode or no match */
|
|
45
49
|
role: NoteRole;
|
|
46
50
|
/** Guidonian short name (e.g. "g", "aa") — null for chromatic pitches with no gamut entry */
|
|
@@ -65,6 +69,8 @@ export interface TabulaOptions {
|
|
|
65
69
|
interpretation?: InterpretationOptions;
|
|
66
70
|
a4Hz?: number;
|
|
67
71
|
transpose?: number;
|
|
72
|
+
/** Detected cadences; used to stamp each row's cadenceRef. */
|
|
73
|
+
cadences?: Cadence[];
|
|
68
74
|
}
|
|
69
75
|
export declare function computeTabula(ir: Score, options?: TabulaOptions): ChantTabulaRow[];
|
|
70
76
|
//# sourceMappingURL=tabula.d.ts.map
|
|
@@ -54,6 +54,15 @@ export function computeTabula(ir, options = {}) {
|
|
|
54
54
|
shapedDurations[i] = shaped[i].shapedDuration;
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
+
// Map each cadence's constituent notes back to its index, keyed by position.
|
|
58
|
+
const cadenceRefByPos = new Map();
|
|
59
|
+
if (options.cadences) {
|
|
60
|
+
for (let ci = 0; ci < options.cadences.length; ci++) {
|
|
61
|
+
for (const [pi, si, ni] of options.cadences[ci].notes) {
|
|
62
|
+
cadenceRefByPos.set(`${pi}:${si}:${ni}`, ci);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
57
66
|
// Position of each note within its neume figure — resets when the
|
|
58
67
|
// (syllableIndex, neumeGroup) pair changes.
|
|
59
68
|
const neumeIndices = [];
|
|
@@ -94,7 +103,9 @@ export function computeTabula(ir, options = {}) {
|
|
|
94
103
|
quilisma: n.context.quilisma,
|
|
95
104
|
liquescent: n.context.liquescent,
|
|
96
105
|
strophicus: n.context.strophicus,
|
|
106
|
+
oriscus: n.context.oriscus,
|
|
97
107
|
divisio: a.divisio,
|
|
108
|
+
cadenceRef: cadenceRefByPos.get(`${a.phraseIndex}:${a.syllableIndex}:${a.noteIndex}`) ?? null,
|
|
98
109
|
role: n.step.role,
|
|
99
110
|
name: n.step.name,
|
|
100
111
|
nomen: n.step.nomen,
|
|
@@ -20,6 +20,7 @@ export interface Context {
|
|
|
20
20
|
quilisma: boolean;
|
|
21
21
|
liquescent: boolean;
|
|
22
22
|
strophicus: boolean;
|
|
23
|
+
oriscus: boolean;
|
|
23
24
|
doubleEpisema: boolean;
|
|
24
25
|
weight: number;
|
|
25
26
|
}
|
|
@@ -55,6 +56,9 @@ export interface ArticulationWeights {
|
|
|
55
56
|
repercussionPrevWeight: number;
|
|
56
57
|
repercussionPrevDuration: number;
|
|
57
58
|
repercussionOriscusWeight: number;
|
|
59
|
+
oriscusWeight: number;
|
|
60
|
+
oriscusDuration: number;
|
|
61
|
+
oriscusPrevWeight: number;
|
|
58
62
|
breakWeight: number;
|
|
59
63
|
dashWeight: number;
|
|
60
64
|
dashDuration: number;
|
|
@@ -118,6 +122,7 @@ export interface ParsedNote {
|
|
|
118
122
|
quilisma: boolean;
|
|
119
123
|
liquescent: boolean;
|
|
120
124
|
strophicus: boolean;
|
|
125
|
+
oriscus: boolean;
|
|
121
126
|
doubleEpisema: boolean;
|
|
122
127
|
}
|
|
123
128
|
export interface RestEvent {
|
|
@@ -4,7 +4,7 @@ import type { Step, StepVariant, Finger, Region } from "./step.js";
|
|
|
4
4
|
import type { Interval, IntervalDirection, IntervalQuality } from "./interval.js";
|
|
5
5
|
import type { Neume, NeumeShape } from "./neume.js";
|
|
6
6
|
import type { GamutOptions } from "./gamut.js";
|
|
7
|
-
import type { ModeData, ModeProfile } from "./modes.js";
|
|
7
|
+
import type { ModeData, ModeProfile, CadenceFigure } from "./modes.js";
|
|
8
8
|
import type { GuidonianEntry, GuidonianVariant } from "./guido.js";
|
|
9
9
|
export type BuiltinTuning = "pythagorean" | "meantone" | "equal" | "ptolemy-intense" | "ptolemy-soft" | "ptolemy-equable";
|
|
10
10
|
export type Tuning = BuiltinTuning | string;
|
|
@@ -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):
|
|
64
|
+
modus(mode: number): Modus;
|
|
49
65
|
tonus(opts?: TonusOpts): Tonus;
|
|
50
66
|
}
|
|
51
67
|
/**
|
|
@@ -56,5 +72,5 @@ export interface Temperamentum {
|
|
|
56
72
|
* @throws Error on invalid tuning, scale, or mode input.
|
|
57
73
|
*/
|
|
58
74
|
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, };
|
|
75
|
+
export type { Scale, ScaleOpts, ScalaFile, RatioResult, Pitch, PitchInput, Step, StepVariant, Finger, Region, Neume, NeumeShape, Interval, IntervalDirection, IntervalQuality, ModeData, ModeProfile, CadenceFigure, GamutOptions, GuidonianEntry, GuidonianVariant, };
|
|
60
76
|
//# sourceMappingURL=api.d.ts.map
|
|
@@ -110,7 +110,25 @@ export function buildTemper(input) {
|
|
|
110
110
|
return buildGamut(scala, gamutOpts);
|
|
111
111
|
},
|
|
112
112
|
modus(mode) {
|
|
113
|
-
|
|
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")
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
export interface ModeProfile {
|
|
2
2
|
mood: string;
|
|
3
|
+
ethos: string;
|
|
3
4
|
phrasing: "recitative" | "lyrical" | "hymnic" | "solemn";
|
|
4
5
|
melodic: "rising" | "falling" | "arch" | "neutral";
|
|
5
6
|
tendency: "melismatic" | "neumatic" | "syllabic" | "neutral";
|
|
6
7
|
}
|
|
8
|
+
export interface CadenceFigure {
|
|
9
|
+
id: string;
|
|
10
|
+
steps: number[];
|
|
11
|
+
}
|
|
7
12
|
export interface ModeData {
|
|
8
13
|
mode: number;
|
|
9
14
|
nomen: string;
|
|
@@ -15,10 +20,7 @@ export interface ModeData {
|
|
|
15
20
|
scalePcs: number[];
|
|
16
21
|
hexachords: ("durum" | "naturale" | "molle")[];
|
|
17
22
|
profile: ModeProfile;
|
|
18
|
-
cadences:
|
|
19
|
-
final: number[];
|
|
20
|
-
tenor: number[];
|
|
21
|
-
};
|
|
23
|
+
cadences: CadenceFigure[];
|
|
22
24
|
modulations: {
|
|
23
25
|
regular: number[];
|
|
24
26
|
conceded: number[];
|
|
@@ -14,8 +14,13 @@ export const MODES = new Map([
|
|
|
14
14
|
tenor: 9,
|
|
15
15
|
scalePcs: [2, 4, 5, 7, 9, 11, 0],
|
|
16
16
|
hexachords: ["naturale"],
|
|
17
|
-
profile: { mood: "serious", phrasing: "lyrical", melodic: "falling", tendency: "melismatic" },
|
|
18
|
-
cadences:
|
|
17
|
+
profile: { mood: "serious", ethos: "gravis", phrasing: "lyrical", melodic: "falling", tendency: "melismatic" },
|
|
18
|
+
cadences: [
|
|
19
|
+
{ id: "mi-re", steps: [1, 0] },
|
|
20
|
+
{ id: "ut-re", steps: [-1, 0] },
|
|
21
|
+
{ id: "sol-fa-re", steps: [3, 2, 0] },
|
|
22
|
+
{ id: "mi-fa-re", steps: [1, 2, 0] },
|
|
23
|
+
],
|
|
19
24
|
modulations: { regular: [2, 9, 5, 7], conceded: [12, 4], initials: [2, 5, 7, 9, 14] },
|
|
20
25
|
ambitus: { lowest: 2, highest: 21, span: 19 },
|
|
21
26
|
species: { fifth: [2, 9], fourth: [9, 2] },
|
|
@@ -33,8 +38,13 @@ export const MODES = new Map([
|
|
|
33
38
|
tenor: 5,
|
|
34
39
|
scalePcs: [2, 4, 5, 7, 9, 11, 0],
|
|
35
40
|
hexachords: ["naturale"],
|
|
36
|
-
profile: { mood: "sad", phrasing: "lyrical", melodic: "arch", tendency: "neumatic" },
|
|
37
|
-
cadences:
|
|
41
|
+
profile: { mood: "sad", ethos: "tristis", phrasing: "lyrical", melodic: "arch", tendency: "neumatic" },
|
|
42
|
+
cadences: [
|
|
43
|
+
{ id: "mi-re", steps: [1, 0] },
|
|
44
|
+
{ id: "ut-re", steps: [-1, 0] },
|
|
45
|
+
{ id: "sol-fa-re", steps: [3, 2, 0] },
|
|
46
|
+
{ id: "mi-fa-re", steps: [1, 2, 0] },
|
|
47
|
+
],
|
|
38
48
|
modulations: { regular: [2, 5, 7, 9], conceded: [0, 4], initials: [0, 2, 4, 5, 7, 9] },
|
|
39
49
|
ambitus: { lowest: 0, highest: 17, span: 17 },
|
|
40
50
|
species: { fifth: [2, 9], fourth: [9, 2] },
|
|
@@ -52,8 +62,12 @@ export const MODES = new Map([
|
|
|
52
62
|
tenor: 0,
|
|
53
63
|
scalePcs: [4, 5, 7, 9, 11, 0, 2],
|
|
54
64
|
hexachords: ["naturale"],
|
|
55
|
-
profile: { mood: "mystic", phrasing: "solemn", melodic: "falling", tendency: "melismatic" },
|
|
56
|
-
cadences:
|
|
65
|
+
profile: { mood: "mystic", ethos: "mysticus", phrasing: "solemn", melodic: "falling", tendency: "melismatic" },
|
|
66
|
+
cadences: [
|
|
67
|
+
{ id: "fa-mi", steps: [1, 0] },
|
|
68
|
+
{ id: "re-mi", steps: [-1, 0] },
|
|
69
|
+
{ id: "sol-fa-mi", steps: [2, 1, 0] },
|
|
70
|
+
],
|
|
57
71
|
modulations: { regular: [4, 0, 7, 9], conceded: [5, 11], initials: [0, 2, 4, 7, 9] },
|
|
58
72
|
ambitus: { lowest: 4, highest: 16, span: 12 },
|
|
59
73
|
species: { fifth: [4, 11], fourth: [11, 4] },
|
|
@@ -71,8 +85,12 @@ export const MODES = new Map([
|
|
|
71
85
|
tenor: 9,
|
|
72
86
|
scalePcs: [4, 5, 7, 9, 11, 0, 2],
|
|
73
87
|
hexachords: ["naturale"],
|
|
74
|
-
profile: { mood: "harmonious", phrasing: "lyrical", melodic: "arch", tendency: "neumatic" },
|
|
75
|
-
cadences:
|
|
88
|
+
profile: { mood: "harmonious", ethos: "harmonicus", phrasing: "lyrical", melodic: "arch", tendency: "neumatic" },
|
|
89
|
+
cadences: [
|
|
90
|
+
{ id: "fa-mi", steps: [1, 0] },
|
|
91
|
+
{ id: "re-mi", steps: [-1, 0] },
|
|
92
|
+
{ id: "sol-fa-mi", steps: [2, 1, 0] },
|
|
93
|
+
],
|
|
76
94
|
modulations: { regular: [4, 9, 7, 0], conceded: [11], initials: [0, 4, 7, 9] },
|
|
77
95
|
ambitus: { lowest: 2, highest: 21, span: 19 },
|
|
78
96
|
species: { fifth: [4, 11], fourth: [11, 4] },
|
|
@@ -90,8 +108,12 @@ export const MODES = new Map([
|
|
|
90
108
|
tenor: 0,
|
|
91
109
|
scalePcs: [5, 7, 9, 11, 0, 2, 4],
|
|
92
110
|
hexachords: ["molle"],
|
|
93
|
-
profile: { mood: "happy", phrasing: "solemn", melodic: "rising", tendency: "melismatic" },
|
|
94
|
-
cadences:
|
|
111
|
+
profile: { mood: "happy", ethos: "laetus", phrasing: "solemn", melodic: "rising", tendency: "melismatic" },
|
|
112
|
+
cadences: [
|
|
113
|
+
{ id: "mi-fa", steps: [-1, 0] },
|
|
114
|
+
{ id: "fa-mi-fa", steps: [0, -1, 0] },
|
|
115
|
+
{ id: "la-sol-fa", steps: [2, 1, 0] },
|
|
116
|
+
],
|
|
95
117
|
modulations: { regular: [5, 0, 7, 12], conceded: [9, 2], initials: [5, 7, 9, 0, 12] },
|
|
96
118
|
ambitus: { lowest: 5, highest: 17, span: 12 },
|
|
97
119
|
species: { fifth: [5, 0], fourth: [0, 5] },
|
|
@@ -109,8 +131,12 @@ export const MODES = new Map([
|
|
|
109
131
|
tenor: 9,
|
|
110
132
|
scalePcs: [5, 7, 9, 11, 0, 2, 4],
|
|
111
133
|
hexachords: ["molle"],
|
|
112
|
-
profile: { mood: "devout", phrasing: "lyrical", melodic: "arch", tendency: "neumatic" },
|
|
113
|
-
cadences:
|
|
134
|
+
profile: { mood: "devout", ethos: "devotus", phrasing: "lyrical", melodic: "arch", tendency: "neumatic" },
|
|
135
|
+
cadences: [
|
|
136
|
+
{ id: "mi-fa", steps: [-1, 0] },
|
|
137
|
+
{ id: "fa-mi-fa", steps: [0, -1, 0] },
|
|
138
|
+
{ id: "la-sol-fa", steps: [2, 1, 0] },
|
|
139
|
+
],
|
|
114
140
|
modulations: { regular: [5, 9, 0, 7], conceded: [2, 12], initials: [0, 2, 4, 5, 7, 9] },
|
|
115
141
|
ambitus: { lowest: 3, highest: 21, span: 18 },
|
|
116
142
|
species: { fifth: [5, 0], fourth: [0, 5] },
|
|
@@ -128,8 +154,13 @@ export const MODES = new Map([
|
|
|
128
154
|
tenor: 2,
|
|
129
155
|
scalePcs: [7, 9, 11, 0, 2, 4, 5],
|
|
130
156
|
hexachords: ["durum"],
|
|
131
|
-
profile: { mood: "angelical", phrasing: "solemn", melodic: "rising", tendency: "melismatic" },
|
|
132
|
-
cadences:
|
|
157
|
+
profile: { mood: "angelical", ethos: "angelicus", phrasing: "solemn", melodic: "rising", tendency: "melismatic" },
|
|
158
|
+
cadences: [
|
|
159
|
+
{ id: "la-sol", steps: [1, 0] },
|
|
160
|
+
{ id: "fa-sol", steps: [-1, 0] },
|
|
161
|
+
{ id: "ut-sol", steps: [3, 0] },
|
|
162
|
+
{ id: "ut-ti-sol", steps: [3, 2, 0] },
|
|
163
|
+
],
|
|
133
164
|
modulations: { regular: [7, 2, 9, 14], conceded: [5, 0], initials: [7, 9, 11, 2, 14] },
|
|
134
165
|
ambitus: { lowest: 7, highest: 19, span: 12 },
|
|
135
166
|
species: { fifth: [7, 2], fourth: [2, 7] },
|
|
@@ -147,8 +178,13 @@ export const MODES = new Map([
|
|
|
147
178
|
tenor: 0,
|
|
148
179
|
scalePcs: [7, 9, 11, 0, 2, 4, 5],
|
|
149
180
|
hexachords: ["durum"],
|
|
150
|
-
profile: { mood: "perfect", phrasing: "lyrical", melodic: "arch", tendency: "neumatic" },
|
|
151
|
-
cadences:
|
|
181
|
+
profile: { mood: "perfect", ethos: "perfectus", phrasing: "lyrical", melodic: "arch", tendency: "neumatic" },
|
|
182
|
+
cadences: [
|
|
183
|
+
{ id: "la-sol", steps: [1, 0] },
|
|
184
|
+
{ id: "fa-sol", steps: [-1, 0] },
|
|
185
|
+
{ id: "ut-sol", steps: [3, 0] },
|
|
186
|
+
{ id: "ut-ti-sol", steps: [3, 2, 0] },
|
|
187
|
+
],
|
|
152
188
|
modulations: { regular: [7, 0, 9, 14], conceded: [2, 5], initials: [0, 2, 4, 7, 9, 12, 14] },
|
|
153
189
|
ambitus: { lowest: 5, highest: 19, span: 14 },
|
|
154
190
|
species: { fifth: [7, 2], fourth: [2, 7] },
|
|
@@ -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
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ModeData } from "./data/modes.js";
|
|
2
|
-
export type { ModeProfile, ModeData } from "./data/modes.js";
|
|
2
|
+
export type { ModeProfile, ModeData, CadenceFigure } from "./data/modes.js";
|
|
3
3
|
export { MODES } from "./data/modes.js";
|
|
4
4
|
/** Return ModeData for mode 1–8. Throws on unknown mode. */
|
|
5
5
|
export declare function getMode(mode: number): ModeData;
|
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, GamutOptions, Tonus, TonusOpts } from "./engines/temper/api.js";
|
|
14
|
-
import type { Score, ScoreOpts, PondusInput, PondusOpts, AccentusInput, AccentusOpts, 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, GamutOptions, Tonus, TonusOpts, Score, ScoreOpts, PondusInput, PondusOpts, AccentusInput, AccentusOpts, 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
|
+
"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",
|