nord-format 0.5.0

Read and write Clavia / Nord keyboard file formats — programs, samples, set lists, settings, backups — with byte-exact round-trips
Documentation
# nord-format

Parse and write **Clavia / Nord** keyboard binary file formats from Rust.

This is the pure format-logic crate of the drawbar toolkit: the `CBIN` container
(both header generations, each with its checksum), and per-model entity layouts
declared once with `#[bitbody]`. Its dependencies are [`crcxx`],
[`thiserror`], and the matching `nord-bits-derive` crate (plus `zip` behind the
`bundle` feature for backup bundles). It does no USB, OS, or I/O beyond
`Read`/`Seek`/`Write` — so it's trivially testable against a specimen corpus and
reusable by higher layers (a device/USB crate, a CLI) without dragging in a
transport stack.

## What it handles

The support map lives in the rustdoc, where it sits beside the code it
describes: the [`formats`] module docs define the three support tiers —
decoded, structurally decoded, container-verified stub — and each decoded
body's struct doc opens with how far read/write goes, what a read validates,
and the provenance of its placements, followed by its generated byte map.

In brief:

- **Decoded** — the Electro 5 program, live slot, song and settings (values
  named, pinned by hardware sweeps); the Stage 2, 3 and 4 programs and presets
  (every documented parameter placed; values raw except where the community
  maps enumerate them).
- **Structurally decoded** — sample instruments (`nsmp`, `nsmp3`, `nsmp4`:
  rename, retune, remap, the per-key map and the sound preset, and the encoded
  audio decoded back to samples in every generation; instruments are also
  *built* from PCM or a Sample Editor project, loops and stereo included), piano
  libraries (`npno`: the CNSP prefix, the stroke directory and every stroke's audio
  span — decoded back to samples, and dropped, narrowed or split by transforms whose
  writer re-lays the container) and
  Nord Sample Editor projects (`nsmpproj`: the editor's text save file — zones,
  strokes and audio files read and edited, new projects written).
- **Container-verified stubs** — every other corpus format: 60+ CBIN tags
  across the whole model line, plus the Lead SysEx/MIDI banks and the `.cn3`
  Electro 2 library, all carried verbatim. ZIP backup bundles parse behind the
  `bundle` feature (read-only).

Writable entities **round-trip byte-for-byte**, verified against a
change-one-knob specimen corpus. **Both `CBIN` container generations are read and
written** — type-1 (crc32 over the body) and the older type-0 (trailing crc16 over
the whole file), so factory files round-trip too — and `inspect` reports container
facts (tag, generation, version, length, checksum verdict) for *any* CBIN file in
O(1) memory, mapped body or not.

## Usage

```rust
use nord_format::{from_path, Entity, Program};
use nord_format::bank::Item; // for `.location()`
use nord_format::formats::ne5::OrganModel;

let entity = from_path("patch.ne5p")?;

if let Entity::Program(Program::Electro5(p)) = entity {
    // `p` is a `Cbin<ne5::Program>`: the CBIN header plus the decoded body,
    // and it derefs to the body, so the panels read as fields.
    println!("location: {:?}", p.location());
    println!(
        "lower/upper: {:?} / {:?}",
        p.center_panel.lower_part, p.center_panel.upper_part
    );

    // Organ state is decoded per model + selected preset:
    let preset = p.organ_panel.preset(OrganModel::B3);
    println!("B3 drawbars: {:?}", p.organ_panel.drawbars(OrganModel::B3, preset));
    println!("B3 vibrato:  {:?}", p.organ_panel.vib_type(OrganModel::B3));
}
```

`from_path` / `from_stream` sniff the container and return an [`Entity`]. Each
format lives under `formats::`, named for the four-character CBIN tag it carries —
or, where a model family shares a prefix across several tags, for that prefix. So
`formats::ne5` holds the concrete `Program`/`Song`/`Settings` layouts and the
`OrganModel` / `VibChorus` / `PercSpeed` decode types, while `formats::nsmp` and
`formats::npno` are single formats shared across the line.

