mod center;
mod effects;
mod organ;
mod panel;
mod piano;
mod sample;
pub use center::{CenterPanel, OrganType};
pub use effects::{EffectsPanel, EqualizerPart, Fx1Type, Fx2Type, Fx3Type, Fx5Type, Routing};
pub use organ::{B3PercSpeed, B3Vib, Drawbars, FarfisaVib, OrganModel, OrganPanel, Preset, VoxVib};
pub use panel::PANEL;
pub use piano::{PianoCategory, PianoPanel};
pub use sample::SamplePanel;
pub use crate::fields::Field;
use crate::bank;
use crate::cbin::{self, Cbin, Header};
use crate::error::{Error, ParseError};
use crate::types::RangedU16Pair;
use std::io::{Read, Seek};
pub const FORMAT: &str = "ne5p";
pub const KNOWN_VERSIONS: &[u32] = &[4];
pub const DEFAULT_VERSION: u32 = 4;
pub const BODY_LEN: usize = 121;
pub const FILE_LEN: usize = 0x2c + BODY_LEN;
pub const BANK_COUNT: u16 = 8;
pub const SLOT_COUNT: u16 = 50;
pub type Location = RangedU16Pair<BANK_COUNT, SLOT_COUNT>;
pub type Bank = bank::Bank<Cbin<Program>, Location>;
#[nord_bits_derive::bitbody(121)]
pub struct Program {
#[bits(0..=15)]
pub program_version: u16,
#[at(0x02..0x09)]
pub center_panel: CenterPanel,
#[at(0x0e..0x16)]
pub piano_panel: PianoPanel,
#[at(0x1a..0x22)]
pub sample_panel: SamplePanel,
#[at(0x22..0x67)]
pub organ_panel: OrganPanel,
#[at(0x67..0x79)]
pub effects_panel: EffectsPanel,
}
impl Default for Program {
fn default() -> Program {
Program {
raw: [0; BODY_LEN],
program_version: DEFAULT_VERSION as u16,
center_panel: CenterPanel::default(),
piano_panel: PianoPanel::default(),
sample_panel: SamplePanel::default(),
organ_panel: OrganPanel::default(),
effects_panel: EffectsPanel::default(),
}
}
}
pub(crate) use crate::formats::known_version;
pub(crate) fn unset_aux(format: &'static str, header: &Header) -> Result<(), Error> {
if header.aux != 0xFFFF_FFFF {
return Err(ParseError::AssertFail(format!(
"{format}: aux is {:#010x}, not the 0xffffffff every slot-addressed file holds",
header.aux,
))
.into());
}
Ok(())
}
pub(crate) fn slot<L: bank::Location>(header: &Header) -> Result<L, Error> {
let (bank, slot) = header.slot();
(bank, slot)
.try_into()
.map_err(|_| ParseError::AssertFail(format!("invalid location: {bank} {slot}")).into())
}
pub fn location(file: &Cbin<Program>) -> Result<Location, Error> {
slot(&file.header)
}
pub fn new(location: Location) -> Cbin<Program> {
Cbin {
header: Header::new(FORMAT, location.inner(), DEFAULT_VERSION),
body: Program::default(),
}
}
pub fn read_from(reader: &mut (impl Read + Seek)) -> Result<Cbin<Program>, Error> {
let file: Cbin<Program> = cbin::read(reader, FORMAT)?;
known_version(FORMAT, file.header.version, KNOWN_VERSIONS)?;
unset_aux(FORMAT, &file.header)?;
location(&file)?;
Ok(file)
}
impl bank::Item<Location> for Cbin<Program> {
fn location(&self) -> Location {
location(self).expect("a program's location is validated at construction")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cbin::Generation;
use std::io::Cursor;
#[test]
fn an_unknown_schema_version_is_refused() {
let program = new((0, 0).try_into().unwrap());
let mut bytes = Vec::new();
program.write_to(&mut Cursor::new(&mut bytes)).unwrap();
assert_eq!(bytes.len(), FILE_LEN);
assert!(read_from(&mut Cursor::new(&mut bytes.clone())).is_ok());
assert_eq!(u32::from_le_bytes(bytes[0x14..0x18].try_into().unwrap()), 4);
bytes[0x14..0x18].copy_from_slice(&5u32.to_le_bytes());
let err = read_from(&mut Cursor::new(&mut bytes)).expect_err("version 5 must not decode");
assert!(
matches!(
err,
Error::Parse(crate::error::ParseError::UnsupportedVersion {
format: "ne5p",
version: 5,
..
})
),
"unhelpful error: {err}",
);
}
#[test]
fn an_unexpected_aux_word_is_refused() {
let program = new((0, 0).try_into().unwrap());
let mut bytes = Vec::new();
program.write_to(&mut Cursor::new(&mut bytes)).unwrap();
bytes[0x10..0x14].copy_from_slice(&0u32.to_le_bytes());
let err = read_from(&mut Cursor::new(&bytes)).expect_err("a set aux must not decode");
assert!(
matches!(err, Error::Parse(ParseError::AssertFail(_))),
"refused for the wrong reason: {err}",
);
}
#[test]
fn every_panels_encode_is_total() {
fn total<P, W>(_: &P)
where
for<'a> W: From<&'a P>,
{
}
let program = new((0, 0).try_into().unwrap());
total::<_, [u8; 7]>(&program.center_panel);
total::<_, [u8; 8]>(&program.piano_panel);
total::<_, [u8; 8]>(&program.sample_panel);
total::<_, [u8; 69]>(&program.organ_panel);
total::<_, [u8; 18]>(&program.effects_panel);
}
fn restamp_crc(bytes: &mut [u8]) {
let crc = crate::crc::crc32(&bytes[0x2c..]);
bytes[0x18..0x1c].copy_from_slice(&crc.to_le_bytes());
}
#[test]
fn no_decode_path_can_skip_validation() {
let program = new((0, 0).try_into().unwrap());
let mut bytes = Vec::new();
program.write_to(&mut Cursor::new(&mut bytes)).unwrap();
let pristine = bytes.clone();
restamp_crc(&mut bytes);
assert_eq!(bytes, pristine, "the CRC helper does not match the writer");
bytes[0x2e] |= 0b1110_0000;
restamp_crc(&mut bytes);
let front = read_from(&mut Cursor::new(&mut bytes))
.expect_err("the front door accepted an undecodable panel");
assert!(
matches!(
front,
Error::Parse(crate::error::ParseError::OutOfBounds { .. })
),
"refused for the wrong reason: {front}",
);
assert!(
CenterPanel::try_from(<[u8; 7]>::try_from(&bytes[0x2e..0x35]).unwrap()).is_err(),
"the conversion itself accepted an undecodable panel",
);
}
#[test]
fn setting_a_field_by_name_moves_only_that_fields_bytes() {
let mut program = new((0, 0).try_into().unwrap());
let mut before = Vec::new();
program.write_to(&mut Cursor::new(&mut before)).unwrap();
program.set_field("center_panel.transpose", "-5").unwrap();
assert_eq!(program.center_panel.transpose.inner(), -5);
let mut after = Vec::new();
program.write_to(&mut Cursor::new(&mut after)).unwrap();
let moved: Vec<usize> = (0..before.len())
.filter(|&i| before[i] != after[i])
.collect();
assert_eq!(moved, vec![0x18, 0x19, 0x1a, 0x1b, 0x31], "{moved:x?}");
}
#[test]
fn an_unknown_path_names_what_it_could_not_find() {
let mut program = new((0, 0).try_into().unwrap());
for (path, wanted) in [
("center_panel.nonesuch", "nonesuch"),
("nonesuch.transpose", "nonesuch"),
("transpose", "transpose"),
] {
let err = program.set_field(path, "0").unwrap_err().to_string();
assert!(err.contains(wanted), "{path}: {err}");
}
}
#[test]
fn every_declared_field_is_settable_by_its_listed_name() {
let mut program = new((0, 0).try_into().unwrap());
let fields = program.fields();
assert!(
fields.iter().any(|f| f.path == "center_panel.transpose"),
"the worked example is missing from the registry",
);
for f in fields {
let (path, value) = (f.path.clone(), f.value.clone());
program
.set_field(&path, &value)
.unwrap_or_else(|e| panic!("{path} = {value:?}: {e}"));
}
}
#[test]
fn a_wide_field_is_spelled_by_its_stored_bits() {
let mut program = new((0, 0).try_into().unwrap());
program
.set_field("organ_panel.b3_preset1_drawbars", "0x087654321")
.unwrap();
assert_eq!(
program.organ_panel.drawbars(OrganModel::B3, Preset::One),
[0, 8, 7, 6, 5, 4, 3, 2, 1],
);
let listed = program
.fields()
.into_iter()
.find(|f| f.path == "organ_panel.b3_preset1_drawbars")
.expect("declared");
assert_eq!(listed.value, "0x87654321");
assert_eq!(listed.display, "[0, 8, 7, 6, 5, 4, 3, 2, 1]");
}
#[test]
fn decode_and_encode_are_inverse() {
for pattern in [0u64, u64::MAX, 0xa5a5_a5a5_a5a5_a5a5, 0x5a5a_5a5a_5a5a_5a5a] {
let raw = pattern.to_be_bytes();
let panel = PianoPanel::try_from(raw).unwrap();
assert_eq!(<[u8; 8]>::from(&panel), raw);
let panel = SamplePanel::try_from(raw).unwrap();
assert_eq!(<[u8; 8]>::from(&panel), raw);
let raw: [u8; 7] = raw[..7].try_into().unwrap();
if let Ok(panel) = CenterPanel::try_from(raw) {
assert_eq!(<[u8; 7]>::from(&panel), raw);
}
}
}
#[test]
fn the_program_body_layout_is_published_as_data() {
use crate::layout::BodyLayout;
let fields = Program::layout();
let center = fields
.iter()
.find(|f| f.path == "center_panel")
.expect("declared");
assert_eq!((center.lo / 8, (center.hi + 1) / 8), (0x02, 0x09));
let nested = center.nested.expect("a panel chains to its own layout");
assert!(
nested().iter().any(|f| f.path == "transpose"),
"the nested layout does not list the panel's fields",
);
let program = new((0, 0).try_into().unwrap());
let paths: Vec<String> = program.fields().into_iter().map(|f| f.path).collect();
assert!(paths.contains(&"center_panel.transpose".to_string()));
assert!(paths.contains(&"piano_panel.id".to_string()));
assert!(paths.contains(&"sample_panel.id".to_string()));
}
#[test]
fn a_type_0_program_is_the_same_body_18_bytes_earlier() {
let mut program = new((3, 7).try_into().unwrap());
let mut v1 = Vec::new();
program.write_to(&mut Cursor::new(&mut v1)).unwrap();
program.header.generation = Generation::V0;
let mut v0 = Vec::new();
program.write_to(&mut Cursor::new(&mut v0)).unwrap();
assert_eq!(v1.len() - v0.len(), 18);
assert_eq!(&v1[0x2c..], &v0[0x18..v0.len() - 2], "bodies differ");
assert_eq!(
&v1[0x08..0x18],
&v0[0x08..0x18],
"shared header fields differ"
);
let back = read_from(&mut Cursor::new(&v0)).unwrap();
assert_eq!(back.header.generation, Generation::V0);
let mut again = Vec::new();
back.write_to(&mut Cursor::new(&mut again)).unwrap();
assert_eq!(again, v0, "type-0 round trip changed the bytes");
}
}