1pub mod codec;
52pub mod encode;
53#[cfg(any(test, feature = "synthetic"))]
56pub mod synthetic;
57
58use crate::cbin::{self, Cbin, Header, RawBody};
59use crate::error::{try_vec, Error, ParseError};
60use std::borrow::Cow;
61use std::collections::{BTreeMap, BTreeSet};
62use std::fmt;
63use std::io::{Read, Seek, Write};
64use std::ops::RangeInclusive;
65
66pub const FORMAT: &str = "npno";
67
68pub const CNSP_MAGIC: &[u8; 4] = b"CNSP";
70
71pub const NOTES: usize = 128;
73
74pub const UNCOVERED: u8 = 0xff;
76
77pub const KNOWN_VERSIONS: &[u32] = &[0x450, 0x464];
81
82const VERSION_SPLIT_NAME: u16 = 0x464;
84
85const KEY_MAP_AT: usize = 0x8c;
86const FINE_TUNE_AT: usize = 0x18c;
87const VERSION_AT: usize = 0x04;
88const VERSION_ECHO_AT: usize = 0x61c;
89const CHANNELS_AT: usize = 0x61e;
90const STROKE_COUNT_AT: usize = 0x620;
91const ROOT_COUNTS_AT: usize = 0x622;
92
93const KIND_AT: usize = 0x18;
95
96const GAIN_AT: usize = 0x40c;
99
100const DAMPER_TOP_AT: usize = 0x40d;
103
104const DIRECTORY_AT: usize = 0x732;
106
107const RECORD: usize = 118;
109
110const REC_START: usize = 0x00;
111const REC_BANK: usize = 0x04;
112const REC_LAYER: usize = 0x05;
113const REC_FRAMES: usize = 0x06;
114const REC_BLOCKS: usize = 0x0a;
115const REC_SEEDS: usize = 0x0c;
116const REC_MARKS: usize = 0x1c;
117const REC_MARK_BLOCK: usize = 0x2c;
118const REC_DECAY: usize = 0x2e;
119const REC_WINDOW: usize = 0x32;
122const REC_TRIM: usize = 0x34;
125const REC_DECAYS: usize = 0x36;
126const REC_ID: usize = 0x6e;
127
128const SEEDS: usize = 4;
130
131const MARKS: usize = 4;
133
134pub const DECAYS: usize = 14;
140const _: () = assert!(REC_DECAYS + DECAYS * 4 == REC_ID);
141
142pub const LADDER_UNITY: u32 = 0x0080_0000;
145
146pub const AUDIO_ALIGN_BIAS: usize = 192;
152
153pub const FINE_TUNE_CENTS_PER_UNIT: f32 = 0.7;
156
157#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
161pub enum Bank {
162 Attack,
164 Resonance,
167 Release,
169}
170
171impl Bank {
172 pub const ALL: [Bank; 3] = [Bank::Attack, Bank::Resonance, Bank::Release];
173
174 pub fn from_code(code: u8) -> Option<Bank> {
175 match code {
176 0 => Some(Bank::Attack),
177 1 => Some(Bank::Resonance),
178 2 => Some(Bank::Release),
179 _ => None,
180 }
181 }
182
183 pub fn code(self) -> u8 {
184 match self {
185 Bank::Attack => 0,
186 Bank::Resonance => 1,
187 Bank::Release => 2,
188 }
189 }
190
191 pub fn name(self) -> &'static str {
192 match self {
193 Bank::Attack => "attack",
194 Bank::Resonance => "resonance",
195 Bank::Release => "release",
196 }
197 }
198}
199
200impl fmt::Display for Bank {
201 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202 f.write_str(self.name())
203 }
204}
205
206#[derive(Debug, Clone, PartialEq, Eq)]
214pub enum Layers {
215 Loudest(usize),
217 Only(BTreeSet<u8>),
219}
220
221#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
223pub struct Change {
224 pub strokes_removed: usize,
225 pub roots_removed: usize,
226 pub keys_uncovered: usize,
227}
228
229pub const NAME_SEPARATOR: char = '#';
231
232#[derive(Clone, Copy)]
234struct TextField {
235 at: usize,
236 len: usize,
237}
238
239impl TextField {
240 const COMBINED: TextField = TextField {
242 at: 0x1c,
243 len: 0x20,
244 };
245 const LONG_NAME: TextField = TextField {
247 at: 0x3c,
248 len: 0x20,
249 };
250 const VOICING: TextField = TextField {
252 at: 0x5c,
253 len: 0x20,
254 };
255
256 const fn capacity(self) -> usize {
258 self.len - 1
259 }
260
261 fn read(self, prefix: &[u8]) -> String {
262 let field = &prefix[self.at..self.at + self.len];
263 let end = field.iter().position(|&b| b == 0).unwrap_or(field.len());
264 String::from_utf8_lossy(&field[..end]).into_owned()
265 }
266
267 fn check_text(text: &str) -> Result<(), Error> {
271 match text.chars().find(|&c| !c.is_ascii_graphic() && c != ' ') {
272 None => Ok(()),
273 Some(bad) => Err(ParseError::AssertFail(format!(
274 "{text:?} holds {bad:?}, which the field would not read back as written; it \
275 carries printable ASCII"
276 ))
277 .into()),
278 }
279 }
280
281 fn check(self, text: &str) -> Result<(), Error> {
283 TextField::check_text(text)?;
284 if text.len() > self.capacity() {
285 return Err(ParseError::OutOfBounds {
286 value: format!("{text:?} ({} bytes)", text.len()),
287 bound: format!("at most {} bytes", self.capacity()),
288 }
289 .into());
290 }
291 Ok(())
292 }
293
294 fn write(self, prefix: &mut [u8], text: &str) -> Result<(), Error> {
295 self.check(text)?;
296 let field = &mut prefix[self.at..self.at + self.len];
297 field.fill(0);
298 field[..text.len()].copy_from_slice(text.as_bytes());
299 Ok(())
300 }
301}
302
303fn check_half(what: &str, text: &str) -> Result<(), Error> {
306 if text.contains(NAME_SEPARATOR) {
307 return Err(ParseError::AssertFail(format!(
308 "the {what} {text:?} holds {NAME_SEPARATOR:?}, which is what splits the name from \
309 the variant in the field they share"
310 ))
311 .into());
312 }
313 TextField::check_text(text)
314}
315
316pub struct Piano {
321 pub file: Cbin<RawBody>,
322}
323
324impl Piano {
325 pub fn new() -> Piano {
326 Piano {
327 file: Cbin {
328 header: Header::new(FORMAT, (0, 0), 0),
329 body: RawBody(Vec::new()),
330 },
331 }
332 }
333
334 pub fn read_from(reader: &mut (impl Read + Seek)) -> Result<Piano, Error> {
335 Ok(Piano {
336 file: cbin::read(reader, FORMAT)?,
337 })
338 }
339
340 pub fn write_to(&self, writer: &mut (impl Write + Seek)) -> Result<(), Error> {
341 self.file.write_to(writer)
342 }
343
344 fn mapped(&self) -> Result<&[u8], Error> {
347 let body = &self.file.body.0;
348 check_mapped(body)?;
349 Ok(body)
350 }
351
352 pub fn stream_version(&self) -> Result<u16, Error> {
354 version_of(&self.file.body.0)
355 }
356
357 pub fn name(&self) -> Result<(String, String), Error> {
361 let body = self.mapped()?;
362 if body.len() < DIRECTORY_AT {
363 return Err(short("the prefix"));
364 }
365 Ok(split_name(&TextField::COMBINED.read(body)))
366 }
367
368 pub fn key_map(&self) -> Result<&[u8], Error> {
371 self.mapped()?
372 .get(KEY_MAP_AT..KEY_MAP_AT + NOTES)
373 .ok_or_else(|| short("the key map"))
374 }
375
376 pub fn library(&self) -> Result<Library<'_>, Error> {
379 Library::parse_body(self.file.header.clone(), &self.file.body.0)
380 }
381}
382
383impl Default for Piano {
384 fn default() -> Self {
385 Self::new()
386 }
387}
388
389impl fmt::Debug for Piano {
390 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
391 f.debug_struct("npno::Piano")
392 .field("header", &self.file.header)
393 .field("body_len", &self.file.body.0.len())
394 .finish()
395 }
396}
397
398fn raw_halves(field: &str) -> (&str, &str) {
401 field.split_once(NAME_SEPARATOR).unwrap_or((field, ""))
402}
403
404fn split_name(field: &str) -> (String, String) {
407 let (name, variant) = raw_halves(field);
408 (name.trim().to_owned(), variant.trim().to_owned())
409}
410
411fn midi_key(what: &str, key: u8) -> Result<usize, Error> {
417 let index = usize::from(key);
418 if index < NOTES {
419 return Ok(index);
420 }
421 Err(ParseError::OutOfBounds {
422 value: format!("{what} {key}"),
423 bound: "a MIDI note from 0 through 127".into(),
424 }
425 .into())
426}
427
428fn short(what: &str) -> Error {
429 ParseError::AssertFail(format!("the body ends inside {what}")).into()
430}
431
432fn version_of(body: &[u8]) -> Result<u16, Error> {
434 if body.get(..4) != Some(CNSP_MAGIC.as_slice()) {
435 return Err(ParseError::AssertFail(format!(
436 "body opens {:02x?}, not the CNSP stream",
437 body.get(..4).unwrap_or_default()
438 ))
439 .into());
440 }
441 let bytes = body
442 .get(VERSION_AT..VERSION_AT + 2)
443 .ok_or_else(|| ParseError::AssertFail("body ends inside the CNSP header".to_string()))?;
444 Ok(u16::from_be_bytes(bytes.try_into().unwrap()))
445}
446
447fn check_mapped(body: &[u8]) -> Result<(), Error> {
449 let version = version_of(body)?;
450 crate::formats::known_version(FORMAT, u32::from(version), KNOWN_VERSIONS)
451}
452
453fn overflow(what: &str) -> Error {
454 ParseError::OutOfBounds {
455 value: what.to_string(),
456 bound: "an offset that fits this platform's address space".into(),
457 }
458 .into()
459}
460
461fn be16(bytes: &[u8], at: usize) -> u16 {
462 u16::from_be_bytes(bytes[at..at + 2].try_into().unwrap())
463}
464
465fn be32(bytes: &[u8], at: usize) -> u32 {
466 u32::from_be_bytes(bytes[at..at + 4].try_into().unwrap())
467}
468
469fn first_audio_offset(directory_end: usize, block: usize) -> Result<usize, Error> {
474 directory_end
475 .checked_add(AUDIO_ALIGN_BIAS)
476 .map(|biased| biased.div_ceil(block))
477 .and_then(|blocks| blocks.checked_mul(block))
478 .and_then(|at| at.checked_sub(AUDIO_ALIGN_BIAS))
479 .ok_or_else(|| overflow("the first audio offset"))
480}
481
482#[derive(Clone)]
487pub struct Stroke<'a> {
488 pub root: u8,
492 record: [u8; RECORD],
493 audio: Cow<'a, [u8]>,
494}
495
496impl<'a> Stroke<'a> {
497 pub fn bank_code(&self) -> u8 {
500 self.record[REC_BANK]
501 }
502
503 pub fn bank(&self) -> Option<Bank> {
504 Bank::from_code(self.bank_code())
505 }
506
507 pub fn layer(&self) -> u8 {
517 self.record[REC_LAYER]
518 }
519
520 pub fn frames(&self) -> u32 {
523 be32(&self.record, REC_FRAMES)
524 }
525
526 pub fn blocks(&self) -> u16 {
527 be16(&self.record, REC_BLOCKS)
528 }
529
530 pub fn trim(&self) -> u16 {
532 be16(&self.record, REC_TRIM)
533 }
534
535 pub fn decay(&self) -> u32 {
537 be32(&self.record, REC_DECAY)
538 }
539
540 pub fn ladder(&self) -> [u32; DECAYS] {
542 std::array::from_fn(|entry| be32(&self.record, REC_DECAYS + entry * 4))
543 }
544
545 pub fn id(&self) -> u32 {
549 be32(&self.record, REC_ID)
550 }
551
552 pub fn seeds(&self) -> [[i16; SEEDS]; 2] {
555 let mut out = [[0i16; SEEDS]; 2];
556 for (channel, group) in out.iter_mut().enumerate() {
557 for (i, slot) in group.iter_mut().enumerate() {
558 *slot = be16(&self.record, REC_SEEDS + (channel * SEEDS + i) * 2) as i16;
559 }
560 }
561 out
562 }
563
564 pub fn audio(&self) -> &[u8] {
566 &self.audio
567 }
568
569 pub fn record(&self) -> &[u8; RECORD] {
572 &self.record
573 }
574}
575
576impl fmt::Debug for Stroke<'_> {
577 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
578 f.debug_struct("Stroke")
579 .field("root", &self.root)
580 .field("bank", &self.bank_code())
581 .field("layer", &self.layer())
582 .field("frames", &self.frames())
583 .field("blocks", &self.blocks())
584 .finish()
585 }
586}
587
588#[derive(Clone)]
596pub struct Library<'a> {
597 pub header: Header,
599 prefix: Vec<u8>,
602 channels: u16,
603 strokes: Vec<Stroke<'a>>,
604}
605
606impl<'a> Library<'a> {
607 pub fn borrow(file: &'a [u8]) -> Result<Library<'a>, Error> {
613 let mut head: &[u8] = file;
614 let (header, _) = cbin::read_header(&mut head)?;
615 if header.tag.as_slice() != FORMAT.as_bytes() {
616 return Err(ParseError::WrongFormat {
617 expected: FORMAT,
618 got: String::from_utf8_lossy(&header.tag).into_owned(),
619 }
620 .into());
621 }
622 let start = usize::try_from(header.generation.body_start())
623 .map_err(|_| overflow("the container's header"))?;
624 let trailer = usize::try_from(header.generation.trailer_len())
625 .map_err(|_| overflow("the container's checksum trailer"))?;
626 let end = file
627 .len()
628 .checked_sub(trailer)
629 .ok_or_else(|| short("the container's checksum trailer"))?;
630 let body = file.get(start..end).ok_or_else(|| short("the header"))?;
631 Library::parse_body(header, body)
632 }
633
634 fn parse_body(header: Header, body: &'a [u8]) -> Result<Library<'a>, Error> {
635 check_mapped(body)?;
636 let prefix = body
637 .get(..DIRECTORY_AT)
638 .ok_or_else(|| short("the prefix"))?;
639
640 let version = be16(prefix, VERSION_AT);
641 let echo = be16(prefix, VERSION_ECHO_AT);
642 if echo != version {
643 return Err(ParseError::AssertFail(format!(
644 "the stream version {version:#06x} is echoed as {echo:#06x}"
645 ))
646 .into());
647 }
648
649 let channels = be16(prefix, CHANNELS_AT);
650 if !(1..=2).contains(&channels) {
651 return Err(ParseError::OutOfBounds {
652 value: format!("{channels} channels"),
653 bound: "1 or 2".into(),
654 }
655 .into());
656 }
657 let block = block_bytes(channels);
658
659 let count = usize::from(be16(prefix, STROKE_COUNT_AT));
660 let counts: Vec<u16> = (0..NOTES)
661 .map(|n| be16(prefix, ROOT_COUNTS_AT + n * 2))
662 .collect();
663 let summed: usize = counts.iter().map(|&c| usize::from(c)).sum();
664 if summed != count {
665 return Err(ParseError::AssertFail(format!(
666 "the per-root counts sum to {summed} where the stroke count is {count}"
667 ))
668 .into());
669 }
670
671 let directory_end = RECORD
672 .checked_mul(count)
673 .and_then(|len| DIRECTORY_AT.checked_add(len))
674 .ok_or_else(|| overflow("the stroke directory"))?;
675 let records = body
676 .get(DIRECTORY_AT..directory_end)
677 .ok_or_else(|| short("the stroke directory"))?;
678
679 let first = first_audio_offset(directory_end, block)?;
680 let pad = body
681 .get(directory_end..first)
682 .ok_or_else(|| short("the alignment gap before the audio"))?;
683 if pad.iter().any(|&b| b != 0) {
684 return Err(ParseError::AssertFail(
685 "the alignment gap before the audio is not zero".into(),
686 )
687 .into());
688 }
689
690 let mut strokes = Vec::new();
691 strokes
692 .try_reserve_exact(count)
693 .map_err(|_| overflow("the stroke list"))?;
694 let mut at = first;
695 let mut roots = counts
696 .iter()
697 .enumerate()
698 .flat_map(|(note, &n)| std::iter::repeat_n(note as u8, usize::from(n)));
699 for i in 0..count {
700 let mut record = [0u8; RECORD];
701 record.copy_from_slice(&records[i * RECORD..(i + 1) * RECORD]);
702 let root = roots.next().expect("the counts sum to the stroke count");
703 let start = be32(&record, REC_START);
704 if usize::try_from(start) != Ok(at) {
705 return Err(ParseError::AssertFail(format!(
706 "stroke {i} starts at {start:#x} where the spans before it end at {at:#x}"
707 ))
708 .into());
709 }
710 let span = usize::from(be16(&record, REC_BLOCKS))
711 .checked_mul(block)
712 .ok_or_else(|| overflow("a stroke's audio span"))?;
713 let end = at.checked_add(span).ok_or_else(|| overflow("the audio"))?;
714 let audio = body
715 .get(at..end)
716 .ok_or_else(|| short("a stroke's audio span"))?;
717 strokes.push(Stroke {
718 root,
719 record,
720 audio: Cow::Borrowed(audio),
721 });
722 at = end;
723 }
724 if at != body.len() {
725 return Err(ParseError::AssertFail(format!(
726 "the audio ends at {at:#x} where the body ends at {:#x}",
727 body.len()
728 ))
729 .into());
730 }
731
732 let library = Library {
733 header,
734 prefix: prefix.to_vec(),
735 channels,
736 strokes,
737 };
738 library.check_key_map()?;
739 Ok(library)
740 }
741
742 fn check_key_map(&self) -> Result<(), Error> {
744 let roots = self.roots();
745 for (key, &root) in self.key_map().iter().enumerate() {
746 if root != UNCOVERED && !roots.contains(&root) {
747 return Err(ParseError::AssertFail(format!(
748 "key {key} plays root {root}, which no stroke records"
749 ))
750 .into());
751 }
752 }
753 Ok(())
754 }
755
756 pub fn stream_version(&self) -> u16 {
757 be16(&self.prefix, VERSION_AT)
758 }
759
760 pub fn channels(&self) -> u16 {
761 self.channels
762 }
763
764 pub fn block_bytes(&self) -> usize {
766 block_bytes(self.channels)
767 }
768
769 pub fn strokes(&self) -> &[Stroke<'a>] {
770 &self.strokes
771 }
772
773 pub fn without_audio(&self) -> Library<'static> {
776 Library {
777 header: self.header.clone(),
778 prefix: self.prefix.clone(),
779 channels: self.channels,
780 strokes: self
781 .strokes
782 .iter()
783 .map(|stroke| Stroke {
784 root: stroke.root,
785 record: stroke.record,
786 audio: Cow::Owned(Vec::new()),
787 })
788 .collect(),
789 }
790 }
791
792 pub fn set_trim(&mut self, index: usize, decibels: u16) -> Result<(), Error> {
794 let count = self.strokes.len();
795 let stroke = self
796 .strokes
797 .get_mut(index)
798 .ok_or_else(|| ParseError::OutOfBounds {
799 value: format!("stroke {index}"),
800 bound: format!("the {count} strokes the directory holds"),
801 })?;
802 stroke.record[REC_TRIM..REC_TRIM + 2].copy_from_slice(&decibels.to_be_bytes());
803 Ok(())
804 }
805
806 pub fn name(&self) -> (String, String) {
808 split_name(&TextField::COMBINED.read(&self.prefix))
809 }
810
811 pub fn key_map(&self) -> &[u8] {
813 &self.prefix[KEY_MAP_AT..KEY_MAP_AT + NOTES]
814 }
815
816 fn key_map_mut(&mut self) -> &mut [u8] {
817 &mut self.prefix[KEY_MAP_AT..KEY_MAP_AT + NOTES]
818 }
819
820 pub fn roots(&self) -> BTreeSet<u8> {
822 self.strokes.iter().map(|s| s.root).collect()
823 }
824
825 pub fn key_root(&self, key: u8) -> Result<Option<u8>, Error> {
828 let root = self.key_map()[midi_key("key", key)?];
829 Ok((root != UNCOVERED).then_some(root))
830 }
831
832 pub fn keys_for(&self, root: u8) -> Vec<u8> {
835 self.key_map()
836 .iter()
837 .enumerate()
838 .filter(|&(_, &r)| r == root)
839 .map(|(key, _)| key as u8)
840 .collect()
841 }
842
843 pub fn fine_tune(&self, key: u8) -> Result<i8, Error> {
846 Ok(self.prefix[FINE_TUNE_AT + midi_key("key", key)?] as i8)
847 }
848
849 pub fn set_fine_tune(&mut self, key: u8, units: i8) -> Result<(), Error> {
854 let at = FINE_TUNE_AT + midi_key("key", key)?;
855 self.prefix[at] = units as u8;
856 Ok(())
857 }
858
859 pub fn gain(&self) -> i8 {
861 self.prefix[GAIN_AT] as i8
862 }
863
864 pub fn set_gain(&mut self, tenths: i8) {
865 self.prefix[GAIN_AT] = tenths as u8;
866 }
867
868 pub fn damper_top(&self) -> u8 {
870 self.prefix[DAMPER_TOP_AT]
871 }
872
873 pub fn set_damper_top(&mut self, key: u8) -> Result<(), Error> {
876 self.prefix[DAMPER_TOP_AT] = midi_key("damper limit", key)? as u8;
877 Ok(())
878 }
879
880 pub fn kind_code(&self) -> u8 {
883 self.prefix[KIND_AT]
884 }
885
886 pub fn set_kind(&mut self, kind: encode::Kind) {
890 self.prefix[KIND_AT] = kind.code();
891 }
892
893 pub fn long_name(&self) -> Option<String> {
901 self.split_field(TextField::LONG_NAME)
902 }
903
904 pub fn voicing(&self) -> Option<String> {
905 self.split_field(TextField::VOICING)
906 }
907
908 fn split_field(&self, field: TextField) -> Option<String> {
909 (self.stream_version() == VERSION_SPLIT_NAME).then(|| field.read(&self.prefix))
910 }
911
912 pub fn set_name(&mut self, name: &str) -> Result<(), Error> {
924 let field = TextField::COMBINED.read(&self.prefix);
925 let variant = raw_halves(&field).1.to_owned();
926 self.set_name_and_variant(name, &variant)
927 }
928
929 pub fn set_variant(&mut self, variant: &str) -> Result<(), Error> {
933 check_half("variant", variant)?;
934 let field = TextField::COMBINED.read(&self.prefix);
935 let combined = format!("{}{NAME_SEPARATOR}{variant}", raw_halves(&field).0);
936 TextField::COMBINED.write(&mut self.prefix, &combined)
937 }
938
939 fn set_name_and_variant(&mut self, name: &str, variant: &str) -> Result<(), Error> {
947 check_half("name", name)?;
948 check_half("variant", variant)?;
949 let combined = format!("{name}{NAME_SEPARATOR}{variant}");
950 let long = (self.stream_version() == VERSION_SPLIT_NAME).then_some(name);
951 TextField::COMBINED.check(&combined)?;
952 if let Some(long) = long {
953 TextField::LONG_NAME.check(long)?;
954 }
955 TextField::COMBINED.write(&mut self.prefix, &combined)?;
956 if let Some(long) = long {
957 TextField::LONG_NAME.write(&mut self.prefix, long)?;
958 }
959 Ok(())
960 }
961
962 pub fn set_voicing(&mut self, voicing: &str) -> Result<(), Error> {
964 if self.stream_version() != VERSION_SPLIT_NAME {
965 return Err(ParseError::AssertFail(format!(
966 "stream {:#06x} carries no voicing field; the variant after the \
967 {NAME_SEPARATOR:?} is where it records one",
968 self.stream_version()
969 ))
970 .into());
971 }
972 TextField::VOICING.write(&mut self.prefix, voicing)
973 }
974
975 pub fn set_key_root(&mut self, key: u8, root: Option<u8>) -> Result<(), Error> {
983 let key = midi_key("key", key)?;
984 if let Some(root) = root {
985 midi_key("root", root)?;
986 if !self.roots().contains(&root) {
987 return Err(ParseError::OutOfBounds {
988 value: format!("root {root}"),
989 bound: "a root the directory records".into(),
990 }
991 .into());
992 }
993 }
994 self.key_map_mut()[key] = root.unwrap_or(UNCOVERED);
995 Ok(())
996 }
997
998 pub fn drop_bank(&mut self, bank: Bank) -> Change {
1004 let code = bank.code();
1005 self.retain(|s| s.bank_code() != code)
1006 }
1007
1008 pub fn keep_layers(&mut self, keep: &Layers) -> Change {
1013 match keep {
1014 Layers::Only(layers) => {
1015 let layers = layers.clone();
1016 self.retain(|s| layers.contains(&s.layer()))
1017 }
1018 Layers::Loudest(n) => {
1019 let mut groups: BTreeMap<(u8, u8), BTreeSet<u8>> = BTreeMap::new();
1020 for stroke in &self.strokes {
1021 groups
1022 .entry((stroke.root, stroke.bank_code()))
1023 .or_default()
1024 .insert(stroke.layer());
1025 }
1026 let kept: BTreeSet<(u8, u8, u8)> = groups
1027 .into_iter()
1028 .flat_map(|((root, bank), layers)| {
1029 layers.into_iter().take(*n).map(move |l| (root, bank, l))
1030 })
1031 .collect();
1032 self.retain(|s| kept.contains(&(s.root, s.bank_code(), s.layer())))
1033 }
1034 }
1035 }
1036
1037 pub fn retain_strokes(&mut self, keep: impl FnMut(&Stroke<'a>) -> bool) -> Change {
1048 self.retain(keep)
1049 }
1050
1051 pub fn cut_range(&mut self, range: RangeInclusive<u8>) -> Result<Change, Error> {
1057 midi_key("the range's lowest key", *range.start())?;
1058 midi_key("the range's highest key", *range.end())?;
1059 Ok(self.restrict(|key| range.contains(&key)))
1060 }
1061
1062 pub fn split_at(&self, key: u8) -> Result<(Library<'a>, Library<'a>), Error> {
1069 midi_key("the split key", key)?;
1070 let mut low = self.clone();
1071 let mut high = self.clone();
1072 low.restrict(|k| k < key);
1073 high.restrict(|k| k >= key);
1074 Ok((low, high))
1075 }
1076
1077 fn restrict(&mut self, keep: impl Fn(u8) -> bool) -> Change {
1079 let mut uncovered = 0;
1080 for (key, slot) in self.key_map_mut().iter_mut().enumerate() {
1081 if !keep(key as u8) && *slot != UNCOVERED {
1082 *slot = UNCOVERED;
1083 uncovered += 1;
1084 }
1085 }
1086 let live: BTreeSet<u8> = self.key_map().iter().copied().collect();
1087 let mut change = self.retain(|s| live.contains(&s.root));
1088 change.keys_uncovered += uncovered;
1089 change
1090 }
1091
1092 fn retain(&mut self, mut keep: impl FnMut(&Stroke<'a>) -> bool) -> Change {
1094 let strokes_before = self.strokes.len();
1095 let roots_before = self.roots().len();
1096 self.strokes.retain(|s| keep(s));
1097 let roots = self.roots();
1098 let mut keys_uncovered = 0;
1099 for slot in self.key_map_mut() {
1100 if *slot != UNCOVERED && !roots.contains(slot) {
1101 *slot = UNCOVERED;
1102 keys_uncovered += 1;
1103 }
1104 }
1105 Change {
1106 strokes_removed: strokes_before - self.strokes.len(),
1107 roots_removed: roots_before - roots.len(),
1108 keys_uncovered,
1109 }
1110 }
1111
1112 pub fn body_len(&self) -> Result<usize, Error> {
1114 let (_, len) = self.extent()?;
1115 Ok(len)
1116 }
1117
1118 fn extent(&self) -> Result<(usize, usize), Error> {
1123 let directory_end = RECORD
1124 .checked_mul(self.strokes.len())
1125 .and_then(|len| DIRECTORY_AT.checked_add(len))
1126 .ok_or_else(|| overflow("the stroke directory"))?;
1127 let block = self.block_bytes();
1128 let first = first_audio_offset(directory_end, block)?;
1129 let mut len = first;
1130 for (index, stroke) in self.strokes.iter().enumerate() {
1131 let span = usize::from(stroke.blocks())
1132 .checked_mul(block)
1133 .ok_or_else(|| overflow("a stroke's audio span"))?;
1134 if stroke.audio.len() != span {
1135 return Err(ParseError::AssertFail(format!(
1136 "stroke {index} holds {} audio bytes where the {} block(s) its record \
1137 states span {span}",
1138 stroke.audio.len(),
1139 stroke.blocks()
1140 ))
1141 .into());
1142 }
1143 len = len.checked_add(span).ok_or_else(|| overflow("the audio"))?;
1144 }
1145 Ok((first, len))
1146 }
1147
1148 pub fn to_body(&self) -> Result<Vec<u8>, Error> {
1156 let count = u16::try_from(self.strokes.len()).map_err(|_| ParseError::OutOfBounds {
1157 value: format!("{} strokes", self.strokes.len()),
1158 bound: "the u16 stroke count the directory holds".into(),
1159 })?;
1160 if self.strokes.windows(2).any(|w| w[0].root > w[1].root) {
1161 return Err(ParseError::AssertFail(
1162 "the strokes are not in ascending root order, which is what the per-root \
1163 counts index them by"
1164 .into(),
1165 )
1166 .into());
1167 }
1168
1169 let (first, len) = self.extent()?;
1170 let mut out = try_vec(len)?;
1171 out[..DIRECTORY_AT].copy_from_slice(&self.prefix);
1172 out[CHANNELS_AT..CHANNELS_AT + 2].copy_from_slice(&self.channels.to_be_bytes());
1173 out[STROKE_COUNT_AT..STROKE_COUNT_AT + 2].copy_from_slice(&count.to_be_bytes());
1174 for note in 0..NOTES {
1175 let n = self
1176 .strokes
1177 .iter()
1178 .filter(|s| usize::from(s.root) == note)
1179 .count();
1180 let n = u16::try_from(n).expect("a per-root count is at most the stroke count");
1181 let at = ROOT_COUNTS_AT + note * 2;
1182 out[at..at + 2].copy_from_slice(&n.to_be_bytes());
1183 }
1184
1185 let mut at = first;
1186 for (i, stroke) in self.strokes.iter().enumerate() {
1187 let start = u32::try_from(at).map_err(|_| ParseError::OutOfBounds {
1188 value: format!("audio offset {at:#x}"),
1189 bound: "the u32 offset a stroke record holds".into(),
1190 })?;
1191 let record = DIRECTORY_AT + i * RECORD;
1192 out[record..record + RECORD].copy_from_slice(&stroke.record);
1193 out[record + REC_START..record + REC_START + 4].copy_from_slice(&start.to_be_bytes());
1194 out[at..at + stroke.audio.len()].copy_from_slice(&stroke.audio);
1195 at += stroke.audio.len();
1196 }
1197 Ok(out)
1198 }
1199
1200 pub fn to_piano(&self) -> Result<Piano, Error> {
1209 Ok(Piano {
1210 file: Cbin {
1211 header: self.header.clone(),
1212 body: RawBody(self.to_body()?),
1213 },
1214 })
1215 }
1216}
1217
1218impl fmt::Debug for Library<'_> {
1219 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1220 let (name, variant) = self.name();
1221 f.debug_struct("npno::Library")
1222 .field("name", &name)
1223 .field("variant", &variant)
1224 .field(
1225 "stream_version",
1226 &format_args!("{:#06x}", self.stream_version()),
1227 )
1228 .field("channels", &self.channels)
1229 .field("strokes", &self.strokes.len())
1230 .field("roots", &self.roots().len())
1231 .finish()
1232 }
1233}
1234
1235fn block_bytes(channels: u16) -> usize {
1236 codec::BLOCK_WORDS * 2 * usize::from(channels)
1237}
1238
1239#[cfg(test)]
1240mod tests {
1241 use super::synthetic::{take, Build};
1242 use super::*;
1243
1244 #[test]
1245 fn the_name_field_splits_on_the_separator() {
1246 let piano = Build::new().piano();
1247 assert_eq!(piano.stream_version().unwrap(), 0x450);
1248 assert_eq!(
1249 piano.name().unwrap(),
1250 ("Test Piano".to_string(), "Variant".to_string())
1251 );
1252 }
1253
1254 #[test]
1255 fn an_unknown_stream_version_still_round_trips_but_does_not_decode() {
1256 let mut build = Build::new();
1257 build.version = 0x500;
1258 let piano = build.piano();
1259 assert_eq!(piano.stream_version().unwrap(), 0x500);
1260 assert!(
1261 piano.name().is_err(),
1262 "the name offset is only pinned on known versions"
1263 );
1264 assert!(piano.key_map().is_err());
1265 assert!(piano.library().is_err());
1266 }
1267
1268 #[test]
1269 fn a_body_without_the_magic_is_refused() {
1270 let mut piano = Build::new().piano();
1271 piano.file.body.0[0] = b'Q';
1272 assert!(piano.name().is_err(), "a non-CNSP body has no name to read");
1273 }
1274
1275 #[test]
1276 fn a_library_rebuilds_to_the_bytes_it_was_read_from() {
1277 let piano = Build::new().piano();
1278 let rebuilt = piano.library().unwrap().to_body().unwrap();
1279 assert_eq!(rebuilt, piano.file.body.0);
1280 }
1281
1282 #[test]
1283 fn the_directory_reports_each_strokes_root_bank_and_layer() {
1284 let piano = Build::new().piano();
1285 let library = piano.library().unwrap();
1286 let seen: Vec<(u8, Option<Bank>, u8)> = library
1287 .strokes()
1288 .iter()
1289 .map(|s| (s.root, s.bank(), s.layer()))
1290 .collect();
1291 assert_eq!(
1292 seen,
1293 [
1294 (60, Some(Bank::Attack), 0),
1295 (60, Some(Bank::Release), 3),
1296 (72, Some(Bank::Attack), 0),
1297 ]
1298 );
1299 assert_eq!(library.keys_for(60), [60, 61]);
1300 }
1301
1302 #[test]
1303 fn a_stroke_whose_start_does_not_abut_the_one_before_is_refused() {
1304 let mut piano = Build::new().piano();
1305 let second = DIRECTORY_AT + RECORD;
1306 let start = be32(&piano.file.body.0, second + REC_START);
1307 piano.file.body.0[second..second + 4].copy_from_slice(&(start + 2).to_be_bytes());
1308 let error = piano.library().unwrap_err().to_string();
1309 assert!(error.contains("stroke 1 starts at"), "{error}");
1310 }
1311
1312 #[test]
1313 fn a_key_routed_to_a_root_no_stroke_records_is_refused() {
1314 let mut build = Build::new();
1315 build.map.push((80, 80));
1316 let error = build.piano().library().unwrap_err().to_string();
1317 assert!(error.contains("key 80 plays root 80"), "{error}");
1318 }
1319
1320 #[test]
1321 fn a_count_table_that_does_not_sum_to_the_stroke_count_is_refused() {
1322 let mut piano = Build::new().piano();
1323 let at = ROOT_COUNTS_AT + 60 * 2;
1324 piano.file.body.0[at..at + 2].copy_from_slice(&5u16.to_be_bytes());
1325 let error = piano.library().unwrap_err().to_string();
1326 assert!(error.contains("per-root counts sum to"), "{error}");
1327 }
1328
1329 #[test]
1330 fn dropping_a_bank_relays_the_audio_and_leaves_the_rest_verbatim() {
1331 let piano = Build::new().piano();
1332 let before = piano.library().unwrap();
1333 let mut after = piano.library().unwrap();
1334 let change = after.drop_bank(Bank::Release);
1335 assert_eq!(
1336 change,
1337 Change {
1338 strokes_removed: 1,
1339 roots_removed: 0,
1340 keys_uncovered: 0
1341 }
1342 );
1343
1344 let body = after.to_body().unwrap();
1345 let trimmed = Piano {
1346 file: Cbin {
1347 header: after.header.clone(),
1348 body: RawBody(body),
1349 },
1350 };
1351 let reparsed = trimmed.library().unwrap();
1352 assert_eq!(reparsed.strokes().len(), 2);
1353 for (kept, moved) in before
1354 .strokes()
1355 .iter()
1356 .filter(|s| s.bank() != Some(Bank::Release))
1357 .zip(reparsed.strokes())
1358 {
1359 assert_eq!(kept.audio(), moved.audio(), "a span moved verbatim");
1360 assert_eq!(kept.id(), moved.id());
1361 assert_eq!(&kept.record()[REC_BANK..], &moved.record()[REC_BANK..]);
1362 }
1363 }
1364
1365 #[test]
1366 fn dropping_every_stroke_of_a_root_uncovers_the_keys_it_played() {
1367 let mut build = Build::new();
1368 build.takes = vec![
1369 take(60, Bank::Attack, 0, 1),
1370 take(72, Bank::Resonance, 0, 1),
1371 ];
1372 let piano = build.piano();
1373 let mut library = piano.library().unwrap();
1374 let change = library.drop_bank(Bank::Resonance);
1375 assert_eq!(change.strokes_removed, 1);
1376 assert_eq!(change.roots_removed, 1);
1377 assert_eq!(change.keys_uncovered, 1);
1378 assert_eq!(library.key_map()[72], UNCOVERED);
1379 library.to_body().unwrap();
1380 }
1381
1382 #[test]
1383 fn keeping_the_loudest_layer_keeps_one_per_root_and_bank() {
1384 let mut build = Build::new();
1385 build.takes = vec![
1386 take(60, Bank::Attack, 0, 1),
1387 take(60, Bank::Attack, 5, 1),
1388 take(60, Bank::Release, 26, 1),
1389 take(60, Bank::Release, 30, 1),
1390 take(72, Bank::Attack, 1, 1),
1391 ];
1392 let piano = build.piano();
1393 let mut library = piano.library().unwrap();
1394 library.keep_layers(&Layers::Loudest(1));
1395 let kept: Vec<(u8, u8, u8)> = library
1396 .strokes()
1397 .iter()
1398 .map(|s| (s.root, s.bank_code(), s.layer()))
1399 .collect();
1400 assert_eq!(kept, [(60, 0, 0), (60, 2, 26), (72, 0, 1)]);
1401 }
1402
1403 #[test]
1404 fn keeping_named_layers_takes_them_wherever_they_occur() {
1405 let mut build = Build::new();
1406 build.takes = vec![
1407 take(60, Bank::Attack, 0, 1),
1408 take(60, Bank::Attack, 5, 1),
1409 take(72, Bank::Attack, 5, 1),
1410 ];
1411 let piano = build.piano();
1412 let mut library = piano.library().unwrap();
1413 library.keep_layers(&Layers::Only([5].into_iter().collect()));
1414 let kept: Vec<(u8, u8)> = library
1415 .strokes()
1416 .iter()
1417 .map(|s| (s.root, s.layer()))
1418 .collect();
1419 assert_eq!(kept, [(60, 5), (72, 5)]);
1420 }
1421
1422 #[test]
1426 fn retaining_strokes_drops_what_the_predicate_rejects_and_nothing_else() {
1427 let mut build = Build::new();
1428 build.takes = vec![
1429 take(60, Bank::Attack, 0, 1),
1430 take(60, Bank::Attack, 5, 1),
1431 take(72, Bank::Attack, 5, 2),
1432 ];
1433 let piano = build.piano();
1434
1435 let mut kept_all = piano.library().unwrap();
1436 let unchanged = kept_all.retain_strokes(|_| true);
1437 assert_eq!(unchanged, Change::default());
1438 assert_eq!(
1439 kept_all.to_body().unwrap(),
1440 piano.file.body.0,
1441 "a predicate that rejects nothing re-lays the body it read"
1442 );
1443
1444 let mut library = piano.library().unwrap();
1445 let change = library.retain_strokes(|s| !(s.root == 72 && s.layer() == 5));
1446 assert_eq!(
1447 change,
1448 Change {
1449 strokes_removed: 1,
1450 roots_removed: 1,
1451 keys_uncovered: 1,
1452 }
1453 );
1454 let left: Vec<(u8, u8)> = library
1455 .strokes()
1456 .iter()
1457 .map(|s| (s.root, s.layer()))
1458 .collect();
1459 assert_eq!(left, [(60, 0), (60, 5)]);
1460 assert_eq!(
1461 library.key_map()[72],
1462 UNCOVERED,
1463 "root 72 lost every stroke, so its key answers nothing"
1464 );
1465 assert_eq!(library.key_map()[60], 60, "and the other root is untouched");
1466 library.to_body().unwrap();
1467 }
1468
1469 #[test]
1472 fn a_synthetic_library_reads_back_as_the_file_it_was_built_as() {
1473 let bytes = Build::new().bytes().unwrap();
1474 let entity = crate::from_stream(&mut std::io::Cursor::new(&bytes)).unwrap();
1475 let crate::Entity::Piano(piano) = &entity else {
1476 panic!("{entity:?} is no piano library");
1477 };
1478 assert_eq!(
1479 piano.name().unwrap(),
1480 ("Test Piano".to_string(), "Variant".to_string())
1481 );
1482 assert_eq!(piano.library().unwrap().strokes().len(), 3);
1483 assert_eq!(crate::to_bytes(&entity).unwrap(), bytes);
1484 }
1485
1486 #[test]
1490 fn borrowing_a_file_reads_it_without_copying_the_audio() {
1491 let bytes = Build::new().bytes().unwrap();
1492 let library = Library::borrow(&bytes).unwrap();
1493
1494 let base = bytes.as_ptr() as usize;
1495 let within = base..base + bytes.len();
1496 for stroke in library.strokes() {
1497 let at = stroke.audio().as_ptr() as usize;
1498 assert!(
1499 within.contains(&at),
1500 "{stroke:?} holds a copy of its audio, not the caller's bytes"
1501 );
1502 }
1503 assert_eq!(library.name(), ("Test Piano".into(), "Variant".into()));
1504 assert_eq!(library.strokes().len(), 3);
1505 assert_eq!(library.to_body().unwrap(), Build::new().body());
1506 }
1507
1508 #[test]
1509 fn borrowing_refuses_a_container_that_is_not_a_whole_piano_library() {
1510 let bytes = Build::new().bytes().unwrap();
1511
1512 let mut other = bytes.clone();
1513 other[0x08..0x0c].copy_from_slice(b"nsmp");
1514 let error = Library::borrow(&other).unwrap_err().to_string();
1515 assert!(error.contains("expected a npno file, got nsmp"), "{error}");
1516
1517 let error = Library::borrow(&bytes[..bytes.len() - 1])
1518 .unwrap_err()
1519 .to_string();
1520 assert!(
1521 error.contains("ends inside a stroke's audio span"),
1522 "{error}"
1523 );
1524 }
1525
1526 #[test]
1527 fn cutting_the_range_drops_the_roots_nothing_plays_any_more() {
1528 let piano = Build::new().piano();
1529 let mut library = piano.library().unwrap();
1530 let change = library.cut_range(0..=70).unwrap();
1531 assert_eq!(change.keys_uncovered, 1);
1532 assert_eq!(change.roots_removed, 1);
1533 assert_eq!(library.roots(), [60].into_iter().collect());
1534 assert_eq!(library.key_map()[72], UNCOVERED);
1535 assert_eq!(library.key_map()[60], 60);
1536 }
1537
1538 #[test]
1539 fn a_split_gives_each_half_the_roots_its_keys_play() {
1540 let piano = Build::new().piano();
1541 let (low, high) = piano.library().unwrap().split_at(70).unwrap();
1542 assert_eq!(low.roots(), [60].into_iter().collect());
1543 assert_eq!(high.roots(), [72].into_iter().collect());
1544 assert_eq!(low.keys_for(60), [60, 61]);
1545 assert_eq!(high.keys_for(72), [72]);
1546 let audio: usize = piano
1547 .library()
1548 .unwrap()
1549 .strokes()
1550 .iter()
1551 .map(|s| s.audio().len())
1552 .sum();
1553 let halves: usize = [&low, &high]
1554 .iter()
1555 .flat_map(|l| l.strokes())
1556 .map(|s| s.audio().len())
1557 .sum();
1558 assert_eq!(
1559 halves, audio,
1560 "a split shares every stroke out exactly once"
1561 );
1562 }
1563
1564 #[test]
1565 fn a_rename_carries_the_long_name_with_it_and_leaves_the_voicing_alone() {
1566 let mut build = Build::new();
1567 build.version = VERSION_SPLIT_NAME;
1568 let piano = build.piano();
1569 let mut library = piano.library().unwrap();
1570 library.set_voicing("Nordiska").unwrap();
1571 library.set_name("Renamed").unwrap();
1572 library.set_variant("Nordiska Sml").unwrap();
1573 assert_eq!(library.name(), ("Renamed".into(), "Nordiska Sml".into()));
1574 assert_eq!(library.long_name().as_deref(), Some("Renamed"));
1575 assert_eq!(
1576 library.voicing().as_deref(),
1577 Some("Nordiska"),
1578 "the voicing is its own field, not the variant's head"
1579 );
1580 }
1581
1582 #[test]
1583 fn the_older_stream_has_no_long_name_or_voicing_to_read_or_write() {
1584 let piano = Build::new().piano();
1585 let mut library = piano.library().unwrap();
1586 assert_eq!(library.stream_version(), 0x450);
1587 assert_eq!(library.long_name(), None);
1588 assert_eq!(library.voicing(), None);
1589 assert!(library.set_voicing("Nordiska").is_err());
1590 }
1591
1592 #[test]
1593 fn a_name_past_the_field_is_refused_without_changing_it() {
1594 let piano = Build::new().piano();
1595 let mut library = piano.library().unwrap();
1596 let too_long = "x".repeat(TextField::COMBINED.capacity());
1597 assert!(library.set_name(&too_long).is_err());
1598 assert_eq!(library.name().0, "Test Piano");
1599 }
1600
1601 #[test]
1602 fn a_remap_to_a_root_the_directory_does_not_record_is_refused() {
1603 let piano = Build::new().piano();
1604 let mut library = piano.library().unwrap();
1605 assert!(library.set_key_root(64, Some(61)).is_err());
1606 library.set_key_root(64, Some(72)).unwrap();
1607 assert_eq!(library.keys_for(72), [64, 72]);
1608 assert_eq!(library.key_root(64).unwrap(), Some(72));
1609 library.set_key_root(64, None).unwrap();
1610 assert_eq!(library.keys_for(72), [72]);
1611 assert_eq!(library.key_root(64).unwrap(), None);
1612 }
1613
1614 #[test]
1615 fn fine_tune_reads_and_writes_the_per_key_byte() {
1616 let piano = Build::new().piano();
1617 let mut library = piano.library().unwrap();
1618 assert_eq!(library.fine_tune(60).unwrap(), 0);
1619 library.set_fine_tune(60, -4).unwrap();
1620 assert_eq!(library.fine_tune(60).unwrap(), -4);
1621 assert_eq!(library.to_body().unwrap()[FINE_TUNE_AT + 60], 0xfc);
1622 }
1623
1624 fn changed(before: &[u8], after: &[u8]) -> Vec<usize> {
1626 assert_eq!(before.len(), after.len(), "the body changed length");
1627 (0..before.len())
1628 .filter(|&at| before[at] != after[at])
1629 .collect()
1630 }
1631
1632 #[test]
1633 fn the_gain_and_the_damper_limit_each_write_one_byte_of_the_prefix() {
1634 let piano = Build::new().piano();
1635 let mut library = piano.library().unwrap();
1636 let before = library.to_body().unwrap();
1637
1638 library.set_gain(-20);
1639 let gained = library.to_body().unwrap();
1640 assert_eq!(library.gain(), -20);
1641 assert_eq!(gained[GAIN_AT], 0xec, "tenths of a decibel, signed");
1642 assert_eq!(changed(&before, &gained), [GAIN_AT]);
1643
1644 library.set_damper_top(90).unwrap();
1645 let damped = library.to_body().unwrap();
1646 assert_eq!(library.damper_top(), 90);
1647 assert_eq!(changed(&gained, &damped), [DAMPER_TOP_AT]);
1648 }
1649
1650 #[test]
1651 fn the_instrument_kind_writes_one_byte_and_reads_back_as_the_kind_it_was_given() {
1652 let piano = Build::new().piano();
1653 let mut library = piano.library().unwrap();
1654 let before = library.to_body().unwrap();
1655
1656 library.set_kind(encode::Kind::Wurlitzer);
1657 let filed = library.to_body().unwrap();
1658 assert_eq!(
1659 encode::Kind::from_code(library.kind_code()),
1660 Some(encode::Kind::Wurlitzer)
1661 );
1662 assert_eq!(changed(&before, &filed), [KIND_AT]);
1663 }
1664
1665 #[test]
1666 fn a_damper_limit_past_the_last_midi_note_is_refused_without_moving_the_one_held() {
1667 let piano = Build::new().piano();
1668 let mut library = piano.library().unwrap();
1669 library.set_damper_top(encode::ALL_KEYS_DAMPED).unwrap();
1670 let before = library.to_body().unwrap();
1671 assert!(library.set_damper_top(NOTES as u8).is_err());
1672 assert_eq!(library.damper_top(), encode::ALL_KEYS_DAMPED);
1673 assert_eq!(library.to_body().unwrap(), before);
1674 }
1675
1676 #[test]
1679 fn a_retrim_writes_both_bytes_of_one_strokes_own_field() {
1680 let piano = Build::new().piano();
1681 let mut library = piano.library().unwrap();
1682 assert!(library.strokes().iter().all(|s| s.trim() == 0));
1683 let before = library.to_body().unwrap();
1684 let at = DIRECTORY_AT + RECORD + REC_TRIM;
1685
1686 library.set_trim(1, 7).unwrap();
1687 let low = library.to_body().unwrap();
1688 assert_eq!(library.strokes()[1].trim(), 7);
1689 assert_eq!(changed(&before, &low), [at + 1]);
1690
1691 library.set_trim(1, 0x0107).unwrap();
1692 let high = library.to_body().unwrap();
1693 assert_eq!(library.strokes()[1].trim(), 0x0107);
1694 assert_eq!(changed(&low, &high), [at]);
1695
1696 let error = library.set_trim(3, 4).unwrap_err().to_string();
1697 assert!(error.contains("stroke 3"), "{error}");
1698 assert_eq!(
1699 library.to_body().unwrap(),
1700 high,
1701 "a refused retrim leaves the directory alone"
1702 );
1703 }
1704
1705 #[test]
1706 fn a_key_above_the_last_midi_note_is_refused_by_every_entry_point() {
1707 let piano = Build::new().piano();
1708 let mut library = piano.library().unwrap();
1709 let last = (NOTES - 1) as u8;
1710 let past = NOTES as u8;
1711
1712 assert!(library.fine_tune(last).is_ok());
1713 assert!(library.key_root(last).is_ok());
1714 assert!(library.set_fine_tune(last, 1).is_ok());
1715 assert!(library.set_key_root(last, None).is_ok());
1716 assert!(library.cut_range(0..=last).is_ok());
1717 assert!(library.split_at(last).is_ok());
1718
1719 assert!(library.fine_tune(past).is_err());
1720 assert!(library.key_root(past).is_err());
1721 assert!(library.set_fine_tune(past, 1).is_err());
1722 assert!(library.set_key_root(past, None).is_err());
1723 assert!(library.set_key_root(0, Some(past)).is_err());
1724 assert!(library.cut_range(0..=past).is_err());
1725 assert!(library.cut_range(past..=past).is_err());
1726 assert!(library.split_at(past).is_err());
1727 }
1728
1729 #[test]
1730 fn a_key_past_the_tune_table_is_refused_rather_than_written_to_the_next_table() {
1731 let piano = Build::new().piano();
1732 let mut library = piano.library().unwrap();
1733 let before = library.to_body().unwrap();
1734 assert!(library.set_fine_tune(NOTES as u8, 32).is_err());
1735 assert_eq!(library.to_body().unwrap(), before);
1736 }
1737
1738 #[test]
1739 fn a_separator_in_a_name_or_a_variant_is_refused() {
1740 let piano = Build::new().piano();
1741 let mut library = piano.library().unwrap();
1742 assert!(library.set_name("Upright#2").is_err());
1743 assert!(library.set_variant("Sml#XL").is_err());
1744 assert_eq!(library.name(), ("Test Piano".into(), "Variant".into()));
1745 }
1746
1747 #[test]
1750 fn a_stroke_holding_other_than_the_blocks_its_record_states_is_not_laid_out() {
1751 let piano = Build::new().piano();
1752 let library = piano.library().unwrap();
1753 assert!(library.to_body().is_ok());
1754
1755 let skeleton = library.without_audio();
1756 let error = skeleton
1757 .to_body()
1758 .expect_err("expected a refusal")
1759 .to_string();
1760 assert!(error.contains("stroke 0 holds 0 audio bytes"), "{error}");
1761 assert!(skeleton.body_len().is_err());
1762 }
1763
1764 #[test]
1767 fn setting_one_half_of_the_name_field_leaves_the_other_as_it_was_written() {
1768 let mut piano = Build::new().piano();
1769 let at = TextField::COMBINED.at;
1770 let padded = b"Grand Imperial # Bdorf XL";
1771 piano.file.body.0[at..at + TextField::COMBINED.len].fill(0);
1772 piano.file.body.0[at..at + padded.len()].copy_from_slice(padded);
1773
1774 assert_eq!(
1775 piano.library().unwrap().name(),
1776 ("Grand Imperial".into(), "Bdorf XL".into())
1777 );
1778
1779 let mut renamed = piano.library().unwrap();
1780 renamed.set_name("Upright").unwrap();
1781 assert_eq!(
1782 TextField::COMBINED.read(&renamed.prefix),
1783 "Upright# Bdorf XL"
1784 );
1785
1786 let mut revoiced = piano.library().unwrap();
1787 revoiced.set_variant("Sml").unwrap();
1788 assert_eq!(
1789 TextField::COMBINED.read(&revoiced.prefix),
1790 "Grand Imperial #Sml"
1791 );
1792 }
1793
1794 #[test]
1795 fn text_the_field_would_not_read_back_is_refused() {
1796 let piano = Build::new().piano();
1797 let mut library = piano.library().unwrap();
1798 assert!(
1799 library.set_name("Flügel").is_err(),
1800 "the field is read as ASCII"
1801 );
1802 assert!(
1803 library.set_variant("Sml\0XL").is_err(),
1804 "a NUL ends the field, hiding everything after it"
1805 );
1806 assert_eq!(library.name(), ("Test Piano".into(), "Variant".into()));
1807 }
1808
1809 #[test]
1810 fn the_first_audio_offset_sits_on_the_block_grid_less_the_bias() {
1811 for block in [1022, 2044] {
1812 for count in [0usize, 1, 38, 2196] {
1813 let end = DIRECTORY_AT + count * RECORD;
1814 let at = first_audio_offset(end, block).unwrap();
1815 assert!(at >= end, "the audio never overlaps the directory");
1816 assert_eq!((at + AUDIO_ALIGN_BIAS) % block, 0);
1817 assert!(at - end < block, "no whole spare block in the gap");
1818 }
1819 }
1820 }
1821}