@real-music-packages/web-core 0.45.2 → 0.46.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/README.md CHANGED
@@ -78,11 +78,20 @@ npm install @real-music-packages/web-core
78
78
  ## Dev
79
79
  ```
80
80
  npm install
81
- npm test # vitest
81
+ npm test # vitest — all 76 files / 788 tests (~28s)
82
82
  npm run build # tsup → dist/ (ESM + .d.ts)
83
83
  npm run build:watch # tsup --watch → rebuild dist/ on every edit (fast dev loop)
84
+
85
+ bash scripts/quick.sh # tests reachable from your diff + tsc (~19s)
86
+ bash scripts/quick.sh origin/main # ...from the whole branch
84
87
  ```
85
88
 
89
+ `quick.sh` runs `vitest --changed`, so only the tests your diff can reach through
90
+ the module graph. `tsc --noEmit` always runs alongside it (~6.5s) — this package is
91
+ a **published dependency**, so a type regression escapes into every app that vendors
92
+ it. If `--changed` matches no test, it prints `TYPES ONLY` rather than a green tick.
93
+ It is not a release gate; publishing still goes through `npm test` + `npm run build`.
94
+
86
95
  **Iterating against a live app without a publish/re-pin?** See
87
96
  [DEVELOPMENT.md](./DEVELOPMENT.md) — `build:watch` + `tools/dev-link.sh <app>`
88
97
  links the local web-core into an app so its vite dev server reflects your edits
