PlanetScore is a browser-based MIDI to sheet music renderer. It parses a MIDI file, converts it to MusicXML, and displays the result as responsive SVG notation with an optional PDF preview and download.
Note: This project was created to support score rendering on Planetbiru Composer.
The project runs as a static web application. No build process or server-side component is required.
PlanetScore was created to solve several practical challenges in digital score rendering:
-
Direct PDF and SVG rendering from MIDI
The primary goal is to take raw MIDI files and produce sheet music in both interactive SVG and printable PDF formats, without relying on heavy external software. -
Lightweight MusicXML intermediary
MusicXML is used only as a structural bridge for notation. It contains just the elements needed for score display (tempo, key, lyrics, staves), not playback data, which keeps the conversion pipeline lean and efficient. -
Client‑side processing
Everything runs in the browser. This avoids server load, scales easily, and makes the application usable even on modest hosting setups without specialized backend services.
- Selective track rendering: Users can choose which tracks or channels to convert, reducing resource usage and making the renderer faster.
- Real‑time interactivity: The SVG renderer supports playhead movement and note highlighting, enabling synchronized playback experiences like karaoke or guided practice.
- Accessibility and portability: As a static web app, it requires no installation or build process — just open
index.htmlin a modern browser. - Flexibility for learning and collaboration: Features like lyric gating, clef‑aware notation, and comment rendering make it suitable for education, rehearsal, and collaborative score editing.
- Cross‑platform simplicity: Runs on any modern browser (Chrome, Edge, Firefox, Safari) with ES6 support, making it widely accessible.
- Parse Standard MIDI files (
.midand.midi) in the browser. - Convert MIDI events to MusicXML 4.0 Partwise format.
- Render MusicXML as scalable SVG sheet music.
- Generate a multi-page PDF score using jsPDF.
- Select one or more MIDI tracks or channels.
- Preserve tempo, time signature, key signature, and lyric metadata.
- Support common time signatures, including 3/4 and 4/4, with automatic note type and dot calculation based on the time signature.
- Automatically split wide note ranges into multiple staves, with a minimum-range floor to protect narrow melodic parts (such as vocals).
- Gate lyrics to the correct channel so they are only rendered when the melody channel is present.
- Support percussion notation on MIDI channel 10.
- Fill rhythmic gaps with rests and split complex durations into tied notes.
- Optionally snap note positions and durations to a musical grid.
- Handle clef-specific notation correctly (treble, bass, and alto clefs) for both ledger lines and key signatures.
- Separate playback mute (audio-only) from score mute (visual filtering).
- Open
index.htmlin a modern web browser. - Choose a MIDI file with the file picker.
- Select the tracks or channels to display.
- Adjust the available score options, such as staff splitting or snapping.
- View the generated MusicXML and SVG score.
- Generate and download the PDF score when needed.
The included Tenggelam.mid file can be used as a sample input.
For local development, serve the project directory with any static HTTP server if your browser restricts local file access. For example:
python -m http.server 8000Then open http://localhost:8000.
The rendered score can be integrated with a MIDI player so the notation stays synchronized with playback. The SVG renderer exposes a moving playhead and active note highlighting that are driven by the current MIDI tick, allowing the score to follow the player in real time.
This is useful for applications such as karaoke-style practice, guided learning, or performance visualization. The score can update the playhead position, scroll the current system into view, and highlight currently active notes while the MIDI track is playing.
Example flow:
player.on('onPlaying', (tick) => {
const pos = midi.header.tickToPosition(tick);
const measure = Math.floor(player.tickToMeasure(tick));
renderer.updatePlayhead(tick, pos, scoreContainer.parentNode, {
scroll: true,
scrollOffset: -20
});
renderer.highlightActiveNotes(tick, measure, {
highlight: true
});
});This makes the score and MIDI player work as a single synchronized playback experience without reloading the score when the player time changes.
MIDI file
|
v
MidiParser
|
v
MidiToMusicXML
|
+--> MusicXMLSVGRenderer --> SVG score preview
|
+--> MusicXMLPDFRenderer --> PDF preview/download
const parsed = MidiParser.parse(arrayBuffer, {
normalize: false,
forceUpdateEvents: true
});MidiParser.parse() returns MIDI header data, tracks, notes, lyrics, controller events, pitch bends, and instrument metadata. Set normalize to shift the first note to tick 0. Set forceUpdateEvents to move initial setup events to tick 0.
const converter = new MidiToMusicXML();
const musicXML = converter.convert(arrayBuffer, {
title: 'My Score',
creator: 'Composer',
selectedChannels: [0, 1],
lyricChannelId: 4,
autoSplit: true,
splitThreshold: 24,
minSplitRange: 30,
snapPosition: 0.125,
snapDuration: 0.125
});Supported conversion options include:
| Option | Type | Default | Description |
|---|---|---|---|
title |
string |
"Song Title" |
Score title. |
creator |
string |
"Composer Name" |
Composer or creator name. |
divisions |
number |
4 |
Divisions per quarter note in the generated MusicXML. |
selectedChannels |
number[] |
null |
MIDI channels to render. If omitted, all channels are rendered. |
selectedTracks |
number | number[] |
null |
Track index (or indices) to render (legacy). Resolves to that track's channels. Meta tracks are always preserved. |
lyricChannelId |
number | null |
null |
1-indexed MIDI channel that carries the lyrics (e.g. 4 = channel index 3). If the channel is absent from the rendered score, lyrics are disabled entirely. |
autoSplit |
boolean |
false |
Automatically split channels with a wide note range. |
splitThreshold |
number |
24 |
Primary threshold (semitones) for automatic split. Lowered to 14 automatically for piano (program 0–7). |
minSplitRange |
number |
30 |
Hard floor (semitones). Parts with a smaller range are never auto-split, regardless of splitThreshold. Set to 0 to disable the floor. |
splitPoint |
number | null |
null |
MIDI note used for a two-staff split. Bypasses both thresholds. |
splitPoints |
number[] | null |
null |
Two MIDI notes used for a three-staff split (e.g. [71, 59]). Bypasses both thresholds. |
muteChannels |
number[] |
[] |
Channels excluded from the rendered score. |
transpose |
number |
0 |
Semitone offset. Drum channel (9) is never transposed. |
snapPosition |
number | null |
null |
Snap note onsets to note fractions, such as 0.125 for eighth notes. |
snapDuration |
number | null |
null |
Snap note durations to note fractions. |
Meta-only tracks are retained during conversion so that lyrics, tempo, and time-signature information remains available in the generated score.
The converter reads the time signature from the MIDI file and uses it to determine note types and dots. For example, in 3/4 and 4/4 time, the note duration is interpreted relative to the beat value (quarter note), and dotted notes are generated when the duration matches a dotted value (e.g., dotted half in 3/4, dotted quarter in 4/4). This ensures that the rendered notation is rhythmically correct for these common time signatures.
Two parameters control automatic staff splitting. Both conditions must pass for a part to be auto-split:
range >= minSplitRange— hard floor. Parts below this are never auto-split.range >= splitThreshold— primary threshold. Adjusted per instrument (14for piano, otherwise the user value).
| Parameter | Role | Overridable? |
|---|---|---|
splitThreshold |
Preference — "split if the range is at least this wide". Lowered automatically for piano. | Yes, by instrument program. |
minSplitRange |
Hard floor — "do NOT split if the range is smaller than this". Evaluated first. | No. |
Example with splitThreshold: 24, minSplitRange: 30:
| Part | Program | Range (semitones) | Result |
|---|---|---|---|
| Flute melody | 73 | 20 | 1 staff |
| Flute melody | 73 | 28 | 1 staff (fails floor 30) |
| Flute melody | 73 | 32 | 2 staves |
| Piano simple | 0 | 18 | 1 staff (fails floor 30) |
| Piano medium | 0 | 32 | 2 staves |
| Organ large | 16 | 50 | 3 staves |
| Drum kit | — | — | 1 staff (always) |
Setting minSplitRange: 0 restores the legacy behavior (only splitThreshold matters).
const renderer = new MusicXMLSVGRenderer('score-container', {
staffSpacing: 90,
partSpacing: 65,
systemSpacing: 80,
autoDetectMobile: true
});
renderer.render(musicXML);The SVG renderer supports responsive layouts, beams, ties, slurs, articulations, grand-staff braces, and an interactive playhead. Use updatePlayhead() to move the playhead and optionally scroll the score during playback.
Ledger lines and key signatures are rendered with correct staff positions for treble (G), bass (F), and alto (C) clefs.
- Ledger lines are drawn only for notes that fall outside the staff range. Any ledger line that would overlap with one of the five staff lines is automatically skipped. Staff range is clef-aware.
- Key signatures use the correct diatonic positions for each clef. Alto clef uses the viola positions:
F4 C4 G4 D4 A3 E4 B3for sharps andB3 E4 A3 D4 G3 C4 F3for flats.
const pdfRenderer = new MusicXMLPDFRenderer();
pdfRenderer.render(musicXML);
pdfRenderer.save('my-score.pdf');The PDF renderer creates a vector-based, multi-page score. It requires the jsPDF browser library, which is loaded from CDN by index.html. Clef-aware ledger lines and key signatures are shared with the SVG renderer.
The muteChannels option affects the rendered score — notes from the listed channels are removed from the MusicXML.
Playback mute is applied separately at the audio layer (via the TimidityPlayer integration) and does not affect the rendered score. In the vocal training app, playback mute is applied with applyMuteToPlayer(), while the score always renders all channels. This lets a user silence one part during practice without hiding it from view.
| File | Purpose |
|---|---|
index.html |
Browser demo and user interface. |
MidiParser.js |
MIDI binary parser. |
MidiToMusicXML.js |
MIDI-to-MusicXML conversion logic. |
MusicXMLSVGRenderer.js |
MusicXML-to-SVG renderer. |
MusicXMLPDFRenderer.js |
MusicXML-to-PDF renderer. |
style.css |
Styles for the standalone page. |
Tenggelam.mid |
Example MIDI file. |
manual.md |
Extended API and implementation documentation. |
*.min.js |
Minified distribution copies of the JavaScript libraries. |
The core parser, converter, and SVG renderer use plain browser JavaScript. PDF generation uses jsPDF, loaded from the CDN reference in index.html.
Use a current version of Chrome, Edge, Firefox, or Safari with support for ES6 JavaScript, ArrayBuffer, the File API, SVG, and Blob URLs.
This project is licensed under the MIT License.