← demo catalog · docs

@projektemacher/tify-plugin-musicxml

Note: This code was generated with AI assistance — opencode, powered by GLM (Z.ai).

MusicXML score rendering and playback for the TIFY IIIF viewer: renders a page's MusicXML notation (OpenSheetMusicDisplay) as an overlay aligned onto the scan, with Web Audio playback (tempo, tracks, instruments) and note/staff highlights that follow the performance.

import { musicxmlPlugin } from '@projektemacher/tify-plugin-musicxml';
import '@projektemacher/tify-plugin-core/style.css';
import '@projektemacher/tify-plugin-musicxml/musicxml.css';

const plugin = await musicxmlPlugin(tify, {
  panelEl: document.querySelector('#panel'),
});

Options: panelEl (required, sidecar unless docked), dock?: boolean | 'auto' (default 'auto'), tei?: string | ((page: number) => string | undefined) (absolute TEI URL, raw TEI XML, or a per-page resolver — overrides the manifest discovery below; a plain string applies to every page), musicxml?: string | ((page: number) => string | undefined) (absolute URL, raw XML, or per-page resolver — wins over the TEI-embedded score), showScoreOverlay? (default true), showHighlights? (default true), regionOverlay?: { enabled?, color?, opacity? } (default off, #ff6400, 25% alpha), pageBorder?: { color? } (default color #008cff).

Double-page spreads

When TIFY shows two pages at once (double-page mode), the panel gains a Page dropdown listing the shown pages — the played page can be switched there. Tapping or clicking one of the shown pages plays that page too; the 1 px outline around the played page, the dropdown selection and the reloaded panel are the feedback. The score, staff regions and playback highlights are placed on the selected page's scan either way.

The panel mirrors the score-page viewer's player: a centered transport row (play/pause/stop with icons above their labels), the tempo, one track row per detected staff — each with a playing checkbox (all on by default, the page sounds as written) and its own instrument selector — and a Score toggle for the rendered overlay plus a Regions group (off by default) — an on/off switch plus one composite rgba selector (color picker with an integrated alpha slider; the slider's track runs transparent → the picked color) that colors fill and border of the persistent staff-region overlay alike. Every control carries a translated tooltip.

In double-page spreads a 1 px outline marks the page being played (pageBorder.color, default playback-blue) — it follows the Page dropdown and tap selection and is removed in single-page mode.

Playback details

Manifest format

Per canvas, the score data is discovered via the canvas rendering entry with format application/tei+xml (or an id ending in .tei.xml), resolved against the manifest URL. That one document (the homr pipeline's per-page TEI, see the homr TEI customization) carries everything:

Without a TEI rendering, the plain MusicXML rendering (format application/vnd.recordare.musicxml+xml, or an id ending in .musicxml) is used as a fallback: playback works, but nothing is mapped onto the scan (no aligned score, no staff regions). Without any score reference the panel reports that the page has no score; playback needs no image at all (an explicit musicxml URL or XML string suffices).

Sanitizing invalid MusicXML

Some transcriptions contain <note> elements carrying both <chord /> and <rest /> — invalid MusicXML (a rest cannot be a chord member). OSMD builds a pitchless chord note for those and crashes during layout with Cannot read properties of undefined (reading 'getHalfTone'), failing the whole page render. The plugin strips <chord /> from any note containing a rest before rendering; the playback parser already ignores pitchless rests.

Manifest generation

Score data (staff TEI + MusicXML) is produced by the homr OMR pipeline (optical music recognition with staff detection), not by an image tiler: image services themselves can be generated with fliiifenleger (generate, --iiif-version V3 recommended).

Example

example/ — manifest URL box with the score panel beside the viewer (plus optional ?tei= / ?musicxml= overrides to try the player without authoring a manifest). Run npm run dev at the repo root and open /packages/plugin-musicxml/example/.

Development

npm run build       # dist/ (ESM + declarations)
npm run typecheck
npm run test:i18n   # catalogue coverage for src/ + example/
npm run test:timeline  # headless regression check for the MusicXML parser
npm run lint

License: AGPL-3.0-or-later.