nord-format 0.4.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]. It depends only on crcxx (plus zip behind the bundle feature for backup bundles) and 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, and the encoded audio decoded back to samples in every generation), piano libraries (npno: the CNSP prefix) 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).

Everything that parses round-trips 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

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.

Features

  • bundle — ZIP-based backup bundles (pulls in the zip stack). Off by default so parse-only consumers stay lean; enable with --features bundle.
  • corpustest-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. Two more suites then check corpus-backed behaviors (tests/corpus_behaviors.rs) and verify that every registered bit affects its declared field (tests/coverage.rs).

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.