pub mod codec;
pub mod encode;
#[cfg(any(test, feature = "synthetic"))]
pub mod synthetic;
use crate::cbin::{self, Cbin, Header, RawBody};
use crate::error::{try_vec, Error, ParseError};
use std::borrow::Cow;
use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use std::io::{Read, Seek, Write};
use std::ops::RangeInclusive;
pub const FORMAT: &str = "npno";
pub const CNSP_MAGIC: &[u8; 4] = b"CNSP";
pub const NOTES: usize = 128;
pub const UNCOVERED: u8 = 0xff;
pub const KNOWN_VERSIONS: &[u32] = &[0x450, 0x464];
const VERSION_SPLIT_NAME: u16 = 0x464;
const KEY_MAP_AT: usize = 0x8c;
const FINE_TUNE_AT: usize = 0x18c;
const VERSION_AT: usize = 0x04;
const VERSION_ECHO_AT: usize = 0x61c;
const CHANNELS_AT: usize = 0x61e;
const STROKE_COUNT_AT: usize = 0x620;
const ROOT_COUNTS_AT: usize = 0x622;
const KIND_AT: usize = 0x18;
const GAIN_AT: usize = 0x40c;
const DAMPER_TOP_AT: usize = 0x40d;
const DIRECTORY_AT: usize = 0x732;
const RECORD: usize = 118;
const REC_START: usize = 0x00;
const REC_BANK: usize = 0x04;
const REC_LAYER: usize = 0x05;
const REC_FRAMES: usize = 0x06;
const REC_BLOCKS: usize = 0x0a;
const REC_SEEDS: usize = 0x0c;
const REC_MARKS: usize = 0x1c;
const REC_MARK_BLOCK: usize = 0x2c;
const REC_DECAY: usize = 0x2e;
const REC_WINDOW: usize = 0x32;
const REC_TRIM: usize = 0x34;
const REC_DECAYS: usize = 0x36;
const REC_ID: usize = 0x6e;
const SEEDS: usize = 4;
const MARKS: usize = 4;
pub const DECAYS: usize = 14;
const _: () = assert!(REC_DECAYS + DECAYS * 4 == REC_ID);
pub const LADDER_UNITY: u32 = 0x0080_0000;
pub const AUDIO_ALIGN_BIAS: usize = 192;
pub const FINE_TUNE_CENTS_PER_UNIT: f32 = 0.7;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Bank {
Attack,
Resonance,
Release,
}
impl Bank {
pub const ALL: [Bank; 3] = [Bank::Attack, Bank::Resonance, Bank::Release];
pub fn from_code(code: u8) -> Option<Bank> {
match code {
0 => Some(Bank::Attack),
1 => Some(Bank::Resonance),
2 => Some(Bank::Release),
_ => None,
}
}
pub fn code(self) -> u8 {
match self {
Bank::Attack => 0,
Bank::Resonance => 1,
Bank::Release => 2,
}
}
pub fn name(self) -> &'static str {
match self {
Bank::Attack => "attack",
Bank::Resonance => "resonance",
Bank::Release => "release",
}
}
}
impl fmt::Display for Bank {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Layers {
Loudest(usize),
Only(BTreeSet<u8>),
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Change {
pub strokes_removed: usize,
pub roots_removed: usize,
pub keys_uncovered: usize,
}
pub const NAME_SEPARATOR: char = '#';
#[derive(Clone, Copy)]
struct TextField {
at: usize,
len: usize,
}
impl TextField {
const COMBINED: TextField = TextField {
at: 0x1c,
len: 0x20,
};
const LONG_NAME: TextField = TextField {
at: 0x3c,
len: 0x20,
};
const VOICING: TextField = TextField {
at: 0x5c,
len: 0x20,
};
const fn capacity(self) -> usize {
self.len - 1
}
fn read(self, prefix: &[u8]) -> String {
let field = &prefix[self.at..self.at + self.len];
let end = field.iter().position(|&b| b == 0).unwrap_or(field.len());
String::from_utf8_lossy(&field[..end]).into_owned()
}
fn check_text(text: &str) -> Result<(), Error> {
match text.chars().find(|&c| !c.is_ascii_graphic() && c != ' ') {
None => Ok(()),
Some(bad) => Err(ParseError::AssertFail(format!(
"{text:?} holds {bad:?}, which the field would not read back as written; it \
carries printable ASCII"
))
.into()),
}
}
fn check(self, text: &str) -> Result<(), Error> {
TextField::check_text(text)?;
if text.len() > self.capacity() {
return Err(ParseError::OutOfBounds {
value: format!("{text:?} ({} bytes)", text.len()),
bound: format!("at most {} bytes", self.capacity()),
}
.into());
}
Ok(())
}
fn write(self, prefix: &mut [u8], text: &str) -> Result<(), Error> {
self.check(text)?;
let field = &mut prefix[self.at..self.at + self.len];
field.fill(0);
field[..text.len()].copy_from_slice(text.as_bytes());
Ok(())
}
}
fn check_half(what: &str, text: &str) -> Result<(), Error> {
if text.contains(NAME_SEPARATOR) {
return Err(ParseError::AssertFail(format!(
"the {what} {text:?} holds {NAME_SEPARATOR:?}, which is what splits the name from \
the variant in the field they share"
))
.into());
}
TextField::check_text(text)
}
pub struct Piano {
pub file: Cbin<RawBody>,
}
impl Piano {
pub fn new() -> Piano {
Piano {
file: Cbin {
header: Header::new(FORMAT, (0, 0), 0),
body: RawBody(Vec::new()),
},
}
}
pub fn read_from(reader: &mut (impl Read + Seek)) -> Result<Piano, Error> {
Ok(Piano {
file: cbin::read(reader, FORMAT)?,
})
}
pub fn write_to(&self, writer: &mut (impl Write + Seek)) -> Result<(), Error> {
self.file.write_to(writer)
}
fn mapped(&self) -> Result<&[u8], Error> {
let body = &self.file.body.0;
check_mapped(body)?;
Ok(body)
}
pub fn stream_version(&self) -> Result<u16, Error> {
version_of(&self.file.body.0)
}
pub fn name(&self) -> Result<(String, String), Error> {
let body = self.mapped()?;
if body.len() < DIRECTORY_AT {
return Err(short("the prefix"));
}
Ok(split_name(&TextField::COMBINED.read(body)))
}
pub fn key_map(&self) -> Result<&[u8], Error> {
self.mapped()?
.get(KEY_MAP_AT..KEY_MAP_AT + NOTES)
.ok_or_else(|| short("the key map"))
}
pub fn library(&self) -> Result<Library<'_>, Error> {
Library::parse_body(self.file.header.clone(), &self.file.body.0)
}
}
impl Default for Piano {
fn default() -> Self {
Self::new()
}
}
impl fmt::Debug for Piano {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("npno::Piano")
.field("header", &self.file.header)
.field("body_len", &self.file.body.0.len())
.finish()
}
}
fn raw_halves(field: &str) -> (&str, &str) {
field.split_once(NAME_SEPARATOR).unwrap_or((field, ""))
}
fn split_name(field: &str) -> (String, String) {
let (name, variant) = raw_halves(field);
(name.trim().to_owned(), variant.trim().to_owned())
}
fn midi_key(what: &str, key: u8) -> Result<usize, Error> {
let index = usize::from(key);
if index < NOTES {
return Ok(index);
}
Err(ParseError::OutOfBounds {
value: format!("{what} {key}"),
bound: "a MIDI note from 0 through 127".into(),
}
.into())
}
fn short(what: &str) -> Error {
ParseError::AssertFail(format!("the body ends inside {what}")).into()
}
fn version_of(body: &[u8]) -> Result<u16, Error> {
if body.get(..4) != Some(CNSP_MAGIC.as_slice()) {
return Err(ParseError::AssertFail(format!(
"body opens {:02x?}, not the CNSP stream",
body.get(..4).unwrap_or_default()
))
.into());
}
let bytes = body
.get(VERSION_AT..VERSION_AT + 2)
.ok_or_else(|| ParseError::AssertFail("body ends inside the CNSP header".to_string()))?;
Ok(u16::from_be_bytes(bytes.try_into().unwrap()))
}
fn check_mapped(body: &[u8]) -> Result<(), Error> {
let version = version_of(body)?;
crate::formats::known_version(FORMAT, u32::from(version), KNOWN_VERSIONS)
}
fn overflow(what: &str) -> Error {
ParseError::OutOfBounds {
value: what.to_string(),
bound: "an offset that fits this platform's address space".into(),
}
.into()
}
fn be16(bytes: &[u8], at: usize) -> u16 {
u16::from_be_bytes(bytes[at..at + 2].try_into().unwrap())
}
fn be32(bytes: &[u8], at: usize) -> u32 {
u32::from_be_bytes(bytes[at..at + 4].try_into().unwrap())
}
fn first_audio_offset(directory_end: usize, block: usize) -> Result<usize, Error> {
directory_end
.checked_add(AUDIO_ALIGN_BIAS)
.map(|biased| biased.div_ceil(block))
.and_then(|blocks| blocks.checked_mul(block))
.and_then(|at| at.checked_sub(AUDIO_ALIGN_BIAS))
.ok_or_else(|| overflow("the first audio offset"))
}
#[derive(Clone)]
pub struct Stroke<'a> {
pub root: u8,
record: [u8; RECORD],
audio: Cow<'a, [u8]>,
}
impl<'a> Stroke<'a> {
pub fn bank_code(&self) -> u8 {
self.record[REC_BANK]
}
pub fn bank(&self) -> Option<Bank> {
Bank::from_code(self.bank_code())
}
pub fn layer(&self) -> u8 {
self.record[REC_LAYER]
}
pub fn frames(&self) -> u32 {
be32(&self.record, REC_FRAMES)
}
pub fn blocks(&self) -> u16 {
be16(&self.record, REC_BLOCKS)
}
pub fn trim(&self) -> u16 {
be16(&self.record, REC_TRIM)
}
pub fn decay(&self) -> u32 {
be32(&self.record, REC_DECAY)
}
pub fn ladder(&self) -> [u32; DECAYS] {
std::array::from_fn(|entry| be32(&self.record, REC_DECAYS + entry * 4))
}
pub fn id(&self) -> u32 {
be32(&self.record, REC_ID)
}
pub fn seeds(&self) -> [[i16; SEEDS]; 2] {
let mut out = [[0i16; SEEDS]; 2];
for (channel, group) in out.iter_mut().enumerate() {
for (i, slot) in group.iter_mut().enumerate() {
*slot = be16(&self.record, REC_SEEDS + (channel * SEEDS + i) * 2) as i16;
}
}
out
}
pub fn audio(&self) -> &[u8] {
&self.audio
}
pub fn record(&self) -> &[u8; RECORD] {
&self.record
}
}
impl fmt::Debug for Stroke<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Stroke")
.field("root", &self.root)
.field("bank", &self.bank_code())
.field("layer", &self.layer())
.field("frames", &self.frames())
.field("blocks", &self.blocks())
.finish()
}
}
#[derive(Clone)]
pub struct Library<'a> {
pub header: Header,
prefix: Vec<u8>,
channels: u16,
strokes: Vec<Stroke<'a>>,
}
impl<'a> Library<'a> {
pub fn borrow(file: &'a [u8]) -> Result<Library<'a>, Error> {
let mut head: &[u8] = file;
let (header, _) = cbin::read_header(&mut head)?;
if header.tag.as_slice() != FORMAT.as_bytes() {
return Err(ParseError::WrongFormat {
expected: FORMAT,
got: String::from_utf8_lossy(&header.tag).into_owned(),
}
.into());
}
let start = usize::try_from(header.generation.body_start())
.map_err(|_| overflow("the container's header"))?;
let trailer = usize::try_from(header.generation.trailer_len())
.map_err(|_| overflow("the container's checksum trailer"))?;
let end = file
.len()
.checked_sub(trailer)
.ok_or_else(|| short("the container's checksum trailer"))?;
let body = file.get(start..end).ok_or_else(|| short("the header"))?;
Library::parse_body(header, body)
}
fn parse_body(header: Header, body: &'a [u8]) -> Result<Library<'a>, Error> {
check_mapped(body)?;
let prefix = body
.get(..DIRECTORY_AT)
.ok_or_else(|| short("the prefix"))?;
let version = be16(prefix, VERSION_AT);
let echo = be16(prefix, VERSION_ECHO_AT);
if echo != version {
return Err(ParseError::AssertFail(format!(
"the stream version {version:#06x} is echoed as {echo:#06x}"
))
.into());
}
let channels = be16(prefix, CHANNELS_AT);
if !(1..=2).contains(&channels) {
return Err(ParseError::OutOfBounds {
value: format!("{channels} channels"),
bound: "1 or 2".into(),
}
.into());
}
let block = block_bytes(channels);
let count = usize::from(be16(prefix, STROKE_COUNT_AT));
let counts: Vec<u16> = (0..NOTES)
.map(|n| be16(prefix, ROOT_COUNTS_AT + n * 2))
.collect();
let summed: usize = counts.iter().map(|&c| usize::from(c)).sum();
if summed != count {
return Err(ParseError::AssertFail(format!(
"the per-root counts sum to {summed} where the stroke count is {count}"
))
.into());
}
let directory_end = RECORD
.checked_mul(count)
.and_then(|len| DIRECTORY_AT.checked_add(len))
.ok_or_else(|| overflow("the stroke directory"))?;
let records = body
.get(DIRECTORY_AT..directory_end)
.ok_or_else(|| short("the stroke directory"))?;
let first = first_audio_offset(directory_end, block)?;
let pad = body
.get(directory_end..first)
.ok_or_else(|| short("the alignment gap before the audio"))?;
if pad.iter().any(|&b| b != 0) {
return Err(ParseError::AssertFail(
"the alignment gap before the audio is not zero".into(),
)
.into());
}
let mut strokes = Vec::new();
strokes
.try_reserve_exact(count)
.map_err(|_| overflow("the stroke list"))?;
let mut at = first;
let mut roots = counts
.iter()
.enumerate()
.flat_map(|(note, &n)| std::iter::repeat_n(note as u8, usize::from(n)));
for i in 0..count {
let mut record = [0u8; RECORD];
record.copy_from_slice(&records[i * RECORD..(i + 1) * RECORD]);
let root = roots.next().expect("the counts sum to the stroke count");
let start = be32(&record, REC_START);
if usize::try_from(start) != Ok(at) {
return Err(ParseError::AssertFail(format!(
"stroke {i} starts at {start:#x} where the spans before it end at {at:#x}"
))
.into());
}
let span = usize::from(be16(&record, REC_BLOCKS))
.checked_mul(block)
.ok_or_else(|| overflow("a stroke's audio span"))?;
let end = at.checked_add(span).ok_or_else(|| overflow("the audio"))?;
let audio = body
.get(at..end)
.ok_or_else(|| short("a stroke's audio span"))?;
strokes.push(Stroke {
root,
record,
audio: Cow::Borrowed(audio),
});
at = end;
}
if at != body.len() {
return Err(ParseError::AssertFail(format!(
"the audio ends at {at:#x} where the body ends at {:#x}",
body.len()
))
.into());
}
let library = Library {
header,
prefix: prefix.to_vec(),
channels,
strokes,
};
library.check_key_map()?;
Ok(library)
}
fn check_key_map(&self) -> Result<(), Error> {
let roots = self.roots();
for (key, &root) in self.key_map().iter().enumerate() {
if root != UNCOVERED && !roots.contains(&root) {
return Err(ParseError::AssertFail(format!(
"key {key} plays root {root}, which no stroke records"
))
.into());
}
}
Ok(())
}
pub fn stream_version(&self) -> u16 {
be16(&self.prefix, VERSION_AT)
}
pub fn channels(&self) -> u16 {
self.channels
}
pub fn block_bytes(&self) -> usize {
block_bytes(self.channels)
}
pub fn strokes(&self) -> &[Stroke<'a>] {
&self.strokes
}
pub fn without_audio(&self) -> Library<'static> {
Library {
header: self.header.clone(),
prefix: self.prefix.clone(),
channels: self.channels,
strokes: self
.strokes
.iter()
.map(|stroke| Stroke {
root: stroke.root,
record: stroke.record,
audio: Cow::Owned(Vec::new()),
})
.collect(),
}
}
pub fn set_trim(&mut self, index: usize, decibels: u16) -> Result<(), Error> {
let count = self.strokes.len();
let stroke = self
.strokes
.get_mut(index)
.ok_or_else(|| ParseError::OutOfBounds {
value: format!("stroke {index}"),
bound: format!("the {count} strokes the directory holds"),
})?;
stroke.record[REC_TRIM..REC_TRIM + 2].copy_from_slice(&decibels.to_be_bytes());
Ok(())
}
pub fn name(&self) -> (String, String) {
split_name(&TextField::COMBINED.read(&self.prefix))
}
pub fn key_map(&self) -> &[u8] {
&self.prefix[KEY_MAP_AT..KEY_MAP_AT + NOTES]
}
fn key_map_mut(&mut self) -> &mut [u8] {
&mut self.prefix[KEY_MAP_AT..KEY_MAP_AT + NOTES]
}
pub fn roots(&self) -> BTreeSet<u8> {
self.strokes.iter().map(|s| s.root).collect()
}
pub fn key_root(&self, key: u8) -> Result<Option<u8>, Error> {
let root = self.key_map()[midi_key("key", key)?];
Ok((root != UNCOVERED).then_some(root))
}
pub fn keys_for(&self, root: u8) -> Vec<u8> {
self.key_map()
.iter()
.enumerate()
.filter(|&(_, &r)| r == root)
.map(|(key, _)| key as u8)
.collect()
}
pub fn fine_tune(&self, key: u8) -> Result<i8, Error> {
Ok(self.prefix[FINE_TUNE_AT + midi_key("key", key)?] as i8)
}
pub fn set_fine_tune(&mut self, key: u8, units: i8) -> Result<(), Error> {
let at = FINE_TUNE_AT + midi_key("key", key)?;
self.prefix[at] = units as u8;
Ok(())
}
pub fn gain(&self) -> i8 {
self.prefix[GAIN_AT] as i8
}
pub fn set_gain(&mut self, tenths: i8) {
self.prefix[GAIN_AT] = tenths as u8;
}
pub fn damper_top(&self) -> u8 {
self.prefix[DAMPER_TOP_AT]
}
pub fn set_damper_top(&mut self, key: u8) -> Result<(), Error> {
self.prefix[DAMPER_TOP_AT] = midi_key("damper limit", key)? as u8;
Ok(())
}
pub fn kind_code(&self) -> u8 {
self.prefix[KIND_AT]
}
pub fn set_kind(&mut self, kind: encode::Kind) {
self.prefix[KIND_AT] = kind.code();
}
pub fn long_name(&self) -> Option<String> {
self.split_field(TextField::LONG_NAME)
}
pub fn voicing(&self) -> Option<String> {
self.split_field(TextField::VOICING)
}
fn split_field(&self, field: TextField) -> Option<String> {
(self.stream_version() == VERSION_SPLIT_NAME).then(|| field.read(&self.prefix))
}
pub fn set_name(&mut self, name: &str) -> Result<(), Error> {
let field = TextField::COMBINED.read(&self.prefix);
let variant = raw_halves(&field).1.to_owned();
self.set_name_and_variant(name, &variant)
}
pub fn set_variant(&mut self, variant: &str) -> Result<(), Error> {
check_half("variant", variant)?;
let field = TextField::COMBINED.read(&self.prefix);
let combined = format!("{}{NAME_SEPARATOR}{variant}", raw_halves(&field).0);
TextField::COMBINED.write(&mut self.prefix, &combined)
}
fn set_name_and_variant(&mut self, name: &str, variant: &str) -> Result<(), Error> {
check_half("name", name)?;
check_half("variant", variant)?;
let combined = format!("{name}{NAME_SEPARATOR}{variant}");
let long = (self.stream_version() == VERSION_SPLIT_NAME).then_some(name);
TextField::COMBINED.check(&combined)?;
if let Some(long) = long {
TextField::LONG_NAME.check(long)?;
}
TextField::COMBINED.write(&mut self.prefix, &combined)?;
if let Some(long) = long {
TextField::LONG_NAME.write(&mut self.prefix, long)?;
}
Ok(())
}
pub fn set_voicing(&mut self, voicing: &str) -> Result<(), Error> {
if self.stream_version() != VERSION_SPLIT_NAME {
return Err(ParseError::AssertFail(format!(
"stream {:#06x} carries no voicing field; the variant after the \
{NAME_SEPARATOR:?} is where it records one",
self.stream_version()
))
.into());
}
TextField::VOICING.write(&mut self.prefix, voicing)
}
pub fn set_key_root(&mut self, key: u8, root: Option<u8>) -> Result<(), Error> {
let key = midi_key("key", key)?;
if let Some(root) = root {
midi_key("root", root)?;
if !self.roots().contains(&root) {
return Err(ParseError::OutOfBounds {
value: format!("root {root}"),
bound: "a root the directory records".into(),
}
.into());
}
}
self.key_map_mut()[key] = root.unwrap_or(UNCOVERED);
Ok(())
}
pub fn drop_bank(&mut self, bank: Bank) -> Change {
let code = bank.code();
self.retain(|s| s.bank_code() != code)
}
pub fn keep_layers(&mut self, keep: &Layers) -> Change {
match keep {
Layers::Only(layers) => {
let layers = layers.clone();
self.retain(|s| layers.contains(&s.layer()))
}
Layers::Loudest(n) => {
let mut groups: BTreeMap<(u8, u8), BTreeSet<u8>> = BTreeMap::new();
for stroke in &self.strokes {
groups
.entry((stroke.root, stroke.bank_code()))
.or_default()
.insert(stroke.layer());
}
let kept: BTreeSet<(u8, u8, u8)> = groups
.into_iter()
.flat_map(|((root, bank), layers)| {
layers.into_iter().take(*n).map(move |l| (root, bank, l))
})
.collect();
self.retain(|s| kept.contains(&(s.root, s.bank_code(), s.layer())))
}
}
}
pub fn retain_strokes(&mut self, keep: impl FnMut(&Stroke<'a>) -> bool) -> Change {
self.retain(keep)
}
pub fn cut_range(&mut self, range: RangeInclusive<u8>) -> Result<Change, Error> {
midi_key("the range's lowest key", *range.start())?;
midi_key("the range's highest key", *range.end())?;
Ok(self.restrict(|key| range.contains(&key)))
}
pub fn split_at(&self, key: u8) -> Result<(Library<'a>, Library<'a>), Error> {
midi_key("the split key", key)?;
let mut low = self.clone();
let mut high = self.clone();
low.restrict(|k| k < key);
high.restrict(|k| k >= key);
Ok((low, high))
}
fn restrict(&mut self, keep: impl Fn(u8) -> bool) -> Change {
let mut uncovered = 0;
for (key, slot) in self.key_map_mut().iter_mut().enumerate() {
if !keep(key as u8) && *slot != UNCOVERED {
*slot = UNCOVERED;
uncovered += 1;
}
}
let live: BTreeSet<u8> = self.key_map().iter().copied().collect();
let mut change = self.retain(|s| live.contains(&s.root));
change.keys_uncovered += uncovered;
change
}
fn retain(&mut self, mut keep: impl FnMut(&Stroke<'a>) -> bool) -> Change {
let strokes_before = self.strokes.len();
let roots_before = self.roots().len();
self.strokes.retain(|s| keep(s));
let roots = self.roots();
let mut keys_uncovered = 0;
for slot in self.key_map_mut() {
if *slot != UNCOVERED && !roots.contains(slot) {
*slot = UNCOVERED;
keys_uncovered += 1;
}
}
Change {
strokes_removed: strokes_before - self.strokes.len(),
roots_removed: roots_before - roots.len(),
keys_uncovered,
}
}
pub fn body_len(&self) -> Result<usize, Error> {
let (_, len) = self.extent()?;
Ok(len)
}
fn extent(&self) -> Result<(usize, usize), Error> {
let directory_end = RECORD
.checked_mul(self.strokes.len())
.and_then(|len| DIRECTORY_AT.checked_add(len))
.ok_or_else(|| overflow("the stroke directory"))?;
let block = self.block_bytes();
let first = first_audio_offset(directory_end, block)?;
let mut len = first;
for (index, stroke) in self.strokes.iter().enumerate() {
let span = usize::from(stroke.blocks())
.checked_mul(block)
.ok_or_else(|| overflow("a stroke's audio span"))?;
if stroke.audio.len() != span {
return Err(ParseError::AssertFail(format!(
"stroke {index} holds {} audio bytes where the {} block(s) its record \
states span {span}",
stroke.audio.len(),
stroke.blocks()
))
.into());
}
len = len.checked_add(span).ok_or_else(|| overflow("the audio"))?;
}
Ok((first, len))
}
pub fn to_body(&self) -> Result<Vec<u8>, Error> {
let count = u16::try_from(self.strokes.len()).map_err(|_| ParseError::OutOfBounds {
value: format!("{} strokes", self.strokes.len()),
bound: "the u16 stroke count the directory holds".into(),
})?;
if self.strokes.windows(2).any(|w| w[0].root > w[1].root) {
return Err(ParseError::AssertFail(
"the strokes are not in ascending root order, which is what the per-root \
counts index them by"
.into(),
)
.into());
}
let (first, len) = self.extent()?;
let mut out = try_vec(len)?;
out[..DIRECTORY_AT].copy_from_slice(&self.prefix);
out[CHANNELS_AT..CHANNELS_AT + 2].copy_from_slice(&self.channels.to_be_bytes());
out[STROKE_COUNT_AT..STROKE_COUNT_AT + 2].copy_from_slice(&count.to_be_bytes());
for note in 0..NOTES {
let n = self
.strokes
.iter()
.filter(|s| usize::from(s.root) == note)
.count();
let n = u16::try_from(n).expect("a per-root count is at most the stroke count");
let at = ROOT_COUNTS_AT + note * 2;
out[at..at + 2].copy_from_slice(&n.to_be_bytes());
}
let mut at = first;
for (i, stroke) in self.strokes.iter().enumerate() {
let start = u32::try_from(at).map_err(|_| ParseError::OutOfBounds {
value: format!("audio offset {at:#x}"),
bound: "the u32 offset a stroke record holds".into(),
})?;
let record = DIRECTORY_AT + i * RECORD;
out[record..record + RECORD].copy_from_slice(&stroke.record);
out[record + REC_START..record + REC_START + 4].copy_from_slice(&start.to_be_bytes());
out[at..at + stroke.audio.len()].copy_from_slice(&stroke.audio);
at += stroke.audio.len();
}
Ok(out)
}
pub fn to_piano(&self) -> Result<Piano, Error> {
Ok(Piano {
file: Cbin {
header: self.header.clone(),
body: RawBody(self.to_body()?),
},
})
}
}
impl fmt::Debug for Library<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let (name, variant) = self.name();
f.debug_struct("npno::Library")
.field("name", &name)
.field("variant", &variant)
.field(
"stream_version",
&format_args!("{:#06x}", self.stream_version()),
)
.field("channels", &self.channels)
.field("strokes", &self.strokes.len())
.field("roots", &self.roots().len())
.finish()
}
}
fn block_bytes(channels: u16) -> usize {
codec::BLOCK_WORDS * 2 * usize::from(channels)
}
#[cfg(test)]
mod tests {
use super::synthetic::{take, Build};
use super::*;
#[test]
fn the_name_field_splits_on_the_separator() {
let piano = Build::new().piano();
assert_eq!(piano.stream_version().unwrap(), 0x450);
assert_eq!(
piano.name().unwrap(),
("Test Piano".to_string(), "Variant".to_string())
);
}
#[test]
fn an_unknown_stream_version_still_round_trips_but_does_not_decode() {
let mut build = Build::new();
build.version = 0x500;
let piano = build.piano();
assert_eq!(piano.stream_version().unwrap(), 0x500);
assert!(
piano.name().is_err(),
"the name offset is only pinned on known versions"
);
assert!(piano.key_map().is_err());
assert!(piano.library().is_err());
}
#[test]
fn a_body_without_the_magic_is_refused() {
let mut piano = Build::new().piano();
piano.file.body.0[0] = b'Q';
assert!(piano.name().is_err(), "a non-CNSP body has no name to read");
}
#[test]
fn a_library_rebuilds_to_the_bytes_it_was_read_from() {
let piano = Build::new().piano();
let rebuilt = piano.library().unwrap().to_body().unwrap();
assert_eq!(rebuilt, piano.file.body.0);
}
#[test]
fn the_directory_reports_each_strokes_root_bank_and_layer() {
let piano = Build::new().piano();
let library = piano.library().unwrap();
let seen: Vec<(u8, Option<Bank>, u8)> = library
.strokes()
.iter()
.map(|s| (s.root, s.bank(), s.layer()))
.collect();
assert_eq!(
seen,
[
(60, Some(Bank::Attack), 0),
(60, Some(Bank::Release), 3),
(72, Some(Bank::Attack), 0),
]
);
assert_eq!(library.keys_for(60), [60, 61]);
}
#[test]
fn a_stroke_whose_start_does_not_abut_the_one_before_is_refused() {
let mut piano = Build::new().piano();
let second = DIRECTORY_AT + RECORD;
let start = be32(&piano.file.body.0, second + REC_START);
piano.file.body.0[second..second + 4].copy_from_slice(&(start + 2).to_be_bytes());
let error = piano.library().unwrap_err().to_string();
assert!(error.contains("stroke 1 starts at"), "{error}");
}
#[test]
fn a_key_routed_to_a_root_no_stroke_records_is_refused() {
let mut build = Build::new();
build.map.push((80, 80));
let error = build.piano().library().unwrap_err().to_string();
assert!(error.contains("key 80 plays root 80"), "{error}");
}
#[test]
fn a_count_table_that_does_not_sum_to_the_stroke_count_is_refused() {
let mut piano = Build::new().piano();
let at = ROOT_COUNTS_AT + 60 * 2;
piano.file.body.0[at..at + 2].copy_from_slice(&5u16.to_be_bytes());
let error = piano.library().unwrap_err().to_string();
assert!(error.contains("per-root counts sum to"), "{error}");
}
#[test]
fn dropping_a_bank_relays_the_audio_and_leaves_the_rest_verbatim() {
let piano = Build::new().piano();
let before = piano.library().unwrap();
let mut after = piano.library().unwrap();
let change = after.drop_bank(Bank::Release);
assert_eq!(
change,
Change {
strokes_removed: 1,
roots_removed: 0,
keys_uncovered: 0
}
);
let body = after.to_body().unwrap();
let trimmed = Piano {
file: Cbin {
header: after.header.clone(),
body: RawBody(body),
},
};
let reparsed = trimmed.library().unwrap();
assert_eq!(reparsed.strokes().len(), 2);
for (kept, moved) in before
.strokes()
.iter()
.filter(|s| s.bank() != Some(Bank::Release))
.zip(reparsed.strokes())
{
assert_eq!(kept.audio(), moved.audio(), "a span moved verbatim");
assert_eq!(kept.id(), moved.id());
assert_eq!(&kept.record()[REC_BANK..], &moved.record()[REC_BANK..]);
}
}
#[test]
fn dropping_every_stroke_of_a_root_uncovers_the_keys_it_played() {
let mut build = Build::new();
build.takes = vec![
take(60, Bank::Attack, 0, 1),
take(72, Bank::Resonance, 0, 1),
];
let piano = build.piano();
let mut library = piano.library().unwrap();
let change = library.drop_bank(Bank::Resonance);
assert_eq!(change.strokes_removed, 1);
assert_eq!(change.roots_removed, 1);
assert_eq!(change.keys_uncovered, 1);
assert_eq!(library.key_map()[72], UNCOVERED);
library.to_body().unwrap();
}
#[test]
fn keeping_the_loudest_layer_keeps_one_per_root_and_bank() {
let mut build = Build::new();
build.takes = vec![
take(60, Bank::Attack, 0, 1),
take(60, Bank::Attack, 5, 1),
take(60, Bank::Release, 26, 1),
take(60, Bank::Release, 30, 1),
take(72, Bank::Attack, 1, 1),
];
let piano = build.piano();
let mut library = piano.library().unwrap();
library.keep_layers(&Layers::Loudest(1));
let kept: Vec<(u8, u8, u8)> = library
.strokes()
.iter()
.map(|s| (s.root, s.bank_code(), s.layer()))
.collect();
assert_eq!(kept, [(60, 0, 0), (60, 2, 26), (72, 0, 1)]);
}
#[test]
fn keeping_named_layers_takes_them_wherever_they_occur() {
let mut build = Build::new();
build.takes = vec![
take(60, Bank::Attack, 0, 1),
take(60, Bank::Attack, 5, 1),
take(72, Bank::Attack, 5, 1),
];
let piano = build.piano();
let mut library = piano.library().unwrap();
library.keep_layers(&Layers::Only([5].into_iter().collect()));
let kept: Vec<(u8, u8)> = library
.strokes()
.iter()
.map(|s| (s.root, s.layer()))
.collect();
assert_eq!(kept, [(60, 5), (72, 5)]);
}
#[test]
fn retaining_strokes_drops_what_the_predicate_rejects_and_nothing_else() {
let mut build = Build::new();
build.takes = vec![
take(60, Bank::Attack, 0, 1),
take(60, Bank::Attack, 5, 1),
take(72, Bank::Attack, 5, 2),
];
let piano = build.piano();
let mut kept_all = piano.library().unwrap();
let unchanged = kept_all.retain_strokes(|_| true);
assert_eq!(unchanged, Change::default());
assert_eq!(
kept_all.to_body().unwrap(),
piano.file.body.0,
"a predicate that rejects nothing re-lays the body it read"
);
let mut library = piano.library().unwrap();
let change = library.retain_strokes(|s| !(s.root == 72 && s.layer() == 5));
assert_eq!(
change,
Change {
strokes_removed: 1,
roots_removed: 1,
keys_uncovered: 1,
}
);
let left: Vec<(u8, u8)> = library
.strokes()
.iter()
.map(|s| (s.root, s.layer()))
.collect();
assert_eq!(left, [(60, 0), (60, 5)]);
assert_eq!(
library.key_map()[72],
UNCOVERED,
"root 72 lost every stroke, so its key answers nothing"
);
assert_eq!(library.key_map()[60], 60, "and the other root is untouched");
library.to_body().unwrap();
}
#[test]
fn a_synthetic_library_reads_back_as_the_file_it_was_built_as() {
let bytes = Build::new().bytes().unwrap();
let entity = crate::from_stream(&mut std::io::Cursor::new(&bytes)).unwrap();
let crate::Entity::Piano(piano) = &entity else {
panic!("{entity:?} is no piano library");
};
assert_eq!(
piano.name().unwrap(),
("Test Piano".to_string(), "Variant".to_string())
);
assert_eq!(piano.library().unwrap().strokes().len(), 3);
assert_eq!(crate::to_bytes(&entity).unwrap(), bytes);
}
#[test]
fn borrowing_a_file_reads_it_without_copying_the_audio() {
let bytes = Build::new().bytes().unwrap();
let library = Library::borrow(&bytes).unwrap();
let base = bytes.as_ptr() as usize;
let within = base..base + bytes.len();
for stroke in library.strokes() {
let at = stroke.audio().as_ptr() as usize;
assert!(
within.contains(&at),
"{stroke:?} holds a copy of its audio, not the caller's bytes"
);
}
assert_eq!(library.name(), ("Test Piano".into(), "Variant".into()));
assert_eq!(library.strokes().len(), 3);
assert_eq!(library.to_body().unwrap(), Build::new().body());
}
#[test]
fn borrowing_refuses_a_container_that_is_not_a_whole_piano_library() {
let bytes = Build::new().bytes().unwrap();
let mut other = bytes.clone();
other[0x08..0x0c].copy_from_slice(b"nsmp");
let error = Library::borrow(&other).unwrap_err().to_string();
assert!(error.contains("expected a npno file, got nsmp"), "{error}");
let error = Library::borrow(&bytes[..bytes.len() - 1])
.unwrap_err()
.to_string();
assert!(
error.contains("ends inside a stroke's audio span"),
"{error}"
);
}
#[test]
fn cutting_the_range_drops_the_roots_nothing_plays_any_more() {
let piano = Build::new().piano();
let mut library = piano.library().unwrap();
let change = library.cut_range(0..=70).unwrap();
assert_eq!(change.keys_uncovered, 1);
assert_eq!(change.roots_removed, 1);
assert_eq!(library.roots(), [60].into_iter().collect());
assert_eq!(library.key_map()[72], UNCOVERED);
assert_eq!(library.key_map()[60], 60);
}
#[test]
fn a_split_gives_each_half_the_roots_its_keys_play() {
let piano = Build::new().piano();
let (low, high) = piano.library().unwrap().split_at(70).unwrap();
assert_eq!(low.roots(), [60].into_iter().collect());
assert_eq!(high.roots(), [72].into_iter().collect());
assert_eq!(low.keys_for(60), [60, 61]);
assert_eq!(high.keys_for(72), [72]);
let audio: usize = piano
.library()
.unwrap()
.strokes()
.iter()
.map(|s| s.audio().len())
.sum();
let halves: usize = [&low, &high]
.iter()
.flat_map(|l| l.strokes())
.map(|s| s.audio().len())
.sum();
assert_eq!(
halves, audio,
"a split shares every stroke out exactly once"
);
}
#[test]
fn a_rename_carries_the_long_name_with_it_and_leaves_the_voicing_alone() {
let mut build = Build::new();
build.version = VERSION_SPLIT_NAME;
let piano = build.piano();
let mut library = piano.library().unwrap();
library.set_voicing("Nordiska").unwrap();
library.set_name("Renamed").unwrap();
library.set_variant("Nordiska Sml").unwrap();
assert_eq!(library.name(), ("Renamed".into(), "Nordiska Sml".into()));
assert_eq!(library.long_name().as_deref(), Some("Renamed"));
assert_eq!(
library.voicing().as_deref(),
Some("Nordiska"),
"the voicing is its own field, not the variant's head"
);
}
#[test]
fn the_older_stream_has_no_long_name_or_voicing_to_read_or_write() {
let piano = Build::new().piano();
let mut library = piano.library().unwrap();
assert_eq!(library.stream_version(), 0x450);
assert_eq!(library.long_name(), None);
assert_eq!(library.voicing(), None);
assert!(library.set_voicing("Nordiska").is_err());
}
#[test]
fn a_name_past_the_field_is_refused_without_changing_it() {
let piano = Build::new().piano();
let mut library = piano.library().unwrap();
let too_long = "x".repeat(TextField::COMBINED.capacity());
assert!(library.set_name(&too_long).is_err());
assert_eq!(library.name().0, "Test Piano");
}
#[test]
fn a_remap_to_a_root_the_directory_does_not_record_is_refused() {
let piano = Build::new().piano();
let mut library = piano.library().unwrap();
assert!(library.set_key_root(64, Some(61)).is_err());
library.set_key_root(64, Some(72)).unwrap();
assert_eq!(library.keys_for(72), [64, 72]);
assert_eq!(library.key_root(64).unwrap(), Some(72));
library.set_key_root(64, None).unwrap();
assert_eq!(library.keys_for(72), [72]);
assert_eq!(library.key_root(64).unwrap(), None);
}
#[test]
fn fine_tune_reads_and_writes_the_per_key_byte() {
let piano = Build::new().piano();
let mut library = piano.library().unwrap();
assert_eq!(library.fine_tune(60).unwrap(), 0);
library.set_fine_tune(60, -4).unwrap();
assert_eq!(library.fine_tune(60).unwrap(), -4);
assert_eq!(library.to_body().unwrap()[FINE_TUNE_AT + 60], 0xfc);
}
fn changed(before: &[u8], after: &[u8]) -> Vec<usize> {
assert_eq!(before.len(), after.len(), "the body changed length");
(0..before.len())
.filter(|&at| before[at] != after[at])
.collect()
}
#[test]
fn the_gain_and_the_damper_limit_each_write_one_byte_of_the_prefix() {
let piano = Build::new().piano();
let mut library = piano.library().unwrap();
let before = library.to_body().unwrap();
library.set_gain(-20);
let gained = library.to_body().unwrap();
assert_eq!(library.gain(), -20);
assert_eq!(gained[GAIN_AT], 0xec, "tenths of a decibel, signed");
assert_eq!(changed(&before, &gained), [GAIN_AT]);
library.set_damper_top(90).unwrap();
let damped = library.to_body().unwrap();
assert_eq!(library.damper_top(), 90);
assert_eq!(changed(&gained, &damped), [DAMPER_TOP_AT]);
}
#[test]
fn the_instrument_kind_writes_one_byte_and_reads_back_as_the_kind_it_was_given() {
let piano = Build::new().piano();
let mut library = piano.library().unwrap();
let before = library.to_body().unwrap();
library.set_kind(encode::Kind::Wurlitzer);
let filed = library.to_body().unwrap();
assert_eq!(
encode::Kind::from_code(library.kind_code()),
Some(encode::Kind::Wurlitzer)
);
assert_eq!(changed(&before, &filed), [KIND_AT]);
}
#[test]
fn a_damper_limit_past_the_last_midi_note_is_refused_without_moving_the_one_held() {
let piano = Build::new().piano();
let mut library = piano.library().unwrap();
library.set_damper_top(encode::ALL_KEYS_DAMPED).unwrap();
let before = library.to_body().unwrap();
assert!(library.set_damper_top(NOTES as u8).is_err());
assert_eq!(library.damper_top(), encode::ALL_KEYS_DAMPED);
assert_eq!(library.to_body().unwrap(), before);
}
#[test]
fn a_retrim_writes_both_bytes_of_one_strokes_own_field() {
let piano = Build::new().piano();
let mut library = piano.library().unwrap();
assert!(library.strokes().iter().all(|s| s.trim() == 0));
let before = library.to_body().unwrap();
let at = DIRECTORY_AT + RECORD + REC_TRIM;
library.set_trim(1, 7).unwrap();
let low = library.to_body().unwrap();
assert_eq!(library.strokes()[1].trim(), 7);
assert_eq!(changed(&before, &low), [at + 1]);
library.set_trim(1, 0x0107).unwrap();
let high = library.to_body().unwrap();
assert_eq!(library.strokes()[1].trim(), 0x0107);
assert_eq!(changed(&low, &high), [at]);
let error = library.set_trim(3, 4).unwrap_err().to_string();
assert!(error.contains("stroke 3"), "{error}");
assert_eq!(
library.to_body().unwrap(),
high,
"a refused retrim leaves the directory alone"
);
}
#[test]
fn a_key_above_the_last_midi_note_is_refused_by_every_entry_point() {
let piano = Build::new().piano();
let mut library = piano.library().unwrap();
let last = (NOTES - 1) as u8;
let past = NOTES as u8;
assert!(library.fine_tune(last).is_ok());
assert!(library.key_root(last).is_ok());
assert!(library.set_fine_tune(last, 1).is_ok());
assert!(library.set_key_root(last, None).is_ok());
assert!(library.cut_range(0..=last).is_ok());
assert!(library.split_at(last).is_ok());
assert!(library.fine_tune(past).is_err());
assert!(library.key_root(past).is_err());
assert!(library.set_fine_tune(past, 1).is_err());
assert!(library.set_key_root(past, None).is_err());
assert!(library.set_key_root(0, Some(past)).is_err());
assert!(library.cut_range(0..=past).is_err());
assert!(library.cut_range(past..=past).is_err());
assert!(library.split_at(past).is_err());
}
#[test]
fn a_key_past_the_tune_table_is_refused_rather_than_written_to_the_next_table() {
let piano = Build::new().piano();
let mut library = piano.library().unwrap();
let before = library.to_body().unwrap();
assert!(library.set_fine_tune(NOTES as u8, 32).is_err());
assert_eq!(library.to_body().unwrap(), before);
}
#[test]
fn a_separator_in_a_name_or_a_variant_is_refused() {
let piano = Build::new().piano();
let mut library = piano.library().unwrap();
assert!(library.set_name("Upright#2").is_err());
assert!(library.set_variant("Sml#XL").is_err());
assert_eq!(library.name(), ("Test Piano".into(), "Variant".into()));
}
#[test]
fn a_stroke_holding_other_than_the_blocks_its_record_states_is_not_laid_out() {
let piano = Build::new().piano();
let library = piano.library().unwrap();
assert!(library.to_body().is_ok());
let skeleton = library.without_audio();
let error = skeleton
.to_body()
.expect_err("expected a refusal")
.to_string();
assert!(error.contains("stroke 0 holds 0 audio bytes"), "{error}");
assert!(skeleton.body_len().is_err());
}
#[test]
fn setting_one_half_of_the_name_field_leaves_the_other_as_it_was_written() {
let mut piano = Build::new().piano();
let at = TextField::COMBINED.at;
let padded = b"Grand Imperial # Bdorf XL";
piano.file.body.0[at..at + TextField::COMBINED.len].fill(0);
piano.file.body.0[at..at + padded.len()].copy_from_slice(padded);
assert_eq!(
piano.library().unwrap().name(),
("Grand Imperial".into(), "Bdorf XL".into())
);
let mut renamed = piano.library().unwrap();
renamed.set_name("Upright").unwrap();
assert_eq!(
TextField::COMBINED.read(&renamed.prefix),
"Upright# Bdorf XL"
);
let mut revoiced = piano.library().unwrap();
revoiced.set_variant("Sml").unwrap();
assert_eq!(
TextField::COMBINED.read(&revoiced.prefix),
"Grand Imperial #Sml"
);
}
#[test]
fn text_the_field_would_not_read_back_is_refused() {
let piano = Build::new().piano();
let mut library = piano.library().unwrap();
assert!(
library.set_name("Flügel").is_err(),
"the field is read as ASCII"
);
assert!(
library.set_variant("Sml\0XL").is_err(),
"a NUL ends the field, hiding everything after it"
);
assert_eq!(library.name(), ("Test Piano".into(), "Variant".into()));
}
#[test]
fn the_first_audio_offset_sits_on_the_block_grid_less_the_bias() {
for block in [1022, 2044] {
for count in [0usize, 1, 38, 2196] {
let end = DIRECTORY_AT + count * RECORD;
let at = first_audio_offset(end, block).unwrap();
assert!(at >= end, "the audio never overlaps the directory");
assert_eq!((at + AUDIO_ALIGN_BIAS) % block, 0);
assert!(at - end < block, "no whole spare block in the gap");
}
}
}
}