## Lossless round-trip is the core invariant

Unknown regions are kept as **raw byte blocks** and decoded values are exposed as
read-only views over them, so `parse → write` is byte-identical even where the
semantics are incomplete. Every newly decoded field is a safe, incremental
refinement — never a risk to the write path.

## Reading and editing a body without naming its fields

Every `#[bitbody]` also generates a **registry**: `fields()` lists every declared
field under a dotted path with its placement, its current value and the values it
accepts, and `set_field(path, value)` writes one back through the type's own parse
— so a field becomes readable and editable by being declared, and no consumer can
fall behind the library.

A field's type says what *kind* of control it is (`fields::ControlKind`): a knob
with a unit, a bipolar knob, a selector, a drawbar with its position in the
register and how the register is packed, a morph slot with the parameter it
moves, a pattern grid with its step shape, a library reference with the
catalogue that resolves it. So a UI picks a
widget from the registry rather than from a table of field names of its own.

What placement cannot say — which controls sit together, in what order, and which
of them the instrument is *using* for the state a file holds — is declared per
format as data, in `panel`:

```rust
let entity = nord_format::from_path("patch.ne5p")?;
if let Some(panel) = nord_format::panel::of(&entity) {
    // Groups are ordered, nestable, and each may carry a condition over the
    // body's own values: an organ registration for a model that is not selected
    // is state the file keeps, not a control the instrument is offering.
}
```

Layouts exist for the Electro 5 and Stage 4 programs so far; `of` answers `None`
elsewhere, and a caller falls back to the flat field list.

## Features

- **`bundle`** — ZIP-based backup bundles (pulls in the `zip` stack). Off by
  default so parse-only consumers stay lean; enable with `--features bundle`.
- **`corpus`***test-only*. Adds the private specimen corpus to the sweep;
  see below. Implies `bundle`, because the corpus holds ZIP banks.

## Tests

Unit tests live inline (`#[cfg(test)] mod tests`) and run on a plain
`cargo test`, alongside `tests/dispatch.rs`, which synthesizes a file for
every registered tag in memory and checks dispatch + round-trip for both
header generations, and the **specimen sweep**, `tests/corpus`: one generated
test per file — container checksum, parse, byte-exact round trip, no
unnameable decoded values, every registry field set to a new value and read
back without moving another, and the file's oracle sidecar
(`<file>.oracle.json`) where it has one. A file joins by being readable; an
oracle by existing beside it.

The sweep always reads `tests/fixtures/` — specimens this crate's own writers
produced, with sidecars saying what was set, committed as the part of the
corpus any checkout can carry.
With `--features corpus` it also reads the private specimen corpus. Three more
suites then run: corpus-backed format and codec behaviors
(`tests/corpus_behaviors.rs`, `tests/codec_behaviors.rs`) and the blind-bit
ledger (`tests/coverage.rs`) — every bit the instrument varies must answer to a
registered field or be listed, by range, as reviewed debt.

```sh
cargo test -p nord-format                       # open suite: unit + dispatch + fixtures sweep

# With the corpus — point at a nord-corpus checkout:
NORD_CORPUS_ROOT=/path/to/nord-corpus \
  cargo test -p nord-format --features corpus

# With nix
nix build .#nord.nord-format-corpus
```

## Disclaimer

Not affiliated with, authorized, or endorsed by Clavia DMI AB. "Nord", "Clavia",
and "Electro" are trademarks of Clavia DMI AB, used here only to identify the
hardware these formats come from. All reverse engineering is of files produced by
Nord hardware, for interoperability.

[`crcxx`]: https://docs.rs/crcxx
[`thiserror`]: https://docs.rs/thiserror
[`Entity`]: https://docs.rs/nord-format
[`formats`]: https://docs.rs/nord-format/latest/nord_format/formats/