Collections (book / playlist)¶
This document defines the text format by which an ordered set of songs — a book or a playlist — is represented in a single neumaRk file.
A collection does not introduce a new language: it is a thin container layer around a sequence of songs, each of which is a standalone neumaRk document as already defined by the rest of the specification. The same parser reads single songs and collections; the distinction is made on the first line of the file.
1. Purpose and role¶
The collection format serves to export, share, and re-import a book
or a playlist as a single .nrk file — self-contained and readable.
The guiding principle is portability: the file is a self-sufficient snapshot of the collection. All songs are embedded as snapshots (frozen copies), so that the file can be opened and imported by anyone, even without access to the original songs and even if the originals change or disappear.
Everything that is product-specific (and not part of the musical language) — cover art, curated tags, sharing, roles, database identifiers — is not part of the file (see §7).
2. Collection and single song¶
A neumaRk file may represent:
- a single song, whose first line is the version declaration
nrk:<major>.<minor>(seeneumaRk_header.md§2); - a collection, whose first line is
nrk-book:<v>ornrk-playlist:<v>(§3).
The distinction is given by the content of the first line alone,
never by the file extension: a .nrk may contain either.
A file starting with nrk: is a single song as always: the collection
format is backward-compatible and does not alter the reading of
existing files.
The term collection is a conceptual umbrella used in this specification and in tooling; it never appears as a token in the file. The type (book or playlist) is written directly into the first-line marker.
3. Opening line and type¶
The first line of a collection is a version declaration that embeds the type:
nrk-book:0.6
or
nrk-playlist:0.6
Characteristics:
- it is mandatory and must be the first line of the file;
- the type (
book/playlist) is part of the marker: reading the first line tells you both that the file is a collection and which type; - the version follows the same
nrk:numbering of the language.
Functional difference between the two types:
- a book is a curated set of songs, without per-song overrides;
- a playlist is an executable sequence in which each song may carry a per-occurrence override (§6).
4. Collection header¶
After the opening line, and any blank lines, a collection header may
appear: a block of directives in the form key: value.
nrk-playlist:0.6
name: My Setlist
desc: live at the Blue Note
| Directive | Meaning | Mandatory |
|---|---|---|
name: |
collection name | no (recommended) |
desc: |
free description | no |
Header directives:
- use
:as the separator (same syntactic family asnrk:); - precede any song block;
- are part of neumaRk (as the
HT)title is for the song): the textual identity of the collection.
A header directive appearing after the first song block is out of place (W155).
5. Song blocks¶
The body of the collection is a sequence of song blocks. Each block:
- is a standalone neumaRk document, starting with its own
nrk:<v>line and continuing with header and datapacks as usual; - is a self-sufficient snapshot (the musical content is embedded, not referenced);
- is copy-pasteable as a valid
.nrkfile on its own.
The song order is the order of the blocks in the file: there is no numeric ordering key.
nrk-playlist:0.6
name: My Setlist
nrk:0.6
First Tune (Author)
F 120bpm
F| Bb| C7| F|
nrk:0.6
Second Tune (Author)
Bb 90bpm
Bb| Eb| F7| Bb|
A collection with no song blocks is degenerate (W156).
6. Per-item override (playlist only)¶
In a playlist, each song may be preceded by an item: line declaring
its overrides for that occurrence in the setlist. The overrides
are not properties of the song (the same song in two playlists may
have different ones): they are a layer of the setlist entry.
item: transpose=+2 notes="capo 3" form=[Intro] [A|8]x2 [B|8] [A|8]
nrk:0.6
My Tune (Author)
…
Rules:
- the
item:line applies to the song block that immediately follows it (the nextnrk:); - it appears only if there is at least one override; its absence means "no overlay";
- it is allowed only in playlists. An
item:line in annrk-book:is out of context (W152).
6.1 Parameter syntax¶
Parameters are key=value pairs separated by spaces. The = sign
distinguishes the inline parameter from the header directive (:),
so that no symbol has a double role.
A value:
- is a simple token (no spaces), e.g.
transpose=+2; or - is quoted
"…"when it contains spaces (literal text, quote-aware as in the rest of neumaRk), e.g.notes="capo 3".
Allowed keys: transpose, notes, form. An unknown key is ignored
with diagnostic W153.
| Key | Type | Meaning |
|---|---|---|
transpose |
signed integer | performance transposition (overlay, ± semitones) |
notes |
text | personal annotation for this entry |
form |
FORM grammar | performance form for this occurrence (§8) |
6.2 form= consumes the rest of the line¶
Since the FORM grammar itself uses "…" containers for labels (see §8),
the form= parameter is not quoted: it consumes everything that
follows up to end of line, verbatim. For this reason form=, when
present, is the last parameter of the item: line.
item: transpose=-1 form="Theme"[A|8]ff [B|4]x2 "Coda"[A|8]&fermata
The quotes here are internal to the FORM grammar (labels), not delimiters of the parameter. (A multi-line form is out of scope in this version.)
A null transpose (+0, no-op) and an empty notes are not errors.
7. Portability mode: inside and outside the file¶
The collection carries only music and minimal structure. Everything that is application product is reconstructed on import from defaults, as when creating a new collection.
In the file (it is neumaRk):
- the type (
nrk-book:/nrk-playlist:); name:/desc:;- the song order (= order of the blocks);
- for playlists: the
transpose/notes/formoverrides onitem:; - each song in full, as a snapshot.
Outside the file (it is not neumaRk):
- cover art, curated tags, subscriber count;
- sharing, roles, permissions;
- the songs' database identifiers;
- the dynamic vs snapshot distinction of playlist entries (on export every entry is resolved to a snapshot).
7.1 The file is intentionally anonymous¶
Consistent with the principle above, a collection file carries no origin identity: no database identifiers, no deduplication keys, no host tags. It is, by design, anonymous.
Consequence: a song's identity (deciding whether two songs are "the same") is not a property of the file — it is a host concern, to be derived from the music (see §7.2), never from an opaque tag stuck to the file. This is deliberate: the format does not carry data it cannot itself understand.
7.2 Import semantics (host behavior)¶
How a host re-imports a collection is not defined by neumaRk — it is application behavior. In leadsheet.app, because the file is anonymous (§7.1):
- importing a book creates copies of the songs in the user's library; import is not idempotent — re-importing the same book recreates the copies;
- deduplication ("do I already have this song?") is deferred and, when it lands, will be based on the melodic fingerprint (a property of the music, shared with search) as an advisory — not on an exact key glued to the file. An exact, durable identity is not obtainable from a deliberately pure file; so dedup is derived from the music and is, by nature, a suggestion rather than an automatism.
7.3 Header absorption on import — header-probe module (lossless)¶
On import, each song is split into header (→ prop) and datapack
(→ clean cont), so an imported song is consistent with WRITE-saved
ones (header in properties, body in cont) and the export→import
round-trip is lossless.
The split uses the real parser (parseText, text→Music) exposed by a
dedicated WASM module (createModuleHeader, source
wasm/src/neumark/header_probe.cpp → parseHeaderOnly), served under
assets/wasm/header/ and lazy-loaded only on import. The library
app's WASM bundle stays intentionally lean (no parser at normal boot); the
parser cost is paid once, on the first import.
In view, which already bundles the parser, parseHeaderOnly is exposed on
the same module (createModuleView) and used by the persisted save
(collection-view.service), with no separate module load.
parseHeaderOnly returns:
prop: the full header in DB shape (tit,crd{mus,lyr,arr,trs},key,bpm,mtr,sty,sub,yr,alt[],lng[]) — values from the parsedMusic, presence from themap.headerranges;cont: the clean datapack (lines after the header, leading/trailing blanks removed).
Fallback: if the module fails to load, import falls back to the
lightweight scan (parse_block_header in collections.cpp, a subset of
fields) with the header left inside cont, which WRITE normalizes on the next
save. The lightweight scan thus remains a safety net, no longer the primary
path.
Known limitation: the split needs the blank line between header and
datapack (which generate_nrk always emits on export); on hand-written files
without that separator parseText cannot tell header from body — same as
WRITE's import.
8. FORM at the collection level¶
The form parameter of a playlist entry (§6) uses the FORM)
grammar defined in neumaRk_play_and_form.md (section boxes,
informative durations |N, labels, dynamics, keywords, free prose).
8.1 Semantics: replacement¶
If a playlist entry carries a form, it replaces the FORM)
section possibly contained in the song's snapshot, for that
occurrence.
It serves to declare: "the form in which we play this song tonight is this, regardless of the song's original form."
- override present → the item
formwins over the song'sFORM); - override absent → the song's
FORM)remains (if present).
It is a replacement, not a merge.
8.2 Why FORM and not PLAY¶
FORM) is position-independent (a single, descriptive section at
the end of the document; the player ignores it): it lends itself to
being declared at the setlist level, where only a reference to a song
exists.
PLAY) instead is positional/inline: its effect depends on where
it is placed among the datapacks (see neumaRk_play_and_form.md §2.1).
At the setlist level there is no position inside the song into which to
inject it, so PLAY) is not expressible as an entry override. The
form override is therefore always descriptive (FORM), never
executed: it does not alter playback, but what the musicians read as
the form.
8.3 Reference resolution¶
The [NAME] boxes of the item form resolve against the M) markers
of the entry's song (the snapshot that follows), with the same model
as neumaRk_play_and_form.md §3.1. A box that matches no marker is
reference broken: rendered as non-executed text, with no error or
warning (neumaRk_play_and_form.md §7.3). This allows informal
setlist forms.
Box diagnostics (W141 duration, W142 repeat, W143 keyword) remain
applicable; document-placement ones (W140 multiple FORM), W145
FORM) not at the end) do not apply to the item form.
9. Parsing rules¶
9.1 Split before musical parsing¶
The collection file is split into blocks at the nrk: lines before
any musical analysis. The song parser receives only the text from
one nrk: line to the next.
Consequence: the collection tokens (nrk-book: / nrk-playlist:,
name: / desc:, item:) live in the segments outside the blocks
and cannot collide with musical content.
9.2 Recognition¶
- first line
nrk-book:→ book collection; - first line
nrk-playlist:→ playlist collection; - first line
nrk:→ single song.
The parser entry-point, which for songs enforces "nrk: must be the
first line", accepts nrk-book: / nrk-playlist: as alternative
first-line tokens. They are distinct literal prefixes: recognition is
additive and unambiguous.
9.3 Blank lines and versions¶
- Blank lines separate the collection header from the blocks and the
blocks from one another, consistent with datapack delimitation
(
neumaRk_datapack.md§1). - The
%%NAME … %%endblock (song versions,neumaRk_versions.md) remains internal to the single song block: it is text the song parser sees, not a collection token. The collection layer does not use%%.
10. Diagnostics¶
| Code | Description |
|---|---|
| W152 | item: line in an nrk-book: collection (books have no per-item overrides) |
| W153 | Unknown override key in item: (allowed: transpose / notes / form) |
| W154 | item: line not followed by a song block (nrk:) |
| W155 | Collection header directive (name: / desc:) after the first song block |
| W156 | Collection with no song blocks |
10.1 Non-errors¶
- a snapshot block whose
FORM)is replaced by the itemform(§8.1); - an item
formbox[NAME]with no matching marker (reference broken, §8.3); transpose=+0(no-op) and emptynotes;- extra blank lines in the header or between blocks.
11. Examples¶
11.1 Playlist with overrides¶
nrk-playlist:0.6
name: Friday Gig
desc: trio set
item: transpose=+2
nrk:0.6
Blue Bossa (Kenny Dorham)
Cm 140bpm
Cm| Fm| Dm7b5| G7| Cm|
item: form=[Intro|4] [A|16]x2 [Solos] [A|16] [Outro]&fermata
nrk:0.6
So What (Miles Davis)
M) [A] [Solos] [Outro]
…datapack…
The first entry plays Blue Bossa transposed +2; the second declares a performance form that replaces the song's own.
11.2 Book (no overrides)¶
nrk-book:0.6
name: My Real Book — Vol. 1
nrk:0.6
Autumn Leaves (J. Kosma)
…
nrk:0.6
All The Things You Are (J. Kern)
…
11.3 Single-song collection¶
nrk-playlist:0.6
name: Solo Spot
nrk:0.6
Naima (J. Coltrane)
…
It degrades transparently: a collection with a single block is valid.
12. Summary¶
| Concept | Syntax | Section |
|---|---|---|
| Book opening | nrk-book:<v> |
§3 |
| Playlist opening | nrk-playlist:<v> |
§3 |
| Collection header | name: / desc: |
§4 |
| Song block | nrk:<v> … (snapshot) |
§5 |
| Order | order of the blocks in the file | §5 |
| Entry override | item: key=value … (playlist only) |
§6 |
| Entry parameters | transpose / notes / form |
§6.1 |
| Entry form | form= (FORM grammar, rest-of-line) |
§6.2, §8 |
| Form replacement | item form replaces the song's FORM) |
§8.1 |
| Split | at nrk: lines, before musical parsing |
§9.1 |
| Portability | all snapshot; cover/tags/roles outside | §7 |
This document defines collections (books and playlists) as
neumaRk's portable container layer, built by reuse around the
song documents and the song form (neumaRk_play_and_form.md).