1use std::path::{Path, PathBuf};
28
29use anyhow::Result;
30use serde::{Deserialize, Serialize};
31use thiserror::Error;
32
33use crate::session::{instrument_key, SessionSelector};
34use crate::state::InstrumentType;
35
36pub const MAX_PRESETS: usize = 128;
45
46pub const MAX_NAME_LEN: usize = 32;
48
49pub const FORMAT_VERSION: u32 = 2;
61
62const LEGACY_VERSION: u32 = 1;
64
65const fn legacy_version() -> u32 {
66 LEGACY_VERSION
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Error)]
74pub enum PresetError {
75 #[error("saved for the {saved}, not the {wanted}")]
76 WrongInstrument { saved: String, wanted: String },
77
78 #[error("saved with {saved} controls, this instrument has {wanted}")]
79 ParamCountMismatch { saved: usize, wanted: usize },
80
81 #[error("saved against a different panel layout ({saved}, this build is {wanted})")]
82 LayoutMismatch { saved: String, wanted: String },
83
84 #[error("file claims {declared} controls but carries {actual}")]
85 Corrupt { declared: usize, actual: usize },
86
87 #[error("a preset needs a name")]
88 NameEmpty,
89
90 #[error("name is {len} characters, the limit is {max}")]
91 NameTooLong { len: usize, max: usize },
92
93 #[error("this instrument already has {max} presets — delete one first")]
94 BankFull { max: usize },
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct PresetFile {
102 pub version: u32,
103 pub instrument: String,
105 pub presets: Vec<Preset>,
106}
107
108#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
116pub struct Preset {
117 pub name: String,
118 pub instrument: String,
119 pub layout: String,
121 pub param_count: usize,
122 pub params: Vec<f32>,
123 #[serde(default)]
131 pub discrete: Vec<SessionSelector>,
132 #[serde(default = "legacy_version")]
134 pub version: u32,
135}
136
137#[derive(Debug, Clone, PartialEq)]
145pub struct LoadedPreset {
146 pub params: Vec<f32>,
148 pub clamped: Vec<(usize, usize, usize)>,
151 pub legacy_selectors: bool,
153}
154
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
157pub enum StoreOutcome {
158 Added,
159 Replaced,
161}
162
163pub fn param_names(instrument: InstrumentType) -> &'static [&'static str] {
171 match instrument {
172 InstrumentType::Synth | InstrumentType::Sampler => &phosphor_dsp::synth::PARAM_NAMES,
173 InstrumentType::DrumRack => &phosphor_dsp::drum_rack::PARAM_NAMES,
174 InstrumentType::DX7 => &phosphor_dsp::dx7::PARAM_NAMES,
175 InstrumentType::Jupiter8 => &phosphor_dsp::jupiter::PARAM_NAMES,
176 InstrumentType::Odyssey => &phosphor_dsp::odyssey::PARAM_NAMES,
177 InstrumentType::Juno60 => &phosphor_dsp::juno::PARAM_NAMES,
178 InstrumentType::Rhodes => &phosphor_dsp::rhodes::PARAM_NAMES,
179 }
180}
181
182pub fn layout_fingerprint(instrument: InstrumentType) -> String {
200 const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
201 const PRIME: u64 = 0x0000_0100_0000_01b3;
202
203 let mut hash = OFFSET;
204 for name in param_names(instrument) {
205 for byte in name.bytes().chain(std::iter::once(0xff)) {
207 hash ^= u64::from(byte);
208 hash = hash.wrapping_mul(PRIME);
209 }
210 }
211 format!("{hash:016x}")
212}
213
214pub fn param_count(instrument: InstrumentType) -> usize {
216 param_names(instrument).len()
217}
218
219pub fn default_dir() -> Option<PathBuf> {
233 crate::paths::preset_dir()
234}
235
236pub fn bank_path(dir: &Path, instrument: InstrumentType) -> PathBuf {
238 dir.join(format!("{}.json", instrument_key(instrument)))
239}
240
241pub fn load_bank(dir: &Path, instrument: InstrumentType) -> Result<PresetFile> {
248 let path = bank_path(dir, instrument);
249 if !path.exists() {
250 return Ok(PresetFile::new(instrument));
251 }
252 let json = std::fs::read_to_string(&path)?;
253 let bank: PresetFile = serde_json::from_str(&json)?;
254 Ok(bank)
255}
256
257pub fn save_bank(dir: &Path, instrument: InstrumentType, bank: &PresetFile) -> Result<()> {
260 std::fs::create_dir_all(dir)?;
261 let path = bank_path(dir, instrument);
262 let json = serde_json::to_string_pretty(bank)?;
263
264 let tmp = path.with_extension("json.tmp");
265 std::fs::write(&tmp, &json)?;
266 std::fs::rename(&tmp, &path)?;
267
268 tracing::debug!("preset bank saved: {} ({} presets)", path.display(), bank.presets.len());
269 Ok(())
270}
271
272impl PresetFile {
275 pub fn new(instrument: InstrumentType) -> Self {
276 Self {
277 version: FORMAT_VERSION,
278 instrument: instrument_key(instrument).to_string(),
279 presets: Vec::new(),
280 }
281 }
282
283 pub fn names(&self) -> Vec<&str> {
285 self.presets.iter().map(|p| p.name.as_str()).collect()
286 }
287
288 pub fn find(&self, name: &str) -> Option<usize> {
293 let name = name.trim();
294 self.presets.iter().position(|p| p.name == name)
295 }
296
297 pub fn store(
305 &mut self,
306 name: &str,
307 instrument: InstrumentType,
308 params: &[f32],
309 ) -> Result<StoreOutcome, PresetError> {
310 let name = name.trim();
311 if name.is_empty() {
312 return Err(PresetError::NameEmpty);
313 }
314 let len = name.chars().count();
315 if len > MAX_NAME_LEN {
316 return Err(PresetError::NameTooLong { len, max: MAX_NAME_LEN });
317 }
318
319 let preset = Preset {
320 name: name.to_string(),
321 instrument: instrument_key(instrument).to_string(),
322 layout: layout_fingerprint(instrument),
323 param_count: params.len(),
324 params: params.to_vec(),
325 discrete: crate::session::selectors_of(instrument, params),
329 version: FORMAT_VERSION,
330 };
331
332 let outcome = match self.find(name) {
333 Some(idx) => {
334 self.presets[idx] = preset;
335 StoreOutcome::Replaced
336 }
337 None => {
338 if self.presets.len() >= MAX_PRESETS {
342 return Err(PresetError::BankFull { max: MAX_PRESETS });
343 }
344 self.presets.push(preset);
345 StoreOutcome::Added
346 }
347 };
348
349 self.version = FORMAT_VERSION;
353 Ok(outcome)
354 }
355
356 pub fn remove(&mut self, index: usize) -> Option<Preset> {
358 (index < self.presets.len()).then(|| self.presets.remove(index))
359 }
360
361 pub fn params_at(
369 &self,
370 index: usize,
371 instrument: InstrumentType,
372 want_count: usize,
373 ) -> Option<Result<LoadedPreset, PresetError>> {
374 let preset = self.presets.get(index)?;
375 Some(preset.check(instrument, want_count).map(|()| preset.resolve(instrument)))
376 }
377}
378
379impl Preset {
380 pub fn check(&self, instrument: InstrumentType, want_count: usize) -> Result<(), PresetError> {
386 let wanted_key = instrument_key(instrument);
387 if self.instrument != wanted_key {
388 return Err(PresetError::WrongInstrument {
389 saved: self.instrument.clone(),
390 wanted: wanted_key.to_string(),
391 });
392 }
393 if self.param_count != self.params.len() {
394 return Err(PresetError::Corrupt {
395 declared: self.param_count,
396 actual: self.params.len(),
397 });
398 }
399 if self.params.len() != want_count {
400 return Err(PresetError::ParamCountMismatch {
401 saved: self.params.len(),
402 wanted: want_count,
403 });
404 }
405 let wanted_layout = layout_fingerprint(instrument);
406 if self.layout != wanted_layout {
407 return Err(PresetError::LayoutMismatch {
408 saved: self.layout.clone(),
409 wanted: wanted_layout,
410 });
411 }
412 Ok(())
413 }
414
415 #[must_use]
423 pub fn resolve(&self, instrument: InstrumentType) -> LoadedPreset {
424 let mut params = self.params.clone();
425 let clamped = crate::session::apply_selectors(instrument, &mut params, &self.discrete);
426 LoadedPreset {
427 params,
428 clamped,
429 legacy_selectors: self.version < FORMAT_VERSION,
430 }
431 }
432}
433
434#[cfg(test)]
435mod tests {
436 use super::*;
437
438 fn scratch(tag: &str) -> PathBuf {
441 let dir = std::env::temp_dir()
442 .join(format!("phosphor-presets-{}-{tag}", std::process::id()));
443 let _ = std::fs::remove_dir_all(&dir);
444 dir
445 }
446
447 fn juno_panel() -> Vec<f32> {
448 phosphor_dsp::juno::PARAM_DEFAULTS.to_vec()
449 }
450
451 #[test]
455 fn a_preset_round_trips_through_the_file() {
456 let dir = scratch("round-trip");
457 let mut panel = juno_panel();
458 panel[phosphor_dsp::juno::P_CUTOFF] = 0.317_25;
459 panel[phosphor_dsp::juno::P_RESO] = 0.812_5;
460 panel[phosphor_dsp::juno::P_PATCH] = phosphor_dsp::juno::patch_knob(24);
465
466 let mut bank = PresetFile::new(InstrumentType::Juno60);
467 assert_eq!(
468 bank.store("evening pad", InstrumentType::Juno60, &panel),
469 Ok(StoreOutcome::Added)
470 );
471 save_bank(&dir, InstrumentType::Juno60, &bank).unwrap();
472
473 let reopened = load_bank(&dir, InstrumentType::Juno60).unwrap();
474 assert_eq!(reopened.names(), vec!["evening pad"]);
475 let loaded = reopened
476 .params_at(0, InstrumentType::Juno60, panel.len())
477 .unwrap()
478 .expect("its own panel should load");
479 for (index, (before, after)) in panel.iter().zip(loaded.params.iter()).enumerate() {
486 if crate::discrete::is_discrete(InstrumentType::Juno60, index) {
487 assert_eq!(
488 crate::discrete::index_of(InstrumentType::Juno60, index, *after),
489 crate::discrete::index_of(InstrumentType::Juno60, index, *before),
490 "control {index} came back on a different position"
491 );
492 } else {
493 assert_eq!(before, after, "control {index} came back changed");
494 }
495 }
496 assert!(loaded.clamped.is_empty());
497 assert!(!loaded.legacy_selectors, "a preset written now is not an old one");
498
499 let _ = std::fs::remove_dir_all(&dir);
500 }
501
502 #[test]
511 fn a_selector_survives_the_bank_growing() {
512 use phosphor_dsp::drum_rack;
513
514 let dir = scratch("bank-grew");
515 let mut panel = drum_rack::PARAM_DEFAULTS.to_vec();
516 panel[drum_rack::P_KIT] = drum_rack::kit_knob(1);
517 assert_eq!(
518 drum_rack::discrete_label(drum_rack::P_KIT, panel[drum_rack::P_KIT]),
519 Some("909"),
520 "this test is pinned to the 909 being position 1"
521 );
522
523 let mut bank = PresetFile::new(InstrumentType::DrumRack);
524 bank.store("my kit", InstrumentType::DrumRack, &panel).unwrap();
525 assert_eq!(
526 bank.presets[0].discrete.iter().find(|s| s.param == drum_rack::P_KIT),
527 Some(&SessionSelector { param: drum_rack::P_KIT, index: 1 }),
528 "the kit was not stored by position"
529 );
530
531 bank.presets[0].params[drum_rack::P_KIT] = 1.5 / 10.0;
535 save_bank(&dir, InstrumentType::DrumRack, &bank).unwrap();
536
537 let reopened = load_bank(&dir, InstrumentType::DrumRack).unwrap();
539 assert_eq!(
540 drum_rack::discrete_label(drum_rack::P_KIT, reopened.presets[0].params[drum_rack::P_KIT]),
541 Some("707"),
542 "the fraction no longer names the 707, so this test proves nothing"
543 );
544 let loaded = reopened
546 .params_at(0, InstrumentType::DrumRack, panel.len())
547 .unwrap()
548 .expect("its own panel should load");
549 assert_eq!(
550 drum_rack::discrete_label(drum_rack::P_KIT, loaded.params[drum_rack::P_KIT]),
551 Some("909"),
552 "the preset opened on a different drum machine"
553 );
554 assert!(loaded.clamped.is_empty());
555 assert!(!loaded.legacy_selectors);
556
557 let _ = std::fs::remove_dir_all(&dir);
558 }
559
560 #[test]
564 fn both_dx7_selectors_survive_a_round_trip() {
565 use phosphor_dsp::dx7;
566
567 let mut panel = dx7::PARAM_DEFAULTS.to_vec();
568 let (bank_knob, patch_knob) = dx7::voice_knobs(147);
569 panel[dx7::P_BANK] = bank_knob;
570 panel[dx7::P_PATCH] = patch_knob;
571
572 let mut bank = PresetFile::new(InstrumentType::DX7);
573 bank.store("timpani", InstrumentType::DX7, &panel).unwrap();
574
575 let stored: Vec<usize> = bank.presets[0].discrete.iter().map(|s| s.param).collect();
576 assert!(stored.contains(&dx7::P_BANK), "the cartridge was not stored");
577 assert!(stored.contains(&dx7::P_PATCH), "the voice was not stored");
578
579 bank.presets[0].params[dx7::P_BANK] = 0.0;
582 bank.presets[0].params[dx7::P_PATCH] = 0.0;
583
584 let loaded = bank
585 .params_at(0, InstrumentType::DX7, panel.len())
586 .unwrap()
587 .expect("its own panel should load");
588 assert_eq!(loaded.params[dx7::P_BANK], bank_knob, "the cartridge did not come back");
589 assert_eq!(loaded.params[dx7::P_PATCH], patch_knob, "the voice did not come back");
590 }
591
592 #[test]
596 fn a_selector_past_the_end_of_the_bank_is_clamped_and_reported() {
597 use phosphor_dsp::drum_rack;
598
599 let panel = drum_rack::PARAM_DEFAULTS.to_vec();
600 let mut bank = PresetFile::new(InstrumentType::DrumRack);
601 bank.store("from the future", InstrumentType::DrumRack, &panel).unwrap();
602 let selector = bank.presets[0]
603 .discrete
604 .iter_mut()
605 .find(|s| s.param == drum_rack::P_KIT)
606 .expect("the kit is a selector");
607 selector.index = 900;
608
609 let loaded = bank
610 .params_at(0, InstrumentType::DrumRack, panel.len())
611 .unwrap()
612 .expect("its own panel should load");
613 assert_eq!(
614 loaded.clamped,
615 vec![(drum_rack::P_KIT, 900, drum_rack::KIT_COUNT - 1)],
616 "a position the rack no longer has was not reported"
617 );
618 assert_eq!(
619 loaded.params[drum_rack::P_KIT],
620 drum_rack::kit_knob(drum_rack::KIT_COUNT - 1),
621 "the kit did not land on the last one the rack has"
622 );
623 }
624
625 #[test]
631 fn a_version_1_preset_loads_from_its_fractions_and_says_so() {
632 use phosphor_dsp::drum_rack;
633
634 let dir = scratch("version-1");
635 let params: Vec<String> = drum_rack::PARAM_DEFAULTS
638 .iter()
639 .enumerate()
640 .map(|(i, v)| {
641 if i == drum_rack::P_KIT { (1.5f32 / 10.0).to_string() } else { v.to_string() }
642 })
643 .collect();
644 let json = format!(
645 r#"{{"version":1,"instrument":"drums","presets":[{{"name":"old",
646 "instrument":"drums","layout":"{}","param_count":{},"params":[{}]}}]}}"#,
647 layout_fingerprint(InstrumentType::DrumRack),
648 drum_rack::PARAM_COUNT,
649 params.join(",")
650 );
651 std::fs::create_dir_all(&dir).unwrap();
652 std::fs::write(bank_path(&dir, InstrumentType::DrumRack), json).unwrap();
653
654 let bank = load_bank(&dir, InstrumentType::DrumRack).unwrap();
655 assert_eq!(bank.version, 1);
656 assert_eq!(bank.presets[0].version, LEGACY_VERSION, "a missing version is version 1");
657 assert!(bank.presets[0].discrete.is_empty());
658
659 let loaded = bank
660 .params_at(0, InstrumentType::DrumRack, drum_rack::PARAM_COUNT)
661 .unwrap()
662 .expect("an old preset still loads");
663 assert!(loaded.legacy_selectors, "an old preset loaded without a word said");
664 assert_eq!(
667 drum_rack::discrete_label(drum_rack::P_KIT, loaded.params[drum_rack::P_KIT]),
668 Some("707")
669 );
670
671 let _ = std::fs::remove_dir_all(&dir);
672 }
673
674 #[test]
678 fn every_selector_on_every_instrument_is_stored() {
679 for instrument in InstrumentType::ALL {
680 let count = param_count(*instrument);
681 let panel = vec![0.5f32; count];
682 let mut bank = PresetFile::new(*instrument);
683 bank.store("all", *instrument, &panel).unwrap();
684
685 let stored: Vec<usize> =
686 bank.presets[0].discrete.iter().map(|s| s.param).collect();
687 let wanted: Vec<usize> = (0..count)
688 .filter(|&p| crate::discrete::is_discrete(*instrument, p))
689 .collect();
690 assert_eq!(stored, wanted, "{instrument:?} did not store all of its selectors");
691 assert!(!wanted.is_empty(), "{instrument:?} has no selectors at all");
692 }
693 }
694
695 #[test]
698 fn a_bank_that_does_not_exist_is_empty() {
699 let dir = scratch("missing");
700 let bank = load_bank(&dir, InstrumentType::DX7).unwrap();
701 assert!(bank.presets.is_empty());
702 assert_eq!(bank.instrument, "dx7");
703 }
704
705 #[test]
710 fn a_preset_with_the_wrong_control_count_is_refused() {
711 let dir = scratch("count");
712 let mut bank = PresetFile::new(InstrumentType::Juno60);
713 bank.store("old panel", InstrumentType::Juno60, &juno_panel()).unwrap();
714 bank.presets[0].params.truncate(16);
716 bank.presets[0].param_count = 16;
717 save_bank(&dir, InstrumentType::Juno60, &bank).unwrap();
718
719 let reopened = load_bank(&dir, InstrumentType::Juno60).unwrap();
720 let want = param_count(InstrumentType::Juno60);
721 assert_eq!(
722 reopened.params_at(0, InstrumentType::Juno60, want).unwrap(),
723 Err(PresetError::ParamCountMismatch { saved: 16, wanted: want })
724 );
725
726 let _ = std::fs::remove_dir_all(&dir);
727 }
728
729 #[test]
733 fn a_preset_from_a_reordered_panel_is_refused() {
734 let panel = juno_panel();
735 let mut preset = Preset {
736 name: "reordered".into(),
737 instrument: "juno60".into(),
738 layout: layout_fingerprint(InstrumentType::Juno60),
739 param_count: panel.len(),
740 params: panel,
741 discrete: Vec::new(),
742 version: FORMAT_VERSION,
743 };
744 assert_eq!(preset.check(InstrumentType::Juno60, 25), Ok(()));
745
746 preset.layout = "0000000000000000".into();
748 assert!(matches!(
749 preset.check(InstrumentType::Juno60, 25),
750 Err(PresetError::LayoutMismatch { .. })
751 ));
752 }
753
754 #[test]
757 fn the_fingerprint_separates_every_instrument() {
758 let mut seen = Vec::new();
759 for inst in InstrumentType::ALL {
760 let fp = layout_fingerprint(*inst);
761 assert_eq!(fp.len(), 16, "{fp} is not a 64-bit fingerprint");
762 seen.push((inst, fp));
763 }
764 for (a, fa) in &seen {
767 for (b, fb) in &seen {
768 let shared_panel = matches!(
769 (a, b),
770 (InstrumentType::Synth, InstrumentType::Sampler)
771 | (InstrumentType::Sampler, InstrumentType::Synth)
772 );
773 if a != b && !shared_panel {
774 assert_ne!(fa, fb, "{a:?} and {b:?} fingerprint the same");
775 }
776 }
777 }
778 }
779
780 #[test]
783 fn a_preset_saved_for_another_instrument_is_refused() {
784 let dir = scratch("instrument");
785 let mut dx7 = PresetFile::new(InstrumentType::DX7);
786 dx7.store("e.piano", InstrumentType::DX7, &phosphor_dsp::dx7::PARAM_DEFAULTS).unwrap();
787
788 let mut juno = PresetFile::new(InstrumentType::Juno60);
790 juno.presets.push(dx7.presets[0].clone());
791 save_bank(&dir, InstrumentType::Juno60, &juno).unwrap();
792
793 let reopened = load_bank(&dir, InstrumentType::Juno60).unwrap();
794 assert_eq!(
795 reopened.params_at(0, InstrumentType::Juno60, 9).unwrap(),
796 Err(PresetError::WrongInstrument {
797 saved: "dx7".into(),
798 wanted: "juno60".into()
799 }),
800 "a DX7 preset loaded into a Juno"
801 );
802
803 assert_ne!(
805 bank_path(&dir, InstrumentType::DX7),
806 bank_path(&dir, InstrumentType::Juno60)
807 );
808
809 let _ = std::fs::remove_dir_all(&dir);
810 }
811
812 #[test]
815 fn saving_over_a_name_replaces_it_in_place() {
816 let mut bank = PresetFile::new(InstrumentType::Juno60);
817 let mut first = juno_panel();
818 first[phosphor_dsp::juno::P_CUTOFF] = 0.2;
819 let mut second = juno_panel();
820 second[phosphor_dsp::juno::P_CUTOFF] = 0.9;
821
822 bank.store("brass", InstrumentType::Juno60, &first).unwrap();
823 bank.store("strings", InstrumentType::Juno60, &juno_panel()).unwrap();
824 assert_eq!(
825 bank.store("brass", InstrumentType::Juno60, &second),
826 Ok(StoreOutcome::Replaced)
827 );
828
829 assert_eq!(bank.names(), vec!["brass", "strings"], "the slot moved or duplicated");
830 assert_eq!(bank.presets[0].params[phosphor_dsp::juno::P_CUTOFF], 0.9);
831
832 assert_eq!(
835 bank.store(" brass ", InstrumentType::Juno60, &first),
836 Ok(StoreOutcome::Replaced)
837 );
838 assert_eq!(bank.presets.len(), 2);
839 }
840
841 #[test]
843 fn the_bank_stops_at_its_limit() {
844 let mut bank = PresetFile::new(InstrumentType::Juno60);
845 let panel = juno_panel();
846 for i in 0..MAX_PRESETS {
847 bank.store(&format!("p{i}"), InstrumentType::Juno60, &panel).unwrap();
848 }
849 assert_eq!(
850 bank.store("one more", InstrumentType::Juno60, &panel),
851 Err(PresetError::BankFull { max: MAX_PRESETS })
852 );
853 assert_eq!(
854 bank.store("p0", InstrumentType::Juno60, &panel),
855 Ok(StoreOutcome::Replaced),
856 "a full bank became read-only"
857 );
858
859 bank.remove(0);
860 assert_eq!(bank.presets.len(), MAX_PRESETS - 1);
861 assert_eq!(
862 bank.store("one more", InstrumentType::Juno60, &panel),
863 Ok(StoreOutcome::Added)
864 );
865 }
866
867 #[test]
869 fn names_are_bounded_and_non_empty() {
870 let mut bank = PresetFile::new(InstrumentType::Juno60);
871 let panel = juno_panel();
872 assert_eq!(
873 bank.store(" ", InstrumentType::Juno60, &panel),
874 Err(PresetError::NameEmpty)
875 );
876 let long = "x".repeat(MAX_NAME_LEN + 1);
877 assert_eq!(
878 bank.store(&long, InstrumentType::Juno60, &panel),
879 Err(PresetError::NameTooLong { len: MAX_NAME_LEN + 1, max: MAX_NAME_LEN })
880 );
881 assert!(bank.presets.is_empty());
882 }
883
884 #[test]
887 fn a_preset_that_contradicts_itself_is_refused() {
888 let panel = juno_panel();
889 let preset = Preset {
890 name: "hand edited".into(),
891 instrument: "juno60".into(),
892 layout: layout_fingerprint(InstrumentType::Juno60),
893 param_count: 99,
894 params: panel.clone(),
895 discrete: Vec::new(),
896 version: FORMAT_VERSION,
897 };
898 assert_eq!(
899 preset.check(InstrumentType::Juno60, panel.len()),
900 Err(PresetError::Corrupt { declared: 99, actual: panel.len() })
901 );
902 }
903
904 #[test]
907 fn every_instrument_has_a_panel() {
908 for inst in InstrumentType::ALL {
909 assert!(param_count(*inst) > 0, "{inst:?} has no parameters");
910 }
911 assert_eq!(param_count(InstrumentType::Juno60), phosphor_dsp::juno::PARAM_COUNT);
912 assert_eq!(param_count(InstrumentType::Jupiter8), phosphor_dsp::jupiter::PARAM_COUNT);
913 assert_eq!(param_count(InstrumentType::DX7), phosphor_dsp::dx7::PARAM_COUNT);
914 assert_eq!(param_count(InstrumentType::Odyssey), phosphor_dsp::odyssey::PARAM_COUNT);
915 assert_eq!(param_count(InstrumentType::Rhodes), phosphor_dsp::rhodes::PARAM_COUNT);
916 assert_eq!(param_count(InstrumentType::DrumRack), phosphor_dsp::drum_rack::PARAM_COUNT);
917 assert_eq!(param_count(InstrumentType::Synth), phosphor_dsp::synth::PARAM_COUNT);
918 assert_eq!(param_count(InstrumentType::Sampler), phosphor_dsp::synth::PARAM_COUNT);
919 }
920}