A system can now be given the measure it starts at, in a First bar field beside key and time. Per slice, since it is the one thing about a replacement that cannot be inherited from the song. Bar numbering is a Score property, so it is set once on the first staff, and made visible only at a line beginning — that vector is fussy: #(#f #t #t) also prints a number mid-system and #(#f #t #f) prints the second bar's rather than the first's. It travels in the bundle's engraving object as `bar`. Two things LilyPond 2.24 was quietly refusing to draw: `\bar ":|"` and the other old repeat names produce nothing at all — no error, no warning, exit status 0, just a missing repeat that you find on the tablet. Every book and forum answer still uses them, so translate them to the modern spellings. `\clef treble_8` unquoted is not an octavated clef either. It parses as a plain treble plus a stray "8" markup that lands under the first note, and the staff then reads an octave off — a tenor line engraved at soprano pitch. Quote it. The source pane, which is where either of those would have been visible, is now a collapsed section at the bottom rather than a permanent slab. It uses the panel's own disclosure helper, lifted out of Editor so both can call it.
367 lines
15 KiB
Markdown
367 lines
15 KiB
Markdown
# 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. A slice is presented as an image; a slice that was engraved rather than
|
||
scanned may also carry the notation it was engraved from.
|
||
|
||
**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.json` is 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.pdf` is 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.json`
|
||
and 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.
|
||
|
||
```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. |
|
||
| `engraving` | object | optional | The notation this slice's image was engraved from, when it was engraved rather than scanned. See [Engraving](#engraving). |
|
||
|
||
**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.
|
||
|
||
## Engraving
|
||
|
||
Most slices are photographs of print: an image and nothing more. A slice that
|
||
was *engraved* — set from notation rather than scanned — can carry the notation
|
||
it came from, in an `engraving` object.
|
||
|
||
```json
|
||
{
|
||
"file": "007.webp",
|
||
"engraving": {
|
||
"lang": "lilypond",
|
||
"key": "aes",
|
||
"time": "4/4",
|
||
"print_time": false,
|
||
"bar": 33,
|
||
"voices": [
|
||
{ "clef": "treble", "notes": "c4 des ees f | ees2. r4", "lyrics": "Kai -- paa -- va sy -- dän" },
|
||
{ "clef": "treble_8", "notes": "aes,4 aes aes aes | aes2. r4" },
|
||
{ "clef": "bass", "notes": "aes,4 ges f ees | aes2. r4" }
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
| Field | Type | | |
|
||
|---|---|---|---|
|
||
| `lang` | string | required | The notation language. `"lilypond"` is the only value defined by this version. |
|
||
| `voices` | array | required | One entry per staff, in the order they are printed top to bottom. At least one. |
|
||
| `key` | string | optional | Key signature, in `lang`'s spelling. For `lilypond`, the tonic of the major spelling: `"aes"`, `"c"`, `"fis"`. |
|
||
| `time` | string | optional | Time signature, as `"4/4"`. |
|
||
| `print_time` | boolean | optional | Whether the time signature is printed on this system. Default `false`. |
|
||
| `bar` | integer | optional | The measure this system starts at, as printed above its first bar. Omitted when the system is not numbered. |
|
||
|
||
Each entry of `voices`:
|
||
|
||
| Field | Type | | |
|
||
|---|---|---|---|
|
||
| `notes` | string | required | The music for this staff, verbatim in `lang`. |
|
||
| `clef` | string | optional | `"treble"`, `"treble_8"`, `"alto"`, `"bass"`. Default `"treble"`. |
|
||
| `lyrics` | string | optional | The words under this staff, verbatim in `lang`. Omitted when the staff has none. |
|
||
|
||
Three properties make this worth carrying:
|
||
|
||
- **It is the source, not a transcription.** The image was engraved from exactly
|
||
these strings. A consumer that re-engraves them gets the same system back.
|
||
- **It is editable.** A wrong note can be corrected here and the slice engraved
|
||
again, which a raster image does not allow.
|
||
- **It is playable.** `voices` are separated per staff with pitches, durations
|
||
and a key, so the passage can be sounded — a practice track, a click, a
|
||
pitch reference — without anyone reading the image.
|
||
|
||
`notes` and `lyrics` are opaque to this format. They are whatever `lang` accepts,
|
||
including constructs the fields above say nothing about: slurs, dynamics,
|
||
tuplets, and the tie idioms that carry a note across a slice boundary. A consumer
|
||
that does not speak `lang` must pass them through unaltered or ignore them, never
|
||
attempt to repair them.
|
||
|
||
An `engraving` **describes the slice above it, not the whole song**. Each is
|
||
self-contained: `key` and `time` are stated per slice, so nothing has to be
|
||
inherited from a neighbour or from the bundle. Slices without an `engraving` are
|
||
scanned, and the two kinds mix freely within one score — re-engraving a single
|
||
ruined system is the ordinary case.
|
||
|
||
A consumer that only presents the score can ignore `engraving` entirely. The
|
||
image is always the authority on what the slice looks like; where an image and
|
||
its engraving disagree, the image is what the producer intended to be read.
|
||
|
||
Notation languages other than `lilypond` may be added by future versions. A
|
||
consumer should ignore an `engraving` whose `lang` it does not know, and present
|
||
the slice image as it would any other.
|
||
|
||
## 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, new `engraving` languages — do not increase it. `engraving` was added
|
||
this way: a bundle carrying one is still a version 1 bundle, and a consumer that
|
||
has never heard of it presents the score unchanged.
|
||
|
||
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:
|
||
|
||
- `v` is a recognised version.
|
||
- `title` is present and non-empty; `slices` is a non-empty array.
|
||
- Every `file` names an entry present in the archive.
|
||
- Every `destination` is within the bounds of `slices`.
|
||
- Every `engraving` has a `lang` and a non-empty `voices`; unknown `lang` values
|
||
are ignored rather than rejected.
|
||
- 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
|
||
```
|
||
|
||
```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" },
|
||
{ "file": "004.webp" },
|
||
{ "file": "005.webp" },
|
||
{ "file": "006.webp" },
|
||
{ "file": "007.webp", "engraving": { "lang": "lilypond", "key": "aes", "time": "4/4",
|
||
"voices": [ { "clef": "treble",
|
||
"notes": "c4 des ees f | ees2. r4",
|
||
"lyrics": "Kai -- paa -- va sy -- dän" },
|
||
{ "clef": "bass",
|
||
"notes": "aes,4 ges f ees | aes2. r4" } ] } },
|
||
{ "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.
|