use crate::cbin::{self, Cbin, Header};
use crate::components::sparse_enum;
use crate::error::{Error, ParseError};
use crate::formats::ne5::{program, song};
use crate::types::RangedI8;
use nord_bits_derive::bitbody;
use std::fmt::{self, Debug, Display, Formatter};
use std::io::{Read, Seek};
pub const FORMAT: &str = "ne5s";
pub const KNOWN_VERSIONS: &[u32] = &[0];
pub const BODY_LEN: usize = 0x4e - 0x2c;
pub const FILE_LEN: usize = 0x2c + BODY_LEN;
pub fn new() -> Cbin<Settings> {
Cbin {
header: Header::new(FORMAT, (0, 0), 0),
body: Settings::default(),
}
}
pub fn read_from(reader: &mut (impl Read + Seek)) -> Result<Cbin<Settings>, Error> {
let file: Cbin<Settings> = cbin::read(reader, FORMAT)?;
program::known_version(FORMAT, file.header.version, KNOWN_VERSIONS)?;
program::unset_aux(FORMAT, &file.header)?;
let (bank, slot) = file.header.slot();
if (bank, slot) != (0, 0) {
return Err(ParseError::AssertFail(format!(
"{FORMAT}: location is {bank} {slot}, and settings live at 0 0"
))
.into());
}
Ok(file)
}
pub type GlobalTranspose = RangedI8<6, -6, 6>;
pub type ResonanceLevel = RangedI8<6, -6, 6>;
pub type FineTune = RangedI8<50, -50, 50>;
#[bitbody(34)]
pub struct Settings {
#[bits(52..=53)]
pub rotary_ctrl_type: RotaryCtrlType,
#[bits(54..=54)]
pub rotary_pedal_mode: RotaryPedalMode,
#[bits(134..=135)]
pub sustain_pedal_mode: SustainPedalMode,
#[bits(43..=44)]
pub sustain_pedal_type: SustainPedalType,
#[bits(45..=47)]
pub ctrl_pedal_type: CtrlPedalType,
#[bits(138..=141)]
pub ctrl_pedal_gain: CtrlPedalGain,
#[bits(117..=117)]
pub b3_trig_mode: B3TrigMode,
#[bits(128..=128)]
pub output_routing: OutputRouting,
#[bits(68..=71)]
pub global_transpose: GlobalTranspose,
#[bits(55..=61)]
pub fine_tune: FineTune,
#[bits(72..=76)]
pub global_channel: MidiChannel,
#[bits(118..=122)]
pub lower_receive_channel: MidiChannel,
#[bits(123..=127)]
pub upper_receive_channel: MidiChannel,
#[bits(129..=133)]
pub upper_split_channel: MidiChannel,
#[bits(38..=39)]
pub control_change_mode: MidiMessageMode,
#[bits(40..=41)]
pub program_change_mode: MidiMessageMode,
#[bits(137..=137)]
pub transpose_at: TransposeAt,
#[bits(64..=67)]
pub piano_string_resonance: ResonanceLevel,
#[bits(109..=111)]
pub b3_tonewheel_mode: TonewheelMode,
#[bits(113..=114)]
pub b3_key_click_level: KeyClickLevel,
#[bits(116..=116)]
pub b3_key_bounce: bool,
#[bits(112..=112)]
pub b3_perc_db9_mute: bool,
#[bits(97..=99)]
pub b3_perc_decay_fast: PercDecay,
#[bits(100..=102)]
pub b3_perc_decay_slow: PercDecay,
#[bits(103..=105)]
pub b3_perc_volume_normal: PercVolume,
#[bits(106..=108)]
pub b3_perc_volume_soft: PercVolume,
#[bits(79..=81)]
pub rotary_speaker_type: RotarySpeakerType,
#[bits(94..=96)]
pub rotary_balance: RotaryBalance,
#[bits(82..=84)]
pub rotary_horn_speed: RotaryRate,
#[bits(88..=90)]
pub rotary_horn_acceleration: RotaryRate,
#[bits(85..=87)]
pub rotary_rotor_speed: RotaryRate,
#[bits(91..=93)]
pub rotary_rotor_acceleration: RotaryRate,
#[bits(16..=16)]
pub startup_set_list_mode: bool,
#[bits(17..=17)]
pub startup_live_mode: bool,
#[bits(19..=20)]
pub startup_live_slot: LiveSlot,
#[bits(21..=29)]
pub startup_program: program::Location,
#[bits(30..=37)]
pub startup_song: song::Location,
}
sparse_enum!(
LiveSlot, 2, {
0 => Live1, "live 1";
1 => Live2, "live 2";
2 => Live3, "live 3";
}
);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Menu {
System,
Midi,
Sound,
}
impl Menu {
pub fn title(&self) -> &'static str {
match self {
Menu::System => "System",
Menu::Midi => "MIDI",
Menu::Sound => "Sound",
}
}
}
pub struct Setting {
pub name: &'static str,
pub value: String,
}
impl Default for Settings {
fn default() -> Self {
Settings::try_from([0; BODY_LEN]).expect("every settings field decodes totally")
}
}
fn on_off(on: bool) -> String {
if on { "on" } else { "off" }.to_string()
}
impl Settings {
pub fn by_menu(&self) -> Vec<(Menu, Vec<Setting>)> {
let at = |name, value: String| Setting { name, value };
vec![
(
Menu::System,
vec![
at("rotary_ctrl_type", self.rotary_ctrl_type.to_string()),
at("rotary_pedal_mode", self.rotary_pedal_mode.to_string()),
at("sustain_pedal_mode", self.sustain_pedal_mode.to_string()),
at("sustain_pedal_type", self.sustain_pedal_type.to_string()),
at("ctrl_pedal_type", self.ctrl_pedal_type.to_string()),
at("ctrl_pedal_gain", self.ctrl_pedal_gain.to_string()),
at("b3_trig_mode", self.b3_trig_mode.to_string()),
at("output_routing", self.output_routing.to_string()),
at(
"global_transpose",
format!("{:+}", self.global_transpose.inner()),
),
at("fine_tune", format!("{:+} cent", self.fine_tune.inner())),
],
),
(
Menu::Midi,
vec![
at("global_channel", self.global_channel.to_string()),
at(
"lower_receive_channel",
self.lower_receive_channel.to_string(),
),
at(
"upper_receive_channel",
self.upper_receive_channel.to_string(),
),
at("upper_split_channel", self.upper_split_channel.to_string()),
at("control_change_mode", self.control_change_mode.to_string()),
at("program_change_mode", self.program_change_mode.to_string()),
at("transpose_at", self.transpose_at.to_string()),
],
),
(
Menu::Sound,
vec![
at(
"piano_string_resonance",
format!("{:+} dB", self.piano_string_resonance.inner()),
),
at("b3_tonewheel_mode", self.b3_tonewheel_mode.to_string()),
at("b3_key_click_level", self.b3_key_click_level.to_string()),
at("b3_key_bounce", on_off(self.b3_key_bounce)),
at("b3_perc_db9_mute", on_off(self.b3_perc_db9_mute)),
at("b3_perc_decay_fast", self.b3_perc_decay_fast.to_string()),
at("b3_perc_decay_slow", self.b3_perc_decay_slow.to_string()),
at(
"b3_perc_volume_normal",
self.b3_perc_volume_normal.to_string(),
),
at("b3_perc_volume_soft", self.b3_perc_volume_soft.to_string()),
at("rotary_speaker_type", self.rotary_speaker_type.to_string()),
at("rotary_balance", self.rotary_balance.to_string()),
at("rotary_horn_speed", self.rotary_horn_speed.to_string()),
at(
"rotary_horn_acceleration",
self.rotary_horn_acceleration.to_string(),
),
at("rotary_rotor_speed", self.rotary_rotor_speed.to_string()),
at(
"rotary_rotor_acceleration",
self.rotary_rotor_acceleration.to_string(),
),
],
),
]
}
}
sparse_enum!(
RotaryCtrlType, 2, {
0 => Closed, "closed";
1 => Open, "open";
2 => HalfMoon, "half moon";
}
);
sparse_enum!(
RotaryPedalMode, 1, {
0 => Hold, "hold";
1 => Toggle, "toggle";
}
);
sparse_enum!(
SustainPedalMode, 2, {
0 => Sustain, "sustain";
1 => SustainRotorHold, "sustain + rotor hold";
2 => SustainRotorToggle, "sustain + rotor toggle";
}
);
sparse_enum!(
SustainPedalType, 2, {
0 => Auto, "auto";
1 => Closed, "closed";
2 => Open, "open";
}
);
sparse_enum!(
CtrlPedalType, 3, {
0 => RolandEv7, "roland ev-7";
1 => YamahaFc7, "yamaha fc-7";
2 => KorgExp2, "korg exp-2";
3 => KorgXvp10, "korg xvp-10";
4 => BossFv500L, "boss fv-500l";
5 => FatarSl, "fatar sl";
}
);
sparse_enum!(
B3TrigMode, 1, {
0 => Normal, "normal";
1 => Fast, "fast";
}
);
sparse_enum!(
OutputRouting, 1, {
0 => Stereo, "stereo";
1 => LowerLeftUpperRight, "lower L / upper R";
}
);
sparse_enum!(
MidiMessageMode, 2, {
0 => Off, "off";
1 => Send, "send";
2 => Receive, "receive";
3 => SendReceive, "send/receive";
}
);
sparse_enum!(
TransposeAt, 1, {
0 => MidiIn, "midi in";
1 => MidiOut, "midi out";
}
);
sparse_enum!(
TonewheelMode, 3, {
0 => Clean, "clean";
1 => Vintage1, "vintage 1";
2 => Vintage2, "vintage 2";
3 => Vintage3, "vintage 3";
}
);
sparse_enum!(
KeyClickLevel, 2, {
0 => Low, "low";
1 => Normal, "normal";
2 => High, "high";
3 => Higher, "higher";
}
);
sparse_enum!(
PercDecay, 3, {
0 => Short, "short";
1 => Medium, "medium";
2 => Long, "long";
}
);
sparse_enum!(
PercVolume, 3, {
0 => Low, "low";
1 => Medium, "medium";
2 => High, "high";
}
);
sparse_enum!(
RotarySpeakerType, 3, {
0 => Rotary122, "122";
1 => Rotary122Close, "122 close";
}
);
sparse_enum!(
RotaryBalance, 3, {
0 => Bass70Horn30, "70/30";
1 => Bass60Horn40, "60/40";
2 => Bass50Horn50, "50/50";
3 => Bass40Horn60, "40/60";
4 => Bass30Horn70, "30/70";
}
);
sparse_enum!(
RotaryRate, 3, {
0 => Low, "low";
1 => Normal, "normal";
2 => High, "high";
}
);
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum MidiChannel {
Channel(u8),
Off,
Unknown(u8),
}
impl MidiChannel {
pub const CHANNELS: u8 = 16;
pub fn channel(number: u8) -> Result<Self, ParseError> {
if !(1..=Self::CHANNELS).contains(&number) {
return Err(ParseError::OutOfBounds {
value: format!("{number}"),
bound: format!("1..={}", Self::CHANNELS),
});
}
Ok(MidiChannel::Channel(number))
}
pub fn number(&self) -> Option<u8> {
match self {
MidiChannel::Channel(n) => Some(*n),
_ => None,
}
}
}
impl crate::bits::Packed for MidiChannel {
const MAX_BITS: u32 = 5;
const DECODE_BITS: u32 = u8::BITS;
const CONTROL: crate::fields::ControlKind = crate::fields::ControlKind::Selector;
type Error = std::convert::Infallible;
fn from_bits(bits: u64) -> Result<Self, Self::Error> {
Ok(match bits as u8 {
n if n < Self::CHANNELS => MidiChannel::Channel(n + 1),
16 => MidiChannel::Off,
other => MidiChannel::Unknown(other),
})
}
fn to_bits(&self) -> u64 {
match self {
MidiChannel::Channel(n) => u64::from(n.saturating_sub(1)),
MidiChannel::Off => 16,
MidiChannel::Unknown(raw) => u64::from(*raw),
}
}
}
impl Debug for MidiChannel {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
MidiChannel::Channel(n) => write!(f, "{n}"),
MidiChannel::Off => f.write_str("off"),
MidiChannel::Unknown(raw) => write!(f, "unknown ({raw})"),
}
}
}
impl Display for MidiChannel {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{self:?}")
}
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct CtrlPedalGain(u8);
impl CtrlPedalGain {
pub const MIN: u8 = 1;
pub const MAX: u8 = 10;
pub fn new(gain: u8) -> Result<Self, ParseError> {
if !(Self::MIN..=Self::MAX).contains(&gain) {
return Err(ParseError::OutOfBounds {
value: format!("{gain}"),
bound: format!("{}..={}", Self::MIN, Self::MAX),
});
}
Ok(CtrlPedalGain(gain))
}
pub fn as_u8(&self) -> u8 {
self.0
}
}
impl Default for CtrlPedalGain {
fn default() -> Self {
CtrlPedalGain(Self::MIN)
}
}
impl crate::bits::Packed for CtrlPedalGain {
const MAX_BITS: u32 = 4;
const DECODE_BITS: u32 = u8::BITS;
const CONTROL: crate::fields::ControlKind =
crate::fields::ControlKind::Knob(crate::fields::Unit::None);
type Error = ParseError;
fn from_bits(bits: u64) -> Result<Self, ParseError> {
CtrlPedalGain::new((bits as u8).saturating_add(1))
}
fn to_bits(&self) -> u64 {
u64::from(self.0 - Self::MIN)
}
}
impl Debug for CtrlPedalGain {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl Display for CtrlPedalGain {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl PartialEq<u8> for CtrlPedalGain {
fn eq(&self, other: &u8) -> bool {
self.0 == *other
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bits::Packed;
use std::collections::{BTreeMap, BTreeSet};
use std::io::Cursor;
const fn body(abs: usize) -> usize {
abs - 0x2c
}
fn panel(bytes: &[(usize, u8)]) -> Settings {
let mut raw = [0u8; BODY_LEN];
for &(at, b) in bytes {
raw[body(at)] = b;
}
Settings::try_from(raw).expect("every settings field decodes totally")
}
#[test]
fn settings_round_trip_at_the_declared_length() {
let settings = new();
let mut bytes = Vec::new();
settings.write_to(&mut Cursor::new(&mut bytes)).unwrap();
assert_eq!(bytes.len(), FILE_LEN);
assert_eq!(&bytes[0x08..0x0c], FORMAT.as_bytes());
let back = read_from(&mut Cursor::new(&mut bytes)).unwrap();
let mut again = Vec::new();
back.write_to(&mut Cursor::new(&mut again)).unwrap();
assert_eq!(bytes, again);
}
#[test]
fn a_settings_file_addressed_to_a_slot_is_refused() {
let mut settings = new();
settings.header.set_slot((1, 0));
let mut bytes = Vec::new();
settings.write_to(&mut Cursor::new(&mut bytes)).unwrap();
let err = read_from(&mut Cursor::new(&bytes))
.expect_err("a located settings file must not decode");
assert!(
matches!(err, Error::Parse(ParseError::AssertFail(_))),
"refused for the wrong reason: {err}",
);
}
#[test]
fn a_settings_field_set_by_name_survives_a_round_trip() {
let mut settings = new();
settings.set_field("global_transpose", "-3").unwrap();
let mut bytes = Vec::new();
settings.write_to(&mut Cursor::new(&mut bytes)).unwrap();
let back = read_from(&mut Cursor::new(&mut bytes)).unwrap();
let listed = back
.fields()
.into_iter()
.find(|f| f.path == "global_transpose")
.expect("declared");
assert_eq!(listed.display, "-3");
}
#[test]
fn every_field_is_listed_under_one_menu() {
let declared: BTreeSet<String> = Settings::field_specs()
.into_iter()
.map(|f| f.name)
.filter(|name| !name.starts_with("startup_"))
.collect();
let mut grouped: BTreeSet<String> = BTreeSet::new();
for (menu, settings) in Settings::default().by_menu() {
for setting in settings {
assert!(
declared.contains(setting.name),
"{} lists {}, which the panel does not declare",
menu.title(),
setting.name,
);
assert!(
grouped.insert(setting.name.to_string()),
"{} is listed under two menus",
setting.name,
);
}
}
let missing: Vec<_> = declared.difference(&grouped).collect();
assert!(missing.is_empty(), "fields with no menu: {missing:?}");
}
#[test]
fn a_menu_renders_the_panels_own_wording() {
let p = panel(&[
(0x2e, 0x13),
(0x2f, 0x24),
(0x30, 0x03),
(0x31, 0xc1),
(0x32, 0x06),
(0x33, 0xdc),
(0x34, 0x66),
(0x35, 0x00),
(0x36, 0x09),
(0x37, 0x25),
(0x38, 0x12),
(0x39, 0x49),
(0x3a, 0x2d),
(0x3b, 0x61),
(0x3c, 0x06),
(0x3d, 0x24),
]);
let rendered: BTreeMap<&str, String> = p
.by_menu()
.into_iter()
.flat_map(|(_, settings)| settings)
.map(|s| (s.name, s.value))
.collect();
for (field, want) in [
("ctrl_pedal_type", "yamaha fc-7"),
("ctrl_pedal_gain", "10"),
("sustain_pedal_mode", "sustain + rotor toggle"),
("output_routing", "stereo"),
("global_transpose", "+0"),
("fine_tune", "+5 cent"),
("global_channel", "1"),
("control_change_mode", "send/receive"),
("transpose_at", "midi in"),
("piano_string_resonance", "+0 dB"),
("b3_tonewheel_mode", "vintage 1"),
("b3_key_bounce", "on"),
("b3_perc_db9_mute", "off"),
("rotary_speaker_type", "122"),
("rotary_balance", "50/50"),
("rotary_rotor_acceleration", "normal"),
] {
assert_eq!(rendered[field], want, "{field}");
}
}
#[test]
fn a_menu_names_an_unrecognized_value_as_unknown() {
let p = panel(&[(0x35, 0x01), (0x36, 0xc0)]);
let shown = p
.by_menu()
.into_iter()
.flat_map(|(_, settings)| settings)
.find(|s| s.name == "rotary_speaker_type")
.expect("declared")
.value;
assert_eq!(shown, "unknown (7)");
}
#[test]
fn global_transpose_stores_minus_six_as_zero() {
for (semitones, stored) in [(-6i8, 0x0u8), (-1, 0x5), (0, 0x6), (1, 0x7), (6, 0xc)] {
let p = panel(&[(0x34, stored)]);
assert_eq!(p.global_transpose, semitones, "stored {stored:#x}");
assert_eq!(
<[u8; BODY_LEN]>::from(&p)[body(0x34)],
stored,
"{semitones} did not write back"
);
}
assert_eq!(panel(&[(0x34, 0xc6)]).global_transpose, 0);
assert_eq!(panel(&[(0x34, 0xc6)]).piano_string_resonance, 6);
}
#[test]
fn fine_tune_stores_minus_fifty_cents_as_zero() {
for (cents, at32, at33) in [
(-50i8, 0x00u8, 0x00u8),
(0, 0x00, 0xc8),
(5, 0x00, 0xdc),
(50, 0x01, 0x90),
] {
let p = panel(&[(0x32, at32), (0x33, at33)]);
assert_eq!(p.fine_tune, cents, "stored {at32:#04x} {at33:#04x}");
let back = <[u8; BODY_LEN]>::from(&p);
assert_eq!(
(back[body(0x32)], back[body(0x33)]),
(at32, at33),
"{cents} did not write back"
);
}
let mut raw = [0u8; BODY_LEN];
raw[body(0x32)] = 0x01;
raw[body(0x33)] = 0xfc;
assert!(Settings::try_from(raw).is_err(), "101 cents decoded");
}
#[test]
fn a_midi_channel_is_stored_zero_based_with_sixteen_for_off() {
for (bits, channel) in [
(0u64, MidiChannel::Channel(1)),
(1, MidiChannel::Channel(2)),
(15, MidiChannel::Channel(16)),
(16, MidiChannel::Off),
(17, MidiChannel::Unknown(17)),
(31, MidiChannel::Unknown(31)),
] {
assert_eq!(MidiChannel::from_bits(bits).unwrap(), channel);
assert_eq!(channel.to_bits(), bits, "{channel:?} does not round-trip");
}
assert_eq!(format!("{:?}", MidiChannel::Channel(7)), "7");
assert_eq!(format!("{:?}", MidiChannel::Off), "off");
}
#[test]
fn ctrl_pedal_gain_is_stored_one_less_than_the_panel_reads() {
for gain in CtrlPedalGain::MIN..=CtrlPedalGain::MAX {
let stored = CtrlPedalGain::new(gain).unwrap().to_bits();
assert_eq!(stored, u64::from(gain) - 1);
assert_eq!(CtrlPedalGain::from_bits(stored).unwrap(), gain);
}
assert!(CtrlPedalGain::new(0).is_err());
assert!(CtrlPedalGain::new(11).is_err());
assert!(CtrlPedalGain::from_bits(10).is_err());
}
#[test]
fn the_default_panel_encodes_and_decodes() {
let p = Settings::default();
assert_eq!(<[u8; BODY_LEN]>::from(&p), [0; BODY_LEN]);
assert_eq!(p.global_transpose, -6);
assert_eq!(p.fine_tune, -50);
assert_eq!(p.ctrl_pedal_gain, 1);
assert_eq!(p.global_channel, MidiChannel::Channel(1));
}
#[test]
fn setting_a_field_moves_only_its_own_bytes() {
let mut p = panel(&[]);
p.b3_tonewheel_mode = TonewheelMode::Vintage3;
let raw = <[u8; BODY_LEN]>::from(&p);
let moved: Vec<usize> = (0..BODY_LEN).filter(|&i| raw[i] != 0).collect();
assert_eq!(moved, vec![body(0x39)], "{moved:x?}");
assert_eq!(raw[body(0x39)], 0x03);
}
#[test]
fn an_unrecognized_pattern_round_trips() {
let p = panel(&[(0x35, 0x01), (0x36, 0xc0)]);
assert!(p.rotary_speaker_type.is_unknown());
let raw = <[u8; BODY_LEN]>::from(&p);
assert_eq!((raw[body(0x35)], raw[body(0x36)]), (0x01, 0xc0));
}
}