pub struct ZoneAudio<'a> {
pub root_key: u8,
pub top_note: u8,
pub low_note: Option<u8>,
pub at: usize,
pub stream: &'a [u8],
}
pub mod codec;
pub mod encode;
pub mod kernel;
pub mod keymap;
pub mod meta;
pub mod section;
pub mod stroke;
pub mod sty;
pub mod zone;
pub use keymap::{KeyTable, Level};
pub use meta::Meta;
pub use section::Section;
pub use stroke::Stroke;
pub use sty::{velocity_level, EqBand, Sty, StyV2, StyV3};
pub use zone::Zone;
pub use zone::ZoneV3;
use crate::cbin::{self, BodyReader, BodyWriter, Cbin, Header};
use crate::error::{Error, ParseError};
use std::fmt;
use std::io::{Read, Seek, Write};
pub const FORMAT: &str = "nsmp";
pub const V3_FROM_VERSION: u32 = 300;
pub const V4_FROM_VERSION: u32 = 400;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Chain {
Early,
Library2,
Wide,
}
impl Chain {
pub fn from_map_version(version: u8) -> Result<Chain, ParseError> {
match version {
keymap::VERSION_EARLY => Ok(Chain::Early),
keymap::VERSION => Ok(Chain::Library2),
other => Err(ParseError::AssertFail(format!(
"map section version {other} has no zone table layout derived from a specimen"
))),
}
}
pub const fn zone_record_len(self) -> usize {
match self {
Chain::Early => 12,
Chain::Library2 | Chain::Wide => 15,
}
}
pub const fn written_for(layout: codec::Layout) -> Chain {
match layout {
codec::Layout::V2 => Chain::Library2,
codec::Layout::V3 | codec::Layout::V4 => Chain::Wide,
}
}
pub const fn names_instrument(self) -> bool {
!matches!(self, Chain::Early)
}
pub const fn flags_the_marked_record(self) -> bool {
!matches!(self, Chain::Early)
}
}
#[derive(Debug)]
pub enum AnyBody {
V2(Sample),
V3(SampleV3),
}
impl cbin::Body for AnyBody {
fn read<R: Read + Seek>(r: &mut BodyReader<'_, R>, header: &Header) -> Result<Self, Error> {
if header.version >= V3_FROM_VERSION {
Ok(AnyBody::V3(<SampleV3 as cbin::Body>::read(r, header)?))
} else {
Ok(AnyBody::V2(<Sample as cbin::Body>::read(r, header)?))
}
}
fn write<W: Write + Seek>(&self, w: &mut BodyWriter<'_, W>) -> Result<(), Error> {
match self {
AnyBody::V2(s) => <Sample as cbin::Body>::write(s, w),
AnyBody::V3(s) => <SampleV3 as cbin::Body>::write(s, w),
}
}
}
#[derive(Clone, Copy)]
pub(super) struct StringField {
at: usize,
next: usize,
}
impl StringField {
pub(super) const NAME: StringField = StringField { at: 12, next: 44 };
pub(super) const NAME_V3: StringField = StringField { at: 10, next: 76 };
pub(super) const fn capacity(self) -> usize {
self.next - self.at - 1
}
fn read(self, payload: &[u8]) -> String {
let span = self.at.min(payload.len())..self.next.min(payload.len());
nul_terminated(&payload[span])
}
pub(super) fn write(self, payload: &mut [u8], value: &str) -> Result<(), Error> {
if value.len() > self.capacity() {
return Err(ParseError::OutOfBounds {
value: format!("{value:?} ({} bytes)", value.len()),
bound: format!("a name of at most {} bytes", self.capacity()),
}
.into());
}
let field = payload
.get_mut(self.at..self.next)
.ok_or_else(|| ParseError::AssertFail("hdr section holds no name field".into()))?;
field.fill(0);
field[..value.len()].copy_from_slice(value.as_bytes());
Ok(())
}
}
fn nul_terminated(bytes: &[u8]) -> String {
let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
String::from_utf8_lossy(&bytes[..end]).into_owned()
}
pub const MAX_NAME_LEN: usize = StringField::NAME.capacity();
pub struct Sample {
pub sections: Vec<Section>,
}
impl cbin::Body for Sample {
fn read<R: Read + Seek>(r: &mut BodyReader<'_, R>, _: &Header) -> Result<Self, Error> {
let remaining = r.remaining();
Ok(Sample {
sections: section::read_chain(r, remaining)?,
})
}
fn write<W: Write + Seek>(&self, w: &mut BodyWriter<'_, W>) -> Result<(), Error> {
for s in &self.sections {
s.write_to(w)?;
}
Ok(())
}
}
pub fn read_from(reader: &mut (impl Read + Seek)) -> Result<Cbin<Sample>, Error> {
cbin::read(reader, FORMAT)
}
#[derive(Debug)]
pub struct SampleV3 {
pub sections: Vec<section::Section4>,
}
impl cbin::Body for SampleV3 {
fn read<R: Read + Seek>(r: &mut BodyReader<'_, R>, _: &Header) -> Result<Self, Error> {
let remaining = r.remaining();
Ok(SampleV3 {
sections: section::read_chain4(r, remaining)?,
})
}
fn write<W: Write + Seek>(&self, w: &mut BodyWriter<'_, W>) -> Result<(), Error> {
for s in &self.sections {
s.write_to(w)?;
}
Ok(())
}
}
pub const MAX_NAME_V3_LEN: usize = StringField::NAME_V3.capacity();
impl Cbin<SampleV3> {
fn hdr(&self) -> Result<§ion::Section4, Error> {
section::find4(&self.body.sections, section::HDR4)
.ok_or_else(|| ParseError::AssertFail("no hdr section".into()).into())
}
pub fn name(&self) -> Result<String, Error> {
Ok(StringField::NAME_V3.read(&self.hdr()?.payload))
}
pub fn sub_name(&self) -> Result<String, Error> {
let payload = &self.hdr()?.payload;
let from = StringField::NAME_V3.next.min(payload.len());
Ok(nul_terminated(&payload[from..]))
}
pub fn stroke_count(&self) -> usize {
self.body
.sections
.iter()
.filter(|s| s.is(section::STK4))
.count()
}
fn stroke_ids(&self) -> Result<Vec<(u32, u8)>, Error> {
self.body
.sections
.iter()
.filter(|s| s.is(section::STK4))
.map(|s| match (stroke_gid(s), s.payload.get(5)) {
(Some(gid), Some(&root)) => Ok((gid, root)),
_ => Err(ParseError::AssertFail(format!(
"stroke payload is {} bytes, too short for its id fields",
s.payload.len()
))
.into()),
})
.collect()
}
pub fn zones(&self) -> Result<Vec<ZoneV3>, Error> {
let map = self.map()?;
Ok(zone::read_v3(
map.version,
&map.payload,
&self.stroke_ids()?,
)?)
}
fn map(&self) -> Result<§ion::Section4, Error> {
section::find4(&self.body.sections, section::MAP4)
.ok_or_else(|| ParseError::AssertFail("no map section".into()).into())
}
pub fn sty(&self) -> Result<Sty, Error> {
let s = section::find4(&self.body.sections, section::STY4)
.ok_or_else(|| ParseError::AssertFail("no sty section".into()))?;
Ok(Sty::parse_wide(s.version, &s.payload)?)
}
pub fn meta(&self) -> Result<Meta, Error> {
let s = section::find4(&self.body.sections, section::META4)
.ok_or_else(|| ParseError::AssertFail("no meta section".into()))?;
Ok(Meta::parse(s.version, &s.payload)?)
}
pub fn chain_len_before_meta(&self) -> usize {
self.body
.sections
.iter()
.take_while(|s| !s.is(section::META4))
.map(section::Section4::encoded_len)
.sum()
}
fn map_mut(&mut self) -> Result<&mut section::Section4, Error> {
section::find_mut4(&mut self.body.sections, section::MAP4)
.ok_or_else(|| ParseError::AssertFail("no map section".into()).into())
}
pub fn zone_table(&self) -> Result<zone::Table, Error> {
let map = self.map()?;
Ok(zone::Table::locate(
map.version,
&map.payload,
&self.stroke_ids()?,
)?)
}
pub fn set_name(&mut self, name: &str) -> Result<(), Error> {
let hdr = section::find_mut4(&mut self.body.sections, section::HDR4)
.ok_or_else(|| ParseError::AssertFail("no hdr section".into()))?;
StringField::NAME_V3.write(&mut hdr.payload, name)
}
pub fn zones_are_editable(&self) -> bool {
match (self.zone_table(), self.map(), self.zones()) {
(Ok(table), Ok(map), Ok(zones)) => table.validate_key_map(&map.payload, &zones).is_ok(),
_ => false,
}
}
fn edit_zone(&mut self, index: usize, field: zone::Field, note: u8) -> Result<(), Error> {
let table = self.zone_table()?;
let mut zones = self.zones()?;
let map = self.map()?;
table.validate_key_map(&map.payload, &zones)?;
let zone = zones
.get_mut(index)
.ok_or_else(|| ParseError::AssertFail(format!("no zone {index}")))?;
match field {
zone::Field::Root => zone.root_key = note,
zone::Field::Top => zone.top_note = note,
zone::Field::Low => zone.low_note = Some(note),
}
let plan = table.plan_key_map(&map.payload, &zones)?;
let map = self.map_mut()?;
table.set(&mut map.payload, index, field, note)?;
for (at, quad) in plan {
map.payload[at..at + quad.len()].copy_from_slice(&quad);
}
Ok(())
}
pub fn set_zone_top_note(&mut self, index: usize, note: u8) -> Result<(), Error> {
self.edit_zone(index, zone::Field::Top, note)
}
pub fn set_zone_low_note(&mut self, index: usize, note: u8) -> Result<(), Error> {
self.edit_zone(index, zone::Field::Low, note)
}
pub fn set_root_key(&mut self, index: usize, note: u8) -> Result<(), Error> {
let gid = self
.zones()?
.get(index)
.ok_or_else(|| ParseError::AssertFail(format!("no zone {index}")))?
.stroke_gid;
let at = self
.body
.sections
.iter()
.position(|s| s.is(section::STK4) && stroke_gid(s) == Some(gid))
.ok_or_else(|| {
ParseError::AssertFail(format!(
"zone {index} names stroke {gid}, which the file does not contain"
))
})?;
self.edit_zone(index, zone::Field::Root, note)?;
stroke::set_root_key(&mut self.body.sections[at].payload, note)?;
Ok(())
}
pub fn stroke_streams(&self) -> Vec<(usize, &[u8])> {
let mut at = 0;
let mut out = Vec::new();
for section in &self.body.sections {
if section.is(section::STK4) {
out.push((at + section::HEADER4_LEN, section.payload.as_slice()));
}
at += section.encoded_len();
}
out
}
pub fn zone_stream(&self, index: usize) -> Result<(usize, &[u8]), Error> {
let zones = self.zones()?;
let zone = zones
.get(index)
.ok_or_else(|| ParseError::AssertFail(format!("no zone {index}")))?;
let mut at = 0;
for section in &self.body.sections {
if section.is(section::STK4) && stroke_gid(section) == Some(zone.stroke_gid) {
return Ok((at + section::HEADER4_LEN, section.payload.as_slice()));
}
at += section.encoded_len();
}
Err(ParseError::AssertFail(format!(
"zone {index} names stroke {}, which the file does not contain",
zone.stroke_gid
))
.into())
}
}
pub fn from_bytes(bytes: &[u8]) -> Result<Cbin<Sample>, Error> {
read_from(&mut std::io::Cursor::new(bytes))
}
fn stroke_id(section: &Section) -> Option<u32> {
let b = section.payload.get(0..4)?;
Some(u32::from_be_bytes(b.try_into().ok()?))
}
fn stroke_gid(section: §ion::Section4) -> Option<u32> {
let b = section.payload.get(0..4)?;
Some(u32::from_be_bytes(b.try_into().ok()?))
}
fn names_stroke(id: u32, named: u8) -> bool {
id as u8 == named
}
impl Cbin<Sample> {
pub fn to_bytes(&self) -> Result<Vec<u8>, Error> {
let mut out = std::io::Cursor::new(Vec::new());
self.write_to(&mut out)?;
Ok(out.into_inner())
}
pub fn name(&self) -> Result<String, Error> {
Ok(StringField::NAME.read(&self.hdr()?.payload))
}
pub fn set_name(&mut self, name: &str) -> Result<(), Error> {
let hdr = section::find_mut(&mut self.body.sections, section::HDR)
.ok_or_else(|| ParseError::AssertFail("no hdr section".into()))?;
StringField::NAME.write(&mut hdr.payload, name)
}
pub fn chain(&self) -> Result<Chain, Error> {
Ok(Chain::from_map_version(self.map()?.version)?)
}
pub fn zones(&self) -> Result<Vec<Zone>, Error> {
Ok(zone::read(self.chain()?, &self.map()?.payload)?)
}
pub fn sty(&self) -> Result<StyV2, Error> {
let s = section::find(&self.body.sections, section::STY)
.ok_or_else(|| ParseError::AssertFail("no sty section".into()))?;
if s.version != sty::VERSION_V2 {
return Err(ParseError::AssertFail(format!(
"sty section version {} has no preset layout derived from a specimen",
s.version
))
.into());
}
Ok(StyV2::parse(&s.payload)?)
}
pub fn set_zone_top_note(&mut self, index: usize, note: u8) -> Result<(), Error> {
let chain = self.chain()?;
let map = section::find_mut(&mut self.body.sections, section::MAP)
.ok_or_else(|| ParseError::AssertFail("no map section".into()))?;
zone::set_top_note(chain, &mut map.payload, index, note)?;
Ok(())
}
pub fn key_table(&self) -> Result<KeyTable, Error> {
self.chain()?;
Ok(KeyTable::read(&self.map()?.payload)?)
}
pub fn set_key_table(&mut self, table: &KeyTable) -> Result<(), Error> {
self.chain()?;
let map = section::find_mut(&mut self.body.sections, section::MAP)
.ok_or_else(|| ParseError::AssertFail("no map section".into()))?;
table.write(&mut map.payload)?;
Ok(())
}
pub fn strokes(&self) -> Result<Vec<Stroke>, Error> {
let zones = self.zones()?;
let by_id = self.strokes_in_file_order()?;
zones
.iter()
.map(|z| {
by_id
.iter()
.find(|(id, _)| names_stroke(*id, z.stroke_id))
.map(|(_, s)| *s)
.ok_or_else(|| {
ParseError::AssertFail(format!(
"zone reaching up to note {} names stroke {}, which the file \
does not contain",
z.top_note, z.stroke_id
))
.into()
})
})
.collect()
}
pub fn stroke_streams(&self) -> Vec<(usize, &[u8])> {
let mut at = 0;
let mut out = Vec::new();
for section in &self.body.sections {
if section.is(section::STK) {
out.push((at + section::HEADER_LEN, section.payload.as_slice()));
}
at += section.encoded_len();
}
out
}
pub fn zone_stream(&self, index: usize) -> Result<(usize, &[u8]), Error> {
let zones = self.zones()?;
let zone = zones
.get(index)
.ok_or_else(|| ParseError::AssertFail(format!("no zone {index}")))?;
let wanted = zone.stroke_id;
let mut at = 0;
for section in &self.body.sections {
if section.is(section::STK)
&& stroke_id(section).is_some_and(|id| names_stroke(id, wanted))
{
return Ok((at + section::HEADER_LEN, section.payload.as_slice()));
}
at += section.encoded_len();
}
Err(ParseError::AssertFail(format!(
"zone {index} names stroke {wanted}, which the file does not contain"
))
.into())
}
fn strokes_in_file_order(&self) -> Result<Vec<(u32, Stroke)>, Error> {
let chain = self.chain()?;
let map_len = self.map()?.payload.len();
let cat_len =
section::find(&self.body.sections, section::CAT).map_or(0, |s| s.payload.len());
self.stroke_sections()
.enumerate()
.map(|(i, s)| {
let id = s
.payload
.get(0..4)
.map(|b| u32::from_be_bytes(b.try_into().unwrap()))
.ok_or_else(|| {
ParseError::AssertFail(format!(
"stroke {i} is {} bytes, too short for its id",
s.payload.len()
))
})?;
Ok((id, stroke::read(&s.payload, chain, i, cat_len, map_len)?))
})
.collect()
}
pub fn set_root_key(&mut self, index: usize, note: u8) -> Result<(), Error> {
let zones = self.zones()?;
let zone = zones
.get(index)
.ok_or_else(|| ParseError::AssertFail(format!("no zone {index}")))?;
let wanted = zone.stroke_id;
let section = self
.body
.sections
.iter_mut()
.filter(|s| s.is(section::STK))
.find(|s| stroke_id(s).is_some_and(|id| names_stroke(id, wanted)))
.ok_or_else(|| {
ParseError::AssertFail(format!(
"zone {index} names stroke {wanted}, which the file does not contain"
))
})?;
stroke::set_root_key(&mut section.payload, note)?;
Ok(())
}
pub fn categories(&self) -> Vec<String> {
let Some(cat) = section::find(&self.body.sections, section::CAT) else {
return Vec::new();
};
let mut out = Vec::new();
let mut i = 0;
while i < cat.payload.len() {
let len = cat.payload[i] as usize;
let from = i + 1;
match cat.payload.get(from..from + len) {
Some(s) if len > 0 && s.iter().all(|&b| (0x20..0x7f).contains(&b)) => {
out.push(String::from_utf8_lossy(s).into_owned());
i = from + len;
}
_ => i += 1,
}
}
out
}
fn stroke_sections(&self) -> impl Iterator<Item = &Section> {
self.body.sections.iter().filter(|s| s.is(section::STK))
}
fn hdr(&self) -> Result<&Section, Error> {
section::find(&self.body.sections, section::HDR)
.ok_or_else(|| ParseError::AssertFail("no hdr section".into()).into())
}
fn map(&self) -> Result<&Section, Error> {
section::find(&self.body.sections, section::MAP)
.ok_or_else(|| ParseError::AssertFail("no map section".into()).into())
}
}
impl fmt::Debug for Sample {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Sample")
.field("sections", &self.sections)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_map_version_selects_the_chain_and_an_unknown_one_refuses() {
assert_eq!(Chain::from_map_version(9).unwrap(), Chain::Early);
assert_eq!(Chain::from_map_version(10).unwrap(), Chain::Library2);
assert!(Chain::from_map_version(11).is_err());
}
#[test]
fn an_unknown_map_version_cannot_use_the_zone_setter() {
let crate::Sample::V2(mut sample) =
encode::instrument(&[0i16; encode::MIN_FRAMES], &encode::Options::new("Test")).unwrap()
else {
panic!("the default options build the narrow chain");
};
let map = section::find_mut(&mut sample.body.sections, section::MAP).unwrap();
map.version = keymap::VERSION + 1;
let before = map.payload.clone();
assert!(sample.zones().is_err());
assert!(sample.set_zone_top_note(0, 60).is_err());
assert_eq!(sample.map().unwrap().payload, before);
}
#[test]
fn an_unknown_map_version_cannot_use_the_keyboard_table() {
let crate::Sample::V2(mut sample) =
encode::instrument(&[0i16; encode::MIN_FRAMES], &encode::Options::new("Test")).unwrap()
else {
panic!("the default options build the narrow chain");
};
let map = section::find_mut(&mut sample.body.sections, section::MAP).unwrap();
map.version = keymap::VERSION + 1;
let before = map.payload.clone();
assert!(sample.key_table().is_err());
assert!(sample.set_key_table(&KeyTable::NEUTRAL).is_err());
assert_eq!(sample.map().unwrap().payload, before);
}
#[test]
fn a_name_field_holds_its_whole_span_less_the_terminator() {
assert_eq!(MAX_NAME_LEN, 31);
assert_eq!(MAX_NAME_V3_LEN, 65);
}
#[test]
fn a_rename_leaves_nothing_of_the_name_it_replaced() {
for field in [StringField::NAME, StringField::NAME_V3] {
let mut payload = vec![0u8; field.next];
let long = "M".repeat(field.capacity());
field.write(&mut payload, &long).unwrap();
field.write(&mut payload, "Short").unwrap();
assert_eq!(field.read(&payload), "Short");
assert!(payload[field.at + 5..field.next].iter().all(|&b| b == 0));
}
}
#[test]
fn a_name_one_byte_past_the_field_is_refused() {
let field = StringField::NAME;
let mut payload = vec![0xffu8; field.next + 8];
let error = field
.write(&mut payload, &"M".repeat(field.capacity() + 1))
.unwrap_err()
.to_string();
assert!(error.contains("at most 31 bytes"), "{error}");
assert!(payload[field.at..].iter().all(|&b| b == 0xff));
}
#[test]
fn a_name_filling_its_field_stops_at_the_field_that_follows() {
let field = StringField::NAME_V3;
let mut payload = vec![0u8; 112];
payload[field.next..field.next + 7].copy_from_slice(b"KG mono");
let long = "M".repeat(field.capacity());
field.write(&mut payload, &long).unwrap();
assert_eq!(field.read(&payload), long);
assert_eq!(nul_terminated(&payload[field.next..]), "KG mono");
}
#[test]
fn a_header_with_no_name_field_reads_back_empty_and_refuses_a_rename() {
assert_eq!(StringField::NAME.read(&[0u8; 18]), "");
assert_eq!(StringField::NAME.read(&[]), "");
assert!(StringField::NAME.write(&mut [0u8; 18], "Name").is_err());
}
}