tonus 0.1.8 → 0.5.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 +132 -108
- package/CHANGELOG.md +598 -1
- package/LICENSE +133 -29
- package/README.md +106 -83
- package/dist/data/am.js +2666 -11196
- package/dist/data/ams.d.ts +5 -0
- package/dist/data/ams.js +122 -0
- package/dist/data/attestation.d.ts +19 -0
- package/dist/data/attestation.js +15716 -0
- package/dist/data/attestation.json +15711 -0
- package/dist/data/cadentiae.d.ts +43 -0
- package/dist/data/cadentiae.js +174 -0
- package/dist/data/cal.js +48 -0
- package/dist/data/census.d.ts +12 -0
- package/dist/data/census.js +36 -0
- package/dist/data/commune-office.d.ts +4 -0
- package/dist/data/commune-office.js +2371 -0
- package/dist/data/commune-office.json +2365 -0
- package/dist/data/commune.js +181 -11
- package/dist/data/corpus-overlap.d.ts +17 -0
- package/dist/data/corpus-overlap.js +299 -5
- package/dist/data/cot.d.ts +5 -0
- package/dist/data/cot.js +172 -0
- package/dist/data/cse.d.ts +5 -0
- package/dist/data/cse.js +122 -0
- package/dist/data/gabc-glyphs.d.ts +45 -0
- package/dist/data/gabc-glyphs.js +122 -0
- package/dist/data/gr.js +754 -6394
- package/dist/data/kyriale.js +116 -116
- package/dist/data/la.js +799 -13419
- package/dist/data/lh.js +113 -3473
- package/dist/data/lu.js +931 -17631
- package/dist/data/nocturnale-romanum.js +1659 -10411
- package/dist/data/office-ferial.d.ts +4 -0
- package/dist/data/office-ferial.js +396 -0
- package/dist/data/office-ferial.json +391 -0
- package/dist/data/office-monastic.d.ts +17 -1
- package/dist/data/office-monastic.js +1403 -466
- package/dist/data/office-psalms-monastic.d.ts +13 -1
- package/dist/data/office-psalms-monastic.js +9 -0
- package/dist/data/propers.js +1 -1
- package/dist/data/psalms.js +22919 -5
- package/dist/data/psm.d.ts +5 -0
- package/dist/data/psm.js +122 -0
- package/dist/data/seasonal-respbreve.d.ts +5 -0
- package/dist/data/seasonal-respbreve.js +41 -0
- package/dist/data/seasonal-respbreve.json +35 -0
- package/dist/data/smufl-glyphs.d.ts +17 -0
- package/dist/data/smufl-glyphs.js +1546 -0
- package/dist/data/smufl-glyphs.json +1530 -0
- package/dist/engines/cal/calendar.d.ts +3 -2
- package/dist/engines/cal/calendar.js +105 -29
- package/dist/engines/cal/data/eras.d.ts +35 -0
- package/dist/engines/cal/data/eras.js +128 -0
- package/dist/engines/cal/date.js +44 -0
- package/dist/engines/cal/types.d.ts +15 -3
- package/dist/engines/cal/types.js +5 -5
- package/dist/engines/census/census.d.ts +7 -0
- package/dist/engines/census/census.js +179 -0
- package/dist/engines/census/types.d.ts +55 -0
- package/dist/engines/census/types.js +8 -0
- package/dist/engines/chant/attest.d.ts +39 -0
- package/dist/engines/chant/attest.js +90 -0
- package/dist/engines/chant/chant.d.ts +16 -4
- package/dist/engines/chant/chant.js +220 -30
- package/dist/engines/chant/data/compline.js +2 -1
- package/dist/engines/chant/data/masses.d.ts +56 -4
- package/dist/engines/chant/data/masses.js +305 -80
- package/dist/engines/chant/data/prime.js +1 -1
- package/dist/engines/chant/hour.js +279 -58
- package/dist/engines/chant/ordinary.d.ts +2 -0
- package/dist/engines/chant/ordinary.js +336 -56
- package/dist/engines/chant/propers.js +55 -5
- package/dist/engines/chant/psalm.d.ts +4 -4
- package/dist/engines/chant/psalm.js +25 -11
- package/dist/engines/chant/syllabify.d.ts +1 -0
- package/dist/engines/chant/syllabify.js +90 -17
- package/dist/engines/chant/types.d.ts +115 -12
- package/dist/engines/chant/types.js +38 -3
- package/dist/engines/harmonia/api.js +4 -0
- package/dist/engines/harmonia/data/doctrines.js +3 -1
- package/dist/engines/harmonia/tabula.d.ts +3 -0
- package/dist/engines/harmonia/tabula.js +1 -0
- package/dist/engines/harmonia/voice.d.ts +4 -0
- package/dist/engines/harmonia/voice.js +8 -4
- package/dist/engines/imprint.js +14 -1
- package/dist/engines/planet/orbital.js +4 -4
- package/dist/engines/planet/planet.d.ts +10 -0
- package/dist/engines/planet/planet.js +30 -3
- package/dist/engines/planet/position.js +13 -10
- package/dist/engines/planet/types.d.ts +1 -0
- package/dist/engines/score/api.d.ts +2 -13
- package/dist/engines/score/api.js +21 -8
- package/dist/engines/score/articulation.js +2 -2
- package/dist/engines/score/cadence.d.ts +76 -0
- package/dist/engines/score/cadence.js +96 -0
- package/dist/engines/score/emitters/accidentals.d.ts +21 -0
- package/dist/engines/score/emitters/accidentals.js +88 -0
- package/dist/engines/score/emitters/atramentum.d.ts +107 -0
- package/dist/engines/score/emitters/atramentum.js +239 -0
- package/dist/engines/score/emitters/breaking.d.ts +62 -0
- package/dist/engines/score/emitters/breaking.js +80 -0
- package/dist/engines/score/emitters/moderna.d.ts +38 -0
- package/dist/engines/score/emitters/moderna.js +612 -0
- package/dist/engines/score/emitters/svg.d.ts +143 -0
- package/dist/engines/score/emitters/svg.js +1328 -0
- package/dist/engines/score/emitters/tracks.d.ts +104 -0
- package/dist/engines/score/emitters/tracks.js +728 -0
- package/dist/engines/score/infer.d.ts +3 -3
- package/dist/engines/score/infer.js +2 -2
- package/dist/engines/score/inscriptio.d.ts +69 -0
- package/dist/engines/score/inscriptio.js +138 -0
- package/dist/engines/score/ir.d.ts +2 -2
- package/dist/engines/score/ir.js +59 -12
- package/dist/engines/score/lyric.d.ts +23 -0
- package/dist/engines/score/lyric.js +234 -0
- package/dist/engines/score/meta.d.ts +2 -2
- package/dist/engines/score/modulation.d.ts +12 -0
- package/dist/engines/score/modulation.js +49 -0
- package/dist/engines/score/neume.js +35 -4
- package/dist/engines/score/parse.js +150 -9
- package/dist/engines/score/phrasing.js +4 -3
- package/dist/engines/score/prosody.d.ts +38 -0
- package/dist/engines/score/prosody.js +70 -6
- package/dist/engines/score/tabula.d.ts +37 -5
- package/dist/engines/score/tabula.js +18 -0
- package/dist/engines/score/types.d.ts +88 -1
- package/dist/engines/temper/api.d.ts +4 -1
- package/dist/engines/temper/api.js +28 -5
- package/dist/engines/temper/data/guido.js +6 -2
- package/dist/engines/temper/data/modes.d.ts +6 -0
- package/dist/engines/temper/data/modes.js +42 -0
- package/dist/engines/temper/data/tones.d.ts +1 -1
- package/dist/engines/temper/data/tones.js +20 -11
- package/dist/engines/temper/interval.js +4 -3
- package/dist/engines/temper/modality.d.ts +11 -2
- package/dist/engines/temper/modality.js +74 -2
- package/dist/engines/temper/modes.d.ts +1 -1
- package/dist/engines/temper/pitch.d.ts +1 -1
- package/dist/engines/temper/pitch.js +12 -2
- package/dist/engines/temper/scale.d.ts +53 -0
- package/dist/engines/temper/scale.js +107 -8
- package/dist/index.d.ts +26 -8
- package/dist/index.js +37 -4
- package/docs/api/calendar.md +279 -0
- package/docs/api/census.md +288 -0
- package/docs/api/chant.md +657 -0
- package/docs/api/heavens.md +346 -0
- package/docs/api/index.md +263 -0
- package/docs/api/score.md +873 -0
- package/docs/api/tuning.md +619 -0
- package/package.json +11 -5
- package/dist/data/office-matins-roman.d.ts +0 -19
- package/dist/data/office-matins-roman.js +0 -4383
- package/dist/data/office-psalms-roman.d.ts +0 -15
- package/dist/data/office-psalms-roman.js +0 -28
- package/dist/data/office-roman.d.ts +0 -19
- package/dist/data/office-roman.js +0 -13792
- package/dist/engines/chant/matutinum.d.ts +0 -33
- package/dist/engines/chant/matutinum.js +0 -81
- package/dist/engines/score/emitters/midi.d.ts +0 -65
- package/dist/engines/score/emitters/midi.js +0 -162
- package/dist/engines/score/emitters/musicxml.d.ts +0 -18
- package/dist/engines/score/emitters/musicxml.js +0 -166
|
@@ -12,6 +12,35 @@ const FINALIS_WEIGHT = 3;
|
|
|
12
12
|
const TENOR_WEIGHT = 2;
|
|
13
13
|
const REGULAR_MOD_WEIGHT = 1;
|
|
14
14
|
const CONCEDED_MOD_WEIGHT = 0.5;
|
|
15
|
+
// `ModeData.recitingNotes` (see data/modes.ts) carries more than the
|
|
16
|
+
// principal tenor for several modes — auxiliary/secondary/pseudo/rare
|
|
17
|
+
// reciting notes, sourced from the Gregorian Modes Degree Summary Tables.
|
|
18
|
+
// A first attempt at wiring this in weighted every rank into degreeWeight
|
|
19
|
+
// unconditionally (via the same max-of-roles `set` used below) and broke
|
|
20
|
+
// ../../../tests/modality.test.mjs's authentic/plagal initials-bonus case for
|
|
21
|
+
// modes 7/8: that test relies on an exact baseline tie between the pair, broken
|
|
22
|
+
// only by the initials bonus. Mode 7's new auxiliary pc 0 partly overlapped
|
|
23
|
+
// a pc it already carried as a conceded modulation degree (weight 0.5),
|
|
24
|
+
// and "same rank, same weight" silently PROMOTED that existing pc to the
|
|
25
|
+
// higher auxiliary weight — mode 8 had no equivalent already-weighted pc to
|
|
26
|
+
// promote, so the promotion alone tipped the tie before the initials bonus
|
|
27
|
+
// got a chance to run.
|
|
28
|
+
//
|
|
29
|
+
// The fix below is additive-only: a non-principal reciting note contributes
|
|
30
|
+
// weight ONLY to a pc that isn't already weighted by the finalis, the tenor,
|
|
31
|
+
// or a modulation degree (see the second loop after `set(data.final, ...)`).
|
|
32
|
+
// It never promotes an existing weight. That keeps the 7/8 tie intact — both
|
|
33
|
+
// modes pick up their (as it happens, shared) new auxiliary pc 11 identically
|
|
34
|
+
// — while still letting genuinely new degrees (e.g. mode 4's pseudo-tenor at
|
|
35
|
+
// pc 5, mode 5's rare recitation on its own final) contribute real signal.
|
|
36
|
+
// `principal` entries are skipped outright: by construction they already
|
|
37
|
+
// coincide with `data.tenor` or `data.final` and would always be a no-op.
|
|
38
|
+
const RECITING_WEIGHT = {
|
|
39
|
+
auxiliary: 1.2,
|
|
40
|
+
secondary: 0.8,
|
|
41
|
+
pseudo: 0.5,
|
|
42
|
+
rare: 0.3,
|
|
43
|
+
};
|
|
15
44
|
// A chant's opening note is a modal signal: each mode lists its valid initials
|
|
16
45
|
// in rank order, most characteristic first (Rockstro's Grove ordering
|
|
17
46
|
// [biblio: rockstro-grove]). Opening on a mode's primary initial boosts it more
|
|
@@ -19,11 +48,33 @@ const CONCEDED_MOD_WEIGHT = 0.5;
|
|
|
19
48
|
// from its plagal partner, since the two share a finalis but rank the same
|
|
20
49
|
// opening pitch differently.
|
|
21
50
|
const INITIAL_BONUS = 0.3;
|
|
51
|
+
// The last note is the treatises' first determinant of mode: a chant comes to
|
|
52
|
+
// rest on its final. Landing on a mode's final is the single strongest signal.
|
|
53
|
+
const FINAL_NOTE_BONUS = 0.5;
|
|
54
|
+
// Tessitura — how high the melody sits above its final — is the classical
|
|
55
|
+
// authentic/plagal separator (an authentic mode ranges a fifth-and-more above the
|
|
56
|
+
// final; its plagal partner straddles it). The bonus peaks when the observed
|
|
57
|
+
// tessitura matches the mode's expected value and falls off linearly over
|
|
58
|
+
// TESSITURA_TOLERANCE.
|
|
59
|
+
//
|
|
60
|
+
// THE CALIBRATION IS PENDING RE-DERIVATION (noted 2026-08-11). These constants
|
|
61
|
+
// were fitted over the corpus at n≈6,666 — a count from before the accidental
|
|
62
|
+
// fix in `parse.ts`, which was emitting a phantom note at every accidental and
|
|
63
|
+
// so moved every mean pitch that used one. The SEPARATION still holds on a
|
|
64
|
+
// re-measure (authentic sits higher than plagal, cleanly), which is what these
|
|
65
|
+
// constants encode, so they are left in force rather than replaced by a worse
|
|
66
|
+
// number. What is not yet re-derived is where exactly the two centres sit.
|
|
67
|
+
const TESSITURA_AUTHENTIC = 4.0;
|
|
68
|
+
const TESSITURA_PLAGAL = 1.7;
|
|
69
|
+
const TESSITURA_TOLERANCE = 3;
|
|
70
|
+
const TESSITURA_WEIGHT = 1.0;
|
|
22
71
|
/**
|
|
23
72
|
* Rank a pitch-class distribution against the eight modes, best fit first.
|
|
24
|
-
*
|
|
73
|
+
* The optional signals sharpen the ranking: the opening note (initials bonus),
|
|
74
|
+
* the closing note (final-note bonus), and the tessitura (authentic/plagal).
|
|
25
75
|
*/
|
|
26
|
-
export function computeModalAffinity(pcDistribution,
|
|
76
|
+
export function computeModalAffinity(pcDistribution, opts = {}) {
|
|
77
|
+
const { firstNotePc, lastNotePc, tessitura } = opts;
|
|
27
78
|
const results = [];
|
|
28
79
|
for (let m = 1; m <= 8; m++) {
|
|
29
80
|
const data = MODES.get(m);
|
|
@@ -41,6 +92,16 @@ export function computeModalAffinity(pcDistribution, firstNotePc) {
|
|
|
41
92
|
set(pc % 12, REGULAR_MOD_WEIGHT);
|
|
42
93
|
set(data.tenor, TENOR_WEIGHT);
|
|
43
94
|
set(data.final, FINALIS_WEIGHT);
|
|
95
|
+
// Additive-only pass: see the comment on RECITING_WEIGHT above for why
|
|
96
|
+
// this fills in gaps rather than using `set` (which would promote an
|
|
97
|
+
// existing weight, not just add a new one).
|
|
98
|
+
for (const rn of data.recitingNotes) {
|
|
99
|
+
if (rn.rank === "principal")
|
|
100
|
+
continue;
|
|
101
|
+
const pc = rn.pc % 12;
|
|
102
|
+
if (!degreeWeight.has(pc))
|
|
103
|
+
degreeWeight.set(pc, RECITING_WEIGHT[rn.rank]);
|
|
104
|
+
}
|
|
44
105
|
let score = 0;
|
|
45
106
|
for (const [pc, w] of degreeWeight)
|
|
46
107
|
score += (pcDistribution[pc] ?? 0) * w;
|
|
@@ -51,6 +112,17 @@ export function computeModalAffinity(pcDistribution, firstNotePc) {
|
|
|
51
112
|
if (rank !== -1)
|
|
52
113
|
score += (INITIAL_BONUS * (initials.length - rank)) / initials.length;
|
|
53
114
|
}
|
|
115
|
+
// Final-note + tessitura: only apply when the chant actually rests on this
|
|
116
|
+
// mode's final. The tessitura then separates the mode from its authentic/
|
|
117
|
+
// plagal partner (which share the final but sit at different heights).
|
|
118
|
+
if (lastNotePc != null && lastNotePc === data.final % 12) {
|
|
119
|
+
score += FINAL_NOTE_BONUS;
|
|
120
|
+
if (tessitura != null) {
|
|
121
|
+
const expected = data.type === "authentic" ? TESSITURA_AUTHENTIC : TESSITURA_PLAGAL;
|
|
122
|
+
const fit = Math.max(0, 1 - Math.abs(tessitura - expected) / TESSITURA_TOLERANCE);
|
|
123
|
+
score += TESSITURA_WEIGHT * fit;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
54
126
|
results.push({ mode: m, alias: data.alias, score });
|
|
55
127
|
}
|
|
56
128
|
return results.sort((a, b) => b.score - a.score);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ModeData } from "./data/modes.js";
|
|
2
|
-
export type { ModeProfile, ModeData, CadenceFigure } from "./data/modes.js";
|
|
2
|
+
export type { ModeProfile, ModeData, CadenceFigure, RecitingNote } 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;
|
|
@@ -35,6 +35,6 @@ export interface PitchContext {
|
|
|
35
35
|
a4?: number;
|
|
36
36
|
}
|
|
37
37
|
export declare function parsePitch(input: PitchInput, ctx?: PitchContext): number;
|
|
38
|
-
export declare function toPitch(input: PitchInput, scale: Scale): Pitch;
|
|
38
|
+
export declare function toPitch(input: PitchInput, scale: Scale, prefer?: "flat" | "sharp"): Pitch;
|
|
39
39
|
export declare function scaleDegreeInMode(midi: number, mode: number): number | null;
|
|
40
40
|
//# sourceMappingURL=pitch.d.ts.map
|
|
@@ -62,13 +62,23 @@ export function parsePitch(input, ctx = {}) {
|
|
|
62
62
|
// Resolves a PitchInput through a Scale into a tuned Pitch.
|
|
63
63
|
// Applies the Scale's transpose and returns a Pitch with midi/pc/oct/spn
|
|
64
64
|
// from the transposed MIDI plus tuning-derived hz/offset/bend/ratio.
|
|
65
|
-
export function toPitch(input, scale) {
|
|
65
|
+
export function toPitch(input, scale, prefer) {
|
|
66
66
|
const rawMidi = parsePitch(input, { mode: scale.mode, a4: scale.a4 });
|
|
67
67
|
const { hz, offset, bend } = midiToHz(rawMidi, scale);
|
|
68
68
|
const midi = clamp(rawMidi + scale.transpose);
|
|
69
69
|
const pc = midi % 12;
|
|
70
70
|
const oct = Math.floor(midi / 12) - 1;
|
|
71
|
-
|
|
71
|
+
// Spell as the source WROTE it when the caller knows. Deriving the spelling
|
|
72
|
+
// from the pitch class alone cannot: pc 1 is D-flat in a chant that wrote a
|
|
73
|
+
// flat and C-sharp in one that wrote a sharp, and the class is identical.
|
|
74
|
+
// PREFER_FLAT_PCS ({3, 8, 10}) is a reasonable default but omits pc 1 and 6,
|
|
75
|
+
// so a written flat landing there came back spelled — and reported in `acc` —
|
|
76
|
+
// as a SHARP, the opposite of what the source said.
|
|
77
|
+
//
|
|
78
|
+
// The Graduale only ever writes B-flat (pc 10, the bmolle), so the corpus
|
|
79
|
+
// never reached the gap; `notatio` passes the hint because a caller may set
|
|
80
|
+
// any GABC accidental on any degree.
|
|
81
|
+
const useFlat = prefer ? prefer === "flat" : PREFER_FLAT_PCS.has(pc);
|
|
72
82
|
const sp = useFlat ? FLAT_SPELLING[pc] : SHARP_SPELLING[pc];
|
|
73
83
|
const accStr = sp.acc === -1 ? "b" : sp.acc === 1 ? "#" : "";
|
|
74
84
|
const spn = `${sp.step}${accStr}${oct}`;
|
|
@@ -20,6 +20,15 @@ export interface RatioResult {
|
|
|
20
20
|
cents: number;
|
|
21
21
|
display: string;
|
|
22
22
|
}
|
|
23
|
+
/** Stern-Brocot rational approximation — the nearest simple fraction to a
|
|
24
|
+
* decimal ratio, as `[numerator, denominator]`.
|
|
25
|
+
*
|
|
26
|
+
* Exported because `toRatio` only reaches it through a STRING, and a caller
|
|
27
|
+
* holding a computed ratio (a gamut row's `ratio`, a string fraction derived
|
|
28
|
+
* from two frequencies) has a number. Without this the caller writes the
|
|
29
|
+
* search again — which is how the site came to carry a second copy of this
|
|
30
|
+
* same algorithm. */
|
|
31
|
+
export declare function approximate(value: number, maxDen?: number): [number, number];
|
|
23
32
|
export declare function toRatio(input: string): RatioResult;
|
|
24
33
|
export declare function parseStep(v: number | string): number;
|
|
25
34
|
export interface ScalaFile {
|
|
@@ -28,6 +37,50 @@ export interface ScalaFile {
|
|
|
28
37
|
}
|
|
29
38
|
export declare function parseScala(input: string): ScalaFile;
|
|
30
39
|
export declare function getPtolemaicRatios(tuning: string): string[] | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* The pure-fifth chain's ET-cents deviation per pitch class, anchored A = 0
|
|
42
|
+
* (the a4-reference anchor `offset` carries under the default tuning). The
|
|
43
|
+
* heji/cents accidental baseline reads THIS, so the emitters and the engine
|
|
44
|
+
* can never disagree about the chain's spelling.
|
|
45
|
+
*/
|
|
46
|
+
export declare function pythagoreanCentsByPc(): number[];
|
|
47
|
+
/** The wolf: what is left of the octave when the chain of fifths runs out. */
|
|
48
|
+
export interface Lupus {
|
|
49
|
+
/** Pitch class the wolf is measured UP from — the chain's last link. */
|
|
50
|
+
from: number;
|
|
51
|
+
/** Pitch class it lands on — the chain's first. */
|
|
52
|
+
to: number;
|
|
53
|
+
/** The two, spelled as the chain spells them. */
|
|
54
|
+
spn: [string, string];
|
|
55
|
+
/** Its size, tempered, in cents. */
|
|
56
|
+
cents: number;
|
|
57
|
+
/** A pure fifth, for the comparison this exists to invite. */
|
|
58
|
+
pure: number;
|
|
59
|
+
/** Signed: positive is wide of a pure fifth, negative narrow. */
|
|
60
|
+
fromPure: number;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* THE WOLF, from the chain the ratios are built on.
|
|
64
|
+
*
|
|
65
|
+
* Twelve fifths do not close an octave. Stack them from E♭ and the twelfth
|
|
66
|
+
* lands beside the first rather than on it, and the gap it leaves has to go
|
|
67
|
+
* somewhere: it goes into the one interval nobody stacked, G♯ up to E♭. That
|
|
68
|
+
* interval is a fifth by position and not by size, which is why it howls.
|
|
69
|
+
*
|
|
70
|
+
* Under pure fifths it comes out NARROW (678.5¢, the Pythagorean comma taken
|
|
71
|
+
* out of it). Temper the fifths and the leftover grows: every cent taken off
|
|
72
|
+
* eleven fifths is given back to this one, so it passes a pure fifth and goes
|
|
73
|
+
* wide (737.6¢ at quarter-comma). That sign change is the thing a reader of
|
|
74
|
+
* the slider is watching, and it is why `fromPure` is signed.
|
|
75
|
+
*
|
|
76
|
+
* Derived from the SAME walk `buildPythagoreanRatios` does, deliberately.
|
|
77
|
+
* There is a closed form (1200·7 − 11·fifth, which agrees to the decimal),
|
|
78
|
+
* but two derivations of one number is how they drift apart.
|
|
79
|
+
*
|
|
80
|
+
* Null when no chain built the scale: a Ptolemaic genus and a custom Scala
|
|
81
|
+
* list are given as ratios, and have no fifths chain to leave a remainder.
|
|
82
|
+
*/
|
|
83
|
+
export declare function wolfOf(commaN: number): Lupus;
|
|
31
84
|
export declare function buildRatios(opts?: ScaleOpts): Scale;
|
|
32
85
|
export declare function midiToHz(midi: number, scala: Scale): {
|
|
33
86
|
hz: number;
|
|
@@ -20,8 +20,15 @@
|
|
|
20
20
|
// ptolemy-harmonics, Harmonics I.15–16]. See expandDiatonicSteps for how a
|
|
21
21
|
// 7-ratio genus is laid onto the fixed gamut, and why.
|
|
22
22
|
import { MODES } from "./modes.js";
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
/** Stern-Brocot rational approximation — the nearest simple fraction to a
|
|
24
|
+
* decimal ratio, as `[numerator, denominator]`.
|
|
25
|
+
*
|
|
26
|
+
* Exported because `toRatio` only reaches it through a STRING, and a caller
|
|
27
|
+
* holding a computed ratio (a gamut row's `ratio`, a string fraction derived
|
|
28
|
+
* from two frequencies) has a number. Without this the caller writes the
|
|
29
|
+
* search again — which is how the site came to carry a second copy of this
|
|
30
|
+
* same algorithm. */
|
|
31
|
+
export function approximate(value, maxDen = 1000) {
|
|
25
32
|
if (value === Math.round(value))
|
|
26
33
|
return [Math.round(value), 1];
|
|
27
34
|
let [a, b, c, d] = [0, 1, 1, 0];
|
|
@@ -128,9 +135,16 @@ export function parseScala(input) {
|
|
|
128
135
|
}
|
|
129
136
|
const PURE_FIFTH = 3 / 2;
|
|
130
137
|
const SYNTONIC_COMMA = 81 / 80;
|
|
131
|
-
// The
|
|
132
|
-
//
|
|
133
|
-
|
|
138
|
+
// The chain of fifths as chromatic pitch classes, running E♭–G♯:
|
|
139
|
+
// E♭ B♭ F C G D A E B F♯ C♯ G♯
|
|
140
|
+
// The naturals sit F–B (ut–fa is a pure 4/3 — b molle exists precisely to make
|
|
141
|
+
// that fourth over F), b molle and E♭ take the flat side, the ficta sharps
|
|
142
|
+
// (F♯ C♯ G♯) the sharp side. This is the received medieval dodecachord; an
|
|
143
|
+
// ascending-only chain from C would spell F as E♯ (a wolf ut–fa of 521.5¢)
|
|
144
|
+
// and b molle as A♯. buildPythagoreanRatios walks this and octave-folds each.
|
|
145
|
+
// The heji/cents baseline in score/emitters/accidentals.ts derives from this
|
|
146
|
+
// same chain via pythagoreanCentsByPc() — keep them coupled.
|
|
147
|
+
const FIFTH_TO_CHROM = [3, 10, 5, 0, 7, 2, 9, 4, 11, 6, 1, 8];
|
|
134
148
|
// Ptolemy's three diatonic genera [biblio: ptolemy-harmonics, Harmonics I.15–16],
|
|
135
149
|
// each a tetrachord (1/1 … 4/3) doubled up a 3/2 to fill the octave:
|
|
136
150
|
// intense (syntonon) — classical just intonation: pure 5/4 major, 6/5 minor.
|
|
@@ -144,6 +158,18 @@ const PTOLEMAIC = {
|
|
|
144
158
|
export function getPtolemaicRatios(tuning) {
|
|
145
159
|
return PTOLEMAIC[tuning];
|
|
146
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* The pure-fifth chain's ET-cents deviation per pitch class, anchored A = 0
|
|
163
|
+
* (the a4-reference anchor `offset` carries under the default tuning). The
|
|
164
|
+
* heji/cents accidental baseline reads THIS, so the emitters and the engine
|
|
165
|
+
* can never disagree about the chain's spelling.
|
|
166
|
+
*/
|
|
167
|
+
export function pythagoreanCentsByPc() {
|
|
168
|
+
const ratios = buildPythagoreanRatios(0); // C-anchored, folded to [1, 2)
|
|
169
|
+
const cents = ratios.map((r, pc) => 1200 * Math.log2(r) - 100 * pc);
|
|
170
|
+
const anchor = cents[9];
|
|
171
|
+
return cents.map((c) => Math.round((c - anchor) * 100) / 100);
|
|
172
|
+
}
|
|
147
173
|
function buildPythagoreanRatios(commaN) {
|
|
148
174
|
const tf = commaN === 0
|
|
149
175
|
? PURE_FIFTH
|
|
@@ -153,9 +179,13 @@ function buildPythagoreanRatios(commaN) {
|
|
|
153
179
|
const pc = FIFTH_TO_CHROM[k];
|
|
154
180
|
out[pc] = foldOct(tf ** k);
|
|
155
181
|
}
|
|
182
|
+
// Anchor on C and fold each pitch class into the C-register [1, 2): the
|
|
183
|
+
// table's contract is "ascending within the C octave, then normalized to
|
|
184
|
+
// root". The E♭–G♯ chain reaches some pcs below C, so without this fold the
|
|
185
|
+
// table comes out octave-scrambled (A at 27/32, a broken ratio() matcher).
|
|
156
186
|
const root = out[0] ?? 1;
|
|
157
187
|
for (let i = 0; i < 12; i++)
|
|
158
|
-
out[i] = (out[i] ?? 1) / root;
|
|
188
|
+
out[i] = foldOct((out[i] ?? 1) / root);
|
|
159
189
|
return out;
|
|
160
190
|
}
|
|
161
191
|
// The natural (white-key) pitch classes in ascending pitch order: C D E F G A B.
|
|
@@ -191,7 +221,52 @@ function normalizeToRoot(ratios, rootPc) {
|
|
|
191
221
|
function ratiosToCents(ratios) {
|
|
192
222
|
return ratios.map((r) => 1200 * Math.log2(r));
|
|
193
223
|
}
|
|
194
|
-
|
|
224
|
+
const PURE_FIFTH_CENTS = 1200 * Math.log2(3 / 2);
|
|
225
|
+
// The chain's own spelling, in its own order. A wolf is named by where it
|
|
226
|
+
// falls in the chain, not by an enharmonic the gamut might prefer: the
|
|
227
|
+
// interval G♯–E♭ is the wolf, and calling either end by its other name
|
|
228
|
+
// ("A♭–D♯") describes a different reading of the same two frequencies.
|
|
229
|
+
const CHAIN_SPELLING = ["Eb", "Bb", "F", "C", "G", "D", "A", "E", "B", "F#", "C#", "G#"];
|
|
230
|
+
/**
|
|
231
|
+
* THE WOLF, from the chain the ratios are built on.
|
|
232
|
+
*
|
|
233
|
+
* Twelve fifths do not close an octave. Stack them from E♭ and the twelfth
|
|
234
|
+
* lands beside the first rather than on it, and the gap it leaves has to go
|
|
235
|
+
* somewhere: it goes into the one interval nobody stacked, G♯ up to E♭. That
|
|
236
|
+
* interval is a fifth by position and not by size, which is why it howls.
|
|
237
|
+
*
|
|
238
|
+
* Under pure fifths it comes out NARROW (678.5¢, the Pythagorean comma taken
|
|
239
|
+
* out of it). Temper the fifths and the leftover grows: every cent taken off
|
|
240
|
+
* eleven fifths is given back to this one, so it passes a pure fifth and goes
|
|
241
|
+
* wide (737.6¢ at quarter-comma). That sign change is the thing a reader of
|
|
242
|
+
* the slider is watching, and it is why `fromPure` is signed.
|
|
243
|
+
*
|
|
244
|
+
* Derived from the SAME walk `buildPythagoreanRatios` does, deliberately.
|
|
245
|
+
* There is a closed form (1200·7 − 11·fifth, which agrees to the decimal),
|
|
246
|
+
* but two derivations of one number is how they drift apart.
|
|
247
|
+
*
|
|
248
|
+
* Null when no chain built the scale: a Ptolemaic genus and a custom Scala
|
|
249
|
+
* list are given as ratios, and have no fifths chain to leave a remainder.
|
|
250
|
+
*/
|
|
251
|
+
export function wolfOf(commaN) {
|
|
252
|
+
const ratios = buildPythagoreanRatios(commaN);
|
|
253
|
+
const cents = ratios.map((r) => 1200 * Math.log2(r));
|
|
254
|
+
const from = FIFTH_TO_CHROM[11];
|
|
255
|
+
const to = FIFTH_TO_CHROM[0];
|
|
256
|
+
// Upward and octave-folded: the chain's ends sit either side of the fold,
|
|
257
|
+
// so the raw difference is negative about as often as not.
|
|
258
|
+
let span = cents[to] - cents[from];
|
|
259
|
+
if (span < 0)
|
|
260
|
+
span += 1200;
|
|
261
|
+
return {
|
|
262
|
+
from,
|
|
263
|
+
to,
|
|
264
|
+
spn: [CHAIN_SPELLING[11], CHAIN_SPELLING[0]],
|
|
265
|
+
cents: span,
|
|
266
|
+
pure: PURE_FIFTH_CENTS,
|
|
267
|
+
fromPure: span - PURE_FIFTH_CENTS,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
195
270
|
export function buildRatios(opts = {}) {
|
|
196
271
|
const mode = opts.mode ?? 1;
|
|
197
272
|
const a4 = opts.a4 ?? 440;
|
|
@@ -202,7 +277,31 @@ export function buildRatios(opts = {}) {
|
|
|
202
277
|
const rootPc = opts.root ?? finalisPc;
|
|
203
278
|
let ratios;
|
|
204
279
|
if (opts.steps != null) {
|
|
205
|
-
|
|
280
|
+
let parsed = opts.steps.map(parseStep);
|
|
281
|
+
// Two USER step conventions circulate, and mistaking one for the other
|
|
282
|
+
// mistunes silently: a DEGREE LIST begins at 1/1 (the ptolemy presets), a
|
|
283
|
+
// SCALA list gives the degrees above an implicit tonic and ends at 2/1
|
|
284
|
+
// (every .scl file). Normalize both to 1/1-first; anything else is
|
|
285
|
+
// ambiguous and throws rather than resolving a plausible-looking wrong
|
|
286
|
+
// tuning. The check applies to USER intake only — string steps. An
|
|
287
|
+
// all-number list is the engine's own resolved table riding back in
|
|
288
|
+
// (notatio carries temperamentum.cents), root-relative by construction.
|
|
289
|
+
const userIntake = opts.steps.some((v) => typeof v === "string");
|
|
290
|
+
if (userIntake) {
|
|
291
|
+
const EPS = 1e-9;
|
|
292
|
+
const firstIsUnison = Math.abs((parsed[0] ?? 0) - 1) < EPS;
|
|
293
|
+
const lastIsOctave = Math.abs((parsed[parsed.length - 1] ?? 0) - 2) < EPS;
|
|
294
|
+
if (!firstIsUnison && lastIsOctave) {
|
|
295
|
+
parsed = [1, ...parsed.slice(0, -1)]; // Scala convention → shift under the tonic
|
|
296
|
+
}
|
|
297
|
+
else if (firstIsUnison && lastIsOctave) {
|
|
298
|
+
parsed = parsed.slice(0, -1); // both stated → drop the closing octave
|
|
299
|
+
}
|
|
300
|
+
else if (!firstIsUnison) {
|
|
301
|
+
throw new RangeError("Custom scale must begin at 1/1 (a degree list) or end at 2/1 (Scala convention)");
|
|
302
|
+
}
|
|
303
|
+
parsed = parsed.map(foldOct);
|
|
304
|
+
}
|
|
206
305
|
if (parsed.length === 7) {
|
|
207
306
|
ratios = expandDiatonicSteps(parsed);
|
|
208
307
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,26 +3,28 @@ import { getChants, getCorpus } from "./engines/chant/chant.js";
|
|
|
3
3
|
import { getPropers } from "./engines/chant/propers.js";
|
|
4
4
|
import { getOrdinary } from "./engines/chant/ordinary.js";
|
|
5
5
|
import { getHour } from "./engines/chant/hour.js";
|
|
6
|
-
import { getMatins } from "./engines/chant/matutinum.js";
|
|
7
6
|
import { getPsalm } from "./engines/chant/psalm.js";
|
|
8
7
|
import { buildTemper } from "./engines/temper/api.js";
|
|
9
8
|
import { buildScore } from "./engines/score/api.js";
|
|
9
|
+
import { inscriptio } from "./engines/score/inscriptio.js";
|
|
10
10
|
import { getCosmos } from "./engines/planet/planet.js";
|
|
11
11
|
import { buildHarmonia } from "./engines/harmonia/api.js";
|
|
12
|
+
import { getCensus } from "./engines/census/census.js";
|
|
12
13
|
import type { FeastQuery, Feast, Pascha, Season, Grade } from "./engines/cal/types.js";
|
|
13
|
-
import type { CantusQuery, Chant, OrdinaryChant, PropriumQuery, OrdinariumQuery, OfficiumQuery, PsalmusQuery,
|
|
14
|
-
import type { Matins, Nocturn } from "./engines/chant/matutinum.js";
|
|
14
|
+
import type { CantusQuery, Chant, OrdinaryChant, PropriumQuery, OrdinariumQuery, OfficiumQuery, PsalmusQuery, Corpus, GenusCount, ModeCount, SharedCount, CorpusLedger, CorpusFullCount, CorpusQuery } from "./engines/chant/types.js";
|
|
15
15
|
import type { TemperamentumInput, Temperamentum, Tuning, TemperamentumOpts, Pitch, PitchInput, Step, Neume, NeumeShape, Interval, ModeData, CadenceFigure, Modus, TunedNote, GamutOptions, Tonus, TonusOpts } from "./engines/temper/api.js";
|
|
16
|
-
import type { Score, ScoreOpts, PondusInput, PondusOpts, AccentusInput, AccentusOpts, Cadence, CadenceTarget, CadenceApproach, Modulation
|
|
16
|
+
import type { Score, ScoreOpts, PondusInput, PondusOpts, AccentusInput, AccentusOpts, Cadence, CadenceTarget, CadenceApproach, Modulation } from "./engines/score/api.js";
|
|
17
|
+
import type { InscriptioOpts, Inscriptio, NoteGeometry, FontSpec, FontSlot, FontEmbed } from "./engines/score/inscriptio.js";
|
|
17
18
|
import type { ChantTabulaRow } from "./engines/score/tabula.js";
|
|
18
19
|
import type { Imprint, Attractor, VowelAttractor, ModalAffinity } from "./engines/imprint.js";
|
|
19
20
|
import type { Prosody, RhythmicProfile, NoteRange, CadenceDistribution } from "./engines/score/prosody.js";
|
|
20
21
|
import type { Harmony, HarmoniaOpts, VoicedBody, VoicedAspect, Frame, Author } from "./engines/harmonia/api.js";
|
|
21
22
|
import type { HarmonyTabulaRow } from "./engines/harmonia/tabula.js";
|
|
22
23
|
import type { PlanetVowel } from "./engines/harmonia/data/vowels.js";
|
|
23
|
-
import type { Note, Performance, Phrase, Syllable, RestEvent, ParseError, ArsisThesis, RhythmicType, CompoundBeat } from "./engines/score/types.js";
|
|
24
|
+
import type { Note, Performance, Phrase, Syllable, LyricRun, RestEvent, ParseError, ArsisThesis, RhythmicType, CompoundBeat } from "./engines/score/types.js";
|
|
24
25
|
import type { VoicedPitch } from "./engines/harmonia/voice.js";
|
|
25
26
|
import type { Cosmos, CosmosQuery, Body, BodyName, Aspect } from "./engines/planet/types.js";
|
|
27
|
+
import type { Census, CensusQuery, CensusBy, CensusGroup, CensusGroupProfile, CensusNeighbor } from "./engines/census/types.js";
|
|
26
28
|
declare const tonus: {
|
|
27
29
|
festum: typeof getFeast;
|
|
28
30
|
pascha: typeof getPascha;
|
|
@@ -31,14 +33,30 @@ declare const tonus: {
|
|
|
31
33
|
proprium: typeof getPropers;
|
|
32
34
|
ordinarium: typeof getOrdinary;
|
|
33
35
|
officium: typeof getHour;
|
|
34
|
-
matutinum: typeof getMatins;
|
|
35
36
|
psalmus: typeof getPsalm;
|
|
36
37
|
temperamentum: typeof buildTemper;
|
|
37
38
|
notatio: typeof buildScore;
|
|
39
|
+
inscriptio: typeof inscriptio;
|
|
38
40
|
caelum: typeof getCosmos;
|
|
39
41
|
harmonia: typeof buildHarmonia;
|
|
42
|
+
census: typeof getCensus;
|
|
40
43
|
};
|
|
41
44
|
export default tonus;
|
|
42
|
-
export {
|
|
43
|
-
|
|
45
|
+
export { SEASON_LABEL, // season code → English name ("adv" → "Advent")
|
|
46
|
+
TEMPORA, // season code → Latin name ("adv" → "Tempus Adventus")
|
|
47
|
+
GRADE_ORDER, // grade code → rank, low is higher
|
|
48
|
+
GRADUS, } from "./engines/cal/types.js";
|
|
49
|
+
export { HORAE, // the eight canonical hours, Matins first — the order is content
|
|
50
|
+
OFFICIA, // office code → Latin genus ("an" → "Antiphona")
|
|
51
|
+
ORDINARIA, // ordinary code → Latin name ("kyrie" → "Kyrie eleison")
|
|
52
|
+
MODI, } from "./engines/chant/types.js";
|
|
53
|
+
export { SOURCES } from "./engines/chant/chant.js";
|
|
54
|
+
export { MODES } from "./engines/temper/data/modes.js";
|
|
55
|
+
export { TONES } from "./engines/temper/data/tones.js";
|
|
56
|
+
export type { PsalmTone, Differentia } from "./engines/temper/data/tones.js";
|
|
57
|
+
export { CADENTIAE, CADENTIAE_POPULATION } from "./data/cadentiae.js";
|
|
58
|
+
export type { CadentiaFamilia } from "./data/cadentiae.js";
|
|
59
|
+
export { SIGNS, SIGNA } from "./engines/planet/planet.js";
|
|
60
|
+
export { CENSUS_GROUPS, CENSUS_ORDER } from "./data/census.js";
|
|
61
|
+
export type { Feast, FeastQuery, Pascha, Season, Grade, Chant, CantusQuery, OrdinaryChant, PropriumQuery, OrdinariumQuery, OfficiumQuery, PsalmusQuery, Corpus, GenusCount, ModeCount, SharedCount, CorpusLedger, CorpusFullCount, CorpusQuery, Census, CensusQuery, CensusBy, CensusGroup, CensusGroupProfile, CensusNeighbor, 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, InscriptioOpts, Inscriptio, NoteGeometry, FontSpec, FontSlot, FontEmbed, ChantTabulaRow, Note, Performance, Phrase, Syllable, LyricRun, RestEvent, ParseError, ArsisThesis, RhythmicType, CompoundBeat, VoicedPitch, Cosmos, CosmosQuery, Body, BodyName, Aspect, Imprint, Attractor, VowelAttractor, ModalAffinity, Prosody, RhythmicProfile, NoteRange, CadenceDistribution, Harmony, HarmoniaOpts, VoicedBody, VoicedAspect, Frame, Author, HarmonyTabulaRow, PlanetVowel, };
|
|
44
62
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -3,12 +3,13 @@ import { getChants, getCorpus } from "./engines/chant/chant.js";
|
|
|
3
3
|
import { getPropers } from "./engines/chant/propers.js";
|
|
4
4
|
import { getOrdinary } from "./engines/chant/ordinary.js";
|
|
5
5
|
import { getHour } from "./engines/chant/hour.js";
|
|
6
|
-
import { getMatins } from "./engines/chant/matutinum.js";
|
|
7
6
|
import { getPsalm } from "./engines/chant/psalm.js";
|
|
8
7
|
import { buildTemper } from "./engines/temper/api.js";
|
|
9
8
|
import { buildScore } from "./engines/score/api.js";
|
|
9
|
+
import { inscriptio } from "./engines/score/inscriptio.js";
|
|
10
10
|
import { getCosmos } from "./engines/planet/planet.js";
|
|
11
11
|
import { buildHarmonia } from "./engines/harmonia/api.js";
|
|
12
|
+
import { getCensus } from "./engines/census/census.js";
|
|
12
13
|
const tonus = {
|
|
13
14
|
festum: getFeast,
|
|
14
15
|
pascha: getPascha,
|
|
@@ -17,14 +18,46 @@ const tonus = {
|
|
|
17
18
|
proprium: getPropers,
|
|
18
19
|
ordinarium: getOrdinary,
|
|
19
20
|
officium: getHour,
|
|
20
|
-
matutinum: getMatins,
|
|
21
21
|
psalmus: getPsalm,
|
|
22
22
|
temperamentum: buildTemper,
|
|
23
23
|
notatio: buildScore,
|
|
24
|
+
inscriptio,
|
|
24
25
|
caelum: getCosmos,
|
|
25
26
|
harmonia: buildHarmonia,
|
|
27
|
+
census: getCensus,
|
|
26
28
|
};
|
|
27
29
|
export default tonus;
|
|
28
|
-
//
|
|
29
|
-
export
|
|
30
|
+
// ── The appendix ──
|
|
31
|
+
// The export law: verbs live on the namespace; return values are plain data;
|
|
32
|
+
// the appendix exports canonical constant tables — nothing with a (). A
|
|
33
|
+
// function that earns public life earns a fifteenth Latin noun instead.
|
|
34
|
+
// A constant is admitted when a caller would otherwise TYPE IT OUT — a mode
|
|
35
|
+
// list, an hour list, the valid `by:` values. Those transcriptions drift, and a
|
|
36
|
+
// caller's drifted copy fails as wrong answers rather than as an error. Naming
|
|
37
|
+
// follows the register rule: a table of Latin values takes a Latin name
|
|
38
|
+
// (TEMPORA, "Tempus Adventus"), a table of codes or English keeps English
|
|
39
|
+
// (SEASON_LABEL, "Advent"). So the name says which one you are holding.
|
|
40
|
+
// cal — the liturgical year
|
|
41
|
+
export { SEASON_LABEL, // season code → English name ("adv" → "Advent")
|
|
42
|
+
TEMPORA, // season code → Latin name ("adv" → "Tempus Adventus")
|
|
43
|
+
GRADE_ORDER, // grade code → rank, low is higher
|
|
44
|
+
GRADUS, // grade code → Latin name
|
|
45
|
+
} from "./engines/cal/types.js";
|
|
46
|
+
// chant — the corpus vocabulary
|
|
47
|
+
export { HORAE, // the eight canonical hours, Matins first — the order is content
|
|
48
|
+
OFFICIA, // office code → Latin genus ("an" → "Antiphona")
|
|
49
|
+
ORDINARIA, // ordinary code → Latin name ("kyrie" → "Kyrie eleison")
|
|
50
|
+
MODI, // mode number → Latin name ("1" → "Modus I")
|
|
51
|
+
} from "./engines/chant/types.js";
|
|
52
|
+
export { SOURCES } from "./engines/chant/chant.js"; // book code → bibliographic record
|
|
53
|
+
// temper — modes, tones, cadences
|
|
54
|
+
export { MODES } from "./engines/temper/data/modes.js";
|
|
55
|
+
export { TONES } from "./engines/temper/data/tones.js";
|
|
56
|
+
export { CADENTIAE, CADENTIAE_POPULATION } from "./data/cadentiae.js";
|
|
57
|
+
// planet — the zodiac
|
|
58
|
+
export { SIGNS, SIGNA } from "./engines/planet/planet.js";
|
|
59
|
+
// census — the field groups and the block index. CENSUS_GROUPS keys are the
|
|
60
|
+
// valid `by:` values AND the `profile` keys; CENSUS_ORDER holds every censused
|
|
61
|
+
// id, so asking whether a chant is in the census stops needing a try/catch.
|
|
62
|
+
export { CENSUS_GROUPS, CENSUS_ORDER } from "./data/census.js";
|
|
30
63
|
//# sourceMappingURL=index.js.map
|