Skip to main content

nord_format/formats/nsmp/
mod.rs

1//! Sample instruments (`.nsmp`) — the Nord Sample Library format.
2//!
3//! Shared across the Nord line rather than specific to one model, so it carries its own
4//! tag rather than a model's. A file is the CBIN header followed by a chain of tagged
5//! [`section`]s: an `hdr` carrying the name, a `cat` of category strings, a `map`
6//! ending in the [`zone`] table, one [`stroke`] per zone, and a trailing `sty`.
7//!
8//! **Strokes are stored verbatim**, so this reads and rewrites instruments byte-exactly
9//! and can retune, rename and remap them without touching a byte of audio, in either
10//! chain. The [`codec`] decodes that audio to samples in every generation — it is one
11//! codec in three sets of units, so a caller only picks the right [`codec::Layout`].
12//! [`encode`] builds a new instrument from PCM in all three generations.
13
14/// A zone and the stroke stream that plays it, ready for [`codec::decode`].
15pub struct ZoneAudio<'a> {
16    pub root_key: u8,
17    pub top_note: u8,
18    /// Lowest note, where the generation stores one. `None` where zones tile and
19    /// a zone's bottom is one above the next-lower zone's top.
20    pub low_note: Option<u8>,
21    /// The stream's offset from the start of the body, which is the base its own
22    /// word directory was written against.
23    pub at: usize,
24    pub stream: &'a [u8],
25}
26
27pub mod codec;
28pub mod encode;
29pub mod kernel;
30pub mod keymap;
31pub mod meta;
32pub mod section;
33pub mod stroke;
34pub mod sty;
35pub mod zone;
36
37pub use keymap::{KeyTable, Level};
38pub use meta::Meta;
39pub use section::Section;
40pub use stroke::Stroke;
41pub use sty::{velocity_level, EqBand, Sty, StyV2, StyV3};
42pub use zone::Zone;
43pub use zone::ZoneV3;
44
45use crate::cbin::{self, BodyReader, BodyWriter, Cbin, Header};
46use crate::error::{Error, ParseError};
47use std::fmt;
48use std::io::{Read, Seek, Write};
49
50pub const FORMAT: &str = "nsmp";
51
52/// The content version at which the body leaves the `NWS` chain for the wide
53/// `NSMP` chain. All generations share the `nsmp` tag; the u32 at `0x14` is the
54/// generation marker, running `format × 100 + revision` — `.nsmp3` content
55/// stores 300 and up, `.nsmp4` 400 and up.
56pub const V3_FROM_VERSION: u32 = 300;
57
58/// The content version at which the wide chain becomes v4. Same chain and the same
59/// stream units as v3 — what changes is the codec, so the number matters to
60/// [`codec::Layout`] rather than to the reader.
61pub const V4_FROM_VERSION: u32 = 400;
62
63/// Which section chain a body's sections form, and the shapes that follow from it.
64///
65/// The narrow chain has two schemas and the content version does not separate them —
66/// it tracks the library release, and releases on both sides of the change carry a
67/// spread of numbers. The gate is the `map` section's own version, which the other
68/// section versions agree with on every specimen.
69///
70/// Inferred from specimens; not confirmed on hardware.
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum Chain {
73    /// `NWS` 8 / `hdr` 8 / `map` 9 / `stk` 8 / `sty` 5, and no `cat` section at all.
74    /// The `hdr` is 18 bytes with no name field: these instruments carry no name,
75    /// and the library's filename is the only one they have.
76    Early,
77    /// `NWS` 11 / `hdr` 9 / `cat` 5 / `map` 10 / `stk` 9 / `sty` 5, the `hdr` naming
78    /// the instrument.
79    Library2,
80    /// The `NSMP` chain of the wide generations.
81    Wide,
82}
83
84impl Chain {
85    /// The narrow chain a `map` section version selects.
86    pub fn from_map_version(version: u8) -> Result<Chain, ParseError> {
87        match version {
88            keymap::VERSION_EARLY => Ok(Chain::Early),
89            keymap::VERSION => Ok(Chain::Library2),
90            other => Err(ParseError::AssertFail(format!(
91                "map section version {other} has no zone table layout derived from a specimen"
92            ))),
93        }
94    }
95
96    /// Bytes per zone record. [`Chain::Library2`] appends a flag and two zero bytes
97    /// to the twelve [`Chain::Early`] carries; every field they share is at the same
98    /// offset.
99    pub const fn zone_record_len(self) -> usize {
100        match self {
101            Chain::Early => 12,
102            Chain::Library2 | Chain::Wide => 15,
103        }
104    }
105
106    /// The chain [`encode`] emits for a stream layout. It writes the current schemas
107    /// only: [`Chain::Early`] is read, never produced.
108    pub const fn written_for(layout: codec::Layout) -> Chain {
109        match layout {
110            codec::Layout::V2 => Chain::Library2,
111            codec::Layout::V3 | codec::Layout::V4 => Chain::Wide,
112        }
113    }
114
115    /// Whether the `hdr` carries an instrument name.
116    pub const fn names_instrument(self) -> bool {
117        !matches!(self, Chain::Early)
118    }
119
120    /// Whether a looped stroke also sets the mark bit on the record its directory
121    /// points at. [`Chain::Early`] never does — the pointer alone marks the loop —
122    /// so a reader that requires the flag rejects those libraries outright.
123    pub const fn flags_the_marked_record(self) -> bool {
124        !matches!(self, Chain::Early)
125    }
126}
127
128/// A body decoded by generation: v2 in full, v3/v4 as a section chain with
129/// strokes verbatim.
130///
131/// ⚠️ The v2 pool also holds versions that are not `2xx` — 8 (the original
132/// Sample Library) and 200 (Sample Library 2.0) — so the gate is "at least
133/// 300", not "exactly 2xx". Inferred from specimens; not confirmed on hardware.
134/// The number tracks the library release rather than the codec. Reported by
135/// public documentation; not confirmed on hardware.
136#[derive(Debug)]
137pub enum AnyBody {
138    V2(Sample),
139    V3(SampleV3),
140}
141
142impl cbin::Body for AnyBody {
143    fn read<R: Read + Seek>(r: &mut BodyReader<'_, R>, header: &Header) -> Result<Self, Error> {
144        if header.version >= V3_FROM_VERSION {
145            Ok(AnyBody::V3(<SampleV3 as cbin::Body>::read(r, header)?))
146        } else {
147            Ok(AnyBody::V2(<Sample as cbin::Body>::read(r, header)?))
148        }
149    }
150
151    fn write<W: Write + Seek>(&self, w: &mut BodyWriter<'_, W>) -> Result<(), Error> {
152        match self {
153            AnyBody::V2(s) => <Sample as cbin::Body>::write(s, w),
154            AnyBody::V3(s) => <SampleV3 as cbin::Body>::write(s, w),
155        }
156    }
157}
158
159/// A fixed-width string field inside a `hdr` payload: where it starts, and where the
160/// next field does.
161///
162/// A `hdr` holds its strings NUL-terminated and zero-padded to the next field, never
163/// length-prefixed, so the field is the same size whatever it holds and the longest
164/// string it takes is its span less the terminator. The editor's own name box stops
165/// well short of that; shipped libraries do not.
166///
167/// Inferred from specimens; not confirmed on hardware.
168#[derive(Clone, Copy)]
169pub(super) struct StringField {
170    at: usize,
171    next: usize,
172}
173
174impl StringField {
175    /// The narrow chain's instrument name. The sub-name follows it.
176    pub(super) const NAME: StringField = StringField { at: 12, next: 44 };
177
178    /// The wide chain's main name, in both wide generations.
179    pub(super) const NAME_V3: StringField = StringField { at: 10, next: 76 };
180
181    /// Longest string this field holds, the terminator excluded.
182    pub(super) const fn capacity(self) -> usize {
183        self.next - self.at - 1
184    }
185
186    /// The string, up to its terminator.
187    ///
188    /// A payload that stops inside the field is read as far as it goes rather than
189    /// refused: the oldest narrow `hdr` is 18 bytes and carries no name at all, and it
190    /// reads back empty.
191    fn read(self, payload: &[u8]) -> String {
192        let span = self.at.min(payload.len())..self.next.min(payload.len());
193        nul_terminated(&payload[span])
194    }
195
196    /// Replaces the string, zero-filling the rest of the field.
197    pub(super) fn write(self, payload: &mut [u8], value: &str) -> Result<(), Error> {
198        if value.len() > self.capacity() {
199            return Err(ParseError::OutOfBounds {
200                value: format!("{value:?} ({} bytes)", value.len()),
201                bound: format!("a name of at most {} bytes", self.capacity()),
202            }
203            .into());
204        }
205        let field = payload
206            .get_mut(self.at..self.next)
207            .ok_or_else(|| ParseError::AssertFail("hdr section holds no name field".into()))?;
208        field.fill(0);
209        field[..value.len()].copy_from_slice(value.as_bytes());
210        Ok(())
211    }
212}
213
214/// What a NUL-terminated, zero-padded field holds. An unterminated field is the whole
215/// of it.
216fn nul_terminated(bytes: &[u8]) -> String {
217    let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
218    String::from_utf8_lossy(&bytes[..end]).into_owned()
219}
220
221/// Longest instrument name the narrow chain holds.
222pub const MAX_NAME_LEN: usize = StringField::NAME.capacity();
223
224/// A sample instrument's body: the section chain, held in file order including
225/// repeats — `stk` appears once per zone. A file is a `Cbin<Sample>`.
226///
227/// Reads and writes byte-exactly, checksum verified. The name, categories, zones
228/// and stroke metadata decode and are editable; the audio stays verbatim.
229pub struct Sample {
230    pub sections: Vec<Section>,
231}
232
233impl cbin::Body for Sample {
234    fn read<R: Read + Seek>(r: &mut BodyReader<'_, R>, _: &Header) -> Result<Self, Error> {
235        let remaining = r.remaining();
236        Ok(Sample {
237            sections: section::read_chain(r, remaining)?,
238        })
239    }
240
241    fn write<W: Write + Seek>(&self, w: &mut BodyWriter<'_, W>) -> Result<(), Error> {
242        for s in &self.sections {
243            s.write_to(w)?;
244        }
245        Ok(())
246    }
247}
248
249/// Reads a whole instrument, verifying its checksum.
250pub fn read_from(reader: &mut (impl Read + Seek)) -> Result<Cbin<Sample>, Error> {
251    cbin::read(reader, FORMAT)
252}
253
254/// A v3/v4 body: the wide-section (`NSMP`) chain, held in file order including
255/// repeats — `stk` appears once per stroke. Sections are preserved verbatim, so
256/// a file round-trips byte-exactly, and the name, zone boundaries and root keys
257/// patch in place without touching the audio.
258///
259/// Every corpus specimen chains `NSMP`, `hdr`, `cat`, `map`, N × `stk`, `sty`,
260/// `meta`, in that order, in both container generations. Inferred from
261/// specimens; not confirmed on hardware.
262///
263/// The stroke payloads are the encoded audio. The enclosing content version selects
264/// [`codec::Layout::V3`] or [`codec::Layout::V4`] through [`codec::Layout::from_version`].
265#[derive(Debug)]
266pub struct SampleV3 {
267    pub sections: Vec<section::Section4>,
268}
269
270impl cbin::Body for SampleV3 {
271    fn read<R: Read + Seek>(r: &mut BodyReader<'_, R>, _: &Header) -> Result<Self, Error> {
272        let remaining = r.remaining();
273        Ok(SampleV3 {
274            sections: section::read_chain4(r, remaining)?,
275        })
276    }
277
278    fn write<W: Write + Seek>(&self, w: &mut BodyWriter<'_, W>) -> Result<(), Error> {
279        for s in &self.sections {
280            s.write_to(w)?;
281        }
282        Ok(())
283    }
284}
285
286/// Longest main name the wide chain holds. The two fields around it are what the
287/// filename convention joins — `Bass Clarinet 2` + `KG  mono` → `Bass Clarinet
288/// 2_KG  mono 3.11`.
289pub const MAX_NAME_V3_LEN: usize = StringField::NAME_V3.capacity();
290
291impl Cbin<SampleV3> {
292    fn hdr(&self) -> Result<&section::Section4, Error> {
293        section::find4(&self.body.sections, section::HDR4)
294            .ok_or_else(|| ParseError::AssertFail("no hdr section".into()).into())
295    }
296
297    /// The instrument's main name.
298    pub fn name(&self) -> Result<String, Error> {
299        Ok(StringField::NAME_V3.read(&self.hdr()?.payload))
300    }
301
302    /// The sub name — the string after the `_` in the vendor's filenames.
303    /// Empty on files that carry none.
304    ///
305    /// It starts where the main name's field ends. Where it ends is unmapped, so this
306    /// reads to the terminator with no field bound behind it and there is no setter.
307    pub fn sub_name(&self) -> Result<String, Error> {
308        let payload = &self.hdr()?.payload;
309        let from = StringField::NAME_V3.next.min(payload.len());
310        Ok(nul_terminated(&payload[from..]))
311    }
312
313    /// How many strokes the body carries — one `stk` section each.
314    pub fn stroke_count(&self) -> usize {
315        self.body
316            .sections
317            .iter()
318            .filter(|s| s.is(section::STK4))
319            .count()
320    }
321
322    /// Each stroke's `(global id, root key)` — the u32 its payload leads with,
323    /// and the byte at offset 5. Inferred from specimens; not confirmed on
324    /// hardware.
325    fn stroke_ids(&self) -> Result<Vec<(u32, u8)>, Error> {
326        self.body
327            .sections
328            .iter()
329            .filter(|s| s.is(section::STK4))
330            .map(|s| match (stroke_gid(s), s.payload.get(5)) {
331                (Some(gid), Some(&root)) => Ok((gid, root)),
332                _ => Err(ParseError::AssertFail(format!(
333                    "stroke payload is {} bytes, too short for its id fields",
334                    s.payload.len()
335                ))
336                .into()),
337            })
338            .collect()
339    }
340
341    /// Keyboard zones, in stored order, which is usually high to low; `map` v14
342    /// files occur in both orders and a record states its own notes. Each zone is
343    /// verified against the stroke it names.
344    pub fn zones(&self) -> Result<Vec<ZoneV3>, Error> {
345        let map = self.map()?;
346        Ok(zone::read_v3(
347            map.version,
348            &map.payload,
349            &self.stroke_ids()?,
350        )?)
351    }
352
353    fn map(&self) -> Result<&section::Section4, Error> {
354        section::find4(&self.body.sections, section::MAP4)
355            .ok_or_else(|| ParseError::AssertFail("no map section".into()).into())
356    }
357
358    /// The instrument's default sound preset, under the schema its section
359    /// version selects.
360    pub fn sty(&self) -> Result<Sty, Error> {
361        let s = section::find4(&self.body.sections, section::STY4)
362            .ok_or_else(|| ParseError::AssertFail("no sty section".into()))?;
363        Ok(Sty::parse_wide(s.version, &s.payload)?)
364    }
365
366    /// The chain's own length, as its closing `meta` section states it.
367    pub fn meta(&self) -> Result<Meta, Error> {
368        let s = section::find4(&self.body.sections, section::META4)
369            .ok_or_else(|| ParseError::AssertFail("no meta section".into()))?;
370        Ok(Meta::parse(s.version, &s.payload)?)
371    }
372
373    /// The length `meta` should state: every section ahead of it on the wire.
374    pub fn chain_len_before_meta(&self) -> usize {
375        self.body
376            .sections
377            .iter()
378            .take_while(|s| !s.is(section::META4))
379            .map(section::Section4::encoded_len)
380            .sum()
381    }
382
383    fn map_mut(&mut self) -> Result<&mut section::Section4, Error> {
384        section::find_mut4(&mut self.body.sections, section::MAP4)
385            .ok_or_else(|| ParseError::AssertFail("no map section".into()).into())
386    }
387
388    /// The zone table, located the same way [`Self::zones`] locates it.
389    ///
390    /// Carries the record layout and, through [`zone::Table::key_map`], what the
391    /// `map`'s per-key table holds.
392    pub fn zone_table(&self) -> Result<zone::Table, Error> {
393        let map = self.map()?;
394        Ok(zone::Table::locate(
395            map.version,
396            &map.payload,
397            &self.stroke_ids()?,
398        )?)
399    }
400
401    /// Renames in place, NUL-padding the rest of the main-name field. The
402    /// sub-name is a separate field and is left alone.
403    pub fn set_name(&mut self, name: &str) -> Result<(), Error> {
404        let hdr = section::find_mut4(&mut self.body.sections, section::HDR4)
405            .ok_or_else(|| ParseError::AssertFail("no hdr section".into()))?;
406        StringField::NAME_V3.write(&mut hdr.payload, name)
407    }
408
409    /// Whether this body's zones can be retuned and remapped.
410    ///
411    /// True wherever the zone table reads and, if the `map` also describes the
412    /// keyboard note by note, that table can be recomputed from the layout.
413    pub fn zones_are_editable(&self) -> bool {
414        match (self.zone_table(), self.map(), self.zones()) {
415            (Ok(table), Ok(map), Ok(zones)) => table.validate_key_map(&map.payload, &zones).is_ok(),
416            _ => false,
417        }
418    }
419
420    /// Apply one zone-record edit, keeping the `map`'s per-key table in step.
421    ///
422    /// The layout the edit produces is worked out and the table planned from it
423    /// before any byte moves, so a layout the partner law cannot read refuses
424    /// rather than half-applying.
425    fn edit_zone(&mut self, index: usize, field: zone::Field, note: u8) -> Result<(), Error> {
426        let table = self.zone_table()?;
427        let mut zones = self.zones()?;
428        let map = self.map()?;
429        table.validate_key_map(&map.payload, &zones)?;
430        let zone = zones
431            .get_mut(index)
432            .ok_or_else(|| ParseError::AssertFail(format!("no zone {index}")))?;
433        match field {
434            zone::Field::Root => zone.root_key = note,
435            zone::Field::Top => zone.top_note = note,
436            zone::Field::Low => zone.low_note = Some(note),
437        }
438        let plan = table.plan_key_map(&map.payload, &zones)?;
439        let map = self.map_mut()?;
440        table.set(&mut map.payload, index, field, note)?;
441        for (at, quad) in plan {
442            map.payload[at..at + quad.len()].copy_from_slice(&quad);
443        }
444        Ok(())
445    }
446
447    /// Sets one zone's top note, in [`Self::zones`] order. The strokes are untouched.
448    pub fn set_zone_top_note(&mut self, index: usize, note: u8) -> Result<(), Error> {
449        self.edit_zone(index, zone::Field::Top, note)
450    }
451
452    /// Sets one zone's lowest note, on the layouts that store one.
453    pub fn set_zone_low_note(&mut self, index: usize, note: u8) -> Result<(), Error> {
454        self.edit_zone(index, zone::Field::Low, note)
455    }
456
457    /// Retunes one zone by moving the note its sample plays untransposed at.
458    ///
459    /// ⚠️ The root key is stored twice — once in the stroke, once duplicated into
460    /// the zone record — and the table stops reading if the two disagree, so both
461    /// move here or neither does.
462    pub fn set_root_key(&mut self, index: usize, note: u8) -> Result<(), Error> {
463        let gid = self
464            .zones()?
465            .get(index)
466            .ok_or_else(|| ParseError::AssertFail(format!("no zone {index}")))?
467            .stroke_gid;
468        // Both copies are located before either moves: a half-written pair is a
469        // file whose zone table no longer reads.
470        let at = self
471            .body
472            .sections
473            .iter()
474            .position(|s| s.is(section::STK4) && stroke_gid(s) == Some(gid))
475            .ok_or_else(|| {
476                ParseError::AssertFail(format!(
477                    "zone {index} names stroke {gid}, which the file does not contain"
478                ))
479            })?;
480        self.edit_zone(index, zone::Field::Root, note)?;
481        stroke::set_root_key(&mut self.body.sections[at].payload, note)?;
482        Ok(())
483    }
484
485    /// Every stroke's encoded stream with its offset from the start of the body, in
486    /// file order.
487    ///
488    /// The offset is the base the stroke's own [`codec::Directory`] is written
489    /// against, so a caller checking those pointers needs this pairing rather than
490    /// the payload alone. Decode the streams with
491    /// [`codec::Layout::from_version(self.header.version)`](codec::Layout::from_version).
492    pub fn stroke_streams(&self) -> Vec<(usize, &[u8])> {
493        let mut at = 0;
494        let mut out = Vec::new();
495        for section in &self.body.sections {
496            if section.is(section::STK4) {
497                out.push((at + section::HEADER4_LEN, section.payload.as_slice()));
498            }
499            at += section.encoded_len();
500        }
501        out
502    }
503
504    /// One zone's encoded stream, in [`Self::zones`] order. Decode it with
505    /// [`codec::Layout::from_version(self.header.version)`](codec::Layout::from_version).
506    ///
507    /// Paired by the global id the zone record names, so it is safe on library
508    /// content whose strokes are not in zone order.
509    pub fn zone_stream(&self, index: usize) -> Result<(usize, &[u8]), Error> {
510        let zones = self.zones()?;
511        let zone = zones
512            .get(index)
513            .ok_or_else(|| ParseError::AssertFail(format!("no zone {index}")))?;
514        let mut at = 0;
515        for section in &self.body.sections {
516            if section.is(section::STK4) && stroke_gid(section) == Some(zone.stroke_gid) {
517                return Ok((at + section::HEADER4_LEN, section.payload.as_slice()));
518            }
519            at += section.encoded_len();
520        }
521        Err(ParseError::AssertFail(format!(
522            "zone {index} names stroke {}, which the file does not contain",
523            zone.stroke_gid
524        ))
525        .into())
526    }
527}
528
529pub fn from_bytes(bytes: &[u8]) -> Result<Cbin<Sample>, Error> {
530    read_from(&mut std::io::Cursor::new(bytes))
531}
532
533/// The global id a `stk` payload leads with.
534fn stroke_id(section: &Section) -> Option<u32> {
535    let b = section.payload.get(0..4)?;
536    Some(u32::from_be_bytes(b.try_into().ok()?))
537}
538
539/// The global id a v3/v4 `stk` payload leads with. Unlike [`stroke_id`]'s
540/// narrow counterpart it is compared whole: a wide zone record stores the same
541/// u32.
542fn stroke_gid(section: &section::Section4) -> Option<u32> {
543    let b = section.payload.get(0..4)?;
544    Some(u32::from_be_bytes(b.try_into().ok()?))
545}
546
547/// Whether a stroke is the one a zone record names.
548///
549/// ⚠️ The record holds one byte and the stroke holds a u32, so the pairing is modulo
550/// 256. Library instruments whose ids run past 255 exist in both narrow chains, and
551/// comparing the whole u32 hands those files a zone table that does not read.
552fn names_stroke(id: u32, named: u8) -> bool {
553    id as u8 == named
554}
555
556impl Cbin<Sample> {
557    /// Serializes, recomputing the checksum over the body it just produced.
558    pub fn to_bytes(&self) -> Result<Vec<u8>, Error> {
559        let mut out = std::io::Cursor::new(Vec::new());
560        self.write_to(&mut out)?;
561        Ok(out.into_inner())
562    }
563
564    /// Instrument name, as the Nord display shows it.
565    ///
566    /// The editor composes this from separate Main, Sub and Aux fields joined with `_`,
567    /// so an empty Sub shows up as a doubled underscore rather than a typo.
568    ///
569    /// ⚠️ Empty on [`Chain::Early`], whose 18-byte `hdr` has no name field at all —
570    /// those instruments carry no name and [`Self::set_name`] has nowhere to put one.
571    /// Ask [`Self::chain`] before reporting the empty string as the name.
572    pub fn name(&self) -> Result<String, Error> {
573        Ok(StringField::NAME.read(&self.hdr()?.payload))
574    }
575
576    /// Renames in place, NUL-padding the rest of the field.
577    pub fn set_name(&mut self, name: &str) -> Result<(), Error> {
578        let hdr = section::find_mut(&mut self.body.sections, section::HDR)
579            .ok_or_else(|| ParseError::AssertFail("no hdr section".into()))?;
580        StringField::NAME.write(&mut hdr.payload, name)
581    }
582
583    /// Which narrow chain this body's sections form, from the `map` section's own
584    /// version. An unknown one refuses rather than decoding on a guess; the section
585    /// chain, the name and the checksum still read.
586    pub fn chain(&self) -> Result<Chain, Error> {
587        Ok(Chain::from_map_version(self.map()?.version)?)
588    }
589
590    /// Keyboard zones, high to low.
591    pub fn zones(&self) -> Result<Vec<Zone>, Error> {
592        Ok(zone::read(self.chain()?, &self.map()?.payload)?)
593    }
594
595    /// The instrument's default sound preset — nine enum-quantised bytes.
596    pub fn sty(&self) -> Result<StyV2, Error> {
597        let s = section::find(&self.body.sections, section::STY)
598            .ok_or_else(|| ParseError::AssertFail("no sty section".into()))?;
599        if s.version != sty::VERSION_V2 {
600            return Err(ParseError::AssertFail(format!(
601                "sty section version {} has no preset layout derived from a specimen",
602                s.version
603            ))
604            .into());
605        }
606        Ok(StyV2::parse(&s.payload)?)
607    }
608
609    /// Sets one zone's top note. The strokes are untouched.
610    pub fn set_zone_top_note(&mut self, index: usize, note: u8) -> Result<(), Error> {
611        let chain = self.chain()?;
612        let map = section::find_mut(&mut self.body.sections, section::MAP)
613            .ok_or_else(|| ParseError::AssertFail("no map section".into()))?;
614        zone::set_top_note(chain, &mut map.payload, index, note)?;
615        Ok(())
616    }
617
618    /// The keyboard map: the instrument's gain and detune, and one record per
619    /// MIDI note.
620    pub fn key_table(&self) -> Result<KeyTable, Error> {
621        // Both narrow chains carry the same table ahead of their zone tables; a `map`
622        // this crate does not recognise may carry something else.
623        self.chain()?;
624        Ok(KeyTable::read(&self.map()?.payload)?)
625    }
626
627    /// Replaces the keyboard map. The zone table and the strokes are untouched.
628    pub fn set_key_table(&mut self, table: &KeyTable) -> Result<(), Error> {
629        // As in `key_table`: the table is shared, an unrecognised `map` is refused.
630        self.chain()?;
631        let map = section::find_mut(&mut self.body.sections, section::MAP)
632            .ok_or_else(|| ParseError::AssertFail("no map section".into()))?;
633        table.write(&mut map.payload)?;
634        Ok(())
635    }
636
637    /// One stroke per zone, **in [`Self::zones`] order** — which is not file order.
638    ///
639    /// Each zone names its stroke by id, and only instruments built in a single editor
640    /// pass have those ids running parallel to the sections. Zipping this against
641    /// `zones()` is therefore safe; indexing it as "the nth `stk` section" is not.
642    pub fn strokes(&self) -> Result<Vec<Stroke>, Error> {
643        let zones = self.zones()?;
644        let by_id = self.strokes_in_file_order()?;
645        zones
646            .iter()
647            .map(|z| {
648                by_id
649                    .iter()
650                    .find(|(id, _)| names_stroke(*id, z.stroke_id))
651                    .map(|(_, s)| *s)
652                    .ok_or_else(|| {
653                        ParseError::AssertFail(format!(
654                            "zone reaching up to note {} names stroke {}, which the file \
655                             does not contain",
656                            z.top_note, z.stroke_id
657                        ))
658                        .into()
659                    })
660            })
661            .collect()
662    }
663
664    /// Every stroke's encoded stream with its offset from the start of the body, in
665    /// file order.
666    ///
667    /// The offset is the base the stroke's own [`codec::Directory`] is written
668    /// against, so a caller checking those pointers needs this pairing rather than
669    /// the payload alone.
670    pub fn stroke_streams(&self) -> Vec<(usize, &[u8])> {
671        let mut at = 0;
672        let mut out = Vec::new();
673        for section in &self.body.sections {
674            if section.is(section::STK) {
675                out.push((at + section::HEADER_LEN, section.payload.as_slice()));
676            }
677            at += section.encoded_len();
678        }
679        out
680    }
681
682    /// One zone's encoded stream, in [`Self::zones`] order, ready for
683    /// [`codec::decode`].
684    ///
685    /// Paired by stroke id like [`Self::strokes`], so it is safe on library content
686    /// that the editor did not build in a single pass.
687    pub fn zone_stream(&self, index: usize) -> Result<(usize, &[u8]), Error> {
688        let zones = self.zones()?;
689        let zone = zones
690            .get(index)
691            .ok_or_else(|| ParseError::AssertFail(format!("no zone {index}")))?;
692        let wanted = zone.stroke_id;
693        let mut at = 0;
694        for section in &self.body.sections {
695            if section.is(section::STK)
696                && stroke_id(section).is_some_and(|id| names_stroke(id, wanted))
697            {
698                return Ok((at + section::HEADER_LEN, section.payload.as_slice()));
699            }
700            at += section.encoded_len();
701        }
702        Err(ParseError::AssertFail(format!(
703            "zone {index} names stroke {wanted}, which the file does not contain"
704        ))
705        .into())
706    }
707
708    /// Every stroke with the global id it carries, in the order the sections appear.
709    ///
710    /// The header length depends on a stroke's *position in the file*, so the read has
711    /// to happen here, before anything reorders them.
712    fn strokes_in_file_order(&self) -> Result<Vec<(u32, Stroke)>, Error> {
713        // The first stroke's header is the remainder of a preamble it shares with
714        // these two, so their sizes are what fixes where its audio starts. The
715        // pre-2.0 chain has no `cat` and a budget that is larger by as much.
716        let chain = self.chain()?;
717        let map_len = self.map()?.payload.len();
718        let cat_len =
719            section::find(&self.body.sections, section::CAT).map_or(0, |s| s.payload.len());
720        self.stroke_sections()
721            .enumerate()
722            .map(|(i, s)| {
723                let id = s
724                    .payload
725                    .get(0..4)
726                    .map(|b| u32::from_be_bytes(b.try_into().unwrap()))
727                    .ok_or_else(|| {
728                        ParseError::AssertFail(format!(
729                            "stroke {i} is {} bytes, too short for its id",
730                            s.payload.len()
731                        ))
732                    })?;
733                Ok((id, stroke::read(&s.payload, chain, i, cat_len, map_len)?))
734            })
735            .collect()
736    }
737
738    /// Retunes one zone by moving the note its sample plays untransposed at.
739    ///
740    /// `index` is into [`Self::zones`], matching [`Self::set_zone_top_note`] — so the
741    /// stroke it reaches is the one that zone names, not the nth section. The two are
742    /// the same file order only for instruments the editor built in a single pass.
743    pub fn set_root_key(&mut self, index: usize, note: u8) -> Result<(), Error> {
744        let zones = self.zones()?;
745        let zone = zones
746            .get(index)
747            .ok_or_else(|| ParseError::AssertFail(format!("no zone {index}")))?;
748        let wanted = zone.stroke_id;
749        let section = self
750            .body
751            .sections
752            .iter_mut()
753            .filter(|s| s.is(section::STK))
754            .find(|s| stroke_id(s).is_some_and(|id| names_stroke(id, wanted)))
755            .ok_or_else(|| {
756                ParseError::AssertFail(format!(
757                    "zone {index} names stroke {wanted}, which the file does not contain"
758                ))
759            })?;
760        stroke::set_root_key(&mut section.payload, note)?;
761        Ok(())
762    }
763
764    /// Category labels, as stored in `cat`: length-prefixed strings.
765    pub fn categories(&self) -> Vec<String> {
766        let Some(cat) = section::find(&self.body.sections, section::CAT) else {
767            return Vec::new();
768        };
769        let mut out = Vec::new();
770        let mut i = 0;
771        while i < cat.payload.len() {
772            let len = cat.payload[i] as usize;
773            let from = i + 1;
774            // A length running past the end means this is not a string here; the
775            // section holds a few leading bytes before the labels start.
776            match cat.payload.get(from..from + len) {
777                Some(s) if len > 0 && s.iter().all(|&b| (0x20..0x7f).contains(&b)) => {
778                    out.push(String::from_utf8_lossy(s).into_owned());
779                    i = from + len;
780                }
781                _ => i += 1,
782            }
783        }
784        out
785    }
786
787    fn stroke_sections(&self) -> impl Iterator<Item = &Section> {
788        self.body.sections.iter().filter(|s| s.is(section::STK))
789    }
790
791    fn hdr(&self) -> Result<&Section, Error> {
792        section::find(&self.body.sections, section::HDR)
793            .ok_or_else(|| ParseError::AssertFail("no hdr section".into()).into())
794    }
795
796    fn map(&self) -> Result<&Section, Error> {
797        section::find(&self.body.sections, section::MAP)
798            .ok_or_else(|| ParseError::AssertFail("no map section".into()).into())
799    }
800}
801
802impl fmt::Debug for Sample {
803    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
804        f.debug_struct("Sample")
805            .field("sections", &self.sections)
806            .finish()
807    }
808}
809
810#[cfg(test)]
811mod tests {
812    use super::*;
813
814    /// The content version tracks the library release and both chains ship several,
815    /// so the `map` section's own version is the gate.
816    #[test]
817    fn the_map_version_selects_the_chain_and_an_unknown_one_refuses() {
818        assert_eq!(Chain::from_map_version(9).unwrap(), Chain::Early);
819        assert_eq!(Chain::from_map_version(10).unwrap(), Chain::Library2);
820        assert!(Chain::from_map_version(11).is_err());
821    }
822
823    #[test]
824    fn an_unknown_map_version_cannot_use_the_zone_setter() {
825        let crate::Sample::V2(mut sample) =
826            encode::instrument(&[0i16; encode::MIN_FRAMES], &encode::Options::new("Test")).unwrap()
827        else {
828            panic!("the default options build the narrow chain");
829        };
830        let map = section::find_mut(&mut sample.body.sections, section::MAP).unwrap();
831        map.version = keymap::VERSION + 1;
832        let before = map.payload.clone();
833        assert!(sample.zones().is_err());
834        assert!(sample.set_zone_top_note(0, 60).is_err());
835        assert_eq!(sample.map().unwrap().payload, before);
836    }
837
838    #[test]
839    fn an_unknown_map_version_cannot_use_the_keyboard_table() {
840        let crate::Sample::V2(mut sample) =
841            encode::instrument(&[0i16; encode::MIN_FRAMES], &encode::Options::new("Test")).unwrap()
842        else {
843            panic!("the default options build the narrow chain");
844        };
845        let map = section::find_mut(&mut sample.body.sections, section::MAP).unwrap();
846        map.version = keymap::VERSION + 1;
847        let before = map.payload.clone();
848        assert!(sample.key_table().is_err());
849        assert!(sample.set_key_table(&KeyTable::NEUTRAL).is_err());
850        assert_eq!(sample.map().unwrap().payload, before);
851    }
852
853    #[test]
854    fn a_name_field_holds_its_whole_span_less_the_terminator() {
855        assert_eq!(MAX_NAME_LEN, 31);
856        assert_eq!(MAX_NAME_V3_LEN, 65);
857    }
858
859    #[test]
860    fn a_rename_leaves_nothing_of_the_name_it_replaced() {
861        for field in [StringField::NAME, StringField::NAME_V3] {
862            let mut payload = vec![0u8; field.next];
863            let long = "M".repeat(field.capacity());
864            field.write(&mut payload, &long).unwrap();
865            field.write(&mut payload, "Short").unwrap();
866            assert_eq!(field.read(&payload), "Short");
867            assert!(payload[field.at + 5..field.next].iter().all(|&b| b == 0));
868        }
869    }
870
871    #[test]
872    fn a_name_one_byte_past_the_field_is_refused() {
873        let field = StringField::NAME;
874        let mut payload = vec![0xffu8; field.next + 8];
875        let error = field
876            .write(&mut payload, &"M".repeat(field.capacity() + 1))
877            .unwrap_err()
878            .to_string();
879        assert!(error.contains("at most 31 bytes"), "{error}");
880        assert!(payload[field.at..].iter().all(|&b| b == 0xff));
881    }
882
883    #[test]
884    fn a_name_filling_its_field_stops_at_the_field_that_follows() {
885        let field = StringField::NAME_V3;
886        let mut payload = vec![0u8; 112];
887        payload[field.next..field.next + 7].copy_from_slice(b"KG mono");
888        let long = "M".repeat(field.capacity());
889        field.write(&mut payload, &long).unwrap();
890        assert_eq!(field.read(&payload), long);
891        assert_eq!(nul_terminated(&payload[field.next..]), "KG mono");
892    }
893
894    /// The oldest narrow `hdr` is 18 bytes and stops inside the name field.
895    #[test]
896    fn a_header_with_no_name_field_reads_back_empty_and_refuses_a_rename() {
897        assert_eq!(StringField::NAME.read(&[0u8; 18]), "");
898        assert_eq!(StringField::NAME.read(&[]), "");
899        assert!(StringField::NAME.write(&mut [0u8; 18], "Name").is_err());
900    }
901}