tonus 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,10 @@
1
- import type { Chant, CantusQuery } from "./types.js";
1
+ import type { Chant, CantusQuery, ChantSource, Corpus } from "./types.js";
2
+ /**
3
+ * Metadata and content breakdown for one corpus book (`tonus.corpus`). Pass a
4
+ * source code; get the book's bibliographic identity plus its genre and mode
5
+ * distributions. Computed on first access from the loaded corpus, then cached.
6
+ */
7
+ export declare function getCorpus(code: ChantSource): Corpus;
2
8
  export declare function resolveChant(id: string | null): Chant | null;
3
9
  export declare function resolveChants(ids: string[]): Chant[];
4
10
  /**
@@ -1,9 +1,11 @@
1
1
  import { OFFICE_LABELS, MODE_LABELS } from "./types.js";
2
+ import { CORPUS_OVERLAP } from "../../data/corpus-overlap.js";
2
3
  import { GR_DATA, GR_SOURCE } from "../../data/gr.js";
3
4
  import { LU_DATA, LU_SOURCE } from "../../data/lu.js";
4
5
  import { LA_DATA, LA_SOURCE } from "../../data/la.js";
5
6
  import { LH_DATA, LH_SOURCE } from "../../data/lh.js";
6
7
  import { AM_DATA, AM_SOURCE } from "../../data/am.js";
8
+ import { NR_DATA, NR_SOURCE } from "../../data/nocturnale-romanum.js";
7
9
  function modusOf(mode) {
8
10
  return mode != null ? (MODE_LABELS[mode] ?? null) : null;
9
11
  }
@@ -65,6 +67,7 @@ const CORPUS = [
65
67
  ...LA_DATA.map((c) => withLabels(c, LA_SOURCE)),
66
68
  ...LH_DATA.map((c) => withLabels(c, LH_SOURCE)),
67
69
  ...AM_DATA.map((c) => withLabels(c, AM_SOURCE)),
70
+ ...NR_DATA.map((c) => withLabels(c, NR_SOURCE)),
68
71
  ];
69
72
  let _byId = null;
70
73
  function byId() {
@@ -72,6 +75,73 @@ function byId() {
72
75
  _byId = new Map(CORPUS.map((c) => [c.id, c]));
73
76
  return _byId;
74
77
  }
78
+ const SOURCES = {
79
+ gr: GR_SOURCE, lu: LU_SOURCE, la: LA_SOURCE, lh: LH_SOURCE, am: AM_SOURCE, nr: NR_SOURCE,
80
+ };
81
+ // Tally a book's genre and mode distribution — computed once per code, cached.
82
+ const _corpusCache = new Map();
83
+ /**
84
+ * Metadata and content breakdown for one corpus book (`tonus.corpus`). Pass a
85
+ * source code; get the book's bibliographic identity plus its genre and mode
86
+ * distributions. Computed on first access from the loaded corpus, then cached.
87
+ */
88
+ export function getCorpus(code) {
89
+ const cached = _corpusCache.get(code);
90
+ if (cached)
91
+ return cached;
92
+ const src = SOURCES[code];
93
+ if (!src)
94
+ throw new Error(`Unknown corpus code: "${code}" (expected gr, lu, la, lh, or am)`);
95
+ const chants = CORPUS.filter((c) => c.source.code === code);
96
+ // Genre distribution — count by office code, descending by count.
97
+ const officeCounts = new Map();
98
+ // Mode distribution — count by mode 1–8; everything else (p/d/e, null) into one bucket.
99
+ const modeCounts = new Map();
100
+ let otherModes = 0;
101
+ for (const c of chants) {
102
+ officeCounts.set(c.office, (officeCounts.get(c.office) ?? 0) + 1);
103
+ if (c.mode != null && MODE_LABELS[c.mode]) {
104
+ modeCounts.set(c.mode, (modeCounts.get(c.mode) ?? 0) + 1);
105
+ }
106
+ else {
107
+ otherModes++;
108
+ }
109
+ }
110
+ const genera = [...officeCounts.entries()]
111
+ .map(([office, count]) => ({ office, genus: OFFICE_LABELS[office] ?? office, count }))
112
+ .sort((a, b) => b.count - a.count);
113
+ const modes = [];
114
+ for (const m of ["1", "2", "3", "4", "5", "6", "7", "8"]) {
115
+ const count = modeCounts.get(m);
116
+ if (count)
117
+ modes.push({ mode: m, modus: MODE_LABELS[m], count });
118
+ }
119
+ if (otherModes > 0)
120
+ modes.push({ mode: null, modus: null, count: otherModes });
121
+ // Pre-dedup relationships (precomputed in tonus-corpus — tonus can't derive
122
+ // them, since it stores only one copy of each shared chant).
123
+ const ov = CORPUS_OVERLAP[code];
124
+ const shared = Object.entries(ov?.shared ?? {})
125
+ .map(([c, count]) => ({ code: c, count }))
126
+ .sort((a, b) => b.count - a.count);
127
+ const result = {
128
+ code,
129
+ book: src.book,
130
+ fullTitle: src.fullTitle ?? null,
131
+ edition: src.edition ?? null,
132
+ year: src.year,
133
+ editor: src.editor,
134
+ scanSource: src.scanSource ?? null,
135
+ count: chants.length,
136
+ total: ov?.total ?? chants.length,
137
+ unique: ov?.unique ?? chants.length,
138
+ shared,
139
+ genera,
140
+ modes,
141
+ };
142
+ _corpusCache.set(code, result);
143
+ return result;
144
+ }
75
145
  function toArray(v) {
76
146
  if (v === undefined)
77
147
  return undefined;
@@ -0,0 +1,33 @@
1
+ import type { Chant, OfficiumQuery } from "./types.js";
2
+ /** One nocturn of a resolved Matins: its responsories (and any antiphons). */
3
+ export interface Nocturn {
4
+ /** 1–3. */
5
+ n: number;
6
+ /** The nocturn's great responsories, in order, as resolved chants. */
7
+ responsories: Chant[];
8
+ /** The nocturn's antiphons, where the Nocturnale carries them (else empty). */
9
+ antiphons: Chant[];
10
+ }
11
+ /** A feast's structured Roman Matins. */
12
+ export interface Matins {
13
+ /** The tonus feast id this Matins was resolved for. */
14
+ feastId: string;
15
+ /** The feast's Latin name from the Nocturnale. */
16
+ name: string;
17
+ /** The rank/class, e.g. "I. classis", "Feria". */
18
+ rank: string;
19
+ /** One nocturn (simple) or three (festal). */
20
+ nocturns: Nocturn[];
21
+ /** The feast whose chants were borrowed by rubric, if any (provenance). */
22
+ redirectedFrom: string | null;
23
+ }
24
+ /**
25
+ * Structured Roman Matins (`tonus.matutinum`) for a feast: the nocturns with
26
+ * their responsories (and antiphons where present), assembled from the
27
+ * Nocturnale Romanum. Without a feast, resolves the default epoch's feast (as
28
+ * `officium` does). Only the Roman rite is served today; other rites return
29
+ * null (the monastic night office is not yet modelled — see office-matins-roman).
30
+ * @returns the Matins structure, or null when no Nocturnale match exists.
31
+ */
32
+ export declare function getMatins(query?: OfficiumQuery): Matins | null;
33
+ //# sourceMappingURL=matutinum.d.ts.map
@@ -0,0 +1,81 @@
1
+ // ---------------------------------------------------------------------------
2
+ // engines/chant/matutinum — structured Roman Matins (nocturns)
3
+ // ---------------------------------------------------------------------------
4
+ // The flat `officium({ hora: "matutinum" })` returns Matins chants as an
5
+ // undifferentiated Chant[] (the best-effort flat view, unchanged). This module
6
+ // adds the STRUCTURED night office for the Roman rite: the 3-nocturn assembly
7
+ // from the Nocturnale Romanum, joined to the tonus calendar by feast id.
8
+ //
9
+ // It is additive and separate — a distinct accessor (`tonus.matutinum`), not a
10
+ // reshape of getHour — so the flat path and every other rite stay as they are.
11
+ // Coverage is what the Nocturnale + the calendar bridge give: the sanctorale and
12
+ // Advent today (see office-matins-roman.ts). A feast with no Nocturnale match
13
+ // returns null, the office's graceful-degradation convention.
14
+ import { resolveChant } from "./chant.js";
15
+ import { getFeast } from "../cal/calendar.js";
16
+ import { temporaSundayId } from "../cal/date.js";
17
+ import { MATINS_ROMAN } from "../../data/office-matins-roman.js";
18
+ let _byFeast = null;
19
+ function byFeast() {
20
+ if (!_byFeast) {
21
+ _byFeast = new Map();
22
+ for (const d of MATINS_ROMAN)
23
+ if (d.tonusFeastId)
24
+ _byFeast.set(d.tonusFeastId, d);
25
+ }
26
+ return _byFeast;
27
+ }
28
+ function resolveDay(day) {
29
+ const nocturns = day.nocturns.map((noc) => {
30
+ const responsories = [];
31
+ const antiphons = [];
32
+ for (const c of noc.chants) {
33
+ const chant = resolveChant(c.id);
34
+ if (!chant)
35
+ continue;
36
+ if (c.type === "A")
37
+ antiphons.push(chant);
38
+ else
39
+ responsories.push(chant); // R (and I invitatory) resolve as responsories
40
+ }
41
+ return { n: noc.n, responsories, antiphons };
42
+ });
43
+ return {
44
+ feastId: day.tonusFeastId,
45
+ name: day.name,
46
+ rank: day.rank,
47
+ nocturns,
48
+ redirectedFrom: day.redirectedFrom,
49
+ };
50
+ }
51
+ /** The Nocturnale Matins for a feast id, or null when the calendar has no match. */
52
+ function matinsForFeastId(feastId) {
53
+ const map = byFeast();
54
+ const day = map.get(feastId) ?? (() => {
55
+ // Temporal feasts resolve their Sunday where a weekday has no proper.
56
+ const sunday = temporaSundayId(feastId);
57
+ return sunday ? map.get(sunday) : undefined;
58
+ })();
59
+ return day ? resolveDay(day) : null;
60
+ }
61
+ /**
62
+ * Structured Roman Matins (`tonus.matutinum`) for a feast: the nocturns with
63
+ * their responsories (and antiphons where present), assembled from the
64
+ * Nocturnale Romanum. Without a feast, resolves the default epoch's feast (as
65
+ * `officium` does). Only the Roman rite is served today; other rites return
66
+ * null (the monastic night office is not yet modelled — see office-matins-roman).
67
+ * @returns the Matins structure, or null when no Nocturnale match exists.
68
+ */
69
+ export function getMatins(query) {
70
+ const rite = query?.rite ?? "romanum";
71
+ if (rite !== "romanum")
72
+ return null;
73
+ const feasts = query?.feast
74
+ ? Array.isArray(query.feast) ? query.feast : [query.feast]
75
+ : null;
76
+ const feast = feasts ? feasts[0] : getFeast()[0];
77
+ if (!feast)
78
+ return null;
79
+ return matinsForFeastId(feast.id);
80
+ }
81
+ //# sourceMappingURL=matutinum.js.map
@@ -2,7 +2,7 @@ import type { Season, Grade, Feast } from "../cal/types.js";
2
2
  export type { Season, Grade, Feast };
3
3
  export type OfficeCode = "an" | "al" | "ca" | "co" | "gr" | "hy" | "in" | "of" | "ps" | "re" | "rb" | "se" | "tr" | "tp" | "or";
4
4
  export type OrdinaryCode = "ky" | "gl" | "cr" | "sa" | "ag" | "be" | "it" | "as" | "va";
5
- export type ChantSource = "gr" | "lu" | "la" | "lh" | "am";
5
+ export type ChantSource = "gr" | "lu" | "la" | "lh" | "am" | "nr";
6
6
  export type CanonicalHour = "matutinum" | "laudes" | "prima" | "tertia" | "sexta" | "nona" | "vesperae" | "completorium";
7
7
  export declare const MODE_LABELS: Readonly<Record<string, string>>;
8
8
  export declare const OFFICE_LABELS: Readonly<Record<OfficeCode, string>>;
@@ -22,8 +22,11 @@ export interface Chant {
22
22
  }[];
23
23
  source: {
24
24
  book: string;
25
+ fullTitle?: string | null;
26
+ edition?: string | null;
25
27
  year: number | null;
26
28
  editor: string | null;
29
+ scanSource?: string | null;
27
30
  code?: ChantSource | "user";
28
31
  };
29
32
  ordinary?: OrdinaryCode;
@@ -35,6 +38,35 @@ export interface OrdinaryChant extends Chant {
35
38
  ordinarium: string;
36
39
  mass: number;
37
40
  }
41
+ export interface GenusCount {
42
+ office: OfficeCode;
43
+ genus: string;
44
+ count: number;
45
+ }
46
+ export interface ModeCount {
47
+ mode: string | null;
48
+ modus: string | null;
49
+ count: number;
50
+ }
51
+ export interface SharedCount {
52
+ code: ChantSource;
53
+ count: number;
54
+ }
55
+ export interface Corpus {
56
+ code: ChantSource;
57
+ book: string;
58
+ fullTitle: string | null;
59
+ edition: string | null;
60
+ year: number | null;
61
+ editor: string | null;
62
+ scanSource: string | null;
63
+ count: number;
64
+ total: number;
65
+ unique: number;
66
+ shared: SharedCount[];
67
+ genera: GenusCount[];
68
+ modes: ModeCount[];
69
+ }
38
70
  export interface CantusQuery {
39
71
  id?: string | string[];
40
72
  gabc?: string;
package/dist/index.d.ts CHANGED
@@ -1,15 +1,17 @@
1
1
  import { getFeast, getPascha } from "./engines/cal/calendar.js";
2
- import { getChants } from "./engines/chant/chant.js";
2
+ 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";
6
7
  import { getPsalm } from "./engines/chant/psalm.js";
7
8
  import { buildTemper } from "./engines/temper/api.js";
8
9
  import { buildScore } from "./engines/score/api.js";
9
10
  import { getCosmos } from "./engines/planet/planet.js";
10
11
  import { buildHarmonia } from "./engines/harmonia/api.js";
11
12
  import type { FeastQuery, Feast, Pascha, Season, Grade } from "./engines/cal/types.js";
12
- import type { CantusQuery, Chant, OrdinaryChant, PropriumQuery, OrdinariumQuery, OfficiumQuery, PsalmusQuery, Rite } from "./engines/chant/types.js";
13
+ import type { CantusQuery, Chant, OrdinaryChant, PropriumQuery, OrdinariumQuery, OfficiumQuery, PsalmusQuery, Rite, Corpus, GenusCount, ModeCount, SharedCount } from "./engines/chant/types.js";
14
+ import type { Matins, Nocturn } from "./engines/chant/matutinum.js";
13
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";
14
16
  import type { Score, ScoreOpts, PondusInput, PondusOpts, AccentusInput, AccentusOpts, Cadence, CadenceTarget, CadenceApproach, Modulation, MidiOpts, MidiEmitResult, MidiJsonResult, MidiJsonEvent, MusicXmlOpts, MusicXmlEmitResult } from "./engines/score/api.js";
15
17
  import type { ChantTabulaRow } from "./engines/score/tabula.js";
@@ -25,9 +27,11 @@ declare const tonus: {
25
27
  festum: typeof getFeast;
26
28
  pascha: typeof getPascha;
27
29
  cantus: typeof getChants;
30
+ corpus: typeof getCorpus;
28
31
  proprium: typeof getPropers;
29
32
  ordinarium: typeof getOrdinary;
30
33
  officium: typeof getHour;
34
+ matutinum: typeof getMatins;
31
35
  psalmus: typeof getPsalm;
32
36
  temperamentum: typeof buildTemper;
33
37
  notatio: typeof buildScore;
@@ -36,5 +40,5 @@ declare const tonus: {
36
40
  };
37
41
  export default tonus;
38
42
  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, Rite, 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, RhythmicType, CompoundBeat, VoicedPitch, Cosmos, CosmosQuery, Body, BodyName, Aspect, Imprint, Attractor, VowelAttractor, ModalAffinity, Prosody, RhythmicProfile, NoteRange, CadenceDistribution, Harmony, HarmoniaOpts, VoicedBody, VoicedAspect, Frame, Author, HarmonyTabulaRow, PlanetVowel, };
43
+ export type { Feast, FeastQuery, Pascha, Season, Grade, Chant, CantusQuery, OrdinaryChant, PropriumQuery, OrdinariumQuery, OfficiumQuery, PsalmusQuery, Rite, Corpus, GenusCount, ModeCount, SharedCount, Matins, Nocturn, 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, RhythmicType, CompoundBeat, VoicedPitch, Cosmos, CosmosQuery, Body, BodyName, Aspect, Imprint, Attractor, VowelAttractor, ModalAffinity, Prosody, RhythmicProfile, NoteRange, CadenceDistribution, Harmony, HarmoniaOpts, VoicedBody, VoicedAspect, Frame, Author, HarmonyTabulaRow, PlanetVowel, };
40
44
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { getFeast, getPascha } from "./engines/cal/calendar.js";
2
- import { getChants } from "./engines/chant/chant.js";
2
+ 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";
6
7
  import { getPsalm } from "./engines/chant/psalm.js";
7
8
  import { buildTemper } from "./engines/temper/api.js";
8
9
  import { buildScore } from "./engines/score/api.js";
@@ -12,9 +13,11 @@ const tonus = {
12
13
  festum: getFeast,
13
14
  pascha: getPascha,
14
15
  cantus: getChants,
16
+ corpus: getCorpus,
15
17
  proprium: getPropers,
16
18
  ordinarium: getOrdinary,
17
19
  officium: getHour,
20
+ matutinum: getMatins,
18
21
  psalmus: getPsalm,
19
22
  temperamentum: buildTemper,
20
23
  notatio: buildScore,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tonus",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
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",