1use std::path::{Path, PathBuf};
27
28use anyhow::Result;
29use serde::{Deserialize, Serialize};
30use thiserror::Error;
31
32use crate::session::{instrument_key, SessionSelector};
33use crate::state::InstrumentType;
34
35pub const MAX_PRESETS: usize = 128;
44
45pub const MAX_NAME_LEN: usize = 32;
47
48pub const FORMAT_VERSION: u32 = 2;
60
61const LEGACY_VERSION: u32 = 1;
63
64const fn legacy_version() -> u32 {
65 LEGACY_VERSION
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Error)]
73pub enum PresetError {
74 #[error("saved for the {saved}, not the {wanted}")]
75 WrongInstrument { saved: String, wanted: String },
76
77 #[error("saved with {saved} controls, this instrument has {wanted}")]
78 ParamCountMismatch { saved: usize, wanted: usize },
79
80 #[error("saved against a different panel layout ({saved}, this build is {wanted})")]
81 LayoutMismatch { saved: String, wanted: String },
82
83 #[error("file claims {declared} controls but carries {actual}")]
84 Corrupt { declared: usize, actual: usize },
85
86 #[error("a preset needs a name")]
87 NameEmpty,
88
89 #[error("name is {len} characters, the limit is {max}")]
90 NameTooLong { len: usize, max: usize },
91
92 #[error("this instrument already has {max} presets — delete one first")]
93 BankFull { max: usize },
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct PresetFile {
101 pub version: u32,
102 pub instrument: String,
104 pub presets: Vec<Preset>,
105}
106
107#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
115pub struct Preset {
116 pub name: String,
117 pub instrument: String,
118 pub layout: String,
120 pub param_count: usize,
121 pub params: Vec<f32>,
122 #[serde(default)]
130 pub discrete: Vec<SessionSelector>,
131 #[serde(default = "legacy_version")]
133 pub version: u32,
134}
135
136#[derive(Debug, Clone, PartialEq)]
144pub struct LoadedPreset {
145 pub params: Vec<f32>,
147 pub clamped: Vec<(usize, usize, usize)>,
150 pub legacy_selectors: bool,
152}
153
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub enum StoreOutcome {
157 Added,
158 Replaced,
160}
161
162pub fn param_names(instrument: InstrumentType) -> &'static [&'static str] {
170 match instrument {
171 InstrumentType::Synth | InstrumentType::Sampler => &phosphor_dsp::synth::PARAM_NAMES,
172 InstrumentType::DrumRack => &phosphor_dsp::drum_rack::PARAM_NAMES,
173 InstrumentType::DX7 => &phosphor_dsp::dx7::PARAM_NAMES,
174 InstrumentType::Jupiter8 => &phosphor_dsp::jupiter::PARAM_NAMES,
175 InstrumentType::Odyssey => &phosphor_dsp::odyssey::PARAM_NAMES,
176 InstrumentType::Juno60 => &phosphor_dsp::juno::PARAM_NAMES,
177 }
178}
179
180pub fn layout_fingerprint(instrument: InstrumentType) -> String {
198 const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
199 const PRIME: u64 = 0x0000_0100_0000_01b3;
200
201 let mut hash = OFFSET;
202 for name in param_names(instrument) {
203 for byte in name.bytes().chain(std::iter::once(0xff)) {
205 hash ^= u64::from(byte);
206 hash = hash.wrapping_mul(PRIME);
207 }
208 }
209 format!("{hash:016x}")
210}
211
212pub fn param_count(instrument: InstrumentType) -> usize {
214 param_names(instrument).len()
215}
216
217pub fn default_dir() -> Option<PathBuf> {
225 std::env::var("HOME")
226 .ok()
227 .map(|home| PathBuf::from(home).join(".phosphor").join("presets"))
228}
229
230pub fn bank_path(dir: &Path, instrument: InstrumentType) -> PathBuf {
232 dir.join(format!("{}.json", instrument_key(instrument)))
233}
234
235pub fn load_bank(dir: &Path, instrument: InstrumentType) -> Result<PresetFile> {
242 let path = bank_path(dir, instrument);
243 if !path.exists() {
244 return Ok(PresetFile::new(instrument));
245 }
246 let json = std::fs::read_to_string(&path)?;
247 let bank: PresetFile = serde_json::from_str(&json)?;
248 Ok(bank)
249}
250
251pub fn save_bank(dir: &Path, instrument: InstrumentType, bank: &PresetFile) -> Result<()> {
254 std::fs::create_dir_all(dir)?;
255 let path = bank_path(dir, instrument);
256 let json = serde_json::to_string_pretty(bank)?;
257
258 let tmp = path.with_extension("json.tmp");
259 std::fs::write(&tmp, &json)?;
260 std::fs::rename(&tmp, &path)?;
261
262 tracing::debug!("preset bank saved: {} ({} presets)", path.display(), bank.presets.len());
263 Ok(())
264}
265
266impl PresetFile {
269 pub fn new(instrument: InstrumentType) -> Self {
270 Self {
271 version: FORMAT_VERSION,
272 instrument: instrument_key(instrument).to_string(),
273 presets: Vec::new(),
274 }
275 }
276
277 pub fn names(&self) -> Vec<&str> {
279 self.presets.iter().map(|p| p.name.as_str()).collect()
280 }
281
282 pub fn find(&self, name: &str) -> Option<usize> {
287 let name = name.trim();
288 self.presets.iter().position(|p| p.name == name)
289 }
290
291 pub fn store(
299 &mut self,
300 name: &str,
301 instrument: InstrumentType,
302 params: &[f32],
303 ) -> Result<StoreOutcome, PresetError> {
304 let name = name.trim();
305 if name.is_empty() {
306 return Err(PresetError::NameEmpty);
307 }
308 let len = name.chars().count();
309 if len > MAX_NAME_LEN {
310 return Err(PresetError::NameTooLong { len, max: MAX_NAME_LEN });
311 }
312
313 let preset = Preset {
314 name: name.to_string(),
315 instrument: instrument_key(instrument).to_string(),
316 layout: layout_fingerprint(instrument),
317 param_count: params.len(),
318 params: params.to_vec(),
319 discrete: crate::session::selectors_of(instrument, params),
323 version: FORMAT_VERSION,
324 };
325
326 let outcome = match self.find(name) {
327 Some(idx) => {
328 self.presets[idx] = preset;
329 StoreOutcome::Replaced
330 }
331 None => {
332 if self.presets.len() >= MAX_PRESETS {
336 return Err(PresetError::BankFull { max: MAX_PRESETS });
337 }
338 self.presets.push(preset);
339 StoreOutcome::Added
340 }
341 };
342
343 self.version = FORMAT_VERSION;
347 Ok(outcome)
348 }
349
350 pub fn remove(&mut self, index: usize) -> Option<Preset> {
352 (index < self.presets.len()).then(|| self.presets.remove(index))
353 }
354
355 pub fn params_at(
363 &self,
364 index: usize,
365 instrument: InstrumentType,
366 want_count: usize,
367 ) -> Option<Result<LoadedPreset, PresetError>> {
368 let preset = self.presets.get(index)?;
369 Some(preset.check(instrument, want_count).map(|()| preset.resolve(instrument)))
370 }
371}
372
373impl Preset {
374 pub fn check(&self, instrument: InstrumentType, want_count: usize) -> Result<(), PresetError> {
380 let wanted_key = instrument_key(instrument);
381 if self.instrument != wanted_key {
382 return Err(PresetError::WrongInstrument {
383 saved: self.instrument.clone(),
384 wanted: wanted_key.to_string(),
385 });
386 }
387 if self.param_count != self.params.len() {
388 return Err(PresetError::Corrupt {
389 declared: self.param_count,
390 actual: self.params.len(),
391 });
392 }
393 if self.params.len() != want_count {
394 return Err(PresetError::ParamCountMismatch {
395 saved: self.params.len(),
396 wanted: want_count,
397 });
398 }
399 let wanted_layout = layout_fingerprint(instrument);
400 if self.layout != wanted_layout {
401 return Err(PresetError::LayoutMismatch {
402 saved: self.layout.clone(),
403 wanted: wanted_layout,
404 });
405 }
406 Ok(())
407 }
408
409 #[must_use]
417 pub fn resolve(&self, instrument: InstrumentType) -> LoadedPreset {
418 let mut params = self.params.clone();
419 let clamped = crate::session::apply_selectors(instrument, &mut params, &self.discrete);
420 LoadedPreset {
421 params,
422 clamped,
423 legacy_selectors: self.version < FORMAT_VERSION,
424 }
425 }
426}
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431
432 fn scratch(tag: &str) -> PathBuf {
435 let dir = std::env::temp_dir()
436 .join(format!("phosphor-presets-{}-{tag}", std::process::id()));
437 let _ = std::fs::remove_dir_all(&dir);
438 dir
439 }
440
441 fn juno_panel() -> Vec<f32> {
442 phosphor_dsp::juno::PARAM_DEFAULTS.to_vec()
443 }
444
445 #[test]
449 fn a_preset_round_trips_through_the_file() {
450 let dir = scratch("round-trip");
451 let mut panel = juno_panel();
452 panel[phosphor_dsp::juno::P_CUTOFF] = 0.317_25;
453 panel[phosphor_dsp::juno::P_RESO] = 0.812_5;
454 panel[phosphor_dsp::juno::P_PATCH] = phosphor_dsp::juno::patch_knob(24);
459
460 let mut bank = PresetFile::new(InstrumentType::Juno60);
461 assert_eq!(
462 bank.store("evening pad", InstrumentType::Juno60, &panel),
463 Ok(StoreOutcome::Added)
464 );
465 save_bank(&dir, InstrumentType::Juno60, &bank).unwrap();
466
467 let reopened = load_bank(&dir, InstrumentType::Juno60).unwrap();
468 assert_eq!(reopened.names(), vec!["evening pad"]);
469 let loaded = reopened
470 .params_at(0, InstrumentType::Juno60, panel.len())
471 .unwrap()
472 .expect("its own panel should load");
473 for (index, (before, after)) in panel.iter().zip(loaded.params.iter()).enumerate() {
480 if crate::discrete::is_discrete(InstrumentType::Juno60, index) {
481 assert_eq!(
482 crate::discrete::index_of(InstrumentType::Juno60, index, *after),
483 crate::discrete::index_of(InstrumentType::Juno60, index, *before),
484 "control {index} came back on a different position"
485 );
486 } else {
487 assert_eq!(before, after, "control {index} came back changed");
488 }
489 }
490 assert!(loaded.clamped.is_empty());
491 assert!(!loaded.legacy_selectors, "a preset written now is not an old one");
492
493 let _ = std::fs::remove_dir_all(&dir);
494 }
495
496 #[test]
505 fn a_selector_survives_the_bank_growing() {
506 use phosphor_dsp::drum_rack;
507
508 let dir = scratch("bank-grew");
509 let mut panel = drum_rack::PARAM_DEFAULTS.to_vec();
510 panel[drum_rack::P_KIT] = drum_rack::kit_knob(1);
511 assert_eq!(
512 drum_rack::discrete_label(drum_rack::P_KIT, panel[drum_rack::P_KIT]),
513 Some("909"),
514 "this test is pinned to the 909 being position 1"
515 );
516
517 let mut bank = PresetFile::new(InstrumentType::DrumRack);
518 bank.store("my kit", InstrumentType::DrumRack, &panel).unwrap();
519 assert_eq!(
520 bank.presets[0].discrete.iter().find(|s| s.param == drum_rack::P_KIT),
521 Some(&SessionSelector { param: drum_rack::P_KIT, index: 1 }),
522 "the kit was not stored by position"
523 );
524
525 bank.presets[0].params[drum_rack::P_KIT] = 1.5 / 10.0;
529 save_bank(&dir, InstrumentType::DrumRack, &bank).unwrap();
530
531 let reopened = load_bank(&dir, InstrumentType::DrumRack).unwrap();
533 assert_eq!(
534 drum_rack::discrete_label(drum_rack::P_KIT, reopened.presets[0].params[drum_rack::P_KIT]),
535 Some("707"),
536 "the fraction no longer names the 707, so this test proves nothing"
537 );
538 let loaded = reopened
540 .params_at(0, InstrumentType::DrumRack, panel.len())
541 .unwrap()
542 .expect("its own panel should load");
543 assert_eq!(
544 drum_rack::discrete_label(drum_rack::P_KIT, loaded.params[drum_rack::P_KIT]),
545 Some("909"),
546 "the preset opened on a different drum machine"
547 );
548 assert!(loaded.clamped.is_empty());
549 assert!(!loaded.legacy_selectors);
550
551 let _ = std::fs::remove_dir_all(&dir);
552 }
553
554 #[test]
558 fn both_dx7_selectors_survive_a_round_trip() {
559 use phosphor_dsp::dx7;
560
561 let mut panel = dx7::PARAM_DEFAULTS.to_vec();
562 let (bank_knob, patch_knob) = dx7::voice_knobs(147);
563 panel[dx7::P_BANK] = bank_knob;
564 panel[dx7::P_PATCH] = patch_knob;
565
566 let mut bank = PresetFile::new(InstrumentType::DX7);
567 bank.store("timpani", InstrumentType::DX7, &panel).unwrap();
568
569 let stored: Vec<usize> = bank.presets[0].discrete.iter().map(|s| s.param).collect();
570 assert!(stored.contains(&dx7::P_BANK), "the cartridge was not stored");
571 assert!(stored.contains(&dx7::P_PATCH), "the voice was not stored");
572
573 bank.presets[0].params[dx7::P_BANK] = 0.0;
576 bank.presets[0].params[dx7::P_PATCH] = 0.0;
577
578 let loaded = bank
579 .params_at(0, InstrumentType::DX7, panel.len())
580 .unwrap()
581 .expect("its own panel should load");
582 assert_eq!(loaded.params[dx7::P_BANK], bank_knob, "the cartridge did not come back");
583 assert_eq!(loaded.params[dx7::P_PATCH], patch_knob, "the voice did not come back");
584 }
585
586 #[test]
590 fn a_selector_past_the_end_of_the_bank_is_clamped_and_reported() {
591 use phosphor_dsp::drum_rack;
592
593 let panel = drum_rack::PARAM_DEFAULTS.to_vec();
594 let mut bank = PresetFile::new(InstrumentType::DrumRack);
595 bank.store("from the future", InstrumentType::DrumRack, &panel).unwrap();
596 let selector = bank.presets[0]
597 .discrete
598 .iter_mut()
599 .find(|s| s.param == drum_rack::P_KIT)
600 .expect("the kit is a selector");
601 selector.index = 900;
602
603 let loaded = bank
604 .params_at(0, InstrumentType::DrumRack, panel.len())
605 .unwrap()
606 .expect("its own panel should load");
607 assert_eq!(
608 loaded.clamped,
609 vec![(drum_rack::P_KIT, 900, drum_rack::KIT_COUNT - 1)],
610 "a position the rack no longer has was not reported"
611 );
612 assert_eq!(
613 loaded.params[drum_rack::P_KIT],
614 drum_rack::kit_knob(drum_rack::KIT_COUNT - 1),
615 "the kit did not land on the last one the rack has"
616 );
617 }
618
619 #[test]
625 fn a_version_1_preset_loads_from_its_fractions_and_says_so() {
626 use phosphor_dsp::drum_rack;
627
628 let dir = scratch("version-1");
629 let params: Vec<String> = drum_rack::PARAM_DEFAULTS
632 .iter()
633 .enumerate()
634 .map(|(i, v)| {
635 if i == drum_rack::P_KIT { (1.5f32 / 10.0).to_string() } else { v.to_string() }
636 })
637 .collect();
638 let json = format!(
639 r#"{{"version":1,"instrument":"drums","presets":[{{"name":"old",
640 "instrument":"drums","layout":"{}","param_count":{},"params":[{}]}}]}}"#,
641 layout_fingerprint(InstrumentType::DrumRack),
642 drum_rack::PARAM_COUNT,
643 params.join(",")
644 );
645 std::fs::create_dir_all(&dir).unwrap();
646 std::fs::write(bank_path(&dir, InstrumentType::DrumRack), json).unwrap();
647
648 let bank = load_bank(&dir, InstrumentType::DrumRack).unwrap();
649 assert_eq!(bank.version, 1);
650 assert_eq!(bank.presets[0].version, LEGACY_VERSION, "a missing version is version 1");
651 assert!(bank.presets[0].discrete.is_empty());
652
653 let loaded = bank
654 .params_at(0, InstrumentType::DrumRack, drum_rack::PARAM_COUNT)
655 .unwrap()
656 .expect("an old preset still loads");
657 assert!(loaded.legacy_selectors, "an old preset loaded without a word said");
658 assert_eq!(
661 drum_rack::discrete_label(drum_rack::P_KIT, loaded.params[drum_rack::P_KIT]),
662 Some("707")
663 );
664
665 let _ = std::fs::remove_dir_all(&dir);
666 }
667
668 #[test]
672 fn every_selector_on_every_instrument_is_stored() {
673 for instrument in InstrumentType::ALL {
674 let count = param_count(*instrument);
675 let panel = vec![0.5f32; count];
676 let mut bank = PresetFile::new(*instrument);
677 bank.store("all", *instrument, &panel).unwrap();
678
679 let stored: Vec<usize> =
680 bank.presets[0].discrete.iter().map(|s| s.param).collect();
681 let wanted: Vec<usize> = (0..count)
682 .filter(|&p| crate::discrete::is_discrete(*instrument, p))
683 .collect();
684 assert_eq!(stored, wanted, "{instrument:?} did not store all of its selectors");
685 assert!(!wanted.is_empty(), "{instrument:?} has no selectors at all");
686 }
687 }
688
689 #[test]
692 fn a_bank_that_does_not_exist_is_empty() {
693 let dir = scratch("missing");
694 let bank = load_bank(&dir, InstrumentType::DX7).unwrap();
695 assert!(bank.presets.is_empty());
696 assert_eq!(bank.instrument, "dx7");
697 }
698
699 #[test]
704 fn a_preset_with_the_wrong_control_count_is_refused() {
705 let dir = scratch("count");
706 let mut bank = PresetFile::new(InstrumentType::Juno60);
707 bank.store("old panel", InstrumentType::Juno60, &juno_panel()).unwrap();
708 bank.presets[0].params.truncate(16);
710 bank.presets[0].param_count = 16;
711 save_bank(&dir, InstrumentType::Juno60, &bank).unwrap();
712
713 let reopened = load_bank(&dir, InstrumentType::Juno60).unwrap();
714 let want = param_count(InstrumentType::Juno60);
715 assert_eq!(
716 reopened.params_at(0, InstrumentType::Juno60, want).unwrap(),
717 Err(PresetError::ParamCountMismatch { saved: 16, wanted: want })
718 );
719
720 let _ = std::fs::remove_dir_all(&dir);
721 }
722
723 #[test]
727 fn a_preset_from_a_reordered_panel_is_refused() {
728 let panel = juno_panel();
729 let mut preset = Preset {
730 name: "reordered".into(),
731 instrument: "juno60".into(),
732 layout: layout_fingerprint(InstrumentType::Juno60),
733 param_count: panel.len(),
734 params: panel,
735 discrete: Vec::new(),
736 version: FORMAT_VERSION,
737 };
738 assert_eq!(preset.check(InstrumentType::Juno60, 25), Ok(()));
739
740 preset.layout = "0000000000000000".into();
742 assert!(matches!(
743 preset.check(InstrumentType::Juno60, 25),
744 Err(PresetError::LayoutMismatch { .. })
745 ));
746 }
747
748 #[test]
751 fn the_fingerprint_separates_every_instrument() {
752 let mut seen = Vec::new();
753 for inst in InstrumentType::ALL {
754 let fp = layout_fingerprint(*inst);
755 assert_eq!(fp.len(), 16, "{fp} is not a 64-bit fingerprint");
756 seen.push((inst, fp));
757 }
758 for (a, fa) in &seen {
761 for (b, fb) in &seen {
762 let shared_panel = matches!(
763 (a, b),
764 (InstrumentType::Synth, InstrumentType::Sampler)
765 | (InstrumentType::Sampler, InstrumentType::Synth)
766 );
767 if a != b && !shared_panel {
768 assert_ne!(fa, fb, "{a:?} and {b:?} fingerprint the same");
769 }
770 }
771 }
772 }
773
774 #[test]
777 fn a_preset_saved_for_another_instrument_is_refused() {
778 let dir = scratch("instrument");
779 let mut dx7 = PresetFile::new(InstrumentType::DX7);
780 dx7.store("e.piano", InstrumentType::DX7, &phosphor_dsp::dx7::PARAM_DEFAULTS).unwrap();
781
782 let mut juno = PresetFile::new(InstrumentType::Juno60);
784 juno.presets.push(dx7.presets[0].clone());
785 save_bank(&dir, InstrumentType::Juno60, &juno).unwrap();
786
787 let reopened = load_bank(&dir, InstrumentType::Juno60).unwrap();
788 assert_eq!(
789 reopened.params_at(0, InstrumentType::Juno60, 9).unwrap(),
790 Err(PresetError::WrongInstrument {
791 saved: "dx7".into(),
792 wanted: "juno60".into()
793 }),
794 "a DX7 preset loaded into a Juno"
795 );
796
797 assert_ne!(
799 bank_path(&dir, InstrumentType::DX7),
800 bank_path(&dir, InstrumentType::Juno60)
801 );
802
803 let _ = std::fs::remove_dir_all(&dir);
804 }
805
806 #[test]
809 fn saving_over_a_name_replaces_it_in_place() {
810 let mut bank = PresetFile::new(InstrumentType::Juno60);
811 let mut first = juno_panel();
812 first[phosphor_dsp::juno::P_CUTOFF] = 0.2;
813 let mut second = juno_panel();
814 second[phosphor_dsp::juno::P_CUTOFF] = 0.9;
815
816 bank.store("brass", InstrumentType::Juno60, &first).unwrap();
817 bank.store("strings", InstrumentType::Juno60, &juno_panel()).unwrap();
818 assert_eq!(
819 bank.store("brass", InstrumentType::Juno60, &second),
820 Ok(StoreOutcome::Replaced)
821 );
822
823 assert_eq!(bank.names(), vec!["brass", "strings"], "the slot moved or duplicated");
824 assert_eq!(bank.presets[0].params[phosphor_dsp::juno::P_CUTOFF], 0.9);
825
826 assert_eq!(
829 bank.store(" brass ", InstrumentType::Juno60, &first),
830 Ok(StoreOutcome::Replaced)
831 );
832 assert_eq!(bank.presets.len(), 2);
833 }
834
835 #[test]
837 fn the_bank_stops_at_its_limit() {
838 let mut bank = PresetFile::new(InstrumentType::Juno60);
839 let panel = juno_panel();
840 for i in 0..MAX_PRESETS {
841 bank.store(&format!("p{i}"), InstrumentType::Juno60, &panel).unwrap();
842 }
843 assert_eq!(
844 bank.store("one more", InstrumentType::Juno60, &panel),
845 Err(PresetError::BankFull { max: MAX_PRESETS })
846 );
847 assert_eq!(
848 bank.store("p0", InstrumentType::Juno60, &panel),
849 Ok(StoreOutcome::Replaced),
850 "a full bank became read-only"
851 );
852
853 bank.remove(0);
854 assert_eq!(bank.presets.len(), MAX_PRESETS - 1);
855 assert_eq!(
856 bank.store("one more", InstrumentType::Juno60, &panel),
857 Ok(StoreOutcome::Added)
858 );
859 }
860
861 #[test]
863 fn names_are_bounded_and_non_empty() {
864 let mut bank = PresetFile::new(InstrumentType::Juno60);
865 let panel = juno_panel();
866 assert_eq!(
867 bank.store(" ", InstrumentType::Juno60, &panel),
868 Err(PresetError::NameEmpty)
869 );
870 let long = "x".repeat(MAX_NAME_LEN + 1);
871 assert_eq!(
872 bank.store(&long, InstrumentType::Juno60, &panel),
873 Err(PresetError::NameTooLong { len: MAX_NAME_LEN + 1, max: MAX_NAME_LEN })
874 );
875 assert!(bank.presets.is_empty());
876 }
877
878 #[test]
881 fn a_preset_that_contradicts_itself_is_refused() {
882 let panel = juno_panel();
883 let preset = Preset {
884 name: "hand edited".into(),
885 instrument: "juno60".into(),
886 layout: layout_fingerprint(InstrumentType::Juno60),
887 param_count: 99,
888 params: panel.clone(),
889 discrete: Vec::new(),
890 version: FORMAT_VERSION,
891 };
892 assert_eq!(
893 preset.check(InstrumentType::Juno60, panel.len()),
894 Err(PresetError::Corrupt { declared: 99, actual: panel.len() })
895 );
896 }
897
898 #[test]
901 fn every_instrument_has_a_panel() {
902 for inst in InstrumentType::ALL {
903 assert!(param_count(*inst) > 0, "{inst:?} has no parameters");
904 }
905 assert_eq!(param_count(InstrumentType::Juno60), phosphor_dsp::juno::PARAM_COUNT);
906 assert_eq!(param_count(InstrumentType::Jupiter8), phosphor_dsp::jupiter::PARAM_COUNT);
907 assert_eq!(param_count(InstrumentType::DX7), phosphor_dsp::dx7::PARAM_COUNT);
908 assert_eq!(param_count(InstrumentType::Odyssey), phosphor_dsp::odyssey::PARAM_COUNT);
909 assert_eq!(param_count(InstrumentType::DrumRack), phosphor_dsp::drum_rack::PARAM_COUNT);
910 assert_eq!(param_count(InstrumentType::Synth), phosphor_dsp::synth::PARAM_COUNT);
911 assert_eq!(param_count(InstrumentType::Sampler), phosphor_dsp::synth::PARAM_COUNT);
912 }
913}