package/dist/audio.js CHANGED
@@ -1,10 +1,3 @@
1
- import {
2
- KEYS_PREFER_FLATS
3
- } from "./chunk-BYZBLH25.js";
4
- import {
5
- getMidiNote,
6
- midiToNoteName
7
- } from "./chunk-GORQ5YMR.js";
8
1
  import {
9
2
  SALAMANDER_CDN_BASE,
10
3
  SALAMANDER_URLS_8,
@@ -12,6 +5,13 @@ import {
12
5
  createSalamanderSampler,
13
6
  generateReverb
14
7
  } from "./chunk-74TMWRNK.js";
8
+ import {
9
+ KEYS_PREFER_FLATS
10
+ } from "./chunk-BYZBLH25.js";
11
+ import {
12
+ getMidiNote,
13
+ midiToNoteName
14
+ } from "./chunk-GORQ5YMR.js";
15
15
 
16
16
  // src/engine.ts
17
17
  var browser = typeof window !== "undefined";
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Re-derive every `<beam>` in `xml` from the time signature: beam within a
3
+ * beat, break at rests, never span a beat boundary.
4
+ *
5
+ * Existing beams are DISCARDED rather than merged — a malformed group
6
+ * (unclosed, or an `end` with no `begin`) is precisely the input this exists
7
+ * to repair, so trusting any of it would defeat the purpose.
8
+ *
9
+ * Details that are engraving decisions, not implementation accidents:
10
+ * - Compound meters (6/8, 9/8, 12/8) group in dotted beats — three eighths —
11
+ * because that is what makes them read as compound rather than as fast
12
+ * simple time. 3/8 and 3/4 are simple and group per beat.
13
+ * - Chord members never carry beams; only a chord's first note does.
14
+ * - Grace notes are left untouched. Their beaming belongs to the ornament
15
+ * they decorate, not to the beat grid.
16
+ * - A beat holding a single beamable note gets NO beam — that note takes a
17
+ * flag, which is correct engraving, not a missing feature.
18
+ * - Beams never cross a staff or voice change; each (staff, voice) stream is
19
+ * grouped independently.
20
+ *
21
+ * Malformed input (fails to parse) is returned unchanged, the same defensive
22
+ * stance as `stampNoteIds`/`injectSystemBreaks` in ./notationXml.ts.
23
+ */
24
+ declare function rebeamByMeter(xml: string): string;
25
+
26
+ export { rebeamByMeter };
@@ -0,0 +1,133 @@
1
+ // src/notationBeams.ts
2
+ var BEAM_COUNT_BY_TYPE = {
3
+ eighth: 1,
4
+ "16th": 2,
5
+ "32nd": 3,
6
+ "64th": 4,
7
+ "128th": 5,
8
+ "256th": 6
9
+ };
10
+ function intText(parent, tag) {
11
+ const el = parent.querySelector(`:scope > ${tag}`);
12
+ const n = el ? Number(el.textContent) : NaN;
13
+ return Number.isFinite(n) ? n : null;
14
+ }
15
+ function insertBeam(note, beam) {
16
+ const before = note.querySelector(":scope > notations, :scope > lyric");
17
+ if (before) note.insertBefore(beam, before);
18
+ else note.appendChild(beam);
19
+ }
20
+ function emitBeams(doc, run) {
21
+ const maxLevel = Math.max(...run.map((n) => n.beams));
22
+ for (let level = 1; level <= maxLevel; level++) {
23
+ let i = 0;
24
+ while (i < run.length) {
25
+ if (run[i].beams < level) {
26
+ i++;
27
+ continue;
28
+ }
29
+ let j = i;
30
+ while (j + 1 < run.length && run[j + 1].beams >= level) j++;
31
+ if (j > i) {
32
+ for (let k = i; k <= j; k++) {
33
+ const beam = doc.createElement("beam");
34
+ beam.setAttribute("number", String(level));
35
+ beam.textContent = k === i ? "begin" : k === j ? "end" : "continue";
36
+ insertBeam(run[k].el, beam);
37
+ }
38
+ } else if (level > 1) {
39
+ const beam = doc.createElement("beam");
40
+ beam.setAttribute("number", String(level));
41
+ beam.textContent = i === 0 ? "forward hook" : "backward hook";
42
+ insertBeam(run[i].el, beam);
43
+ }
44
+ i = j + 1;
45
+ }
46
+ }
47
+ }
48
+ function rebeamByMeter(xml) {
49
+ const doc = new DOMParser().parseFromString(xml, "application/xml");
50
+ if (doc.querySelector("parsererror")) return xml;
51
+ for (const part of Array.from(doc.querySelectorAll("score-partwise > part"))) {
52
+ let divisions = 1;
53
+ let beats = 4;
54
+ let beatType = 4;
55
+ for (const measure of Array.from(part.querySelectorAll("measure"))) {
56
+ const attrs = measure.querySelector(":scope > attributes");
57
+ if (attrs) {
58
+ divisions = intText(attrs, "divisions") ?? divisions;
59
+ const time = attrs.querySelector(":scope > time");
60
+ if (time) {
61
+ beats = intText(time, "beats") ?? beats;
62
+ beatType = intText(time, "beat-type") ?? beatType;
63
+ }
64
+ }
65
+ const beatDivs = divisions * 4 / beatType;
66
+ const compound = beatType >= 8 && beats % 3 === 0 && beats > 3;
67
+ const groupDivs = compound ? beatDivs * 3 : beatDivs;
68
+ if (!(groupDivs > 0)) continue;
69
+ const streams = /* @__PURE__ */ new Map();
70
+ let pos = 0;
71
+ for (const el of Array.from(measure.children)) {
72
+ if (el.tagName === "backup") {
73
+ pos -= intText(el, "duration") ?? 0;
74
+ continue;
75
+ }
76
+ if (el.tagName === "forward") {
77
+ pos += intText(el, "duration") ?? 0;
78
+ continue;
79
+ }
80
+ if (el.tagName !== "note") continue;
81
+ const isGrace = !!el.querySelector(":scope > grace");
82
+ const isChord = !!el.querySelector(":scope > chord");
83
+ const isRest = !!el.querySelector(":scope > rest");
84
+ const dur = intText(el, "duration") ?? 0;
85
+ const type = el.querySelector(":scope > type")?.textContent?.trim() ?? "";
86
+ if (!isGrace) {
87
+ for (const b of Array.from(el.querySelectorAll(":scope > beam"))) b.remove();
88
+ }
89
+ if (!isGrace && !isChord) {
90
+ const staff = el.querySelector(":scope > staff")?.textContent?.trim() ?? "1";
91
+ const voice = el.querySelector(":scope > voice")?.textContent?.trim() ?? "1";
92
+ const key = `${staff}:${voice}`;
93
+ let stream = streams.get(key);
94
+ if (!stream) {
95
+ stream = [];
96
+ streams.set(key, stream);
97
+ }
98
+ stream.push({
99
+ el,
100
+ start: pos,
101
+ beams: isRest ? 0 : BEAM_COUNT_BY_TYPE[type] ?? 0
102
+ });
103
+ pos += dur;
104
+ }
105
+ }
106
+ for (const notes of streams.values()) {
107
+ let run = [];
108
+ const flush = () => {
109
+ if (run.length >= 2) emitBeams(doc, run);
110
+ run = [];
111
+ };
112
+ for (const note of notes) {
113
+ if (note.beams === 0) {
114
+ flush();
115
+ continue;
116
+ }
117
+ if (run.length) {
118
+ const prev = run[run.length - 1];
119
+ const sameBeat = Math.floor(prev.start / groupDivs) === Math.floor(note.start / groupDivs);
120
+ if (!sameBeat) flush();
121
+ }
122
+ run.push(note);
123
+ }
124
+ flush();
125
+ }
126
+ }
127
+ }
128
+ return new XMLSerializer().serializeToString(doc);
129
+ }
130
+ export {
131
+ rebeamByMeter
132
+ };
133
+ //# sourceMappingURL=notationBeams.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/notationBeams.ts"],"sourcesContent":["// Beam grouping — the part of engraving the ENGRAVER cannot supply.\n//\n// Verovio does NOT auto-beam imported MusicXML. Strip a score's `<beam>`\n// elements and every eighth/sixteenth comes out with an individual flag\n// (measured on the RSR corpus: 566 beams removed -> 0 beams, 22 flags). Beam\n// grouping is therefore entirely whatever the source file encodes, and a\n// generator that emits beams spanning beat boundaries — or unbalanced groups\n// (`begin` with no `end`) — produces engraving that nothing downstream can\n// improve by rendering harder.\n//\n// This lives in web-core rather than in a consumer app on purpose: beaming is\n// a fact about how notation is DISPLAYED, which this package owns. An app\n// re-deriving it would be inferring something the notation layer holds\n// authoritatively — the exact move the owned-layer rule exists to prevent.\n//\n// Sibling of ./notationXml.ts (stampNoteIds / injectSystemBreaks) and follows\n// its conventions: pure XML-in → XML-out, DOMParser/XMLSerializer, and\n// malformed input returned unchanged rather than thrown on.\n\nconst BEAM_COUNT_BY_TYPE: Record<string, number> = {\n eighth: 1, '16th': 2, '32nd': 3, '64th': 4, '128th': 5, '256th': 6,\n};\n\ninterface BeamNote {\n el: Element;\n /** Divisions from the start of the measure. */\n start: number;\n /** Number of beams this note's `<type>` implies; 0 for rests. */\n beams: number;\n}\n\nfunction intText(parent: Element, tag: string): number | null {\n const el = parent.querySelector(`:scope > ${tag}`);\n const n = el ? Number(el.textContent) : NaN;\n return Number.isFinite(n) ? n : null;\n}\n\n/** Where a `<beam>` may legally sit inside `<note>`: after `<stem>`/`<staff>`,\n * before `<notations>`/`<lyric>`. Verovio is lenient about this, but emitting\n * schema-ordered output keeps the result usable by anything else that reads\n * MusicXML. */\nfunction insertBeam(note: Element, beam: Element): void {\n const before = note.querySelector(':scope > notations, :scope > lyric');\n if (before) note.insertBefore(beam, before);\n else note.appendChild(beam);\n}\n\n/** Emit level-1 begin/continue/end across `run`, then per-level maximal runs\n * for levels 2+, with hooks for a shorter note that has no partner. */\nfunction emitBeams(doc: Document, run: BeamNote[]): void {\n const maxLevel = Math.max(...run.map((n) => n.beams));\n for (let level = 1; level <= maxLevel; level++) {\n let i = 0;\n while (i < run.length) {\n if (run[i].beams < level) { i++; continue; }\n let j = i;\n while (j + 1 < run.length && run[j + 1].beams >= level) j++;\n if (j > i) {\n for (let k = i; k <= j; k++) {\n const beam = doc.createElement('beam');\n beam.setAttribute('number', String(level));\n beam.textContent = k === i ? 'begin' : k === j ? 'end' : 'continue';\n insertBeam(run[k].el, beam);\n }\n } else if (level > 1) {\n // A lone 16th among 8ths gets a hook, not a beam it has no partner\n // for. It points backward unless it opens the group.\n const beam = doc.createElement('beam');\n beam.setAttribute('number', String(level));\n beam.textContent = i === 0 ? 'forward hook' : 'backward hook';\n insertBeam(run[i].el, beam);\n }\n i = j + 1;\n }\n }\n}\n\n/**\n * Re-derive every `<beam>` in `xml` from the time signature: beam within a\n * beat, break at rests, never span a beat boundary.\n *\n * Existing beams are DISCARDED rather than merged — a malformed group\n * (unclosed, or an `end` with no `begin`) is precisely the input this exists\n * to repair, so trusting any of it would defeat the purpose.\n *\n * Details that are engraving decisions, not implementation accidents:\n * - Compound meters (6/8, 9/8, 12/8) group in dotted beats — three eighths —\n * because that is what makes them read as compound rather than as fast\n * simple time. 3/8 and 3/4 are simple and group per beat.\n * - Chord members never carry beams; only a chord's first note does.\n * - Grace notes are left untouched. Their beaming belongs to the ornament\n * they decorate, not to the beat grid.\n * - A beat holding a single beamable note gets NO beam — that note takes a\n * flag, which is correct engraving, not a missing feature.\n * - Beams never cross a staff or voice change; each (staff, voice) stream is\n * grouped independently.\n *\n * Malformed input (fails to parse) is returned unchanged, the same defensive\n * stance as `stampNoteIds`/`injectSystemBreaks` in ./notationXml.ts.\n */\nexport function rebeamByMeter(xml: string): string {\n const doc = new DOMParser().parseFromString(xml, 'application/xml');\n if (doc.querySelector('parsererror')) return xml;\n\n for (const part of Array.from(doc.querySelectorAll('score-partwise > part'))) {\n // divisions and the time signature persist across measures until restated.\n let divisions = 1;\n let beats = 4;\n let beatType = 4;\n\n for (const measure of Array.from(part.querySelectorAll('measure'))) {\n const attrs = measure.querySelector(':scope > attributes');\n if (attrs) {\n divisions = intText(attrs, 'divisions') ?? divisions;\n const time = attrs.querySelector(':scope > time');\n if (time) {\n beats = intText(time, 'beats') ?? beats;\n beatType = intText(time, 'beat-type') ?? beatType;\n }\n }\n\n const beatDivs = (divisions * 4) / beatType;\n const compound = beatType >= 8 && beats % 3 === 0 && beats > 3;\n const groupDivs = compound ? beatDivs * 3 : beatDivs;\n if (!(groupDivs > 0)) continue;\n\n // Walk in document order, tracking position through backup/forward, and\n // bucket notes by the (staff, voice) stream they belong to.\n const streams = new Map<string, BeamNote[]>();\n let pos = 0;\n\n for (const el of Array.from(measure.children)) {\n if (el.tagName === 'backup') { pos -= intText(el, 'duration') ?? 0; continue; }\n if (el.tagName === 'forward') { pos += intText(el, 'duration') ?? 0; continue; }\n if (el.tagName !== 'note') continue;\n\n const isGrace = !!el.querySelector(':scope > grace');\n const isChord = !!el.querySelector(':scope > chord');\n const isRest = !!el.querySelector(':scope > rest');\n const dur = intText(el, 'duration') ?? 0;\n const type = el.querySelector(':scope > type')?.textContent?.trim() ?? '';\n\n // Wipe first: every beam in the output is re-derived below. Grace\n // notes keep whatever they came with.\n if (!isGrace) {\n for (const b of Array.from(el.querySelectorAll(':scope > beam'))) b.remove();\n }\n\n if (!isGrace && !isChord) {\n const staff = el.querySelector(':scope > staff')?.textContent?.trim() ?? '1';\n const voice = el.querySelector(':scope > voice')?.textContent?.trim() ?? '1';\n const key = `${staff}:${voice}`;\n let stream = streams.get(key);\n if (!stream) { stream = []; streams.set(key, stream); }\n stream.push({\n el, start: pos,\n beams: isRest ? 0 : (BEAM_COUNT_BY_TYPE[type] ?? 0),\n });\n // Grace notes steal no time; chord members share the previous onset.\n pos += dur;\n }\n }\n\n for (const notes of streams.values()) {\n let run: BeamNote[] = [];\n const flush = (): void => {\n if (run.length >= 2) emitBeams(doc, run);\n run = [];\n };\n for (const note of notes) {\n if (note.beams === 0) { flush(); continue; }\n if (run.length) {\n const prev = run[run.length - 1];\n const sameBeat = Math.floor(prev.start / groupDivs) === Math.floor(note.start / groupDivs);\n if (!sameBeat) flush();\n }\n run.push(note);\n }\n flush();\n }\n }\n }\n\n return new XMLSerializer().serializeToString(doc);\n}\n"],"mappings":"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","names":[]}