Skip to main content

nord_format/
lib.rs

1//! Parse and write Clavia / Nord keyboard binary file formats.
2//!
3//! > This is an unofficial, community project: **not affiliated with, endorsed
4//! > by, or supported by Clavia DMI AB**. "Nord" and the instrument names are
5//! > Clavia's trademarks, used here only to identify which files this crate
6//! > reads.
7//!
8//! The formats — programs, live slots, songs, settings, presets, synth
9//! patches, sample and piano libraries, across the Nord keyboard range — are
10//! reverse engineered from specimen files and hardware observation, never
11//! from Clavia's software, and are in varying states of completion: some
12//! bodies decode to named fields, others are container-verified and kept
13//! verbatim. [`formats`] is the map of what exists and how far each format's
14//! decoding goes.
15//!
16//! Completeness never gates I/O. Every supported file reads and writes
17//! whether its body decodes fully, partially, or not at all: decoded values
18//! are views over a verbatim body, bits no field claims survive untouched,
19//! and `to_bytes(from_stream(x)) == x` bit-for-bit (archives are read-only).
20//! That invariant is tested against a private corpus of real files.
21//!
22//! [`from_path`] / [`from_stream`] sniff any supported file and decode it
23//! into an [`Entity`]; [`to_bytes`] is the inverse.
24//!
25//! Runtime dependencies are `crcxx` and `thiserror` (plus `zip` behind the
26//! `bundle` feature), and no I/O happens beyond `Read`/`Seek`/`Write`, so the
27//! crate runs anywhere `std` does — wasm included. Device access lives in the
28//! companion `nord-usb` crate, in the same repository.
29
30pub mod accept;
31pub mod bank;
32pub mod bits;
33pub mod cbin;
34pub mod components;
35pub mod crc;
36pub mod error;
37pub mod fields;
38pub mod formats;
39pub mod layout;
40pub mod note;
41pub mod panel;
42pub mod types;
43pub mod util;
44pub mod wav;
45
46use crate::cbin::{Cbin, RawBody};
47use crate::formats::{
48    cn3, midi, nc2, nc2d, nd2, nd3, ne3, ne4, ne5, ne6, ne7, ng2, nl4, nla1, no3, np, np2, np3,
49    np4, np5, npip, npno, ns2, ns3, ns4, nsclassic, nsmp, nsmpproj, nw, nw2, sysex,
50};
51use std::fs::File;
52use std::io::{BufReader, Read, Seek};
53use std::path::Path;
54use util::{peek, FileType};
55
56use crate::error::{Error, ParseError};
57
58/// A ZIP archive: an Electro 5 bundle or backup, or a Drum-family bank.
59#[cfg(feature = "bundle")]
60#[derive(Debug)]
61pub enum Bundle {
62    Drum2Bank(nd2::bank::Bank),
63    Drum3KitBank(nd3::kit_bank::KitBank),
64    Electro5(ne5::Bundle),
65    /// A ZIP of CBIN files under any mix of tags — every model's bundle/backup
66    /// shape. Reported by public documentation; not confirmed on hardware.
67    /// Members are kept container-verified and raw, under their archive paths —
68    /// which encode the slot, uninterpreted here.
69    Members(Vec<(String, Cbin<RawBody>)>),
70}
71
72/// A stored program, one variant per model. Only the Electro 5 and the three
73/// Stages decode anything of the body; the rest are container-verified stubs.
74///
75/// Left unboxed for the reason [`Entity`] gives.
76#[allow(clippy::large_enum_variant)]
77#[derive(Debug)]
78pub enum Program {
79    C2(Cbin<RawBody>),
80    C2D(Cbin<RawBody>),
81    /// A Nord Drum 2 program (`nd2p`), usually met inside a bank archive.
82    Drum2(Cbin<RawBody>),
83    /// A Nord Drum 3P kit (`nd3k`) — the model's program-equivalent.
84    Drum3(Cbin<RawBody>),
85    /// Electro 3 and 3HP — the file does not say which.
86    Electro3(Cbin<RawBody>),
87    /// Electro 4 and 4D — likewise.
88    Electro4(Cbin<RawBody>),
89    Electro5(Cbin<ne5::Program>),
90    Electro6(Cbin<RawBody>),
91    Electro7(Cbin<RawBody>),
92    Grand(Cbin<RawBody>),
93    Lead4(Cbin<RawBody>),
94    LeadA1(Cbin<RawBody>),
95    Organ3(Cbin<RawBody>),
96    Piano1(Cbin<RawBody>),
97    Piano2(Cbin<RawBody>),
98    Piano3(Cbin<RawBody>),
99    Piano4(Cbin<RawBody>),
100    Piano5(Cbin<RawBody>),
101    /// Stage 2 and 2 EX.
102    Stage2(Cbin<ns2::Program>),
103    Stage3(Cbin<ns3::Program>),
104    Stage4(Cbin<ns4::Program>),
105    /// Stage Classic and Stage EX.
106    StageClassic(Cbin<RawBody>),
107    Wave(Cbin<RawBody>),
108    Wave2(Cbin<RawBody>),
109}
110
111/// The live buffer — the panel as it stands, not a saved program. Same body as
112/// [`Program`], under its own format tag.
113///
114/// Left unboxed for the reason [`Entity`] gives.
115#[allow(clippy::large_enum_variant)]
116#[derive(Debug)]
117pub enum Live {
118    Electro4(Cbin<RawBody>),
119    Electro5(Cbin<ne5::Program>),
120    Electro6(Cbin<RawBody>),
121    Electro7(Cbin<RawBody>),
122    Grand(Cbin<RawBody>),
123    Piano1(Cbin<RawBody>),
124    Piano2(Cbin<RawBody>),
125    Piano3(Cbin<RawBody>),
126    Piano4(Cbin<RawBody>),
127    Piano5(Cbin<RawBody>),
128    Stage2(Cbin<ns2::Program>),
129    Stage3(Cbin<ns3::Program>),
130    Stage4(Cbin<ns4::Program>),
131    Wave2(Cbin<RawBody>),
132}
133
134/// A stored song / set list, one variant per model that has them. Only the
135/// Electro 5 body decodes; the Stage 3 is container-verified verbatim.
136#[derive(Debug)]
137pub enum Song {
138    Electro5(Cbin<ne5::Song>),
139    Stage3(Cbin<RawBody>),
140}
141
142/// The instrument's global settings, one variant per model. Only the Electro 5
143/// body decodes; the rest are container-verified stubs.
144#[derive(Debug)]
145pub enum Settings {
146    C2(Cbin<RawBody>),
147    C2D(Cbin<RawBody>),
148    Electro4(Cbin<RawBody>),
149    Electro5(Cbin<ne5::Settings>),
150    Electro6(Cbin<RawBody>),
151    Electro7(Cbin<RawBody>),
152    Grand(Cbin<RawBody>),
153    Lead4(Cbin<RawBody>),
154    LeadA1(Cbin<RawBody>),
155    Organ3(Cbin<RawBody>),
156    Piano1(Cbin<RawBody>),
157    Piano2(Cbin<RawBody>),
158    Piano3(Cbin<RawBody>),
159    Piano4(Cbin<RawBody>),
160    Piano5(Cbin<RawBody>),
161    Stage2(Cbin<RawBody>),
162    Stage3(Cbin<RawBody>),
163    Stage4(Cbin<RawBody>),
164    Wave(Cbin<RawBody>),
165    Wave2(Cbin<RawBody>),
166}
167
168/// A synth patch, on the models that bank them separately from programs. Only
169/// the Stage 4's decodes.
170///
171/// Left unboxed for the reason [`Entity`] gives.
172#[allow(clippy::large_enum_variant)]
173#[derive(Debug)]
174pub enum Synth {
175    Stage2(Cbin<RawBody>),
176    Stage3(Cbin<ns3::SynthPreset>),
177    Stage4(Cbin<ns4::synth::SynthPreset>),
178    StageClassic(Cbin<RawBody>),
179}
180
181/// A Lead performance — the multi-slot layer above that family's programs.
182#[derive(Debug)]
183pub enum Performance {
184    Lead4(Cbin<RawBody>),
185    LeadA1(Cbin<RawBody>),
186}
187
188/// A stored organ preset, on the models that keep them as files.
189///
190/// Left unboxed for the reason [`Entity`] gives.
191#[allow(clippy::large_enum_variant)]
192#[derive(Debug)]
193pub enum OrganPreset {
194    /// Electro 3 and 3HP (`neop`).
195    Electro3(Cbin<RawBody>),
196    /// Stage 4 (`ns4o`).
197    Stage4(Cbin<ns4::organ_preset::OrganPreset>),
198}
199
200/// A stored piano preset, on the models that keep them as files.
201#[derive(Debug)]
202pub enum PianoPreset {
203    /// Stage 4 (`ns4n`).
204    Stage4(Cbin<ns4::piano_preset::PianoPreset>),
205}
206
207/// A sample instrument, decoded by generation: all three share the `nsmp` tag,
208/// and the header version says which schema the body holds.
209#[derive(Debug)]
210pub enum Sample {
211    V2(Cbin<nsmp::Sample>),
212    /// The nsmp3/nsmp4 generations: section chain decoded, strokes stored
213    /// verbatim and decodable through [`nsmp::codec`].
214    V3(Cbin<nsmp::SampleV3>),
215}
216
217impl Sample {
218    pub fn name(&self) -> Result<String, Error> {
219        match self {
220            Sample::V2(s) => s.name(),
221            Sample::V3(s) => s.name(),
222        }
223    }
224
225    /// Longest name this generation's field takes.
226    pub fn max_name_len(&self) -> usize {
227        match self {
228            Sample::V2(_) => nsmp::MAX_NAME_LEN,
229            Sample::V3(_) => nsmp::MAX_NAME_V3_LEN,
230        }
231    }
232
233    pub fn set_name(&mut self, name: &str) -> Result<(), Error> {
234        match self {
235            Sample::V2(s) => s.set_name(name),
236            Sample::V3(s) => s.set_name(name),
237        }
238    }
239
240    /// Move the note a zone's sample plays untransposed at.
241    pub fn set_root_key(&mut self, index: usize, note: u8) -> Result<(), Error> {
242        match self {
243            Sample::V2(s) => s.set_root_key(index, note),
244            Sample::V3(s) => s.set_root_key(index, note),
245        }
246    }
247
248    pub fn set_zone_top_note(&mut self, index: usize, note: u8) -> Result<(), Error> {
249        match self {
250            Sample::V2(s) => s.set_zone_top_note(index, note),
251            Sample::V3(s) => s.set_zone_top_note(index, note),
252        }
253    }
254
255    /// Whether this generation stores a zone's lowest note.
256    ///
257    /// False where zones tile — a zone reaches down to one above the next-lower zone's
258    /// top, so only the top note is stored — which is what makes
259    /// [`Self::set_zone_low_note`] refuse there.
260    pub fn has_low_note(&self) -> bool {
261        matches!(self, Sample::V3(_))
262    }
263
264    /// Move a zone's lowest note, on the generations that store one — see
265    /// [`Self::has_low_note`].
266    pub fn set_zone_low_note(&mut self, index: usize, note: u8) -> Result<(), Error> {
267        match self {
268            Sample::V2(_) => Err(ParseError::AssertFail("v2 stores no low note".into()).into()),
269            Sample::V3(s) => s.set_zone_low_note(index, note),
270        }
271    }
272
273    /// Whether this instrument's zones can be retuned and remapped. Its name
274    /// always can.
275    ///
276    /// False where the zone table does not read, or where a `map` that also
277    /// describes the keyboard note by note cannot be recomputed from the layout.
278    /// The setters say which at length.
279    pub fn zones_are_editable(&self) -> bool {
280        match self {
281            Sample::V2(s) => s.zones().is_ok() && s.strokes().is_ok(),
282            Sample::V3(s) => s.zones_are_editable(),
283        }
284    }
285
286    /// Which section chain this body's sections form. A narrow body whose `map`
287    /// version names no chain we have a specimen of reports the error.
288    pub fn chain(&self) -> Result<nsmp::Chain, Error> {
289        match self {
290            Sample::V2(s) => s.chain(),
291            Sample::V3(_) => Ok(nsmp::Chain::Wide),
292        }
293    }
294
295    /// Which generation's units this body's stroke streams are in. A content version
296    /// past the generations the codec describes is refused rather than guessed at.
297    pub fn layout(&self) -> Result<nsmp::codec::Layout, Error> {
298        match self {
299            Sample::V2(_) => Ok(nsmp::codec::Layout::V2),
300            Sample::V3(s) => nsmp::codec::Layout::from_version(s.header.version).ok_or_else(|| {
301                ParseError::OutOfBounds {
302                    value: format!("content version {}", s.header.version),
303                    bound: format!(
304                        "the generations this codec describes, below {}",
305                        nsmp::codec::V5_FROM_VERSION
306                    ),
307                }
308                .into()
309            }),
310        }
311    }
312
313    /// The generation to name in a report, taken from the content version rather
314    /// than the file name.
315    pub fn generation(&self) -> &'static str {
316        match self {
317            Sample::V2(_) => "v2",
318            Sample::V3(s) if s.header.version >= nsmp::V4_FROM_VERSION => "v4",
319            Sample::V3(_) => "v3",
320        }
321    }
322
323    /// Every zone in stored order, paired with the stream that plays it.
324    ///
325    /// One codec reads all three generations, so the only thing that branches here
326    /// is which accessors reach the zones and their streams.
327    pub fn zones(&self) -> Result<Vec<nsmp::ZoneAudio<'_>>, Error> {
328        match self {
329            Sample::V2(s) => {
330                let zones = s.zones()?;
331                let strokes = s.strokes()?;
332                zones
333                    .iter()
334                    .zip(&strokes)
335                    .enumerate()
336                    .map(|(i, (zone, stroke))| {
337                        let (at, stream) = s.zone_stream(i)?;
338                        Ok(nsmp::ZoneAudio {
339                            root_key: stroke.root_key,
340                            top_note: zone.top_note,
341                            low_note: None,
342                            at,
343                            stream,
344                        })
345                    })
346                    .collect()
347            }
348            Sample::V3(s) => {
349                let zones = s.zones()?;
350                zones
351                    .iter()
352                    .enumerate()
353                    .map(|(i, zone)| {
354                        let (at, stream) = s.zone_stream(i)?;
355                        Ok(nsmp::ZoneAudio {
356                            root_key: zone.root_key,
357                            top_note: zone.top_note,
358                            low_note: zone.low_note,
359                            at,
360                            stream,
361                        })
362                    })
363                    .collect()
364            }
365        }
366    }
367
368    /// Every stroke's stream in file order, whether or not a zone names it.
369    pub fn stroke_streams(&self) -> Vec<(usize, &[u8])> {
370        match self {
371            Sample::V2(s) => s.stroke_streams(),
372            Sample::V3(s) => s.stroke_streams(),
373        }
374    }
375
376    /// Serializes, recomputing the checksum over the body it just produced.
377    pub fn to_bytes(&self) -> Result<Vec<u8>, Error> {
378        let mut out = std::io::Cursor::new(Vec::new());
379        match self {
380            Sample::V2(s) => s.write_to(&mut out),
381            Sample::V3(s) => s.write_to(&mut out),
382        }?;
383        Ok(out.into_inner())
384    }
385}
386
387/// One decoded file.
388///
389/// The decoded program variants are much the largest: a decoded panel holds its
390/// fields *and* the bytes it came from. Left unboxed — one of these exists per
391/// file being read, never in a collection.
392#[allow(clippy::large_enum_variant)]
393#[derive(Debug)]
394pub enum Entity {
395    /// An Electro 2 sample library — the one non-CBIN library format.
396    Cne3(cn3::Cne3),
397    Live(Live),
398    /// A MIDI carrier for a Lead SysEx bank, verbatim.
399    Midi(midi::Midi),
400    OrganPreset(OrganPreset),
401    /// A piano library (`npno`).
402    Piano(npno::Piano),
403    /// A Stage Classic piano library (`nsp`). ⚠️ Megabytes, allocated whole —
404    /// [`cbin::inspect`] answers container questions in O(1).
405    PianoLibrary(Cbin<RawBody>),
406    PianoPreset(PianoPreset),
407    /// A C2 pipe-organ library (`npip`). Same caution as [`Entity::PianoLibrary`].
408    PipeLibrary(Cbin<RawBody>),
409    Performance(Performance),
410    Program(Program),
411    Sample(Sample),
412    /// A Nord Sample Editor project (`.nsmpproj`) — the text file the editor
413    /// saves and generates an `nsmp` from.
414    SampleProject(nsmpproj::Project),
415    Settings(Settings),
416    Song(Song),
417    Synth(Synth),
418    /// A Lead 1/2/2X/3 SysEx dump, verbatim.
419    Sysex(sysex::Sysex),
420    #[cfg(feature = "bundle")]
421    Bundle(Bundle),
422}
423
424/// Sniff `reader` and decode one supported file into an [`Entity`] — the
425/// counterpart to [`to_bytes`]. The container class comes from the leading
426/// bytes; a CBIN body is then dispatched on the format tag at offset 8.
427pub fn from_stream(reader: &mut (impl Read + Seek + Sized)) -> Result<Entity, Error> {
428    let header = peek(reader)?;
429
430    match header.file_type {
431        #[cfg(feature = "bundle")]
432        FileType::Zip => read_zip(reader),
433        #[cfg(not(feature = "bundle"))]
434        FileType::Zip => {
435            Err(ParseError::UnknownFileType("zip (bundle feature disabled)".to_string()).into())
436        }
437        FileType::Sysex => Ok(Entity::Sysex(sysex::Sysex::read_from(reader)?)),
438        FileType::Midi => Ok(Entity::Midi(midi::Midi::read_from(reader)?)),
439        FileType::Cne3 => Ok(Entity::Cne3(cn3::Cne3::read_from(reader)?)),
440        FileType::SampleProject => Ok(Entity::SampleProject(nsmpproj::Project::read_from(reader)?)),
441        FileType::Cbin => read_cbin(reader, header.format.as_str()),
442        e => Err(ParseError::UnknownFileType(e.as_str().to_string()).into()),
443    }
444}
445
446/// One CBIN file, dispatched by the tag at offset 8.
447fn read_cbin(reader: &mut (impl Read + Seek), tag: &str) -> Result<Entity, Error> {
448    use Entity as E;
449
450    Ok(match tag {
451        nsmp::FORMAT => {
452            let file: Cbin<nsmp::AnyBody> = cbin::read(reader, nsmp::FORMAT)?;
453            let header = file.header;
454            E::Sample(match file.body {
455                nsmp::AnyBody::V2(body) => Sample::V2(Cbin { header, body }),
456                nsmp::AnyBody::V3(body) => Sample::V3(Cbin { header, body }),
457            })
458        }
459        npno::FORMAT => E::Piano(npno::Piano::read_from(reader)?),
460        npip::pipe_library::FORMAT => E::PipeLibrary(npip::pipe_library::read_from(reader)?),
461        nsclassic::piano_library::FORMAT => {
462            E::PianoLibrary(nsclassic::piano_library::read_from(reader)?)
463        }
464
465        ne3::program::FORMAT => E::Program(Program::Electro3(ne3::program::read_from(reader)?)),
466        ne3::organ_preset::FORMAT => {
467            E::OrganPreset(OrganPreset::Electro3(ne3::organ_preset::read_from(reader)?))
468        }
469        ne4::program::FORMAT => E::Program(Program::Electro4(ne4::program::read_from(reader)?)),
470        ne4::live::FORMAT => E::Live(Live::Electro4(ne4::live::read_from(reader)?)),
471        ne4::settings::FORMAT => E::Settings(Settings::Electro4(ne4::settings::read_from(reader)?)),
472        ne5::program::FORMAT => E::Program(Program::Electro5(ne5::program::read_from(reader)?)),
473        ne5::live::FORMAT => E::Live(Live::Electro5(ne5::live::read_from(reader)?)),
474        ne5::song::FORMAT => E::Song(Song::Electro5(ne5::song::read_from(reader)?)),
475        ne5::settings::FORMAT => E::Settings(Settings::Electro5(ne5::settings::read_from(reader)?)),
476        ne6::program::FORMAT => E::Program(Program::Electro6(ne6::program::read_from(reader)?)),
477        ne6::live::FORMAT => E::Live(Live::Electro6(ne6::live::read_from(reader)?)),
478        ne6::settings::FORMAT => E::Settings(Settings::Electro6(ne6::settings::read_from(reader)?)),
479        ne7::program::FORMAT => E::Program(Program::Electro7(ne7::program::read_from(reader)?)),
480        ne7::live::FORMAT => E::Live(Live::Electro7(ne7::live::read_from(reader)?)),
481        ne7::settings::FORMAT => E::Settings(Settings::Electro7(ne7::settings::read_from(reader)?)),
482
483        nsclassic::program::FORMAT => E::Program(Program::StageClassic(
484            nsclassic::program::read_from(reader)?,
485        )),
486        nsclassic::synth::FORMAT => {
487            E::Synth(Synth::StageClassic(nsclassic::synth::read_from(reader)?))
488        }
489        ns2::program::FORMAT => E::Program(Program::Stage2(ns2::program::read_from(reader)?)),
490        ns2::live::FORMAT => E::Live(Live::Stage2(ns2::live::read_from(reader)?)),
491        ns2::synth::FORMAT => E::Synth(Synth::Stage2(ns2::synth::read_from(reader)?)),
492        ns2::settings::FORMAT => E::Settings(Settings::Stage2(ns2::settings::read_from(reader)?)),
493        ns3::program::FORMAT => E::Program(Program::Stage3(ns3::program::read_from(reader)?)),
494        ns3::live::FORMAT => E::Live(Live::Stage3(ns3::live::read_from(reader)?)),
495        ns3::song::FORMAT => E::Song(Song::Stage3(ns3::song::read_from(reader)?)),
496        ns3::synth::FORMAT => E::Synth(Synth::Stage3(ns3::synth::read_from(reader)?)),
497        ns3::settings::FORMAT => E::Settings(Settings::Stage3(ns3::settings::read_from(reader)?)),
498        ns4::program::FORMAT => E::Program(Program::Stage4(ns4::program::read_from(reader)?)),
499        ns4::live::FORMAT => E::Live(Live::Stage4(ns4::live::read_from(reader)?)),
500        ns4::synth::FORMAT => E::Synth(Synth::Stage4(ns4::synth::read_from(reader)?)),
501        ns4::piano_preset::FORMAT => {
502            E::PianoPreset(PianoPreset::Stage4(ns4::piano_preset::read_from(reader)?))
503        }
504        ns4::organ_preset::FORMAT => {
505            E::OrganPreset(OrganPreset::Stage4(ns4::organ_preset::read_from(reader)?))
506        }
507        ns4::settings::FORMAT => E::Settings(Settings::Stage4(ns4::settings::read_from(reader)?)),
508
509        np::program::FORMAT => E::Program(Program::Piano1(np::program::read_from(reader)?)),
510        np::live::FORMAT => E::Live(Live::Piano1(np::live::read_from(reader)?)),
511        np::settings::FORMAT => E::Settings(Settings::Piano1(np::settings::read_from(reader)?)),
512        np2::program::FORMAT => E::Program(Program::Piano2(np2::program::read_from(reader)?)),
513        np2::live::FORMAT => E::Live(Live::Piano2(np2::live::read_from(reader)?)),
514        np2::settings::FORMAT => E::Settings(Settings::Piano2(np2::settings::read_from(reader)?)),
515        np3::program::FORMAT => E::Program(Program::Piano3(np3::program::read_from(reader)?)),
516        np3::live::FORMAT => E::Live(Live::Piano3(np3::live::read_from(reader)?)),
517        np3::settings::FORMAT => E::Settings(Settings::Piano3(np3::settings::read_from(reader)?)),
518        np4::program::FORMAT => E::Program(Program::Piano4(np4::program::read_from(reader)?)),
519        np4::live::FORMAT => E::Live(Live::Piano4(np4::live::read_from(reader)?)),
520        np4::settings::FORMAT => E::Settings(Settings::Piano4(np4::settings::read_from(reader)?)),
521        np5::program::FORMAT => E::Program(Program::Piano5(np5::program::read_from(reader)?)),
522        np5::live::FORMAT => E::Live(Live::Piano5(np5::live::read_from(reader)?)),
523        np5::settings::FORMAT => E::Settings(Settings::Piano5(np5::settings::read_from(reader)?)),
524        ng2::program::FORMAT => E::Program(Program::Grand(ng2::program::read_from(reader)?)),
525        ng2::live::FORMAT => E::Live(Live::Grand(ng2::live::read_from(reader)?)),
526        ng2::settings::FORMAT => E::Settings(Settings::Grand(ng2::settings::read_from(reader)?)),
527
528        nw::program::FORMAT => E::Program(Program::Wave(nw::program::read_from(reader)?)),
529        nw::settings::FORMAT => E::Settings(Settings::Wave(nw::settings::read_from(reader)?)),
530        nw2::program::FORMAT => E::Program(Program::Wave2(nw2::program::read_from(reader)?)),
531        nw2::live::FORMAT => E::Live(Live::Wave2(nw2::live::read_from(reader)?)),
532        nw2::settings::FORMAT => E::Settings(Settings::Wave2(nw2::settings::read_from(reader)?)),
533
534        nc2::program::FORMAT => E::Program(Program::C2(nc2::program::read_from(reader)?)),
535        nc2::settings::FORMAT => E::Settings(Settings::C2(nc2::settings::read_from(reader)?)),
536        nc2d::program::FORMAT => E::Program(Program::C2D(nc2d::program::read_from(reader)?)),
537        nc2d::settings::FORMAT => E::Settings(Settings::C2D(nc2d::settings::read_from(reader)?)),
538        no3::program::FORMAT => E::Program(Program::Organ3(no3::program::read_from(reader)?)),
539        no3::settings::FORMAT => E::Settings(Settings::Organ3(no3::settings::read_from(reader)?)),
540
541        // Leads (the CBIN generation; the older Leads ship SysEx).
542        nl4::program::FORMAT => E::Program(Program::Lead4(nl4::program::read_from(reader)?)),
543        nl4::performance::FORMAT => {
544            E::Performance(Performance::Lead4(nl4::performance::read_from(reader)?))
545        }
546        nl4::settings::FORMAT => E::Settings(Settings::Lead4(nl4::settings::read_from(reader)?)),
547        nla1::program::FORMAT => E::Program(Program::LeadA1(nla1::program::read_from(reader)?)),
548        nla1::performance::FORMAT => {
549            E::Performance(Performance::LeadA1(nla1::performance::read_from(reader)?))
550        }
551        nla1::settings::FORMAT => E::Settings(Settings::LeadA1(nla1::settings::read_from(reader)?)),
552
553        nd2::program::FORMAT => E::Program(Program::Drum2(nd2::program::read_from(reader)?)),
554        nd3::kit::FORMAT => E::Program(Program::Drum3(nd3::kit::read_from(reader)?)),
555
556        e => return Err(ParseError::UnknownFormat(e.to_string()).into()),
557    })
558}
559
560/// Which archive a ZIP is, from the members the walks below will see.
561#[cfg(feature = "bundle")]
562enum ZipKind {
563    Electro5,
564    Drum2,
565    Drum3,
566    Members,
567}
568
569/// One ZIP file: an Electro 5 bundle or backup (it carries a `meta.xml`
570/// manifest), or a Drum bank (members are all one CBIN format).
571#[cfg(feature = "bundle")]
572fn read_zip(reader: &mut (impl Read + Seek)) -> Result<Entity, Error> {
573    let start = reader.stream_position()?;
574    let kind = {
575        let zip = zip::ZipArchive::new(&mut *reader)?;
576        // The entries the walks skip are not members: a directory holds no file, and a
577        // backup manifest describes the archive. Classifying on them would call an
578        // archive of directories a bundle of none, and a `kits/` entry would stop a drum
579        // bank being one.
580        let names: Vec<&str> = zip
581            .file_names()
582            .filter(|name| !is_dir_entry(name) && !name.ends_with("meta.xml"))
583            .collect();
584        // An archive with nothing in it would satisfy the all-members checks below
585        // vacuously and read as a drum bank holding no programs.
586        if names.is_empty() {
587            return Err(ParseError::AssertFail("the archive holds no members".into()).into());
588        }
589        // ⚠️ `meta.xml` is shared across product families; only `.ne5*` members identify
590        // an Electro 5 bundle.
591        if names.iter().any(|n| {
592            std::path::Path::new(n)
593                .extension()
594                .is_some_and(|e| e.to_string_lossy().starts_with("ne5"))
595        }) {
596            ZipKind::Electro5
597        } else if names.iter().all(|n| n.ends_with(".nd2p")) {
598            ZipKind::Drum2
599        } else if names.iter().all(|n| n.ends_with(".nd3k")) {
600            ZipKind::Drum3
601        } else {
602            // Anything else — a bundle only if every member is a CBIN file,
603            // which `zip_raw_members` decides below.
604            ZipKind::Members
605        }
606    };
607    reader.seek(std::io::SeekFrom::Start(start))?;
608
609    Ok(Entity::Bundle(match kind {
610        ZipKind::Drum2 => Bundle::Drum2Bank(nd2::bank::read_from(reader)?),
611        ZipKind::Drum3 => Bundle::Drum3KitBank(nd3::kit_bank::read_from(reader)?),
612        ZipKind::Members => Bundle::Members(formats::zip_raw_members(reader)?),
613        ZipKind::Electro5 => Bundle::Electro5(ne5::Bundle::read_from(reader)?),
614    }))
615}
616
617/// A directory entry, spelled as `zip`'s own `is_dir` spells it — the name alone, since
618/// classification reads the archive's names rather than its entries.
619#[cfg(feature = "bundle")]
620fn is_dir_entry(name: &str) -> bool {
621    name.ends_with('/') || name.ends_with('\\')
622}
623
624/// [`from_stream`] over a buffered read of the file at `path`.
625pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Entity, Error> {
626    from_stream(&mut BufReader::new(File::open(path)?))
627}
628
629#[cfg(test)]
630mod registry_tests {
631    use super::*;
632
633    /// A registry read and written through the entity lands on the same field the
634    /// body's own accessors reach, so neither consumer needs to name the body type.
635    #[test]
636    fn the_entity_registry_reads_and_writes_the_body() {
637        let mut entity = Entity::Program(Program::Electro5(ne5::program::new(
638            (0, 0).try_into().unwrap(),
639        )));
640
641        let before = entity.registry().unwrap().fields();
642        assert!(before.iter().any(|f| f.path == "center_panel.transpose"));
643
644        entity
645            .registry_mut()
646            .unwrap()
647            .set_field("center_panel.transpose", "-5")
648            .unwrap();
649        let after = entity.registry().unwrap().fields();
650        let transpose = after
651            .iter()
652            .find(|f| f.path == "center_panel.transpose")
653            .unwrap();
654        assert_eq!(transpose.value, "-5");
655    }
656
657    /// A stub-backed entity has no registry, and says so the same way in both
658    /// directions.
659    #[test]
660    fn a_stub_has_no_registry() {
661        let file = Cbin {
662            header: cbin::Header::new("ne6p", (0, 0), 1),
663            body: RawBody(vec![0; 16]),
664        };
665        let mut entity = Entity::Program(Program::Electro6(file));
666        assert!(entity.registry().is_none());
667        assert!(entity.registry_mut().is_none());
668    }
669
670    /// A song's fields are private, so its registry would list nothing — it is
671    /// deliberately not a registry entity, and `Song::set` is its editing surface.
672    #[test]
673    fn a_song_is_not_a_registry_entity() {
674        let song = ne5::song::new(
675            (0, 0).try_into().unwrap(),
676            ne5::song::DEFAULT_VERSION,
677            [(0, 0).try_into().unwrap(); 4],
678        )
679        .unwrap();
680        assert!(Entity::Song(Song::Electro5(song)).registry().is_none());
681    }
682}
683
684#[cfg(all(test, feature = "bundle"))]
685mod bundle_tests {
686    use super::*;
687    use crate::cbin::{Cbin, Header, RawBody};
688    use std::io::{Cursor, Write};
689
690    fn member(tag: &str) -> Vec<u8> {
691        let file = Cbin {
692            header: Header::new(tag, (0, 0), 4),
693            body: RawBody(vec![0x5A; 16]),
694        };
695        let mut out = Cursor::new(Vec::new());
696        file.write_to(&mut out).unwrap();
697        out.into_inner()
698    }
699
700    /// A stored archive of `members`; a name ending in `/` becomes a directory entry.
701    fn archive(members: &[(&str, &[u8])]) -> Vec<u8> {
702        let mut zip = zip::ZipWriter::new(Cursor::new(Vec::new()));
703        let stored = zip::write::SimpleFileOptions::default()
704            .compression_method(zip::CompressionMethod::Stored);
705        for (name, bytes) in members {
706            match name.strip_suffix('/') {
707                Some(directory) => zip.add_directory(directory, stored).unwrap(),
708                None => {
709                    zip.start_file(name.to_string(), stored).unwrap();
710                    zip.write_all(bytes).unwrap();
711                }
712            }
713        }
714        zip.finish().unwrap().into_inner()
715    }
716
717    /// A ZIP of mixed CBIN members — the reported family bundle shape — reads
718    /// as [`Bundle::Members`] with paths preserved.
719    #[test]
720    fn a_zip_of_mixed_cbin_members_is_a_bundle() {
721        let a = member("ns3f");
722        let b = member("ns3y");
723        let bytes = archive(&[("Bank A/One.ns3f", &a), ("presets/Two.ns3y", &b)]);
724
725        let entity = from_stream(&mut Cursor::new(bytes)).unwrap();
726        let Entity::Bundle(Bundle::Members(members)) = entity else {
727            panic!("decoded to something other than a member bundle");
728        };
729        assert_eq!(members.len(), 2);
730        assert_eq!(members[0].0, "Bank A/One.ns3f");
731        assert_eq!(&members[0].1.header.tag, b"ns3f");
732        assert_eq!(&members[1].1.header.tag, b"ns3y");
733    }
734
735    /// An empty archive satisfies every all-members check vacuously, so it has to be
736    /// refused up front rather than read as a drum bank holding no programs.
737    #[test]
738    fn an_empty_zip_is_refused() {
739        let bytes = archive(&[]);
740        assert!(from_stream(&mut Cursor::new(bytes)).is_err());
741    }
742
743    /// A directory entry holds no file and a manifest describes the archive, so an
744    /// archive of nothing else holds no members — the same refusal as an empty one,
745    /// rather than a bundle of none.
746    #[test]
747    fn a_zip_of_directories_and_a_manifest_is_refused() {
748        let bytes = archive(&[("kits/", b""), ("meta.xml", b"<meta/>")]);
749        let err = from_stream(&mut Cursor::new(bytes)).unwrap_err();
750        assert!(
751            err.to_string().contains("no members"),
752            "refused for the wrong reason: {err}"
753        );
754    }
755
756    /// A backup's directory entries are not members, so they do not stop a bank whose
757    /// files are all one CBIN format being read as that bank.
758    #[test]
759    fn a_directory_entry_does_not_hide_a_drum_bank() {
760        let program = member("nd2p");
761        let bytes = archive(&[("kits/", b""), ("kits/One.nd2p", &program)]);
762        let entity = from_stream(&mut Cursor::new(bytes)).unwrap();
763        assert!(
764            matches!(entity, Entity::Bundle(Bundle::Drum2Bank(_))),
765            "a `kits/` entry left it classified as {}",
766            entity.identity().kind,
767        );
768    }
769
770    /// A ZIP holding anything that is not a CBIN file is not a bundle.
771    #[test]
772    fn a_zip_with_a_non_cbin_member_is_refused() {
773        let a = member("ns3f");
774        let bytes = archive(&[("One.ns3f", &a), ("readme.txt", b"hello")]);
775        assert!(from_stream(&mut Cursor::new(bytes)).is_err());
776    }
777
778    #[test]
779    fn a_zip_is_read_from_the_callers_current_position() {
780        let member = member("ns3f");
781        let bytes = archive(&[("Bank A/One.ns3f", &member)]);
782        let prefix_len = 7;
783        let mut prefixed = vec![0xa5; prefix_len];
784        prefixed.extend(bytes);
785        let mut reader = Cursor::new(prefixed);
786        reader.set_position(prefix_len as u64);
787
788        let entity = from_stream(&mut reader).unwrap();
789        assert!(matches!(entity, Entity::Bundle(Bundle::Members(_))));
790    }
791}
792
793/// Serialize an [`Entity`] back to the bytes of its file — the counterpart to
794/// [`from_stream`].
795///
796/// For every format this crate reads, `to_bytes(from_stream(x)) == x` byte-for-byte,
797/// whichever header generation `x` carries. That is the crate's central invariant —
798/// decoded values are read-only views over a verbatim body, so a re-emit cannot
799/// drift — and `nord verify` exists to check it against real specimens. Fixed-length
800/// formats declare their body length on their [`cbin::Body`] impl, and the container
801/// refuses to emit a wrong-sized file.
802///
803/// Bundles are unsupported: a bundle is a ZIP walk over other entities, not a
804/// re-emittable structure.
805pub fn to_bytes(entity: &Entity) -> Result<Vec<u8>, Error> {
806    use std::io::Cursor;
807
808    let mut out = Cursor::new(Vec::new());
809    entity.write_to(&mut out)?;
810    Ok(out.into_inner())
811}
812
813/// What an entity is: a human label and the format tag its file carries.
814#[derive(Debug, Clone, Copy, PartialEq, Eq)]
815pub struct Identity {
816    /// `"Electro 6 program"` — model then role, as the summary prints it.
817    pub kind: &'static str,
818    /// The CBIN tag, or the carrier name (`zip`, `syx`, `mid`, `cn3`).
819    pub format: &'static str,
820}
821
822macro_rules! registry_bodies {
823    ($($body:ty),* $(,)?) => {$(
824        impl fields::Registry for Cbin<$body> {
825            fn fields(&self) -> Vec<fields::Field> {
826                self.body.fields()
827            }
828            fn field_values(&self) -> Vec<fields::FieldValue> {
829                self.body.field_values()
830            }
831            fn set_field(&mut self, path: &str, value: &str) -> Result<(), fields::FieldError> {
832                self.body.set_field(path, value)
833            }
834        }
835    )*};
836}
837
838registry_bodies!(
839    ne5::Program,
840    ne5::Settings,
841    ns2::Program,
842    ns3::Program,
843    ns3::SynthPreset,
844    ns4::Program,
845    ns4::organ_preset::OrganPreset,
846    ns4::piano_preset::PianoPreset,
847    ns4::synth::SynthPreset,
848);
849
850/// The registry-declaring entities, read through `&` or `&mut` as asked. One
851/// list serving both directions. The live buffer is the program body under
852/// another tag, so the two share an arm. `ne5::Song` declares no public
853/// fields — its registry would be empty, so it is not one of these.
854macro_rules! with_registry {
855    ($entity:expr, $($reference:tt)*) => {
856        match $entity {
857            Entity::Program(Program::Electro5(f)) | Entity::Live(Live::Electro5(f)) => {
858                Some(f as $($reference)* dyn fields::Registry)
859            }
860            Entity::Program(Program::Stage2(f)) | Entity::Live(Live::Stage2(f)) => {
861                Some(f as $($reference)* dyn fields::Registry)
862            }
863            Entity::Program(Program::Stage3(f)) | Entity::Live(Live::Stage3(f)) => {
864                Some(f as $($reference)* dyn fields::Registry)
865            }
866            Entity::Program(Program::Stage4(f)) | Entity::Live(Live::Stage4(f)) => {
867                Some(f as $($reference)* dyn fields::Registry)
868            }
869            Entity::Settings(Settings::Electro5(f)) => Some(f as $($reference)* dyn fields::Registry),
870            Entity::Synth(Synth::Stage3(f)) => Some(f as $($reference)* dyn fields::Registry),
871            Entity::Synth(Synth::Stage4(f)) => Some(f as $($reference)* dyn fields::Registry),
872            Entity::OrganPreset(OrganPreset::Stage4(f)) => {
873                Some(f as $($reference)* dyn fields::Registry)
874            }
875            Entity::PianoPreset(PianoPreset::Stage4(f)) => {
876                Some(f as $($reference)* dyn fields::Registry)
877            }
878            _ => None,
879        }
880    };
881}
882
883impl Entity {
884    /// The container of a stub-backed entity — every variant whose body is
885    /// container-verified but undecoded. `None` for the decoded formats and the
886    /// non-CBIN carriers.
887    pub fn raw(&self) -> Option<&Cbin<RawBody>> {
888        use {Live as L, OrganPreset as OP, Program as P, Settings as St, Synth as Sy};
889        match self {
890            Entity::Program(
891                P::C2(f)
892                | P::C2D(f)
893                | P::Drum2(f)
894                | P::Drum3(f)
895                | P::Electro3(f)
896                | P::Electro4(f)
897                | P::Electro6(f)
898                | P::Electro7(f)
899                | P::Grand(f)
900                | P::Lead4(f)
901                | P::LeadA1(f)
902                | P::Organ3(f)
903                | P::Piano1(f)
904                | P::Piano2(f)
905                | P::Piano3(f)
906                | P::Piano4(f)
907                | P::Piano5(f)
908                | P::StageClassic(f)
909                | P::Wave(f)
910                | P::Wave2(f),
911            )
912            | Entity::Live(
913                L::Electro4(f)
914                | L::Electro6(f)
915                | L::Electro7(f)
916                | L::Grand(f)
917                | L::Piano1(f)
918                | L::Piano2(f)
919                | L::Piano3(f)
920                | L::Piano4(f)
921                | L::Piano5(f)
922                | L::Wave2(f),
923            )
924            | Entity::Settings(
925                St::C2(f)
926                | St::C2D(f)
927                | St::Electro4(f)
928                | St::Electro6(f)
929                | St::Electro7(f)
930                | St::Grand(f)
931                | St::Lead4(f)
932                | St::LeadA1(f)
933                | St::Organ3(f)
934                | St::Piano1(f)
935                | St::Piano2(f)
936                | St::Piano3(f)
937                | St::Piano4(f)
938                | St::Piano5(f)
939                | St::Stage2(f)
940                | St::Stage3(f)
941                | St::Stage4(f)
942                | St::Wave(f)
943                | St::Wave2(f),
944            )
945            | Entity::Song(Song::Stage3(f))
946            | Entity::Synth(Sy::Stage2(f) | Sy::StageClassic(f))
947            | Entity::Performance(Performance::Lead4(f) | Performance::LeadA1(f))
948            | Entity::OrganPreset(OP::Electro3(f))
949            | Entity::PianoLibrary(f)
950            | Entity::PipeLibrary(f) => Some(f),
951            _ => None,
952        }
953    }
954
955    /// The generated field registry behind this entity, for reading.
956    /// `None` for the container-verified stubs and the non-panel carriers.
957    pub fn registry(&self) -> Option<&dyn fields::Registry> {
958        with_registry!(self, &)
959    }
960
961    /// The registry again, for setting fields. The same bodies answer both:
962    /// a body that lists its fields but refuses to set them cannot be
963    /// declared here.
964    pub fn registry_mut(&mut self) -> Option<&mut dyn fields::Registry> {
965        with_registry!(self, &mut)
966    }
967
968    /// The entity's [`Identity`]: its human label and the tag its file carries.
969    pub fn identity(&self) -> Identity {
970        use {Live as L, Program as P, Settings as St};
971        let id = |kind, format| Identity { kind, format };
972        match self {
973            Entity::Program(p) => match p {
974                P::C2(_) => id("C2 program", nc2::program::FORMAT),
975                P::C2D(_) => id("C2D program", nc2d::program::FORMAT),
976                P::Drum2(_) => id("Drum 2 program", nd2::program::FORMAT),
977                P::Drum3(_) => id("Drum 3P kit", nd3::kit::FORMAT),
978                P::Electro3(_) => id("Electro 3 program", ne3::program::FORMAT),
979                P::Electro4(_) => id("Electro 4 program", ne4::program::FORMAT),
980                P::Electro5(_) => id("Electro 5 program", ne5::program::FORMAT),
981                P::Electro6(_) => id("Electro 6 program", ne6::program::FORMAT),
982                P::Electro7(_) => id("Electro 7 program", ne7::program::FORMAT),
983                P::Grand(_) => id("Grand program", ng2::program::FORMAT),
984                P::Lead4(_) => id("Lead 4 program", nl4::program::FORMAT),
985                P::LeadA1(_) => id("Lead A1 program", nla1::program::FORMAT),
986                P::Organ3(_) => id("no3 organ program", no3::program::FORMAT),
987                P::Piano1(_) => id("Piano program", np::program::FORMAT),
988                P::Piano2(_) => id("Piano 2 program", np2::program::FORMAT),
989                P::Piano3(_) => id("Piano 3 program", np3::program::FORMAT),
990                P::Piano4(_) => id("Piano 4 program", np4::program::FORMAT),
991                P::Piano5(_) => id("Piano 5 program", np5::program::FORMAT),
992                P::Stage2(_) => id("Stage 2 program", ns2::program::FORMAT),
993                P::Stage3(_) => id("Stage 3 program", ns3::program::FORMAT),
994                P::Stage4(_) => id("Stage 4 program", ns4::program::FORMAT),
995                P::StageClassic(_) => id("Stage Classic program", nsclassic::program::FORMAT),
996                P::Wave(_) => id("Wave program", nw::program::FORMAT),
997                P::Wave2(_) => id("Wave 2 program", nw2::program::FORMAT),
998            },
999            Entity::Live(l) => match l {
1000                L::Electro4(_) => id("Electro 4 live slot", ne4::live::FORMAT),
1001                L::Electro5(_) => id("Electro 5 live slot", ne5::live::FORMAT),
1002                L::Electro6(_) => id("Electro 6 live slot", ne6::live::FORMAT),
1003                L::Electro7(_) => id("Electro 7 live slot", ne7::live::FORMAT),
1004                L::Grand(_) => id("Grand live slot", ng2::live::FORMAT),
1005                L::Piano1(_) => id("Piano live slot", np::live::FORMAT),
1006                L::Piano2(_) => id("Piano 2 live slot", np2::live::FORMAT),
1007                L::Piano3(_) => id("Piano 3 live slot", np3::live::FORMAT),
1008                L::Piano4(_) => id("Piano 4 live slot", np4::live::FORMAT),
1009                L::Piano5(_) => id("Piano 5 live slot", np5::live::FORMAT),
1010                L::Stage2(_) => id("Stage 2 live slot", ns2::live::FORMAT),
1011                L::Stage3(_) => id("Stage 3 live slot", ns3::live::FORMAT),
1012                L::Stage4(_) => id("Stage 4 live slot", ns4::live::FORMAT),
1013                L::Wave2(_) => id("Wave 2 live slot", nw2::live::FORMAT),
1014            },
1015            Entity::Settings(s) => match s {
1016                St::C2(_) => id("C2 settings", nc2::settings::FORMAT),
1017                St::C2D(_) => id("C2D settings", nc2d::settings::FORMAT),
1018                St::Electro4(_) => id("Electro 4 settings", ne4::settings::FORMAT),
1019                St::Electro5(_) => id("Electro 5 settings", ne5::settings::FORMAT),
1020                St::Electro6(_) => id("Electro 6 settings", ne6::settings::FORMAT),
1021                St::Electro7(_) => id("Electro 7 settings", ne7::settings::FORMAT),
1022                St::Grand(_) => id("Grand settings", ng2::settings::FORMAT),
1023                St::Lead4(_) => id("Lead 4 settings", nl4::settings::FORMAT),
1024                St::LeadA1(_) => id("Lead A1 settings", nla1::settings::FORMAT),
1025                St::Organ3(_) => id("no3 organ settings", no3::settings::FORMAT),
1026                St::Piano1(_) => id("Piano settings", np::settings::FORMAT),
1027                St::Piano2(_) => id("Piano 2 settings", np2::settings::FORMAT),
1028                St::Piano3(_) => id("Piano 3 settings", np3::settings::FORMAT),
1029                St::Piano4(_) => id("Piano 4 settings", np4::settings::FORMAT),
1030                St::Piano5(_) => id("Piano 5 settings", np5::settings::FORMAT),
1031                St::Stage2(_) => id("Stage 2 settings", ns2::settings::FORMAT),
1032                St::Stage3(_) => id("Stage 3 settings", ns3::settings::FORMAT),
1033                St::Stage4(_) => id("Stage 4 settings", ns4::settings::FORMAT),
1034                St::Wave(_) => id("Wave settings", nw::settings::FORMAT),
1035                St::Wave2(_) => id("Wave 2 settings", nw2::settings::FORMAT),
1036            },
1037            Entity::Song(Song::Electro5(_)) => id("Electro 5 song / set", ne5::song::FORMAT),
1038            Entity::Song(Song::Stage3(_)) => id("Stage 3 song", ns3::song::FORMAT),
1039            Entity::Synth(Synth::Stage2(_)) => id("Stage 2 synth patch", ns2::synth::FORMAT),
1040            Entity::Synth(Synth::Stage3(_)) => id("Stage 3 synth patch", ns3::synth::FORMAT),
1041            Entity::Synth(Synth::Stage4(_)) => id("Stage 4 synth preset", ns4::synth::FORMAT),
1042            Entity::Synth(Synth::StageClassic(_)) => {
1043                id("Stage Classic synth patch", nsclassic::synth::FORMAT)
1044            }
1045            Entity::Performance(Performance::Lead4(_)) => {
1046                id("Lead 4 performance", nl4::performance::FORMAT)
1047            }
1048            Entity::Performance(Performance::LeadA1(_)) => {
1049                id("Lead A1 performance", nla1::performance::FORMAT)
1050            }
1051            Entity::OrganPreset(OrganPreset::Electro3(_)) => {
1052                id("Electro 3 organ preset", ne3::organ_preset::FORMAT)
1053            }
1054            Entity::OrganPreset(OrganPreset::Stage4(_)) => {
1055                id("Stage 4 organ preset", ns4::organ_preset::FORMAT)
1056            }
1057            Entity::PianoPreset(PianoPreset::Stage4(_)) => {
1058                id("Stage 4 piano preset", ns4::piano_preset::FORMAT)
1059            }
1060            Entity::Piano(_) => id("piano library", npno::FORMAT),
1061            Entity::PianoLibrary(_) => id(
1062                "Stage Classic piano library",
1063                nsclassic::piano_library::FORMAT,
1064            ),
1065            Entity::PipeLibrary(_) => id("C2 pipe library", npip::pipe_library::FORMAT),
1066            Entity::Sample(Sample::V2(_)) => id("sample instrument", nsmp::FORMAT),
1067            Entity::Sample(Sample::V3(_)) => id("sample instrument (nsmp3/nsmp4)", nsmp::FORMAT),
1068            Entity::SampleProject(_) => id("Sample Editor project", nsmpproj::FORMAT),
1069            Entity::Sysex(_) => id("SysEx dump", "syx"),
1070            Entity::Midi(_) => id("MIDI file", "mid"),
1071            Entity::Cne3(_) => id("Electro 2 library", "cn3"),
1072            #[cfg(feature = "bundle")]
1073            Entity::Bundle(_) => id("bundle", "zip"),
1074        }
1075    }
1076
1077    /// Re-encode to `w`, byte-exact for anything read and unedited.
1078    ///
1079    /// Bundles are the one exception: the archive layer does not re-encode, so a
1080    /// bundle refuses rather than writing something almost like its source.
1081    pub fn write_to(&self, w: &mut (impl std::io::Write + Seek)) -> Result<(), Error> {
1082        match self {
1083            Entity::Cne3(f) => f.write_to(w),
1084            Entity::Live(l) => match l {
1085                Live::Electro4(f)
1086                | Live::Electro6(f)
1087                | Live::Electro7(f)
1088                | Live::Grand(f)
1089                | Live::Piano1(f)
1090                | Live::Piano2(f)
1091                | Live::Piano3(f)
1092                | Live::Piano4(f)
1093                | Live::Piano5(f)
1094                | Live::Wave2(f) => f.write_to(w),
1095                Live::Electro5(f) => f.write_to(w),
1096                Live::Stage4(f) => f.write_to(w),
1097                Live::Stage2(f) => f.write_to(w),
1098                Live::Stage3(f) => f.write_to(w),
1099            },
1100            Entity::Midi(f) => f.write_to(w),
1101            Entity::OrganPreset(OrganPreset::Electro3(f))
1102            | Entity::PianoLibrary(f)
1103            | Entity::PipeLibrary(f) => f.write_to(w),
1104            Entity::OrganPreset(OrganPreset::Stage4(f)) => f.write_to(w),
1105            Entity::PianoPreset(PianoPreset::Stage4(f)) => f.write_to(w),
1106            Entity::Piano(f) => f.write_to(w),
1107            Entity::Performance(Performance::Lead4(f))
1108            | Entity::Performance(Performance::LeadA1(f)) => f.write_to(w),
1109            Entity::Program(p) => match p {
1110                Program::C2(f)
1111                | Program::C2D(f)
1112                | Program::Drum2(f)
1113                | Program::Drum3(f)
1114                | Program::Electro3(f)
1115                | Program::Electro4(f)
1116                | Program::Electro6(f)
1117                | Program::Electro7(f)
1118                | Program::Grand(f)
1119                | Program::Lead4(f)
1120                | Program::LeadA1(f)
1121                | Program::Organ3(f)
1122                | Program::Piano1(f)
1123                | Program::Piano2(f)
1124                | Program::Piano3(f)
1125                | Program::Piano4(f)
1126                | Program::Piano5(f)
1127                | Program::StageClassic(f)
1128                | Program::Wave(f)
1129                | Program::Wave2(f) => f.write_to(w),
1130                Program::Electro5(f) => f.write_to(w),
1131                Program::Stage2(f) => f.write_to(w),
1132                Program::Stage3(f) => f.write_to(w),
1133                Program::Stage4(f) => f.write_to(w),
1134            },
1135            Entity::Sample(Sample::V2(f)) => f.write_to(w),
1136            Entity::Sample(Sample::V3(f)) => f.write_to(w),
1137            Entity::SampleProject(f) => f.write_to(w),
1138            Entity::Settings(s) => match s {
1139                Settings::C2(f)
1140                | Settings::C2D(f)
1141                | Settings::Electro4(f)
1142                | Settings::Electro6(f)
1143                | Settings::Electro7(f)
1144                | Settings::Grand(f)
1145                | Settings::Lead4(f)
1146                | Settings::LeadA1(f)
1147                | Settings::Organ3(f)
1148                | Settings::Piano1(f)
1149                | Settings::Piano2(f)
1150                | Settings::Piano3(f)
1151                | Settings::Piano4(f)
1152                | Settings::Piano5(f)
1153                | Settings::Stage2(f)
1154                | Settings::Stage3(f)
1155                | Settings::Stage4(f)
1156                | Settings::Wave(f)
1157                | Settings::Wave2(f) => f.write_to(w),
1158                Settings::Electro5(f) => f.write_to(w),
1159            },
1160            Entity::Song(Song::Electro5(f)) => f.write_to(w),
1161            Entity::Song(Song::Stage3(f)) => f.write_to(w),
1162            Entity::Synth(Synth::Stage2(f)) | Entity::Synth(Synth::StageClassic(f)) => {
1163                f.write_to(w)
1164            }
1165            Entity::Synth(Synth::Stage3(f)) => f.write_to(w),
1166            Entity::Synth(Synth::Stage4(f)) => f.write_to(w),
1167            Entity::Sysex(f) => f.write_to(w),
1168            #[cfg(feature = "bundle")]
1169            Entity::Bundle(_) => Err(ParseError::AssertFail(
1170                "bundles are archives; re-encoding one is not supported".into(),
1171            )
1172            .into()),
1173        }
1174    }
1175}