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