Files
noteman-slicer/docs/bundle-format.md
T
Esa Kataja 8f670cf7db Engrave window: bar numbers, and two silent LilyPond faults
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.
2026-07-29 13:44:42 +03:00

367 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.