Skip to content

V2.4#8

Draft
zz85 wants to merge 32 commits into
gh-pagesfrom
v2.4
Draft

V2.4#8
zz85 wants to merge 32 commits into
gh-pagesfrom
v2.4

Conversation

@zz85
Copy link
Copy Markdown
Owner

@zz85 zz85 commented Mar 17, 2026

No description provided.

zz85 added 30 commits March 15, 2026 00:07
… defs, measure-level elements

- Fix MS2 format (.mscz v2.x): notes/rests are direct Measure children, not
  wrapped in <voice> elements. Parser now falls back to measureEl when no voice
  wrappers exist.
- Extract default clef from Part/Staff definitions (<defaultClef>, <clef>) so
  bass/alto/tenor staves get correct initial clef instead of defaulting to treble.
- Walk Measure-level children for KeySig/TimeSig/Tempo/Clef (MS3+ can place
  these outside <voice> elements).
- Replace all firstElementChild/nextElementSibling iteration with childNodes-based
  helpers (forEachChildElement, toArray) for xmldom compatibility.
- Add webmscore integration: vendored WASM library, Bun postinstall patch for
  WASM loading, CLI utility (scripts/webmscore.js), and comparison script that
  runs both parsers against all musescores/*.mscz files.
… mapping

- Emit C-major key signature when no KeySig element present (implied)
- Detect ties in MS2 format (<Tie id> + <endSpanner id>) in addition to
  MS3+ format (<Spanner type=Tie>)
- Fix instrument <clef staff=N> attribution: match clef to correct staff
  index instead of always using first clef for staff 0 (was making piano
  treble staff default to bass)
- Improve comparison script: handle grand-staff merging (split MusicXML
  events by staff number when our parser produces more staves)
Read tpc2 (written TPC) for transposing instruments like Bb clarinet,
F horn, etc. Adjust concert MIDI pitch by transposeChromatic to derive
correct written note name and octave for notation rendering.
…und in first measure

Mid-score clef changes were incorrectly setting hadInitialClef=true,
preventing the default clef from Part definitions from being inserted.
This caused MS4 piano files to show bass clef on treble staff.
…clef changes

- Support MuseScore v1.14 format: root <museScore> acts as Score element
  when no <Score> wrapper exists. Parse v1.x <cleflist><clef idx=N/> with
  numeric clef index mapping (from MuseScore v2 ClefType enum).
- Handle v1.x <name> as fallback for <trackName> in Part definitions.
- Fix hadInitialClef/KeySig/TimeSig: only set when found in first measure
  (mi===0), not on mid-score changes. This prevented default clef insertion
  when the only Clef element appeared later in the score.
- KeySig: v1.x uses <subtype>N</subtype> instead of <accidental>N</accidental>.
  Fix parseKeySigAccidentals to use xmlText presence check instead of xmlInt
  with undefined default (xmlInt returned 0 for missing elements, masking
  the fallback chain).
- TimeSig: v1.x uses <nom1>/<den> instead of <sigN>/<sigD>.
- KeyList: v1.x stores initial key sig in <Part><Staff><keylist><key idx=N/>,
  used as default when no KeySig appears in measure data.
- Extract both helpers (parseKeySigAccidentals, parseTimeSigValues) to avoid
  code duplication between measure-level and voice-level handlers.
- Post-process tokens to infer tie ends: when a note has tie=1, the next
  note with matching name+octave gets tieEnd=1. Fixes v1.x format where
  tie ends have no explicit marker.
- Comparison script: separate real diffs from info-only (voice2+ unsupported).
  Files with only voice2+ are marked [INFO] and count as passed.
Parse voice 2+ content from <voice> elements and merge into the token stream
at correct tick positions. Voice 2 rests advance the counter but aren't
rendered. Voice 2+ notes are marked with _voice property and inserted by
tick position for proper interleaving with voice 1 content.
Parser:
- Parse voice 2+ content in MS3/MS4 files: iterate all <voice> elements,
  merge notes into token stream at correct tick positions
- Voice 2 rests advance tick counter but aren't rendered

Comparison:
- Replace sequential note-by-note comparison with measure-based pitch
  inventory matching. Tolerant of voice interleaving and ordering differences.
- Per-staff primary voice detection for split-staff comparisons
When a ref pitch doesn't match exactly, try matching with octave +-1 as
a fallback. This accounts for ottava spans where webmscore exports sounding
pitch while our parser correctly reads written pitch.
…teCount diffs

- Allow +-2 octave shifts for 15ma and compound ottava+clef shifts
- Only report noteCount diff when there are also pitch errors or >1% difference
Detect the most common octave shift in each measure and apply it before
matching. Handles cases where 8va spans an entire measure (all notes
shifted by the same amount). Falls back to per-note +-1 for residual
mismatches.
Expose speed/setSpeed() on PlaybackController, backed by local _speed
state so changes made before lazy scheduler init are preserved. Wire a
range slider (0.1x-3x) and number input (0.1x-4x) in the playback
toolbar, kept in bidirectional sync.
Extract shared constants and helpers from musescore-parser.js into
music-import-utils.js, then build a new MusicXML parser that produces
the same canonical token format. Covers Phase 1+2: notes, rests,
chords, clefs, key/time signatures, ties, articulations, dynamics,
tempo, barlines, repeats, volta endings, pedal, and flow directions.

Files:
- src/music-import-utils.js — shared constants/helpers for XML importers
- src/musicxml-import.js — MusicXML parser (format detection, .mxl ZIP
  extraction, score-partwise conversion, all element handlers)
- src/musescore-parser.js — refactored to import from shared utils
- src/main.js — format detection routing + interpreter skip for MusicXML
- index.html — file input accepts .musicxml/.mxl/.xml
- test/musicxml-import.test.js — 74 tests (unit + end-to-end + round-trip)
- plans/musicxml-import.md — architecture & implementation plan

Tests: 240 pass, 0 fail (166 existing + 74 new)
Remove voice filtering for single-staff parts — backup/forward already
handle tick positioning for secondary voices. Previously voice 2+ notes
were silently dropped (e.g., Libertango piano duet lost 78 notes).

Result: 18/18 webmscore files now show exact note count match (100%).
…ice tests

- Use 'staff N' naming instead of hardcoded treble/bass labels
- Simplify skip-other-staff timing advance logic
- Add end-to-end tests for grand staff parts and multi-voice with backup
Route .mscz/.mscx files through the vendored WebMscore WASM library to
export MusicXML, then parse via our MusicXML importer. This gives full-
fidelity MuseScore import including cross-staff notes, all voices, grace
notes, and features the hand-written JS parser doesn't yet support.

- src/webmscore-loader.js: lazy-loads WebMscore (~3MB WASM) on demand
- src/main.js: new processMuseScoreViaWebMscore() with automatic
  fallback to the JS parser if WebMscore fails
- index.html: toolbar dropdown to switch between 'WebMscore' and
  'JS Parser' modes (persisted in localStorage)

WebMscore is the default. If it fails (e.g., unsupported format),
the JS parser is tried automatically with a console warning.
…NWC codes

The audio tempo map expects beatDuration in whole-note fractions
(0.25 = quarter, 0.5 = half, 0.125 = eighth). The MusicXML parser
was setting it to DURATION_MAP values (4 = quarter, 2 = half, 8 =
eighth), causing the formula quarterBpm = bpm * (4 / 0.25) = bpm * 16
— making playback 16x too fast.

Fix: use 1/DURATION_MAP[beatUnit] to convert to fractions. Also added
beatDuration to the MuseScore parser (previously omitted, worked by
accident via the 0.25 default fallback). Added beat-unit-dot support
for dotted tempo markings.
Page layout mode now supports four view arrangements:
- Single: vertical stack (existing behavior, default)
- Fit Width: single column with auto-zoom to fill viewport width,
  recalculates on window resize
- Two-Up: side-by-side page pairs (book spread view)
- Horizontal: left-to-right horizontal scroll (single row)

Implementation:
- constants.js: pageViewMode state + getter/setter
- typeset.js: compute per-page {x,y} positions based on view mode,
  apply per-system X offsets for non-vertical arrangements, update
  page background drawing to use position array
- main.js: page view mode selector with localStorage persistence,
  fit-width auto-zoom after render and on window resize
- index.html: dropdown selector visible only in page mode
… nav

Separate concerns properly:

Zoom controls (next to zoom slider):
- Fit Width (W) and Fit Height (H) toggle buttons auto-zoom to
  fill viewport. Clicking again disengages. Dragging the zoom
  slider also disengages fit mode. Recalculates on window resize.
  Persisted in localStorage.

Page view modes (dropdown, visible in Page layout):
- Vertical: pages stacked vertically, free scrolling (default)
- Single Page: one page at a time with prev/next navigation
- Two-Up: side-by-side pairs (book spread view)
- Horizontal: left-to-right horizontal scroll

Single-page mode includes:
- Page indicator (e.g. '3 / 12')
- Prev/Next buttons that scroll to the target page
- Nav controls visible only when in single-page mode

Implementation:
- constants.js: renamed 'single' to 'vertical', added zoomFitMode
  state (none/width/height)
- main.js: onZoomSliderInput() disengages fit mode, applyZoomFit()
  for width/height, scrollToPage() for single-page nav
- typeset.js: single-page layout uses same vertical positions
  (navigation works via scroll position)
- index.html: W/H buttons next to zoom slider, page nav controls
The nwc2xml/ directory is gitignored so these tests fail in CI.
Use describe.skip when the test data directory doesn't exist.
Complete the two remaining MusicXML import items:

- score-timewise: refactor convertPart/detectStaffCount to accept
  measure element arrays, add convertScoreTimewise that collects
  <part> elements per part-id across timewise measures and feeds
  them to the shared pipeline (7 new tests, equivalence verified)

- WebMscore loader: add 30s load timeout, script tag cleanup on
  failure, retry support, empty XML output detection, safe
  score.destroy(), and resetWebMscore() for recovery
Page mode enhancements:
- Page numbers rendered at bottom center of each page
- Copyright text drawn in bottom margin of page 1 (replaces DOM footer)
- Page nav toolbar visible in all page view modes, not just single-page
- Jump-to-page input: type a page number and press Enter to navigate
- Scroll-based page tracking updates indicator as user scrolls
- Viewport culling for page backgrounds (skip offscreen pages)
Extract TickTracker to src/layout/tick-tracker.js with dual-map alignment:
barlineTicks stores pre-gap barline positions for vertical stacking,
maxTicks stores post-gap positions for note alignment. Barlines use
alignBarline() (first-barline-wins), notes use alignWithMax() with
Math.max to prevent backward snapping past barline gaps. Barlines
removed from tabbableTypes so they no longer drift tabCounter.
zz85 added 2 commits March 21, 2026 20:00
…okens

Header tokens (Clef, KeySig, TimeSig) each advance tabCounter by 0.25.
When staves have different numbers of headers (e.g. one omits a Clef),
notes start at different tab offsets, causing barlines that should align
across staves to have different tabValues. Add a post-processing pass
in SightReader.read() that normalizes header offsets so all staves'
musical content begins at the same tab time.
…alignment

MusicXML and MuseScore imports emit a closing barline token at the end of
the last measure while the layout code also draws one from the staff-level
endingBar property, producing a duplicate barline and an extra bar number
in scroll mode.

- Add hasTrailingBarline() helper to detect token streams that already end
  with a barline, and skip the endingBar rendering in that case
- Skip bar numbers for barlines that follow the last note in scroll mode
- Reorder addStave/barline so the final stave segment extends to the
  ending barline rather than stopping before it, with a small nudge past
  the thick line's right edge for full visual coverage
- Never justify the last system in wrap/page modes so stave lines end at
  the final barline instead of stretching to the page edge
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant