pub mod section;
pub mod stroke;
pub mod zone;
pub use section::Section;
pub use stroke::Stroke;
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 LIBRARY_2_VERSION: u32 = 200;
#[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),
}
}
}
const NAME_AT: usize = 12;
pub const MAX_NAME_LEN: usize = 14;
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> {
Ok(Sample {
sections: section::read_chain(r)?,
})
}
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> {
Ok(SampleV3 {
sections: section::read_chain4(r)?,
})
}
fn write<W: Write + Seek>(&self, w: &mut BodyWriter<'_, W>) -> Result<(), Error> {
for s in &self.sections {
s.write_to(w)?;
}
Ok(())
}
}
const NAME_V3_AT: usize = 10;
const NAME_V3_SUB_AT: usize = 76;
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())
}
fn hdr_field(&self, from: usize, to: Option<usize>) -> Result<String, Error> {
let hdr = self.hdr()?;
let field = match to {
Some(to) => hdr.payload.get(from..to),
None => hdr.payload.get(from..),
}
.ok_or_else(|| {
ParseError::AssertFail(format!("hdr section is {} bytes", hdr.payload.len()))
})?;
let end = field.iter().position(|&b| b == 0).unwrap_or(field.len());
Ok(String::from_utf8_lossy(&field[..end]).into_owned())
}
pub fn name(&self) -> Result<String, Error> {
self.hdr_field(NAME_V3_AT, Some(NAME_V3_SUB_AT))
}
pub fn sub_name(&self) -> Result<String, Error> {
self.hdr_field(NAME_V3_SUB_AT, None)
}
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 (s.payload.get(0..4), s.payload.get(5)) {
(Some(gid), Some(&root)) => Ok((u32::from_be_bytes(gid.try_into().unwrap()), 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 = section::find4(&self.body.sections, section::MAP4)
.ok_or_else(|| ParseError::AssertFail("no map section".into()))?;
Ok(zone::read_v3(
map.version,
&map.payload,
&self.stroke_ids()?,
)?)
}
}
pub fn from_bytes(bytes: &[u8]) -> Result<Cbin<Sample>, Error> {
read_from(&mut std::io::Cursor::new(bytes))
}
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> {
let hdr = self.hdr()?;
let from = hdr.payload.get(NAME_AT..).ok_or_else(|| {
ParseError::AssertFail(format!("hdr section is {} bytes", hdr.payload.len()))
})?;
let end = from.iter().position(|&b| b == 0).unwrap_or(from.len());
Ok(String::from_utf8_lossy(&from[..end]).into_owned())
}
pub fn set_name(&mut self, name: &str) -> Result<(), Error> {
if name.len() > MAX_NAME_LEN {
return Err(ParseError::OutOfBounds {
value: format!("{name:?} ({} bytes)", name.len()),
bound: format!("a name of at most {MAX_NAME_LEN} bytes"),
}
.into());
}
let hdr = section::find_mut(&mut self.body.sections, section::HDR)
.ok_or_else(|| ParseError::AssertFail("no hdr section".into()))?;
let field = hdr
.payload
.get_mut(NAME_AT..NAME_AT + MAX_NAME_LEN)
.ok_or_else(|| ParseError::AssertFail("hdr section is too short for a name".into()))?;
field.fill(0);
field[..name.len()].copy_from_slice(name.as_bytes());
Ok(())
}
fn require_known_layout(&self) -> Result<(), Error> {
if self.header.version < LIBRARY_2_VERSION {
return Err(ParseError::AssertFail(format!(
"content version {} predates Sample Library 2.0 and lays out its zone \
table differently; only the section chain and name are decoded",
self.header.version
))
.into());
}
Ok(())
}
pub fn zones(&self) -> Result<Vec<Zone>, Error> {
self.require_known_layout()?;
Ok(zone::read(&self.map()?.payload)?)
}
pub fn set_zone_top_note(&mut self, index: usize, note: u8) -> Result<(), Error> {
let map = section::find_mut(&mut self.body.sections, section::MAP)
.ok_or_else(|| ParseError::AssertFail("no map section".into()))?;
zone::set_top_note(&mut map.payload, index, note)?;
Ok(())
}
pub fn strokes(&self) -> Result<Vec<Stroke>, Error> {
self.require_known_layout()?;
let zones = self.zones()?;
let by_id = self.strokes_in_file_order()?;
zones
.iter()
.map(|z| {
by_id
.iter()
.find(|(id, _)| *id == u32::from(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()
}
fn strokes_in_file_order(&self) -> Result<Vec<(u32, Stroke)>, Error> {
let map_len = self.map()?.payload.len();
let cat_len = section::find(&self.body.sections, section::CAT)
.map(|s| s.payload.len())
.ok_or_else(|| ParseError::AssertFail("no cat section".into()))?;
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, 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 = u32::from(zone.stroke_id);
let section = self
.body
.sections
.iter_mut()
.filter(|s| s.is(section::STK))
.find(|s| {
s.payload
.get(0..4)
.map(|b| u32::from_be_bytes(b.try_into().unwrap()))
== Some(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()
}
}