use std::io::Cursor;
use std::ops::Range;
use nord_format::cbin::Cbin;
use nord_format::fields::{ControlKind, Field, FieldError};
use nord_format::formats::{ne5, ns2, ns3, ns4};
use nord_format::{Entity, Live, OrganPreset, PianoPreset, Program, Settings, Song, Synth};
use crate::drawbar_widget;
trait Editable {
fn fields(&self) -> Vec<Field>;
fn set_field(&mut self, path: &str, value: &str) -> Result<(), FieldError>;
}
macro_rules! editable {
($body:ty) => {
impl Editable for Cbin<$body> {
fn fields(&self) -> Vec<Field> {
self.body.fields()
}
fn set_field(&mut self, path: &str, value: &str) -> Result<(), FieldError> {
self.body.set_field(path, value)
}
}
};
}
editable!(ne5::Program);
editable!(ne5::Settings);
editable!(ns2::Program);
editable!(ns3::Program);
editable!(ns3::SynthPreset);
editable!(ns4::Program);
editable!(ns4::organ_preset::OrganPreset);
editable!(ns4::piano_preset::PianoPreset);
editable!(ns4::synth::SynthPreset);
macro_rules! registry {
($entity:expr, $($reference:tt)*) => {
match $entity {
Entity::Live(Live::Electro5(f)) | Entity::Program(Program::Electro5(f)) => {
Some(f as $($reference)* dyn Editable)
}
Entity::Live(Live::Stage2(f)) | Entity::Program(Program::Stage2(f)) => {
Some(f as $($reference)* dyn Editable)
}
Entity::Live(Live::Stage3(f)) | Entity::Program(Program::Stage3(f)) => {
Some(f as $($reference)* dyn Editable)
}
Entity::Live(Live::Stage4(f)) | Entity::Program(Program::Stage4(f)) => {
Some(f as $($reference)* dyn Editable)
}
Entity::OrganPreset(OrganPreset::Stage4(f)) => Some(f as $($reference)* dyn Editable),
Entity::PianoPreset(PianoPreset::Stage4(f)) => Some(f as $($reference)* dyn Editable),
Entity::Settings(Settings::Electro5(f)) => Some(f as $($reference)* dyn Editable),
Entity::Synth(Synth::Stage3(f)) => Some(f as $($reference)* dyn Editable),
Entity::Synth(Synth::Stage4(f)) => Some(f as $($reference)* dyn Editable),
_ => None,
}
};
}
fn body(entity: &Entity) -> Option<&dyn Editable> {
registry!(entity, &)
}
fn body_mut(entity: &mut Entity) -> Option<&mut dyn Editable> {
registry!(entity, &mut)
}
pub fn fields_of(entity: &Entity) -> Option<Vec<Field>> {
body(entity).map(|body| body.fields())
}
pub fn has_registry(entity: &Entity) -> bool {
body(entity).is_some()
}
pub fn is_electro5_panel(entity: &Entity) -> bool {
matches!(
entity,
Entity::Program(Program::Electro5(_)) | Entity::Live(Live::Electro5(_))
)
}
pub fn is_electro5_settings(entity: &Entity) -> bool {
matches!(entity, Entity::Settings(Settings::Electro5(_)))
}
pub fn is_set_list(entity: &Entity) -> bool {
matches!(entity, Entity::Song(Song::Electro5(_)))
}
pub fn apply(bytes: &[u8], sets: &[(String, String)]) -> Result<(Vec<Field>, Vec<u8>), String> {
let mut entity =
nord_format::from_stream(&mut Cursor::new(bytes)).map_err(|e| e.to_string())?;
{
let body = body_mut(&mut entity).ok_or("this entity has no field registry")?;
for (path, value) in sets {
body.set_field(path, value).map_err(|e| e.to_string())?;
}
}
let fields = body(&entity)
.ok_or("this entity has no field registry")?
.fields();
let out = nord_format::to_bytes(&entity).map_err(|e| e.to_string())?;
Ok((fields, out))
}
const CHOICE_MAX: usize = 24;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Control {
Toggle,
Choice,
Number {
min: i64,
max: i64,
},
Bar,
Register,
Stored,
}
impl Control {
pub fn of(field: &Field, legal: &[String]) -> Control {
match field.spec.control {
ControlKind::Drawbar if field.spec.width == drawbar_widget::REGISTER_BITS => {
Control::Register
}
ControlKind::Drawbar if field.spec.width == drawbar_widget::BAR_BITS => Control::Bar,
ControlKind::Pattern | ControlKind::Reference => Control::Stored,
ControlKind::Toggle if legal == ["false", "true"] => Control::Toggle,
ControlKind::Bipolar(_)
| ControlKind::Knob(_)
| ControlKind::Morph
| ControlKind::Shift(_) => turned(legal),
_ => picked(legal),
}
}
}
fn turned(legal: &[String]) -> Control {
match contiguous(legal) {
Some((min, max)) if min < max => Control::Number { min, max },
_ if legal.is_empty() => Control::Stored,
_ => Control::Choice,
}
}
fn picked(legal: &[String]) -> Control {
match (1..=CHOICE_MAX).contains(&legal.len()) {
true => Control::Choice,
false => turned(legal),
}
}
fn contiguous(legal: &[String]) -> Option<(i64, i64)> {
let mut values = Vec::with_capacity(legal.len());
for value in legal {
values.push(value.trim_start_matches('+').parse::<i64>().ok()?);
}
let min = *values.iter().min()?;
let max = *values.iter().max()?;
(max.checked_sub(min)? + 1 == values.len() as i64).then_some((min, max))
}
pub struct DiffRow {
pub at: usize,
pub before: u8,
pub after: u8,
pub note: &'static str,
}
fn checksum_bytes(file: &[u8]) -> Option<(Range<usize>, &'static str)> {
if file.len() < 8 || &file[0..4] != nord_format::cbin::MAGIC {
return None;
}
match u32::from_le_bytes(file[4..8].try_into().ok()?) {
0 => Some((file.len() - 2..file.len(), " (file crc16)")),
1 => Some((0x18..0x1c, " (body crc32)")),
_ => None,
}
}
pub fn byte_diff(before: &[u8], after: &[u8]) -> Vec<DiffRow> {
if before.len() != after.len() {
return Vec::new();
}
let checksum = checksum_bytes(after);
before
.iter()
.zip(after)
.enumerate()
.filter(|(_, (b, a))| b != a)
.map(|(at, (&b, &a))| DiffRow {
at,
before: b,
after: a,
note: match &checksum {
Some((range, label)) if range.contains(&at) => label,
_ => "",
},
})
.collect()
}
#[cfg(test)]
pub mod blank {
use nord_format::cbin::{Cbin, Header, RawBody};
use nord_format::formats::{ne5, ns2, ns3, ns4};
use nord_format::{Entity, OrganPreset, PianoPreset, Program, Song, Synth};
pub fn stage3_song() -> Vec<u8> {
let file = Cbin {
header: Header::new(ns3::song::FORMAT, (0, 0), 0),
body: RawBody(vec![0u8; ns3::song::BODY_LEN as usize]),
};
nord_format::to_bytes(&Entity::Song(Song::Stage3(file))).expect("a stub encodes")
}
pub fn electro5_song() -> Vec<u8> {
let at =
|slot: u16| -> ne5::program::Location { (0, slot).try_into().expect("a program slot") };
let here: ne5::song::Location = (0, 0).try_into().expect("a song slot");
let song = ne5::song::new(
here,
ne5::song::DEFAULT_VERSION,
[at(0), at(1), at(2), at(3)],
);
nord_format::to_bytes(&Entity::Song(Song::Electro5(song))).expect("a song encodes")
}
macro_rules! blank {
($name:ident, $body:ty, $len:expr, $format:expr, $versions:expr, $wrap:expr) => {
pub fn $name() -> Vec<u8> {
let body = <$body>::try_from([0u8; $len]).expect("a zeroed body decodes");
let version = *$versions.last().expect("a format knows a version");
let file = Cbin {
header: Header::new($format, (0, 0), version),
body,
};
nord_format::to_bytes(&$wrap(file)).expect("a blank file encodes")
}
};
}
blank!(
stage2_program,
ns2::Program,
ns2::program::BODY_LEN,
ns2::program::FORMAT,
ns2::program::KNOWN_VERSIONS,
|f| Entity::Program(Program::Stage2(f))
);
blank!(
stage3_synth,
ns3::SynthPreset,
ns3::synth::BODY_LEN,
ns3::synth::FORMAT,
ns3::synth::KNOWN_VERSIONS,
|f| Entity::Synth(Synth::Stage3(f))
);
blank!(
stage4_program,
ns4::Program,
ns4::program::BODY_LEN,
ns4::program::FORMAT,
ns4::program::KNOWN_VERSIONS,
|f| Entity::Program(Program::Stage4(f))
);
blank!(
stage4_organ_preset,
ns4::organ_preset::OrganPreset,
ns4::organ_preset::BODY_LEN,
ns4::organ_preset::FORMAT,
ns4::organ_preset::KNOWN_VERSIONS,
|f| Entity::OrganPreset(OrganPreset::Stage4(f))
);
blank!(
stage4_piano_preset,
ns4::piano_preset::PianoPreset,
ns4::piano_preset::BODY_LEN,
ns4::piano_preset::FORMAT,
ns4::piano_preset::KNOWN_VERSIONS,
|f| Entity::PianoPreset(PianoPreset::Stage4(f))
);
blank!(
stage4_synth,
ns4::synth::SynthPreset,
ns4::synth::BODY_LEN,
ns4::synth::FORMAT,
ns4::synth::KNOWN_VERSIONS,
|f| Entity::Synth(Synth::Stage4(f))
);
}
#[cfg(test)]
mod tests {
use super::*;
fn program() -> Vec<u8> {
let entity = Entity::Program(Program::Electro5(ne5::program::new(
(0, 0).try_into().unwrap(),
)));
nord_format::to_bytes(&entity).unwrap()
}
#[test]
fn a_set_changes_the_field_it_names_and_nothing_else() {
let bytes = program();
let (before, _) = apply(&bytes, &[]).unwrap();
let (after, edited) = apply(&bytes, &[("center_panel.gain".into(), "96".into())]).unwrap();
let moved: Vec<&str> = before
.iter()
.zip(&after)
.filter(|(b, a)| b.display != a.display)
.map(|(_, a)| a.path.as_str())
.collect();
assert_eq!(moved, ["center_panel.gain"]);
assert_eq!(edited.len(), bytes.len());
}
#[test]
fn an_out_of_range_value_is_refused_by_the_library() {
let err = apply(&program(), &[("center_panel.gain".into(), "200".into())])
.err()
.expect("200 is not a gain");
assert!(err.contains("not a value of gain"), "{err}");
assert!(err.contains("0 .. 127"), "{err}");
}
#[test]
fn a_refusal_anywhere_in_a_batch_applies_none_of_it() {
let bytes = program();
let sets = [
("center_panel.transpose_enabled".into(), "true".into()),
("center_panel.transpose".into(), "99".into()),
];
assert!(apply(&bytes, &sets).is_err());
let (fields, _) = apply(&bytes, &[]).unwrap();
let enabled = fields
.iter()
.find(|f| f.path == "center_panel.transpose_enabled")
.unwrap();
assert_eq!(enabled.value, "false");
}
#[test]
fn the_checksum_bytes_are_annotated_as_bookkeeping() {
let bytes = program();
let (_, edited) = apply(&bytes, &[("center_panel.gain".into(), "96".into())]).unwrap();
let diff = byte_diff(&bytes, &edited);
assert!(!diff.is_empty());
let annotated: Vec<usize> = diff
.iter()
.filter(|row| row.note.contains("crc32"))
.map(|row| row.at)
.collect();
assert!(annotated.iter().all(|at| (0x18..0x1c).contains(at)));
assert!(!annotated.is_empty(), "the crc32 must have moved");
assert!(
diff.iter().any(|row| row.note.is_empty()),
"the edit itself must show as an unannotated byte",
);
}
#[test]
fn only_a_gapless_run_of_integers_becomes_a_slider() {
let full: Vec<String> = (0..128).map(|n| n.to_string()).collect();
assert_eq!(contiguous(&full), Some((0, 127)));
let gapped: Vec<String> = vec!["0".into(), "1".into(), "9".into()];
assert_eq!(contiguous(&gapped), None);
let named: Vec<String> = vec!["Organ".into(), "Piano".into()];
assert_eq!(contiguous(&named), None);
}
fn control_of(fields: &[Field], path: &str) -> Control {
let field = fields
.iter()
.find(|f| f.path == path)
.unwrap_or_else(|| panic!("{path} is declared"));
Control::of(field, &(field.spec.legal)())
}
#[test]
fn a_register_field_picks_the_drawbar_control() {
let bytes = program();
let (fields, _) = apply(&bytes, &[]).unwrap();
assert_eq!(
control_of(&fields, "organ_panel.b3_preset1_drawbars"),
Control::Register
);
assert_eq!(
control_of(&fields, "center_panel.gain"),
Control::Number { min: 0, max: 127 }
);
}
#[test]
fn a_drawbar_is_a_register_or_a_bar_by_its_width() {
let (stage4, _) = apply(&blank::stage4_program(), &[]).unwrap();
assert_eq!(control_of(&stage4, "organ_a.drawbar_1"), Control::Bar);
let (electro5, _) = apply(&program(), &[]).unwrap();
assert_eq!(
control_of(&electro5, "organ_panel.vox_preset1_drawbars"),
Control::Register
);
}
#[test]
fn the_declared_kind_picks_the_control() {
let (fields, _) = apply(&blank::stage4_program(), &[]).unwrap();
assert_eq!(control_of(&fields, "split_enabled"), Control::Toggle);
assert_eq!(control_of(&fields, "piano_a.piano_type"), Control::Choice);
assert_eq!(
control_of(&fields, "organ_a_volume"),
Control::Number { min: 0, max: 127 }
);
assert_eq!(control_of(&fields, "piano_a.model_id"), Control::Stored);
assert_eq!(
control_of(&fields, "synth_a_performance.sample_slot"),
Control::Number { min: 0, max: 4095 }
);
}
#[test]
fn every_registry_backed_body_reads_and_writes() {
for bytes in [
blank::stage2_program(),
blank::stage3_synth(),
blank::stage4_organ_preset(),
blank::stage4_piano_preset(),
blank::stage4_program(),
blank::stage4_synth(),
] {
let (fields, out) = apply(&bytes, &[]).expect("a blank body round-trips");
assert!(!fields.is_empty());
assert_eq!(out, bytes, "an empty set changes nothing");
}
}
#[test]
fn a_register_round_trips_through_the_widgets_spelling() {
let bytes = program();
let (fields, _) = apply(&bytes, &[]).unwrap();
let register = fields
.iter()
.find(|f| f.path == "organ_panel.vox_preset1_drawbars")
.unwrap();
let bits = drawbar_widget::parse(®ister.value).unwrap();
let spelled = drawbar_widget::spell(drawbar_widget::bits(drawbar_widget::bars(bits)));
assert_eq!(spelled, register.value);
let (after, _) = apply(&bytes, &[(register.path.clone(), "0x888800000".into())]).unwrap();
let edited = after.iter().find(|f| f.path == register.path).unwrap();
assert_eq!(
drawbar_widget::bars(drawbar_widget::parse(&edited.value).unwrap()),
[8, 8, 8, 8, 0, 0, 0, 0, 0]
);
}
}