Skip to content

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> (see neumaRk_header.md §2);
  • a collection, whose first line is nrk-book:<v> or nrk-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 as nrk:);
  • 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 .nrk file 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 next nrk:);
  • 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 an nrk-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 / form overrides on item:;
  • 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.cppparseHeaderOnly), 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 parsed Music, presence from the map.header ranges;
  • 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 form wins over the song's FORM);
  • 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 … %%end block (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 item form (§8.1);
  • an item form box [NAME] with no matching marker (reference broken, §8.3);
  • transpose=+0 (no-op) and empty notes;
  • 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).