The bundle is documented as a standalone format rather than as a note between two programs: producers and consumers are generic, the marker vocabulary is defined musically rather than by what a viewer does with it, and image properties are stated as guarantees with the reasoning where it is not obvious. Anything that reads scores can implement it without knowing this tool exists. Taking that view changed the substance in three places. Marker types now carry their musical meaning rather than a UI mapping. The rule against re-importing became a statement about identity - indices mean something only within one bundle, so two bundles of a piece are independent documents. And forward-compatibility rules were added, which a protocol needs and a handover note did not: ignore unknown fields and marker types, refuse an unknown version. Tempo is now an integer, beats per minute, exported as a JSON number and omitted when blank; the editor accepts digits only. A figure can drive a metronome or a click track where a verbal marking cannot, and readers do not agree on what Andante means. This needs the consumer's column changed from free-form text, which the format document flags. Every figure in the document comes from a real export.
11 KiB
Score Bundle Format, version 1
A container for one musical score, prepared for continuous-scroll display.
A bundle holds the score as a sequence of images — one per system of music — together with the metadata that names the piece and the markers that describe how a performer navigates it. It is self-contained: nothing outside the file is needed to present the score.
This document defines the format. It does not describe any particular program that writes or reads one.
Terminology
Slice — one system of music: a single line spanning all voices, typically four to twelve bars, with lyrics intact. A slice is the atomic unit of the format. Slices are images and carry no notion of pitch, voice or beat.
Marker — a semantic annotation attached to a slice, describing a navigational feature printed in the score: a rehearsal letter, a repeat, a jump.
Producer — anything that writes a bundle. Consumer — anything that reads one.
Container
A bundle is a ZIP archive.
<name>.zip
├── song.json manifest: metadata, slice order, markers
├── original.pdf the source document (optional)
├── 001.webp
├── 002.webp
└── … one file per slice
song.jsonis required and must be at the archive root.- Slice images are at the archive root. Their names are given in
song.json; the zero-padded numbering shown is conventional, not required. original.pdfis optional. When present it is the document the score was prepared from, carried along for printing or archival. It is not required to present the score and consumers may ignore it.- No directories, and no entries beyond those referenced by the manifest plus the optional PDF.
- Compression method is unconstrained. Producers typically deflate
song.jsonand store the images and PDF, which are already compressed.
For scale: a twelve-page, twenty-four-slice choral score runs about 2.4 MB, of which roughly 830 KB is the source PDF and the rest slice images at ~20 KB each.
Manifest
song.json is UTF-8 encoded JSON.
{
"v": 1,
"title": "Ketun joululaulu",
"composer": "trad.",
"arranger": "P. Rapi",
"tempo": 92,
"slices": [
{
"file": "001.webp",
"markers": [
{ "type": "rehearsal_letter", "label": "A" }
]
},
{
"file": "002.webp",
"markers": [
{ "type": "segno" },
{ "type": "to_coda", "destination": 7 }
]
},
{ "file": "003.webp" }
]
}
Top-level fields
| Field | Type | ||
|---|---|---|---|
v |
integer | required | Format version. 1 for this document. |
slices |
array | required | Ordered, at least one entry. See below. |
title |
string | required | The name of the piece. |
subtitle |
string | optional | Alternate or translated title. |
composer |
string | optional | Who wrote the music. |
original_artist |
string | optional | Who originally performed the work, where that differs from the composer. |
arranger |
string | optional | Who adapted it for these forces. |
lyricist |
string | optional | Who wrote the words. |
translator |
string | optional | Who translated the words. |
tempo |
integer | optional | Beats per minute. |
voices |
string | optional | The parts in this arrangement, as free text. |
Optional fields are omitted when they have no value. A consumer will not encounter an empty string or a null in place of an absent field.
tempo is a number, never a word: a figure can drive a metronome or a click
track, and verbal markings are not interchangeable between readers.
Unrecognised top-level fields may be added by future versions. A consumer should ignore fields it does not know rather than reject the bundle.
Slices
Each entry of slices is an object:
| Field | Type | ||
|---|---|---|---|
file |
string | required | Name of the image entry in the archive. |
markers |
array | optional | Markers on this slice. Omitted when there are none. |
The array order is the reading order of the score. It is the only ordering the format defines. Filenames often sort into the same order, but a consumer must not derive order from them.
A slice's index is its zero-based position in this array. Indices are the only identifiers the format has, and they are meaningful only within one bundle.
Slice images
Every slice image in a bundle satisfies the following. A consumer can rely on these and does not need to inspect the images to lay them out.
- Format: WebP, losslessly encoded. (Lossless rather than lossy because engraved music is line art — large flat areas separated by thin high-contrast strokes — which lossless encoders compress better than lossy ones as well as exactly.)
- RGBA, with all three colour channels zero. The image is carried entirely by the alpha channel: ink is opaque black, paper is fully transparent, and antialiased edges are partially transparent. Compositing a slice over a background of any colour reproduces the printed appearance on that colour of paper.
- Uniform width within a bundle. Every slice has the same pixel width, so a consumer can lay them out in a single column without measuring. Systems shorter than the widest are padded on the right with transparent pixels; they end early rather than stretching.
- Width is at most 1920 pixels, and is frequently less. A narrower bundle is not a defect: images are never enlarged beyond the resolution of their source, because that adds bytes and softness without adding detail. Consumers should scale to fit their own layout and should not treat 1920 as a target.
- Height varies per slice, being the height of that system.
- Slices need not be rectangular in content. Where two systems interleave — for example a section label printed level with the previous system's lyric line — the boundary between them steps, and each slice is delivered as its bounding box with the region belonging to its neighbour left transparent. This requires nothing special from a consumer; it composites correctly.
Images are presentation-ready. They have already been deskewed, cropped, levelled and scaled as a set. Re-encoding, re-cropping or re-scaling them individually will at best waste work and at worst break the uniformity the format guarantees.
One specific hazard is worth naming, because it is silent: an image pipeline that discards the alpha channel rather than compositing it will turn every slice into a solid black rectangle, since the colour channels are all zero.
Markers
A marker annotates the slice it appears on.
| Field | Type | ||
|---|---|---|---|
type |
string | required | One of the vocabulary below. |
label |
string | optional | Free text. Meaningful for rehearsal_letter, section_label and volta. |
destination |
integer | optional | Index into slices. Present on jump types. |
A slice may carry several markers. Their order within the array is not significant.
Vocabulary
Named positions — places a performer may be directed to:
type |
Meaning |
|---|---|
rehearsal_letter |
A boxed letter or number printed above a system, used to say "from C". label holds it. |
section_label |
A named section: INTRO, VERSE, CHORUS. label holds the name. |
segno |
The 𝄋 sign, target of a dal segno. |
coda |
The 𝄌 sign, beginning of the closing section. |
fine |
The end of the piece when reached by a da capo or dal segno. |
Structural notation — printed context, affecting how the music is read but not directing the reader elsewhere:
type |
Meaning |
|---|---|
repeat_start |
The start of a repeated passage. |
repeat_end |
The end of a repeated passage. |
volta |
An alternative ending bracket. label holds its number. |
Jumps — points where the reader is directed to another slice:
type |
Meaning |
|---|---|
to_coda |
"To Coda": leave here for the coda. |
ds_al_coda |
Dal segno al coda: return to the segno. |
ds_al_fine |
Dal segno al fine: return to the segno and play to the fine. |
dc_al_coda |
Da capo al coda: return to the beginning. |
dc_al_fine |
Da capo al fine: return to the beginning and play to the fine. |
generic_jump |
An unclassified jump. |
Every jump marker states its destination explicitly, as an index into
slices. A consumer does not need to infer where a jump leads by searching for
a matching coda or segno, and must not assume a bundle contains only one of
each. A destination always refers to an existing index.
Unrecognised marker types may be added by future versions. A consumer should ignore markers it does not understand rather than reject the bundle.
Versioning
v is an integer that increases when a change would break an existing consumer.
Additions that a consumer can safely ignore — new optional fields, new marker
types — do not increase it.
A consumer should refuse a bundle whose v it does not recognise rather than
attempt to interpret it.
Validating a bundle
A consumer is advised to check:
vis a recognised version.titleis present and non-empty;slicesis a non-empty array.- Every
filenames an entry present in the archive. - Every
destinationis within the bounds ofslices. - Archive entry names contain no path separators, no
.., and no absolute paths, as with any archive from an untrusted source.
Identity and updates
A bundle describes one complete score. The format has no notion of updating a previously read bundle: there are no stable identifiers, and a slice's index is meaningful only within the bundle that contains it.
Two bundles of the same piece are therefore independent documents, not versions of one. A consumer that stores imported bundles and assigns its own identifiers should treat a second bundle as a new score rather than merging it into an existing one — jump destinations resolved against the first bundle's slices do not survive being repointed at a second bundle's.
Complete example
A 24-slice bundle, abbreviated:
song.zip
├── song.json 1.4 KB
├── original.pdf 827 KB
├── 001.webp 21 KB 1489 × 1058
├── 002.webp 18 KB 1489 × 818
├── …
└── 024.webp 1489 px wide, like every other slice
{
"v": 1,
"title": "Ketun joululaulu",
"composer": "trad.",
"arranger": "P. Rapi",
"tempo": 92,
"slices": [
{ "file": "001.webp", "markers": [ { "type": "rehearsal_letter", "label": "A" } ] },
{ "file": "002.webp", "markers": [ { "type": "segno" },
{ "type": "to_coda", "destination": 7 } ] },
{ "file": "003.webp" },
{ "file": "004.webp" },
{ "file": "005.webp" },
{ "file": "006.webp" },
{ "file": "007.webp" },
{ "file": "008.webp", "markers": [ { "type": "coda" } ] },
{ "file": "009.webp" }
]
}
Reading the score means presenting 001.webp through 024.webp in that order,
in one column, each scaled to the same width. A reader who follows the to_coda
on slice index 1 continues at slice index 7.