@real-music-packages/web-core 0.45.2 → 0.47.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 +10 -1
- package/dist/audio.js +7 -7
- package/dist/chunk-JCDNEJBT.js +176 -0
- package/dist/chunk-JCDNEJBT.js.map +1 -0
- package/dist/notationBeams.d.ts +26 -0
- package/dist/notationBeams.js +133 -0
- package/dist/notationBeams.js.map +1 -0
- package/dist/notationPlayerVerovio.d.ts +26 -57
- package/dist/notationPlayerVerovio.js +27 -136
- package/dist/notationPlayerVerovio.js.map +1 -1
- package/dist/notationXml.d.ts +175 -0
- package/dist/notationXml.js +19 -0
- package/dist/notationXml.js.map +1 -0
- package/dist/scene/index.d.ts +410 -2
- package/dist/scene/index.js +725 -21
- package/dist/scene/index.js.map +1 -1
- package/package.json +9 -1
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,176 @@
|
|
|
1
|
+
// src/notationXml.ts
|
|
2
|
+
function stampNoteIds(xml) {
|
|
3
|
+
const doc = new DOMParser().parseFromString(xml, "application/xml");
|
|
4
|
+
if (doc.querySelector("parsererror")) return xml;
|
|
5
|
+
const parts = Array.from(doc.querySelectorAll("score-partwise > part"));
|
|
6
|
+
parts.forEach((part, partIdx) => {
|
|
7
|
+
for (const measure of Array.from(part.querySelectorAll("measure"))) {
|
|
8
|
+
const number = measure.getAttribute("number") ?? "0";
|
|
9
|
+
const notes = Array.from(measure.children).filter((el) => el.tagName === "note");
|
|
10
|
+
notes.forEach((note, noteIdx) => {
|
|
11
|
+
note.setAttribute("id", `n-${partIdx}-${number}-${noteIdx}`);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
return new XMLSerializer().serializeToString(doc);
|
|
16
|
+
}
|
|
17
|
+
function measureCount(xml) {
|
|
18
|
+
const doc = new DOMParser().parseFromString(xml, "application/xml");
|
|
19
|
+
if (doc.querySelector("parsererror")) return 0;
|
|
20
|
+
const root = doc.documentElement;
|
|
21
|
+
if (!root || root.tagName !== "score-partwise") return 0;
|
|
22
|
+
const [firstPart] = childrenNamed(root, "part");
|
|
23
|
+
return firstPart ? childrenNamed(firstPart, "measure").length : 0;
|
|
24
|
+
}
|
|
25
|
+
function injectSystemBreaks(xml, breakBeforeBarIndexes) {
|
|
26
|
+
const doc = new DOMParser().parseFromString(xml, "application/xml");
|
|
27
|
+
if (doc.querySelector("parsererror")) return xml;
|
|
28
|
+
const root = doc.documentElement;
|
|
29
|
+
if (!root || root.tagName !== "score-partwise") return xml;
|
|
30
|
+
for (const part of childrenNamed(root, "part")) {
|
|
31
|
+
const measures = childrenNamed(part, "measure");
|
|
32
|
+
for (const pos of breakBeforeBarIndexes) {
|
|
33
|
+
if (!Number.isInteger(pos) || pos <= 0 || pos >= measures.length) continue;
|
|
34
|
+
const measure = measures[pos];
|
|
35
|
+
const existingPrint = firstChildNamed(measure, "print");
|
|
36
|
+
if (existingPrint) {
|
|
37
|
+
existingPrint.setAttribute("new-system", "yes");
|
|
38
|
+
} else {
|
|
39
|
+
const printEl = doc.createElement("print");
|
|
40
|
+
printEl.setAttribute("new-system", "yes");
|
|
41
|
+
measure.insertBefore(printEl, measure.firstChild);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return new XMLSerializer().serializeToString(doc);
|
|
46
|
+
}
|
|
47
|
+
function balancedSystemBreaks(totalMeasures, systems) {
|
|
48
|
+
if (systems <= 1 || totalMeasures <= 0) return [];
|
|
49
|
+
const per = Math.ceil(totalMeasures / systems);
|
|
50
|
+
if (per <= 0) return [];
|
|
51
|
+
const breaks = [];
|
|
52
|
+
for (let b = per; b < totalMeasures; b += per) breaks.push(b);
|
|
53
|
+
return breaks;
|
|
54
|
+
}
|
|
55
|
+
function barsPerLineBreaks(totalBars, every) {
|
|
56
|
+
if (!Number.isFinite(totalBars) || !Number.isFinite(every)) return [];
|
|
57
|
+
if (every <= 0 || totalBars <= 0) return [];
|
|
58
|
+
let lines = Math.ceil(totalBars / every);
|
|
59
|
+
let per = Math.ceil(totalBars / lines);
|
|
60
|
+
while (lines > 1 && totalBars - (lines - 1) * per < 2) {
|
|
61
|
+
lines--;
|
|
62
|
+
per = Math.ceil(totalBars / lines);
|
|
63
|
+
}
|
|
64
|
+
return balancedSystemBreaks(totalBars, lines);
|
|
65
|
+
}
|
|
66
|
+
function sectionAwareBreaks(totalBars, sectionStarts, every) {
|
|
67
|
+
if (!Number.isFinite(totalBars) || totalBars <= 0) return [];
|
|
68
|
+
const usable = sectionStarts.filter((b) => Number.isInteger(b) && b > 0 && b < totalBars);
|
|
69
|
+
const boundaries = Array.from(/* @__PURE__ */ new Set([0, ...usable])).sort((a, b) => a - b);
|
|
70
|
+
const result = /* @__PURE__ */ new Set();
|
|
71
|
+
if (every > 0) {
|
|
72
|
+
for (let i = 0; i < boundaries.length; i++) {
|
|
73
|
+
const start = boundaries[i];
|
|
74
|
+
const end = i + 1 < boundaries.length ? boundaries[i + 1] : totalBars;
|
|
75
|
+
for (const pos of barsPerLineBreaks(end - start, every)) result.add(start + pos);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
for (const s of usable) result.add(s);
|
|
79
|
+
return Array.from(result).sort((a, b) => a - b);
|
|
80
|
+
}
|
|
81
|
+
var STEP_SEMITONE = { C: 0, D: 2, E: 4, F: 5, G: 7, A: 9, B: 11 };
|
|
82
|
+
function firstChildNamed(el, tag) {
|
|
83
|
+
for (const c of Array.from(el.children)) if (c.tagName === tag) return c;
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
function childrenNamed(el, tag) {
|
|
87
|
+
return Array.from(el.children).filter((c) => c.tagName === tag);
|
|
88
|
+
}
|
|
89
|
+
function textOf(el) {
|
|
90
|
+
return el && el.textContent != null ? el.textContent.trim() : null;
|
|
91
|
+
}
|
|
92
|
+
function pitchMidiFromElement(pitchEl) {
|
|
93
|
+
const step = textOf(firstChildNamed(pitchEl, "step")) ?? "C";
|
|
94
|
+
const octave = Number(textOf(firstChildNamed(pitchEl, "octave")) ?? "4");
|
|
95
|
+
const alter = Number(textOf(firstChildNamed(pitchEl, "alter")) ?? "0");
|
|
96
|
+
return 12 * (octave + 1) + (STEP_SEMITONE[step] ?? 0) + Math.trunc(alter);
|
|
97
|
+
}
|
|
98
|
+
function partStaffCount(part) {
|
|
99
|
+
let maxDeclared = 0;
|
|
100
|
+
for (const attrs of childrenNamed(part, "measure").flatMap((m) => childrenNamed(m, "attributes"))) {
|
|
101
|
+
const staves = Number(textOf(firstChildNamed(attrs, "staves")) ?? "0");
|
|
102
|
+
if (staves > maxDeclared) maxDeclared = staves;
|
|
103
|
+
}
|
|
104
|
+
if (maxDeclared > 0) return maxDeclared;
|
|
105
|
+
let maxStaff = 0;
|
|
106
|
+
for (const measure of childrenNamed(part, "measure")) {
|
|
107
|
+
for (const note of childrenNamed(measure, "note")) {
|
|
108
|
+
const staff = Number(textOf(firstChildNamed(note, "staff")) ?? "0");
|
|
109
|
+
if (staff > maxStaff) maxStaff = staff;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return Math.max(1, maxStaff);
|
|
113
|
+
}
|
|
114
|
+
function noteModelFromXml(stampedXml) {
|
|
115
|
+
const model = /* @__PURE__ */ new Map();
|
|
116
|
+
let doc;
|
|
117
|
+
try {
|
|
118
|
+
doc = new DOMParser().parseFromString(stampedXml, "application/xml");
|
|
119
|
+
} catch {
|
|
120
|
+
return model;
|
|
121
|
+
}
|
|
122
|
+
if (doc.querySelector("parsererror")) return model;
|
|
123
|
+
const root = doc.documentElement;
|
|
124
|
+
if (!root || root.tagName !== "score-partwise") return model;
|
|
125
|
+
const parts = childrenNamed(root, "part");
|
|
126
|
+
let staffOffset = 0;
|
|
127
|
+
for (const part of parts) {
|
|
128
|
+
const staffCount = partStaffCount(part);
|
|
129
|
+
let divisions = 1;
|
|
130
|
+
const measureEls = childrenNamed(part, "measure");
|
|
131
|
+
measureEls.forEach((measureEl, measureIndex) => {
|
|
132
|
+
for (const child of Array.from(measureEl.children)) {
|
|
133
|
+
if (child.tagName === "attributes") {
|
|
134
|
+
const divText = textOf(firstChildNamed(child, "divisions"));
|
|
135
|
+
if (divText) divisions = Number(divText) || divisions;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (child.tagName !== "note") continue;
|
|
139
|
+
const note = child;
|
|
140
|
+
const id = note.getAttribute("id");
|
|
141
|
+
if (!id) continue;
|
|
142
|
+
const isGrace = !!firstChildNamed(note, "grace");
|
|
143
|
+
const isRest = !!firstChildNamed(note, "rest");
|
|
144
|
+
const pitchEl = firstChildNamed(note, "pitch");
|
|
145
|
+
const staffNumber = Number(textOf(firstChildNamed(note, "staff")) ?? "1") || 1;
|
|
146
|
+
const durationText = textOf(firstChildNamed(note, "duration"));
|
|
147
|
+
const durationReal = isGrace || !durationText ? 0 : Number(durationText) / divisions / 4;
|
|
148
|
+
const tieStopDirect = childrenNamed(note, "tie").some((t) => t.getAttribute("type") === "stop");
|
|
149
|
+
const notationsEl = firstChildNamed(note, "notations");
|
|
150
|
+
const tieStopNotated = notationsEl ? childrenNamed(notationsEl, "tied").some((t) => t.getAttribute("type") === "stop") : false;
|
|
151
|
+
model.set(id, {
|
|
152
|
+
midi: pitchEl ? pitchMidiFromElement(pitchEl) : null,
|
|
153
|
+
isRest,
|
|
154
|
+
tieContinuation: tieStopDirect || tieStopNotated,
|
|
155
|
+
staffIndex: staffOffset + Math.max(0, staffNumber - 1),
|
|
156
|
+
durationReal,
|
|
157
|
+
measureIndex,
|
|
158
|
+
hidden: note.getAttribute("print-object") === "no"
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
staffOffset += staffCount;
|
|
163
|
+
}
|
|
164
|
+
return model;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export {
|
|
168
|
+
stampNoteIds,
|
|
169
|
+
measureCount,
|
|
170
|
+
injectSystemBreaks,
|
|
171
|
+
balancedSystemBreaks,
|
|
172
|
+
barsPerLineBreaks,
|
|
173
|
+
sectionAwareBreaks,
|
|
174
|
+
noteModelFromXml
|
|
175
|
+
};
|
|
176
|
+
//# sourceMappingURL=chunk-JCDNEJBT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/notationXml.ts"],"sourcesContent":["// Pure MusicXML→MusicXML/model helpers for `createVerovioNotationPlayer`\n// (notationPlayerVerovio.ts, 0.40.0) — the id-stamping + note-model half of\n// its \"join model ids to Verovio's rendered SVG\" approach (that module's\n// header doc, point 1). No DOM rendering here; everything in this file is\n// pure XML-in → XML/data-out, unit-testable with no live SVG at all.\n//\n// OWNERSHIP NOTE — `stampNoteIds`'s deterministic id scheme\n// (`n-{partIdx}-{measureNumber}-{noteIdxInMeasure}`) is DELIBERATELY\n// duplicated, byte-for-byte, from stave-web-sightread's\n// `src/lib/bach/xmlTransforms.ts:stampNoteIds` (part of that repo's Verovio\n// transform pipeline, docs/superpowers/specs/2026-08-11-verovio-player-design.md\n// §1). stave's copy is the one actually wired into `parseReduction` (its\n// `noteIds` per span must match what got stamped onto the MusicXML BEFORE\n// Verovio ever saw it), so stave remains that scheme's owner for the\n// pipeline's purposes. This copy exists so `createVerovioNotationPlayer`\n// works correctly for ANY caller — including ones that hand in un-stamped\n// MusicXML — without a runtime dependency from web-core back into an app\n// repo (the wrong direction for a shared package). Both copies are pure\n// functions of a note's POSITION (never of any id already present), so\n// calling either one on already-stamped input reproduces the exact same\n// ids — the two copies can never drift apart in OBSERVABLE behavior even\n// though they are physically two files. If the scheme ever needs to change,\n// change it in BOTH places (this file's own tests pin the scheme\n// independently of stave's, so a one-sided edit fails a test here).\nexport function stampNoteIds(xml: string): string {\n const doc = new DOMParser().parseFromString(xml, 'application/xml');\n if (doc.querySelector('parsererror')) return xml;\n\n const parts = Array.from(doc.querySelectorAll('score-partwise > part'));\n parts.forEach((part, partIdx) => {\n for (const measure of Array.from(part.querySelectorAll('measure'))) {\n const number = measure.getAttribute('number') ?? '0';\n const notes = Array.from(measure.children).filter((el) => el.tagName === 'note');\n notes.forEach((note, noteIdx) => {\n note.setAttribute('id', `n-${partIdx}-${number}-${noteIdx}`);\n });\n }\n });\n\n return new XMLSerializer().serializeToString(doc);\n}\n\n// ─── injectSystemBreaks / balancedSystemBreaks — widow-avoidance helpers ──\n//\n// Used by notationPlayerVerovio.ts's widow pass (`engraveOnce`): when\n// `breaks: 'auto'` leaves a lone trailing measure on its own system (a\n// \"widow\" — Verovio's own `breaksNoWidow` only guards the last PAGE, not the\n// last SYSTEM, per that module's doc), the player forces exact line breaks\n// via `injectSystemBreaks` + `breaks: 'line'`, choosing the break points with\n// `balancedSystemBreaks` so the same number of systems comes out evenly\n// filled instead of front-loaded-then-orphaned.\n//\n// CONTRACT DUPLICATION NOTICE — stave-web-sightread's\n// `src/lib/bach/xmlTransforms.ts` has its OWN `injectSystemBreaks`,\n// `barsPerLineBreaks` and `sectionAwareBreaks`, written on the reasoning\n// that \"a shared package must not runtime-depend on an app repo for\n// something this small\". That reasoning is backwards for break POLICY: the\n// engraver is what owns where systems begin, so those copies are being\n// RETIRED in favour of the ones here, reached through\n// `VerovioLayoutOptions.barsPerLine` / `.sectionStarts` rather than by the\n// app hand-injecting breaks of its own.\n//\n// Until that app-side change lands, both copies exist and must stay in sync.\n// (The app's `injectSystemBreaks` uses `querySelectorAll('measure')` where\n// this one walks direct children only — this one is the stricter of the two\n// on scores with nested measure-like elements.)\n\n/**\n * Insert `<print new-system=\"yes\"/>` as the FIRST child of the `<measure>`\n * at each 0-based position in `breakBeforeBarIndexes`, in EVERY `<part>`\n * (a score's parts share one measure timeline, so a break must be encoded\n * once per part for Verovio to line the systems up across staves). A\n * position that is ≤ 0 or ≥ that part's own measure count is silently\n * ignored FOR THAT PART (0 is a no-op — a part already starts a new system\n * at its own first measure). Idempotent: a measure that already carries a\n * `<print>` element (from a prior call, or from the source data) gets\n * `new-system=\"yes\"` SET on that existing element rather than gaining a\n * second one — calling this twice with the same positions serializes\n * identically both times. Malformed input (fails to parse) is returned\n * unchanged, same defensive style as `stampNoteIds` above. Only ever ADDS\n * `<print>` elements — never touches a `<note>` — so `stampNoteIds`'s\n * position-based id scheme is unaffected by a prior or subsequent call to\n * this function (see this file's own tests for the pinned invariant).\n */\n/**\n * How many `<measure>` elements the FIRST `<part>` has — the `totalBars`\n * that `barsPerLineBreaks`/`sectionAwareBreaks` plan against.\n *\n * The first part specifically, because that is the coordinate system\n * `injectSystemBreaks` positions live in: a score's parts share one measure\n * timeline, so the first part's ordinals index every part. And ORDINALS, not\n * `<measure number>` attributes — scores skip and repeat bar numbers, so a\n * count is the only safe answer. Malformed input, or a document with no\n * part, returns 0 rather than throwing.\n */\nexport function measureCount(xml: string): number {\n const doc = new DOMParser().parseFromString(xml, 'application/xml');\n if (doc.querySelector('parsererror')) return 0;\n const root = doc.documentElement;\n if (!root || root.tagName !== 'score-partwise') return 0;\n const [firstPart] = childrenNamed(root, 'part');\n return firstPart ? childrenNamed(firstPart, 'measure').length : 0;\n}\n\nexport function injectSystemBreaks(xml: string, breakBeforeBarIndexes: readonly number[]): string {\n const doc = new DOMParser().parseFromString(xml, 'application/xml');\n if (doc.querySelector('parsererror')) return xml;\n\n const root = doc.documentElement;\n if (!root || root.tagName !== 'score-partwise') return xml;\n\n for (const part of childrenNamed(root, 'part')) {\n const measures = childrenNamed(part, 'measure');\n for (const pos of breakBeforeBarIndexes) {\n if (!Number.isInteger(pos) || pos <= 0 || pos >= measures.length) continue;\n const measure = measures[pos];\n const existingPrint = firstChildNamed(measure, 'print');\n if (existingPrint) {\n existingPrint.setAttribute('new-system', 'yes');\n } else {\n const printEl = doc.createElement('print');\n printEl.setAttribute('new-system', 'yes');\n measure.insertBefore(printEl, measure.firstChild);\n }\n }\n }\n\n return new XMLSerializer().serializeToString(doc);\n}\n\n/**\n * Evenly-spread system-break positions for `totalMeasures` measures across\n * `systems` systems: `[per, 2·per, …]` (each strictly `< totalMeasures`),\n * with `per = Math.ceil(totalMeasures / systems)` — `Math.ceil` naturally\n * front-loads any remainder into the EARLIER systems (e.g. 7 measures / 2\n * systems -> per=4 -> systems of 4+3, never 3+4), which is what keeps a\n * later system from being the short/orphaned one. `systems <= 1` (nothing to\n * break between) returns `[]`.\n */\nexport function balancedSystemBreaks(totalMeasures: number, systems: number): number[] {\n if (systems <= 1 || totalMeasures <= 0) return [];\n const per = Math.ceil(totalMeasures / systems);\n if (per <= 0) return [];\n const breaks: number[] = [];\n for (let b = per; b < totalMeasures; b += per) breaks.push(b);\n return breaks;\n}\n\n/**\n * Balanced system-break positions for a target BARS-PER-LINE layout — the\n * caller-facing sibling of `balancedSystemBreaks`, which takes a system\n * COUNT. Rather than stepping `every` bars at a time from bar 0 (which\n * strands a short remainder — a 5-bar excerpt at `every: 4` renders 4+1, a\n * widow of our own making), this decides how many LINES the excerpt needs\n * (`ceil(totalBars / every)`) and hands that count to\n * `balancedSystemBreaks`. `(5, 4)` is `[3]` (3+2) instead of `[4]` (4+1);\n * `(9, 4)` is `[3, 6]` (3+3+3) instead of `[4, 8]` (4+4+1).\n *\n * WIDOW BACK-OFF: a small `every` against a small `totalBars` can make the\n * straight `ceil(totalBars / every)` line count land on a trailing line of\n * exactly 1 bar — the same widow this function exists to remove, reappearing\n * at a different total/every combination (`lines = 3` on 7 bars: `per = 3`\n * fills two 3-bar lines and leaves 1 bar for the third). The line count is\n * therefore backed down (never below 1) until the trailing line holds at\n * least 2 bars — 7 bars lands on 2 lines (4+3) instead of 3 (3+3+1), which\n * is the same \"fewer, fuller lines\" direction `every` already asks for.\n * This never changes the result for any total/every whose straight `ceil`\n * count already avoids a 1-bar trailing line.\n *\n * `every <= 0` (the \"auto — no forced breaks\" sentinel callers resolve to)\n * or a non-positive/non-finite `totalBars` returns `[]`. Never throws.\n *\n * Ported from stave-web-sightread (`src/lib/bach/xmlTransforms.ts`), whose\n * copy this replaces — see the CONTRACT DUPLICATION NOTICE above. Reached by\n * consumers through `VerovioLayoutOptions.barsPerLine` rather than called\n * directly, so the player owns both the plan and its interaction with the\n * widow pass and the readability floor.\n */\nexport function barsPerLineBreaks(totalBars: number, every: number): number[] {\n if (!Number.isFinite(totalBars) || !Number.isFinite(every)) return [];\n if (every <= 0 || totalBars <= 0) return [];\n let lines = Math.ceil(totalBars / every);\n let per = Math.ceil(totalBars / lines);\n while (lines > 1 && totalBars - (lines - 1) * per < 2) {\n lines--;\n per = Math.ceil(totalBars / lines);\n }\n return balancedSystemBreaks(totalBars, lines);\n}\n\n/**\n * Like `barsPerLineBreaks`, but each SECTION's span is balanced on its own\n * instead of one balance running across the whole excerpt — so a line never\n * ends part-way into a neighbouring section's opening bars, and a section's\n * own bars are never split into a widowed last line either.\n *\n * `sectionStarts` are 0-BASED POSITIONS into the excerpt (the same\n * coordinate `injectSystemBreaks` consumes), NOT MusicXML `<measure\n * number>` attributes — scores skip bar numbers, so a caller holding\n * bar-numbered section labels must convert first. `0` (the excerpt's own\n * start) is never a break and is ignored if present.\n *\n * Every section start > 0 is a HARD break in its own right — a new section\n * always begins its own line, not subject to balancing — unioned with\n * `barsPerLineBreaks`' balanced positions computed WITHIN each section's\n * span (its start up to the next section's start, or `totalBars` for the\n * last) and offset back into excerpt-absolute positions.\n *\n * `every <= 0` suppresses the balanced component, matching\n * `barsPerLineBreaks`, but section-start breaks still apply — that is the\n * \"sections only, otherwise let the engraver decide\" mode. Out-of-range and\n * non-integer starts are ignored. The result is sorted, de-duplicated, and\n * every entry is a valid `injectSystemBreaks` position (integer, `> 0`,\n * `< totalBars`). Never throws.\n */\nexport function sectionAwareBreaks(\n totalBars: number,\n sectionStarts: readonly number[],\n every: number,\n): number[] {\n if (!Number.isFinite(totalBars) || totalBars <= 0) return [];\n const usable = sectionStarts.filter((b) => Number.isInteger(b) && b > 0 && b < totalBars);\n const boundaries = Array.from(new Set([0, ...usable])).sort((a, b) => a - b);\n\n const result = new Set<number>();\n\n if (every > 0) {\n for (let i = 0; i < boundaries.length; i++) {\n const start = boundaries[i];\n const end = i + 1 < boundaries.length ? boundaries[i + 1] : totalBars;\n for (const pos of barsPerLineBreaks(end - start, every)) result.add(start + pos);\n }\n }\n\n for (const s of usable) result.add(s);\n\n return Array.from(result).sort((a, b) => a - b);\n}\n\n// ─── noteModelFromXml — the note MODEL half of the join ───────────────────\n\n/** One `<note>`'s MODEL identity — the ground truth `EngravedNote`\n * (notationPlayerSvg.ts) needs that Verovio's rendered SVG cannot supply on\n * its own (a rendered `g.note`/`g.rest` carries geometry, not pitch/tie/\n * duration semantics). Keyed by the note's stamped `id` in\n * `notePositions()`'s return of `verovioEngravedNotes`\n * (notationPlayerVerovio.ts). */\nexport interface NoteModel {\n /** `12*(octave+1) + stepSemitone + alter` — the standard MusicXML→MIDI\n * conversion (the same formula stave-web-sightread's own\n * `practice/probeInputs.ts:pitchMidi` uses — a universal formula, not app\n * logic, so this is not an owned-layer violation to restate here). `null`\n * for a rest or an `<unpitched>` note (no `<pitch>` child) — mirrors\n * `EngravedNote.midi`'s own \"null covers both\" contract. */\n midi: number | null;\n /** Has a `<rest/>` child. */\n isRest: boolean;\n /** True for the STOP half of a tie — a direct `<tie type=\"stop\">` child OR\n * `<notations><tied type=\"stop\">` (exporters vary on which they emit;\n * either counts) — matches `EngravedNote.tieContinuation`'s \"continuation\n * note of a tie, not the struck start\" contract. */\n tieContinuation: boolean;\n /** 0-based, matching `EngravedNote.staffIndex`'s \"0 = top staff of the\n * system\" contract: every `<part>` is walked in document order, and\n * every DISTINCT staff within it (by `<attributes><staves>` when\n * present, else the highest `<staff>` number any of its notes uses, else\n * 1) is assigned the next index — so a single-part 2-staff piano score\n * numbers 0/1 by `<staff>`, and a 2-part 1-staff-per-part reduction (this\n * app's own chord+bass shape) numbers 0/1 by PART, with neither case\n * needing different code. */\n staffIndex: number;\n /** Whole-note fraction (0.25 = quarter) — `duration / divisions / 4`,\n * divisions tracked per-part from the LAST `<attributes><divisions>`\n * seen at or before this note (MusicXML: divisions persist until\n * overridden, default 1). 0 for a grace note (no `<duration>` child —\n * the true, spec-correct signal; never guessed from `<type>`). */\n durationReal: number;\n /** 0-based position of this note's `<measure>` among its OWN `<part>`'s\n * measure children, in document order. Informational only — the live\n * join (`verovioOnsetColumns`/`verovioEngravedNotes`, both in\n * notationPlayerVerovio.ts) resolves the note's RENDERED measure index\n * from the live SVG DOM independently (Verovio's own render order, which\n * is what geometry/hit-testing must agree with), never from this field. */\n measureIndex: number;\n /** `print-object=\"no\"` on the source `<note>` (stave's `hideDoubledNotes`\n * sets this on editorially-doubled notes/rests before handing MusicXML to\n * this player). Verovio's importer HONORS this for `<note>` elements\n * carrying a `<pitch>` (renders `visibility=\"hidden\"` on its own,\n * empirically confirmed against a real 6.2.0 render) but does NOT honor\n * it for `<rest>` notes (the `<g class=\"rest\">` renders fully visible\n * regardless — same empirical check). `createVerovioNotationPlayer`\n * reads this field to force `visibility=\"hidden\"` after every render for\n * ANY id where it's true — a no-op re-application on notes Verovio\n * already hid, and the actual fix on the rests it doesn't (see that\n * module's `applyPrintObjectHiding`). */\n hidden: boolean;\n}\n\nconst STEP_SEMITONE: Record<string, number> = { C: 0, D: 2, E: 4, F: 5, G: 7, A: 9, B: 11 };\n\nfunction firstChildNamed(el: Element, tag: string): Element | null {\n for (const c of Array.from(el.children)) if (c.tagName === tag) return c;\n return null;\n}\nfunction childrenNamed(el: Element, tag: string): Element[] {\n return Array.from(el.children).filter((c) => c.tagName === tag);\n}\nfunction textOf(el: Element | null): string | null {\n return el && el.textContent != null ? el.textContent.trim() : null;\n}\n\nfunction pitchMidiFromElement(pitchEl: Element): number {\n const step = textOf(firstChildNamed(pitchEl, 'step')) ?? 'C';\n const octave = Number(textOf(firstChildNamed(pitchEl, 'octave')) ?? '4');\n const alter = Number(textOf(firstChildNamed(pitchEl, 'alter')) ?? '0');\n return 12 * (octave + 1) + (STEP_SEMITONE[step] ?? 0) + Math.trunc(alter);\n}\n\n/** How many staves `part` uses: the max `<attributes><staves>N</staves>`\n * seen anywhere in it (the authoritative declaration when present), else\n * the highest `<note><staff>N</staff></note>` number any of its notes\n * uses, else 1 (a plain single-staff part declares neither). */\nfunction partStaffCount(part: Element): number {\n let maxDeclared = 0;\n for (const attrs of childrenNamed(part, 'measure').flatMap((m) => childrenNamed(m, 'attributes'))) {\n const staves = Number(textOf(firstChildNamed(attrs, 'staves')) ?? '0');\n if (staves > maxDeclared) maxDeclared = staves;\n }\n if (maxDeclared > 0) return maxDeclared;\n\n let maxStaff = 0;\n for (const measure of childrenNamed(part, 'measure')) {\n for (const note of childrenNamed(measure, 'note')) {\n const staff = Number(textOf(firstChildNamed(note, 'staff')) ?? '0');\n if (staff > maxStaff) maxStaff = staff;\n }\n }\n return Math.max(1, maxStaff);\n}\n\n/**\n * Pure(ish) — MusicXML-in, `id → NoteModel`-out. Walks every `<part>` in\n * document order, then every `<measure>` in document order, then every\n * DIRECT child in document order — `<attributes>` updates the part's own\n * `divisions` cursor; every other non-`<note>` child (`<backup>`,\n * `<forward>`, `<direction>`, …) is a structural/timeline element this\n * function has NO use for (it reads each note's OWN `<duration>` directly,\n * never a cursor POSITION — see `durationReal`'s doc — so unlike\n * `walkMeasureNotes`-style position walks, `<backup>`/`<forward>` need no\n * special handling here beyond being correctly skipped, which plain\n * tag-name filtering already does) — and every `<note>` becomes one model\n * entry, keyed by its `id` attribute (a note with NO `id` — i.e. input that\n * was never run through `stampNoteIds` — is silently skipped: it has no key\n * to join the render against, so there is nothing useful to record).\n *\n * Never throws: malformed input (fails to parse, or no `<score-partwise>`\n * root) returns an empty Map.\n */\nexport function noteModelFromXml(stampedXml: string): Map<string, NoteModel> {\n const model = new Map<string, NoteModel>();\n let doc: Document;\n try {\n doc = new DOMParser().parseFromString(stampedXml, 'application/xml');\n } catch {\n return model;\n }\n if (doc.querySelector('parsererror')) return model;\n const root = doc.documentElement;\n if (!root || root.tagName !== 'score-partwise') return model;\n\n const parts = childrenNamed(root, 'part');\n let staffOffset = 0;\n\n for (const part of parts) {\n const staffCount = partStaffCount(part);\n let divisions = 1;\n const measureEls = childrenNamed(part, 'measure');\n\n measureEls.forEach((measureEl, measureIndex) => {\n for (const child of Array.from(measureEl.children)) {\n if (child.tagName === 'attributes') {\n const divText = textOf(firstChildNamed(child, 'divisions'));\n if (divText) divisions = Number(divText) || divisions;\n continue;\n }\n if (child.tagName !== 'note') continue;\n const note = child;\n const id = note.getAttribute('id');\n if (!id) continue;\n\n const isGrace = !!firstChildNamed(note, 'grace');\n const isRest = !!firstChildNamed(note, 'rest');\n const pitchEl = firstChildNamed(note, 'pitch');\n const staffNumber = Number(textOf(firstChildNamed(note, 'staff')) ?? '1') || 1;\n const durationText = textOf(firstChildNamed(note, 'duration'));\n const durationReal =\n isGrace || !durationText ? 0 : Number(durationText) / divisions / 4;\n\n const tieStopDirect = childrenNamed(note, 'tie').some((t) => t.getAttribute('type') === 'stop');\n const notationsEl = firstChildNamed(note, 'notations');\n const tieStopNotated = notationsEl\n ? childrenNamed(notationsEl, 'tied').some((t) => t.getAttribute('type') === 'stop')\n : false;\n\n model.set(id, {\n midi: pitchEl ? pitchMidiFromElement(pitchEl) : null,\n isRest,\n tieContinuation: tieStopDirect || tieStopNotated,\n staffIndex: staffOffset + Math.max(0, staffNumber - 1),\n durationReal,\n measureIndex,\n hidden: note.getAttribute('print-object') === 'no',\n });\n }\n });\n\n staffOffset += staffCount;\n }\n\n return model;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -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":";AAmBA,IAAM,qBAA6C;AAAA,EACjD,QAAQ;AAAA,EAAG,QAAQ;AAAA,EAAG,QAAQ;AAAA,EAAG,QAAQ;AAAA,EAAG,SAAS;AAAA,EAAG,SAAS;AACnE;AAUA,SAAS,QAAQ,QAAiB,KAA4B;AAC5D,QAAM,KAAK,OAAO,cAAc,YAAY,GAAG,EAAE;AACjD,QAAM,IAAI,KAAK,OAAO,GAAG,WAAW,IAAI;AACxC,SAAO,OAAO,SAAS,CAAC,IAAI,IAAI;AAClC;AAMA,SAAS,WAAW,MAAe,MAAqB;AACtD,QAAM,SAAS,KAAK,cAAc,oCAAoC;AACtE,MAAI,OAAQ,MAAK,aAAa,MAAM,MAAM;AAAA,MACrC,MAAK,YAAY,IAAI;AAC5B;AAIA,SAAS,UAAU,KAAe,KAAuB;AACvD,QAAM,WAAW,KAAK,IAAI,GAAG,IAAI,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC;AACpD,WAAS,QAAQ,GAAG,SAAS,UAAU,SAAS;AAC9C,QAAI,IAAI;AACR,WAAO,IAAI,IAAI,QAAQ;AACrB,UAAI,IAAI,CAAC,EAAE,QAAQ,OAAO;AAAE;AAAK;AAAA,MAAU;AAC3C,UAAI,IAAI;AACR,aAAO,IAAI,IAAI,IAAI,UAAU,IAAI,IAAI,CAAC,EAAE,SAAS,MAAO;AACxD,UAAI,IAAI,GAAG;AACT,iBAAS,IAAI,GAAG,KAAK,GAAG,KAAK;AAC3B,gBAAM,OAAO,IAAI,cAAc,MAAM;AACrC,eAAK,aAAa,UAAU,OAAO,KAAK,CAAC;AACzC,eAAK,cAAc,MAAM,IAAI,UAAU,MAAM,IAAI,QAAQ;AACzD,qBAAW,IAAI,CAAC,EAAE,IAAI,IAAI;AAAA,QAC5B;AAAA,MACF,WAAW,QAAQ,GAAG;AAGpB,cAAM,OAAO,IAAI,cAAc,MAAM;AACrC,aAAK,aAAa,UAAU,OAAO,KAAK,CAAC;AACzC,aAAK,cAAc,MAAM,IAAI,iBAAiB;AAC9C,mBAAW,IAAI,CAAC,EAAE,IAAI,IAAI;AAAA,MAC5B;AACA,UAAI,IAAI;AAAA,IACV;AAAA,EACF;AACF;AAyBO,SAAS,cAAc,KAAqB;AACjD,QAAM,MAAM,IAAI,UAAU,EAAE,gBAAgB,KAAK,iBAAiB;AAClE,MAAI,IAAI,cAAc,aAAa,EAAG,QAAO;AAE7C,aAAW,QAAQ,MAAM,KAAK,IAAI,iBAAiB,uBAAuB,CAAC,GAAG;AAE5E,QAAI,YAAY;AAChB,QAAI,QAAQ;AACZ,QAAI,WAAW;AAEf,eAAW,WAAW,MAAM,KAAK,KAAK,iBAAiB,SAAS,CAAC,GAAG;AAClE,YAAM,QAAQ,QAAQ,cAAc,qBAAqB;AACzD,UAAI,OAAO;AACT,oBAAY,QAAQ,OAAO,WAAW,KAAK;AAC3C,cAAM,OAAO,MAAM,cAAc,eAAe;AAChD,YAAI,MAAM;AACR,kBAAQ,QAAQ,MAAM,OAAO,KAAK;AAClC,qBAAW,QAAQ,MAAM,WAAW,KAAK;AAAA,QAC3C;AAAA,MACF;AAEA,YAAM,WAAY,YAAY,IAAK;AACnC,YAAM,WAAW,YAAY,KAAK,QAAQ,MAAM,KAAK,QAAQ;AAC7D,YAAM,YAAY,WAAW,WAAW,IAAI;AAC5C,UAAI,EAAE,YAAY,GAAI;AAItB,YAAM,UAAU,oBAAI,IAAwB;AAC5C,UAAI,MAAM;AAEV,iBAAW,MAAM,MAAM,KAAK,QAAQ,QAAQ,GAAG;AAC7C,YAAI,GAAG,YAAY,UAAU;AAAE,iBAAO,QAAQ,IAAI,UAAU,KAAK;AAAG;AAAA,QAAU;AAC9E,YAAI,GAAG,YAAY,WAAW;AAAE,iBAAO,QAAQ,IAAI,UAAU,KAAK;AAAG;AAAA,QAAU;AAC/E,YAAI,GAAG,YAAY,OAAQ;AAE3B,cAAM,UAAU,CAAC,CAAC,GAAG,cAAc,gBAAgB;AACnD,cAAM,UAAU,CAAC,CAAC,GAAG,cAAc,gBAAgB;AACnD,cAAM,SAAS,CAAC,CAAC,GAAG,cAAc,eAAe;AACjD,cAAM,MAAM,QAAQ,IAAI,UAAU,KAAK;AACvC,cAAM,OAAO,GAAG,cAAc,eAAe,GAAG,aAAa,KAAK,KAAK;AAIvE,YAAI,CAAC,SAAS;AACZ,qBAAW,KAAK,MAAM,KAAK,GAAG,iBAAiB,eAAe,CAAC,EAAG,GAAE,OAAO;AAAA,QAC7E;AAEA,YAAI,CAAC,WAAW,CAAC,SAAS;AACxB,gBAAM,QAAQ,GAAG,cAAc,gBAAgB,GAAG,aAAa,KAAK,KAAK;AACzE,gBAAM,QAAQ,GAAG,cAAc,gBAAgB,GAAG,aAAa,KAAK,KAAK;AACzE,gBAAM,MAAM,GAAG,KAAK,IAAI,KAAK;AAC7B,cAAI,SAAS,QAAQ,IAAI,GAAG;AAC5B,cAAI,CAAC,QAAQ;AAAE,qBAAS,CAAC;AAAG,oBAAQ,IAAI,KAAK,MAAM;AAAA,UAAG;AACtD,iBAAO,KAAK;AAAA,YACV;AAAA,YAAI,OAAO;AAAA,YACX,OAAO,SAAS,IAAK,mBAAmB,IAAI,KAAK;AAAA,UACnD,CAAC;AAED,iBAAO;AAAA,QACT;AAAA,MACF;AAEA,iBAAW,SAAS,QAAQ,OAAO,GAAG;AACpC,YAAI,MAAkB,CAAC;AACvB,cAAM,QAAQ,MAAY;AACxB,cAAI,IAAI,UAAU,EAAG,WAAU,KAAK,GAAG;AACvC,gBAAM,CAAC;AAAA,QACT;AACA,mBAAW,QAAQ,OAAO;AACxB,cAAI,KAAK,UAAU,GAAG;AAAE,kBAAM;AAAG;AAAA,UAAU;AAC3C,cAAI,IAAI,QAAQ;AACd,kBAAM,OAAO,IAAI,IAAI,SAAS,CAAC;AAC/B,kBAAM,WAAW,KAAK,MAAM,KAAK,QAAQ,SAAS,MAAM,KAAK,MAAM,KAAK,QAAQ,SAAS;AACzF,gBAAI,CAAC,SAAU,OAAM;AAAA,UACvB;AACA,cAAI,KAAK,IAAI;AAAA,QACf;AACA,cAAM;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,SAAO,IAAI,cAAc,EAAE,kBAAkB,GAAG;AAClD;","names":[]}
|
|
@@ -1,64 +1,8 @@
|
|
|
1
1
|
import { N as NotationLayout } from './notationGeometry-DqVBgL7F.js';
|
|
2
2
|
import { EngravedNote } from './notationPlayerSvg.js';
|
|
3
|
+
import { NoteModel } from './notationXml.js';
|
|
3
4
|
import './promo.js';
|
|
4
5
|
|
|
5
|
-
/** One `<note>`'s MODEL identity — the ground truth `EngravedNote`
|
|
6
|
-
* (notationPlayerSvg.ts) needs that Verovio's rendered SVG cannot supply on
|
|
7
|
-
* its own (a rendered `g.note`/`g.rest` carries geometry, not pitch/tie/
|
|
8
|
-
* duration semantics). Keyed by the note's stamped `id` in
|
|
9
|
-
* `notePositions()`'s return of `verovioEngravedNotes`
|
|
10
|
-
* (notationPlayerVerovio.ts). */
|
|
11
|
-
interface NoteModel {
|
|
12
|
-
/** `12*(octave+1) + stepSemitone + alter` — the standard MusicXML→MIDI
|
|
13
|
-
* conversion (the same formula stave-web-sightread's own
|
|
14
|
-
* `practice/probeInputs.ts:pitchMidi` uses — a universal formula, not app
|
|
15
|
-
* logic, so this is not an owned-layer violation to restate here). `null`
|
|
16
|
-
* for a rest or an `<unpitched>` note (no `<pitch>` child) — mirrors
|
|
17
|
-
* `EngravedNote.midi`'s own "null covers both" contract. */
|
|
18
|
-
midi: number | null;
|
|
19
|
-
/** Has a `<rest/>` child. */
|
|
20
|
-
isRest: boolean;
|
|
21
|
-
/** True for the STOP half of a tie — a direct `<tie type="stop">` child OR
|
|
22
|
-
* `<notations><tied type="stop">` (exporters vary on which they emit;
|
|
23
|
-
* either counts) — matches `EngravedNote.tieContinuation`'s "continuation
|
|
24
|
-
* note of a tie, not the struck start" contract. */
|
|
25
|
-
tieContinuation: boolean;
|
|
26
|
-
/** 0-based, matching `EngravedNote.staffIndex`'s "0 = top staff of the
|
|
27
|
-
* system" contract: every `<part>` is walked in document order, and
|
|
28
|
-
* every DISTINCT staff within it (by `<attributes><staves>` when
|
|
29
|
-
* present, else the highest `<staff>` number any of its notes uses, else
|
|
30
|
-
* 1) is assigned the next index — so a single-part 2-staff piano score
|
|
31
|
-
* numbers 0/1 by `<staff>`, and a 2-part 1-staff-per-part reduction (this
|
|
32
|
-
* app's own chord+bass shape) numbers 0/1 by PART, with neither case
|
|
33
|
-
* needing different code. */
|
|
34
|
-
staffIndex: number;
|
|
35
|
-
/** Whole-note fraction (0.25 = quarter) — `duration / divisions / 4`,
|
|
36
|
-
* divisions tracked per-part from the LAST `<attributes><divisions>`
|
|
37
|
-
* seen at or before this note (MusicXML: divisions persist until
|
|
38
|
-
* overridden, default 1). 0 for a grace note (no `<duration>` child —
|
|
39
|
-
* the true, spec-correct signal; never guessed from `<type>`). */
|
|
40
|
-
durationReal: number;
|
|
41
|
-
/** 0-based position of this note's `<measure>` among its OWN `<part>`'s
|
|
42
|
-
* measure children, in document order. Informational only — the live
|
|
43
|
-
* join (`verovioOnsetColumns`/`verovioEngravedNotes`, both in
|
|
44
|
-
* notationPlayerVerovio.ts) resolves the note's RENDERED measure index
|
|
45
|
-
* from the live SVG DOM independently (Verovio's own render order, which
|
|
46
|
-
* is what geometry/hit-testing must agree with), never from this field. */
|
|
47
|
-
measureIndex: number;
|
|
48
|
-
/** `print-object="no"` on the source `<note>` (stave's `hideDoubledNotes`
|
|
49
|
-
* sets this on editorially-doubled notes/rests before handing MusicXML to
|
|
50
|
-
* this player). Verovio's importer HONORS this for `<note>` elements
|
|
51
|
-
* carrying a `<pitch>` (renders `visibility="hidden"` on its own,
|
|
52
|
-
* empirically confirmed against a real 6.2.0 render) but does NOT honor
|
|
53
|
-
* it for `<rest>` notes (the `<g class="rest">` renders fully visible
|
|
54
|
-
* regardless — same empirical check). `createVerovioNotationPlayer`
|
|
55
|
-
* reads this field to force `visibility="hidden"` after every render for
|
|
56
|
-
* ANY id where it's true — a no-op re-application on notes Verovio
|
|
57
|
-
* already hid, and the actual fix on the rests it doesn't (see that
|
|
58
|
-
* module's `applyPrintObjectHiding`). */
|
|
59
|
-
hidden: boolean;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
6
|
/** Restated independently, not imported as a VALUE from notationPlayerSvg.ts
|
|
63
7
|
* — same reasoning as `MAX_ENGRAVE_WIDTH_VRV` vs `MAX_ENGRAVE_WIDTH_SVG`
|
|
64
8
|
* (below): a runtime (non-`type`) import from notationPlayerSvg.ts would
|
|
@@ -397,6 +341,31 @@ interface VerovioLayoutOptions {
|
|
|
397
341
|
* glyphs get, can opt back into the pre-existing behavior.
|
|
398
342
|
*/
|
|
399
343
|
minFitFactor?: number;
|
|
344
|
+
/**
|
|
345
|
+
* BREAK PLAN — target bars per line. When set (`> 0`), the player plans
|
|
346
|
+
* system breaks itself (`sectionAwareBreaks`, balanced with a widow
|
|
347
|
+
* back-off) and encodes them into the document before the first engrave,
|
|
348
|
+
* forcing `breaks: 'line'`. Leave unset (or `0`) to let Verovio's own
|
|
349
|
+
* `breaks: 'auto'` decide, which is the default and is content-aware.
|
|
350
|
+
*
|
|
351
|
+
* Combine with `sectionStarts` to keep the balancing inside each section.
|
|
352
|
+
* A PLAYER option, not a Verovio one — stripped in `verovioRenderOptions`
|
|
353
|
+
* before `setOptions` sees it.
|
|
354
|
+
*/
|
|
355
|
+
barsPerLine?: number;
|
|
356
|
+
/**
|
|
357
|
+
* BREAK PLAN — 0-based measure POSITIONS at which a musical section
|
|
358
|
+
* begins. Each one > 0 becomes a hard system break, so a section always
|
|
359
|
+
* starts its own line; `barsPerLine`'s balancing then runs within each
|
|
360
|
+
* section's span rather than across the whole excerpt.
|
|
361
|
+
*
|
|
362
|
+
* POSITIONS, not MusicXML `<measure number>` attributes — scores skip bar
|
|
363
|
+
* numbers, so a caller holding bar-numbered section labels must convert
|
|
364
|
+
* first (`measureCount`'s doc explains the coordinate). Setting this alone
|
|
365
|
+
* (no `barsPerLine`) is the "break at sections, otherwise let the engraver
|
|
366
|
+
* decide" mode. A PLAYER option — stripped in `verovioRenderOptions`.
|
|
367
|
+
*/
|
|
368
|
+
sectionStarts?: readonly number[];
|
|
400
369
|
}
|
|
401
370
|
/**
|
|
402
371
|
* Layout defaults applied to EVERY engrave (initial + every reflow) unless a
|