Sheet music player for MusicXML scores. It renders a score, plays it back with transport, tempo and per-instrument volume controls, and can keep everyone in a room on the same note as a presenter plays, steps or clicks through the piece.
This package is the player: the controls, the layout and the room sync. It does not contain the engine that engraves and plays a score. You provide that engine through the required loadEngine prop, and nothing renders until you do.
Setup is three steps: install the package, add an engine, render a score.
npm install @hiyve/react-music-notation @hiyve/utilities @mui/material @mui/icons-material @emotion/react @emotion/styled
Synced playback across a room additionally needs:
npm install @hiyve/react @hiyve/react-semantic-relay
The player works with any OpenSheetMusicDisplay-compatible build. Which build you choose decides what your users get:
| Option A: public build | Option B: a build with playback | |
|---|---|---|
| Where it comes from | opensheetmusicdisplay on npm (BSD-3-Clause) |
A build that includes the audio player classes PlaybackManager, BasicAudioPlayer and LinearTimingSource, such as the build OpenSheetMusicDisplay provides to its sponsors |
| Score rendering | Yes | Yes |
| Cursor and note-by-note stepping | Yes | Yes |
| Play, pause, stop | No | Yes |
| Tempo and per-instrument volume | No | Yes |
| Click a note to hear it | No | Yes |
You can start with option A and move to option B later; only the loadEngine function changes.
Install it:
npm install opensheetmusicdisplay
Create the loader once, in a module of its own:
// notationEngine.ts
export const loadEngine = () => import('opensheetmusicdisplay');
The player shows the score with previous-note and next-note buttons. The playback controls are hidden.
Get the build. You need one file: the minified browser build, usually named opensheetmusicdisplay.min.js. It is not on public npm. You obtain it from its publisher and use it under its licence terms.
Serve the file from your app. Either put it in your static folder, so that public/vendor/opensheetmusicdisplay.min.js is served at /vendor/opensheetmusicdisplay.min.js, or keep it beside your source and let your bundler emit it:
const engineUrl = new URL('./vendor/opensheetmusicdisplay.min.js', import.meta.url).href;
Load it as a script, as shown next. Do not import the file as a module.
Create the loader once, in a module of its own:
// notationEngine.ts
import { createScriptEngineLoader } from '@hiyve/react-music-notation';
export const loadEngine = createScriptEngineLoader('/vendor/opensheetmusicdisplay.min.js');
createScriptEngineLoader(url, options?) adds the script to the page the first time a player needs it and reuses it afterwards, however many players you render.
| Option | Type | Default | Description |
|---|---|---|---|
globalName |
string |
'opensheetmusicdisplay' |
Name of the global the script defines |
nonce |
string |
– | CSP nonce to set on the script element |
Either way, the engine is downloaded only when a player first mounts, so it adds nothing to your initial page load.
import { MusicXmlPlayer } from '@hiyve/react-music-notation';
import { loadEngine } from './notationEngine';
function ScoreViewer({ url, onClose }: { url: string; onClose: () => void }) {
return (
<MusicXmlPlayer
fileUrl={url}
fileName="Minuet in G"
loadEngine={loadEngine}
onClose={onClose}
onError={(error) => console.error(error)}
/>
);
}
By default the player covers the viewport with its own header. Use variant="inline" to fill a container you size yourself:
<div style={{ height: 600 }}>
<MusicXmlPlayer variant="inline" fileUrl={url} loadEngine={loadEngine} />
</div>
Pass onError while you set up; the message tells you which step failed.
| What you see | Cause | Fix |
|---|---|---|
Failed to load the notation engine script |
The engine URL is wrong, or your Content Security Policy blocks it | Open the URL in the browser; allow its origin in script-src |
The notation engine script loaded but did not define window.opensheetmusicdisplay |
The file is not a browser build, or it defines a different global | Use the minified browser build; set globalName if the global differs |
The notation engine does not provide an OpenSheetMusicDisplay class |
loadEngine resolved with something that is not an engine |
Return the engine module itself from loadEngine |
Failed to fetch the score: 403 Forbidden (or another status) |
The browser could not read fileUrl |
Check the URL, its expiry and its CORS headers |
| The score shows but there is no Play button | The engine has no playback classes (option A) | Expected with the public build; use a build with playback |
| Play works but there is no sound | The instrument samples could not be downloaded | Allow the sample hosts in connect-src, or set soundfont (see Instrument samples) |
To check an engine in code, engineSupportsPlayback(engine) returns whether it can play audio.
SyncedMusicXmlPlayer mirrors the presenter's actions to every follower. A follower's controls are locked while it follows.
import { SyncedMusicXmlPlayer } from '@hiyve/react-music-notation/sync';
function RoomScore({ file, url, localUserId, ownerId }) {
return (
<SyncedMusicXmlPlayer
variant="inline"
fileUrl={url}
loadEngine={loadEngine}
fileId={file.fileId}
localUserId={localUserId}
isPresenter={localUserId === ownerId}
presenterId={ownerId}
enabled
/>
);
}
Render it under a SemanticRelayProvider from @hiyve/react-semantic-relay. Every participant must pass the same fileId for the same score.
Followers only accept commands sent by presenterId. Until presenterId is known they ignore every command, so pass it as soon as you have it.
useMusicXmlPlayerSync gives you the transport without the component, for cases where you render MusicXmlPlayer yourself:
import { useRef } from 'react';
import { MusicXmlPlayer, type MusicXmlPlayerHandle } from '@hiyve/react-music-notation';
import { useMusicXmlPlayerSync } from '@hiyve/react-music-notation/sync';
function CustomSyncedScore({ url, fileId, localUserId, isPresenter, presenterId }) {
const playerRef = useRef<MusicXmlPlayerHandle>(null);
const { broadcast, isFollowing } = useMusicXmlPlayerSync({
localUserId,
isPresenter,
presenterId,
fileId,
enabled: true,
onRemoteCommand: (command) => playerRef.current?.apply(command),
});
return (
<MusicXmlPlayer
ref={playerRef}
variant="inline"
fileUrl={url}
loadEngine={loadEngine}
controlsLocked={isFollowing}
onLocalCommand={broadcast}
/>
);
}
FileManager from @hiyve/react-collaboration recognises .musicxml files. Register the player as their viewer:
import { FileManager } from '@hiyve/react-collaboration';
import { MusicXmlPlayer } from '@hiyve/react-music-notation';
<FileManager
customViewers={{
musicxml: (_data, _file, _onClose, url) =>
url ? <MusicXmlPlayer variant="inline" fileUrl={url} loadEngine={loadEngine} /> : null,
}}
/>
| Prop | Type | Default | Description |
|---|---|---|---|
fileUrl |
string |
required | URL of the MusicXML document |
loadEngine |
NotationEngineLoader |
required | Supplies the notation engine |
fileName |
string |
– | Title shown in the overlay header |
onClose |
() => void |
– | Called when the user closes the overlay |
variant |
'overlay' | 'inline' |
'overlay' |
Cover the viewport with a header, or fill the parent with none |
controlsLocked |
boolean |
false |
Disable every control and ignore clicks on the score |
onLocalCommand |
(command: MusicXmlPlayerCommand) => void |
– | Called for each action the local user performs |
onError |
(error: Error) => void |
– | Called when the engine or the score fails to load |
labels |
Partial<MusicXmlPlayerLabels> |
– | Text overrides |
colors |
Partial<MusicXmlPlayerColors> |
– | Colour overrides |
soundfont |
MusicXmlSoundfontOptions |
– | Where playback fetches instrument samples |
sx |
SxProps<Theme> |
– | Styles merged onto the root element |
The component's ref exposes apply(command), which performs a command as if the user had done it without reporting it through onLocalCommand.
Takes every MusicXmlPlayer prop except controlsLocked and onLocalCommand, plus:
| Prop | Type | Default | Description |
|---|---|---|---|
localUserId |
string |
required | The local user's id |
isPresenter |
boolean |
required | True for the one participant everyone else follows |
enabled |
boolean |
required | Presenter: share my actions. Follower: follow the presenter |
fileId |
string |
required | Identifies the open score; the same on every client |
presenterId |
string |
– | The presenter's user id. Followers ignore commands from anyone else |
allowUnverifiedPresenter |
boolean |
false |
Accept commands from any sender while presenterId is empty |
Takes the six sync props above, plus:
| Option | Type | Description |
|---|---|---|
onRemoteCommand |
(command: MusicXmlPlayerCommand) => void |
Called with each command a follower should apply |
onError |
(error: Error) => void |
Called when a published command could not be delivered |
Returns:
| Field | Type | Description |
|---|---|---|
broadcast |
(command: MusicXmlPlayerCommand) => void |
Publish a local action. Does nothing unless presenting with sync on |
isFollowing |
boolean |
True when this client is following the presenter |
MusicXmlPlayerCommand is one of:
| Command | Meaning |
|---|---|
{ t: 'play' } |
Start or resume playback |
{ t: 'pause' } |
Pause playback |
{ t: 'stop' } |
Stop and return to the start |
{ t: 'next' } / { t: 'back' } |
Step one note forward or back |
{ t: 'tempo', bpm } |
Set the tempo |
{ t: 'volume', instrument, volume } |
Set an instrument's volume, 0–1. instrument is its position in the score |
{ t: 'mute', instrument, muted } |
Mute or unmute an instrument |
{ t: 'noteAt', timestamp } |
Move to a position in the score and sound the notes there |
<MusicXmlPlayer
fileUrl={url}
loadEngine={loadEngine}
labels={{ title: 'Partitur', play: 'Abspielen', pause: 'Pause', stop: 'Stopp' }}
/>
| Label | Default |
|---|---|
title |
Sheet Music |
back |
Go back |
close |
Close |
previousNote |
Previous note |
nextNote |
Next note |
play |
Play |
pause |
Pause |
stop |
Stop |
tempo |
Tempo |
tempoUnit |
BPM |
volume |
Volume |
muteInstrument |
Mute |
unmuteInstrument |
Unmute |
noInstruments |
No instruments detected |
unknownInstrument |
Unknown |
loading |
Loading sheet music… |
loadError |
Failed to load sheet music |
DEFAULT_LABELS holds the defaults and mergeLabels(overrides) returns the defaults with your overrides applied.
Each colour is an MUI theme palette path or any CSS colour.
<MusicXmlPlayer
fileUrl={url}
loadEngine={loadEngine}
colors={{ toolbar: 'grey.900', cursor: '#ff3366' }}
/>
| Colour | Default | Used for |
|---|---|---|
background |
background.default |
The player surface |
toolbar |
background.paper |
Header and control bar |
border |
divider |
Borders under the header and control bar |
sheet |
#FFFFFF |
The page the score is drawn on |
cursor |
primary.main |
The playback cursor |
DEFAULT_COLORS holds the defaults and mergeColors(overrides) returns the defaults with your overrides applied.
During playback the engine downloads instrument samples. To serve them from your own host, pass soundfont:
<MusicXmlPlayer
fileUrl={url}
loadEngine={loadEngine}
soundfont={{
baseUrl: 'https://cdn.example.com/soundfonts/',
percussionBaseUrl: 'https://cdn.example.com/soundfonts/FluidR3_GM/',
}}
/>
The host must be laid out like the midi-js-soundfonts collection.
loadEngine.fileUrl must be fetchable from the browser (CORS applies) and return uncompressed MusicXML. Compressed .mxl files are not supported.fetch, so its host must be allowed by connect-src. With a playback engine and no soundfont override, samples are fetched from https://gleitz.github.io and https://paulrosen.github.io. An engine loaded with createScriptEngineLoader must be allowed by script-src.SyncedMusicXmlPlayer and useMusicXmlPlayerSync must be used under a SemanticRelayProvider from @hiyve/react-semantic-relay.@hiyve/react-music-notation| Export | Kind | Purpose |
|---|---|---|
MusicXmlPlayer |
component | The score player |
createScriptEngineLoader |
function | Loader for an engine hosted as a script |
engineSupportsPlayback |
function | Whether an engine can play audio |
DEFAULT_LABELS, mergeLabels |
defaults | Label defaults and merge |
DEFAULT_COLORS, mergeColors |
defaults | Colour defaults and merge |
readCursorTimestamp |
function | The cursor's position in a score display, or null |
readSelectionStartTimestamp |
function | The position of the last clicked note in a score display, or null |
pollForSelectionChange |
function | Wait for a value to change, with a timeout; returns a cancel function |
MusicXmlPlayerProps, MusicXmlPlayerHandle, MusicXmlPlayerCommand |
types | Player props, ref handle and commands |
MusicXmlPlayerLabels, MusicXmlPlayerColors, MusicXmlSoundfontOptions |
types | Customization |
NotationEngine, NotationEngineLoader, ScriptEngineLoaderOptions |
types | The engine contract |
NotationDisplay, NotationCursor, NotationCursorIterator, NotationSheet, NotationTimestamp, NotationInstrument, NotationInstrumentMap, NotationPlaybackManager, NotationPlaybackListener, NotationAudioPlayer, NotationSoundfontOptions, NotationTimingSource, PollDeps |
types | The parts of an engine the player uses |
@hiyve/react-music-notation/sync| Export | Kind | Purpose |
|---|---|---|
SyncedMusicXmlPlayer |
component | A player the presenter drives for every follower |
useMusicXmlPlayerSync |
hook | The sync transport on its own |
MUSICXML_PLAYER_SYNC_TOPIC |
constant | The relay topic sync messages are published on |
SyncedMusicXmlPlayerProps, UseMusicXmlPlayerSyncOptions, UseMusicXmlPlayerSyncResult, MusicXmlPlayerSyncPayload |
types | Sync props, options, result and message shape |