nord_format/formats/ne5/program/
mod.rs1mod center;
14mod effects;
15mod organ;
16mod panel;
17mod piano;
18mod sample;
19
20pub use center::{CenterPanel, OrganType};
21pub use effects::{EffectsPanel, EqualizerPart, Fx1Type, Fx2Type, Fx3Type, Fx5Type, Routing};
22pub use organ::{B3PercSpeed, B3Vib, Drawbars, FarfisaVib, OrganModel, OrganPanel, Preset, VoxVib};
23pub use panel::PANEL;
24pub use piano::{PianoCategory, PianoPanel};
25pub use sample::SamplePanel;
26
27pub use crate::fields::Field;
28
29use crate::bank;
30use crate::cbin::{self, Cbin, Header};
31use crate::error::{Error, ParseError};
32use crate::types::RangedU16Pair;
33
34use std::io::{Read, Seek};
35
36pub const FORMAT: &str = "ne5p";
37pub const KNOWN_VERSIONS: &[u32] = &[4];
40pub const DEFAULT_VERSION: u32 = 4;
43pub const BODY_LEN: usize = 121;
45pub const FILE_LEN: usize = 0x2c + BODY_LEN;
49pub const BANK_COUNT: u16 = 8;
50pub const SLOT_COUNT: u16 = 50;
51
52pub type Location = RangedU16Pair<BANK_COUNT, SLOT_COUNT>;
53pub type Bank = bank::Bank<Cbin<Program>, Location>;
54
55#[nord_bits_derive::bitbody(121)]
63pub struct Program {
64 #[bits(0..=15)]
66 pub program_version: u16,
67
68 #[at(0x02..0x09)]
69 pub center_panel: CenterPanel,
70
71 #[at(0x0e..0x16)]
72 pub piano_panel: PianoPanel,
73
74 #[at(0x1a..0x22)]
75 pub sample_panel: SamplePanel,
76
77 #[at(0x22..0x67)]
78 pub organ_panel: OrganPanel,
79
80 #[at(0x67..0x79)]
81 pub effects_panel: EffectsPanel,
82}
83
84impl Default for Program {
85 fn default() -> Program {
86 Program {
87 raw: [0; BODY_LEN],
88 program_version: DEFAULT_VERSION as u16,
89 center_panel: CenterPanel::default(),
90 piano_panel: PianoPanel::default(),
91 sample_panel: SamplePanel::default(),
92 organ_panel: OrganPanel::default(),
93 effects_panel: EffectsPanel::default(),
94 }
95 }
96}
97
98pub(crate) use crate::formats::known_version;
99
100pub(crate) fn unset_aux(format: &'static str, header: &Header) -> Result<(), Error> {
108 if header.aux != 0xFFFF_FFFF {
109 return Err(ParseError::AssertFail(format!(
110 "{format}: aux is {:#010x}, not the 0xffffffff every slot-addressed file holds",
111 header.aux,
112 ))
113 .into());
114 }
115 Ok(())
116}
117
118pub(crate) fn slot<L: bank::Location>(header: &Header) -> Result<L, Error> {
120 let (bank, slot) = header.slot();
121 (bank, slot)
122 .try_into()
123 .map_err(|_| ParseError::AssertFail(format!("invalid location: {bank} {slot}")).into())
124}
125
126pub fn location(file: &Cbin<Program>) -> Result<Location, Error> {
132 slot(&file.header)
133}
134
135pub fn new(location: Location) -> Cbin<Program> {
137 Cbin {
138 header: Header::new(FORMAT, location.inner(), DEFAULT_VERSION),
139 body: Program::default(),
140 }
141}
142
143pub fn read_from(reader: &mut (impl Read + Seek)) -> Result<Cbin<Program>, Error> {
144 let file: Cbin<Program> = cbin::read(reader, FORMAT)?;
145 known_version(FORMAT, file.header.version, KNOWN_VERSIONS)?;
146 unset_aux(FORMAT, &file.header)?;
147 location(&file)?;
148 Ok(file)
149}
150
151impl bank::Item<Location> for Cbin<Program> {
155 fn location(&self) -> Location {
156 location(self).expect("a program's location is validated at construction")
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use crate::cbin::Generation;
165 use std::io::Cursor;
166
167 #[test]
174 fn an_unknown_schema_version_is_refused() {
175 let program = new((0, 0).try_into().unwrap());
176 let mut bytes = Vec::new();
177 program.write_to(&mut Cursor::new(&mut bytes)).unwrap();
178 assert_eq!(bytes.len(), FILE_LEN);
179
180 assert!(read_from(&mut Cursor::new(&mut bytes.clone())).is_ok());
182
183 assert_eq!(u32::from_le_bytes(bytes[0x14..0x18].try_into().unwrap()), 4);
185 bytes[0x14..0x18].copy_from_slice(&5u32.to_le_bytes());
186
187 let err = read_from(&mut Cursor::new(&mut bytes)).expect_err("version 5 must not decode");
188 assert!(
190 matches!(
191 err,
192 Error::Parse(crate::error::ParseError::UnsupportedVersion {
193 format: "ne5p",
194 version: 5,
195 ..
196 })
197 ),
198 "unhelpful error: {err}",
199 );
200 }
201
202 #[test]
204 fn an_unexpected_aux_word_is_refused() {
205 let program = new((0, 0).try_into().unwrap());
206 let mut bytes = Vec::new();
207 program.write_to(&mut Cursor::new(&mut bytes)).unwrap();
208
209 bytes[0x10..0x14].copy_from_slice(&0u32.to_le_bytes());
211 let err = read_from(&mut Cursor::new(&bytes)).expect_err("a set aux must not decode");
212 assert!(
213 matches!(err, Error::Parse(ParseError::AssertFail(_))),
214 "refused for the wrong reason: {err}",
215 );
216 }
217
218 #[test]
224 fn every_panels_encode_is_total() {
225 fn total<P, W>(_: &P)
226 where
227 for<'a> W: From<&'a P>,
228 {
229 }
230
231 let program = new((0, 0).try_into().unwrap());
232 total::<_, [u8; 7]>(&program.center_panel);
233 total::<_, [u8; 8]>(&program.piano_panel);
234 total::<_, [u8; 8]>(&program.sample_panel);
235 total::<_, [u8; 69]>(&program.organ_panel);
236 total::<_, [u8; 18]>(&program.effects_panel);
237 }
238
239 fn restamp_crc(bytes: &mut [u8]) {
242 let crc = crate::crc::crc32(&bytes[0x2c..]);
243 bytes[0x18..0x1c].copy_from_slice(&crc.to_le_bytes());
244 }
245
246 #[test]
253 fn no_decode_path_can_skip_validation() {
254 let program = new((0, 0).try_into().unwrap());
255 let mut bytes = Vec::new();
256 program.write_to(&mut Cursor::new(&mut bytes)).unwrap();
257
258 let pristine = bytes.clone();
260 restamp_crc(&mut bytes);
261 assert_eq!(bytes, pristine, "the CRC helper does not match the writer");
262
263 bytes[0x2e] |= 0b1110_0000;
265 restamp_crc(&mut bytes);
266
267 let front = read_from(&mut Cursor::new(&mut bytes))
268 .expect_err("the front door accepted an undecodable panel");
269 assert!(
271 matches!(
272 front,
273 Error::Parse(crate::error::ParseError::OutOfBounds { .. })
274 ),
275 "refused for the wrong reason: {front}",
276 );
277 assert!(
278 CenterPanel::try_from(<[u8; 7]>::try_from(&bytes[0x2e..0x35]).unwrap()).is_err(),
279 "the conversion itself accepted an undecodable panel",
280 );
281 }
282
283 #[test]
285 fn setting_a_field_by_name_moves_only_that_fields_bytes() {
286 let mut program = new((0, 0).try_into().unwrap());
287 let mut before = Vec::new();
288 program.write_to(&mut Cursor::new(&mut before)).unwrap();
289
290 program.set_field("center_panel.transpose", "-5").unwrap();
291 assert_eq!(program.center_panel.transpose.inner(), -5);
292
293 let mut after = Vec::new();
294 program.write_to(&mut Cursor::new(&mut after)).unwrap();
295
296 let moved: Vec<usize> = (0..before.len())
299 .filter(|&i| before[i] != after[i])
300 .collect();
301 assert_eq!(moved, vec![0x18, 0x19, 0x1a, 0x1b, 0x31], "{moved:x?}");
302 }
303
304 #[test]
307 fn an_unknown_path_names_what_it_could_not_find() {
308 let mut program = new((0, 0).try_into().unwrap());
309 for (path, wanted) in [
310 ("center_panel.nonesuch", "nonesuch"),
311 ("nonesuch.transpose", "nonesuch"),
312 ("transpose", "transpose"),
313 ] {
314 let err = program.set_field(path, "0").unwrap_err().to_string();
315 assert!(err.contains(wanted), "{path}: {err}");
316 }
317 }
318
319 #[test]
321 fn every_declared_field_is_settable_by_its_listed_name() {
322 let mut program = new((0, 0).try_into().unwrap());
323 let fields = program.fields();
324 assert!(
325 fields.iter().any(|f| f.path == "center_panel.transpose"),
326 "the worked example is missing from the registry",
327 );
328
329 for f in fields {
332 let (path, value) = (f.path.clone(), f.value.clone());
333 program
334 .set_field(&path, &value)
335 .unwrap_or_else(|e| panic!("{path} = {value:?}: {e}"));
336 }
337 }
338
339 #[test]
342 fn a_wide_field_is_spelled_by_its_stored_bits() {
343 let mut program = new((0, 0).try_into().unwrap());
344 program
345 .set_field("organ_panel.b3_preset1_drawbars", "0x087654321")
346 .unwrap();
347 assert_eq!(
348 program.organ_panel.drawbars(OrganModel::B3, Preset::One),
349 [0, 8, 7, 6, 5, 4, 3, 2, 1],
350 );
351
352 let listed = program
353 .fields()
354 .into_iter()
355 .find(|f| f.path == "organ_panel.b3_preset1_drawbars")
356 .expect("declared");
357 assert_eq!(listed.value, "0x87654321");
358 assert_eq!(listed.display, "[0, 8, 7, 6, 5, 4, 3, 2, 1]");
359 }
360
361 #[test]
363 fn decode_and_encode_are_inverse() {
364 for pattern in [0u64, u64::MAX, 0xa5a5_a5a5_a5a5_a5a5, 0x5a5a_5a5a_5a5a_5a5a] {
365 let raw = pattern.to_be_bytes();
366 let panel = PianoPanel::try_from(raw).unwrap();
367 assert_eq!(<[u8; 8]>::from(&panel), raw);
368
369 let panel = SamplePanel::try_from(raw).unwrap();
370 assert_eq!(<[u8; 8]>::from(&panel), raw);
371
372 let raw: [u8; 7] = raw[..7].try_into().unwrap();
373 if let Ok(panel) = CenterPanel::try_from(raw) {
374 assert_eq!(<[u8; 7]>::from(&panel), raw);
375 }
376 }
377 }
378
379 #[test]
383 fn the_program_body_layout_is_published_as_data() {
384 use crate::layout::BodyLayout;
385
386 let fields = Program::layout();
387 let center = fields
388 .iter()
389 .find(|f| f.path == "center_panel")
390 .expect("declared");
391 assert_eq!((center.lo / 8, (center.hi + 1) / 8), (0x02, 0x09));
392 let nested = center.nested.expect("a panel chains to its own layout");
393 assert!(
394 nested().iter().any(|f| f.path == "transpose"),
395 "the nested layout does not list the panel's fields",
396 );
397
398 let program = new((0, 0).try_into().unwrap());
400 let paths: Vec<String> = program.fields().into_iter().map(|f| f.path).collect();
401 assert!(paths.contains(&"center_panel.transpose".to_string()));
402 assert!(paths.contains(&"piano_panel.id".to_string()));
403 assert!(paths.contains(&"sample_panel.id".to_string()));
404 }
405
406 #[test]
409 fn a_type_0_program_is_the_same_body_18_bytes_earlier() {
410 let mut program = new((3, 7).try_into().unwrap());
411 let mut v1 = Vec::new();
412 program.write_to(&mut Cursor::new(&mut v1)).unwrap();
413
414 program.header.generation = Generation::V0;
415 let mut v0 = Vec::new();
416 program.write_to(&mut Cursor::new(&mut v0)).unwrap();
417
418 assert_eq!(v1.len() - v0.len(), 18);
419 assert_eq!(&v1[0x2c..], &v0[0x18..v0.len() - 2], "bodies differ");
420 assert_eq!(
421 &v1[0x08..0x18],
422 &v0[0x08..0x18],
423 "shared header fields differ"
424 );
425
426 let back = read_from(&mut Cursor::new(&v0)).unwrap();
427 assert_eq!(back.header.generation, Generation::V0);
428 let mut again = Vec::new();
429 back.write_to(&mut Cursor::new(&mut again)).unwrap();
430 assert_eq!(again, v0, "type-0 round trip changed the bytes");
431 }
432}