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 InstrumentType::LittlePhatty => &phosphor_dsp::phatty::PARAM_NAMES,
180 InstrumentType::Prophet6 => &phosphor_dsp::prophet6::PARAM_NAMES,
181 }
182}
183
184pub fn layout_fingerprint(instrument: InstrumentType) -> String {
202 const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
203 const PRIME: u64 = 0x0000_0100_0000_01b3;
204
205 let mut hash = OFFSET;
206 for name in param_names(instrument) {
207 for byte in name.bytes().chain(std::iter::once(0xff)) {
209 hash ^= u64::from(byte);
210 hash = hash.wrapping_mul(PRIME);
211 }
212 }
213 format!("{hash:016x}")
214}
215
216pub fn param_count(instrument: InstrumentType) -> usize {
218 param_names(instrument).len()
219}
220
221pub fn default_dir() -> Option<PathBuf> {
235 crate::paths::preset_dir()
236}
237
238pub fn bank_path(dir: &Path, instrument: InstrumentType) -> PathBuf {
240 dir.join(format!("{}.json", instrument_key(instrument)))
241}
242
243pub fn load_bank(dir: &Path, instrument: InstrumentType) -> Result<PresetFile> {
250 let path = bank_path(dir, instrument);
251 if !path.exists() {
252 return Ok(PresetFile::new(instrument));
253 }
254 let json = std::fs::read_to_string(&path)?;
255 let bank: PresetFile = serde_json::from_str(&json)?;
256 Ok(bank)
257}
258
259pub fn save_bank(dir: &Path, instrument: InstrumentType, bank: &PresetFile) -> Result<()> {
262 std::fs::create_dir_all(dir)?;
263 let path = bank_path(dir, instrument);
264 let json = serde_json::to_string_pretty(bank)?;
265
266 let tmp = path.with_extension("json.tmp");
267 std::fs::write(&tmp, &json)?;
268 std::fs::rename(&tmp, &path)?;
269
270 tracing::debug!("preset bank saved: {} ({} presets)", path.display(), bank.presets.len());
271 Ok(())
272}
273
274impl PresetFile {
277 pub fn new(instrument: InstrumentType) -> Self {
278 Self {
279 version: FORMAT_VERSION,
280 instrument: instrument_key(instrument).to_string(),
281 presets: Vec::new(),
282 }
283 }
284
285 pub fn names(&self) -> Vec<&str> {
287 self.presets.iter().map(|p| p.name.as_str()).collect()
288 }
289
290 pub fn find(&self, name: &str) -> Option<usize> {
295 let name = name.trim();
296 self.presets.iter().position(|p| p.name == name)
297 }
298
299 pub fn store(
307 &mut self,
308 name: &str,
309 instrument: InstrumentType,
310 params: &[f32],
311 ) -> Result<StoreOutcome, PresetError> {
312 let name = name.trim();
313 if name.is_empty() {
314 return Err(PresetError::NameEmpty);
315 }
316 let len = name.chars().count();
317 if len > MAX_NAME_LEN {
318 return Err(PresetError::NameTooLong { len, max: MAX_NAME_LEN });
319 }
320
321 let preset = Preset {
322 name: name.to_string(),
323 instrument: instrument_key(instrument).to_string(),
324 layout: layout_fingerprint(instrument),
325 param_count: params.len(),
326 params: params.to_vec(),
327 discrete: crate::session::selectors_of(instrument, params),
331 version: FORMAT_VERSION,
332 };
333
334 let outcome = match self.find(name) {
335 Some(idx) => {
336 self.presets[idx] = preset;
337 StoreOutcome::Replaced
338 }
339 None => {
340 if self.presets.len() >= MAX_PRESETS {
344 return Err(PresetError::BankFull { max: MAX_PRESETS });
345 }
346 self.presets.push(preset);
347 StoreOutcome::Added
348 }
349 };
350
351 self.version = FORMAT_VERSION;
355 Ok(outcome)
356 }
357
358 pub fn remove(&mut self, index: usize) -> Option<Preset> {
360 (index < self.presets.len()).then(|| self.presets.remove(index))
361 }
362
363 pub fn params_at(
371 &self,
372 index: usize,
373 instrument: InstrumentType,
374 want_count: usize,
375 ) -> Option<Result<LoadedPreset, PresetError>> {
376 let preset = self.presets.get(index)?;
377 Some(preset.check(instrument, want_count).map(|()| preset.resolve(instrument)))
378 }
379}
380
381impl Preset {
382 pub fn check(&self, instrument: InstrumentType, want_count: usize) -> Result<(), PresetError> {
388 let wanted_key = instrument_key(instrument);
389 if self.instrument != wanted_key {
390 return Err(PresetError::WrongInstrument {
391 saved: self.instrument.clone(),
392 wanted: wanted_key.to_string(),
393 });
394 }
395 if self.param_count != self.params.len() {
396 return Err(PresetError::Corrupt {
397 declared: self.param_count,
398 actual: self.params.len(),
399 });
400 }
401 if self.params.len() != want_count {
402 return Err(PresetError::ParamCountMismatch {
403 saved: self.params.len(),
404 wanted: want_count,
405 });
406 }
407 let wanted_layout = layout_fingerprint(instrument);
408 if self.layout != wanted_layout {
409 return Err(PresetError::LayoutMismatch {
410 saved: self.layout.clone(),
411 wanted: wanted_layout,
412 });
413 }
414 Ok(())
415 }
416
417 #[must_use]
425 pub fn resolve(&self, instrument: InstrumentType) -> LoadedPreset {
426 let mut params = self.params.clone();
427 let clamped = crate::session::apply_selectors(instrument, &mut params, &self.discrete);
428 LoadedPreset {
429 params,
430 clamped,
431 legacy_selectors: self.version < FORMAT_VERSION,
432 }
433 }
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439
440 fn scratch(tag: &str) -> PathBuf {
443 let dir = std::env::temp_dir()
444 .join(format!("phosphor-presets-{}-{tag}", std::process::id()));
445 let _ = std::fs::remove_dir_all(&dir);
446 dir
447 }
448
449 fn juno_panel() -> Vec<f32> {
450 phosphor_dsp::juno::PARAM_DEFAULTS.to_vec()
451 }
452
453 #[test]
457 fn a_preset_round_trips_through_the_file() {
458 let dir = scratch("round-trip");
459 let mut panel = juno_panel();
460 panel[phosphor_dsp::juno::P_CUTOFF] = 0.317_25;
461 panel[phosphor_dsp::juno::P_RESO] = 0.812_5;
462 panel[phosphor_dsp::juno::P_PATCH] = phosphor_dsp::juno::patch_knob(24);
467
468 let mut bank = PresetFile::new(InstrumentType::Juno60);
469 assert_eq!(
470 bank.store("evening pad", InstrumentType::Juno60, &panel),
471 Ok(StoreOutcome::Added)
472 );
473 save_bank(&dir, InstrumentType::Juno60, &bank).unwrap();
474
475 let reopened = load_bank(&dir, InstrumentType::Juno60).unwrap();
476 assert_eq!(reopened.names(), vec!["evening pad"]);
477 let loaded = reopened
478 .params_at(0, InstrumentType::Juno60, panel.len())
479 .unwrap()
480 .expect("its own panel should load");
481 for (index, (before, after)) in panel.iter().zip(loaded.params.iter()).enumerate() {
488 if crate::discrete::is_discrete(InstrumentType::Juno60, index) {
489 assert_eq!(
490 crate::discrete::index_of(InstrumentType::Juno60, index, *after),
491 crate::discrete::index_of(InstrumentType::Juno60, index, *before),
492 "control {index} came back on a different position"
493 );
494 } else {
495 assert_eq!(before, after, "control {index} came back changed");
496 }
497 }
498 assert!(loaded.clamped.is_empty());
499 assert!(!loaded.legacy_selectors, "a preset written now is not an old one");
500
501 let _ = std::fs::remove_dir_all(&dir);
502 }
503
504 #[test]
513 fn a_selector_survives_the_bank_growing() {
514 use phosphor_dsp::drum_rack;
515
516 let dir = scratch("bank-grew");
517 let mut panel = drum_rack::PARAM_DEFAULTS.to_vec();
518 panel[drum_rack::P_KIT] = drum_rack::kit_knob(1);
519 assert_eq!(
520 drum_rack::discrete_label(drum_rack::P_KIT, panel[drum_rack::P_KIT]),
521 Some("909"),
522 "this test is pinned to the 909 being position 1"
523 );
524
525 let mut bank = PresetFile::new(InstrumentType::DrumRack);
526 bank.store("my kit", InstrumentType::DrumRack, &panel).unwrap();
527 assert_eq!(
528 bank.presets[0].discrete.iter().find(|s| s.param == drum_rack::P_KIT),
529 Some(&SessionSelector { param: drum_rack::P_KIT, index: 1 }),
530 "the kit was not stored by position"
531 );
532
533 bank.presets[0].params[drum_rack::P_KIT] = 1.5 / 10.0;
537 save_bank(&dir, InstrumentType::DrumRack, &bank).unwrap();
538
539 let reopened = load_bank(&dir, InstrumentType::DrumRack).unwrap();
541 assert_eq!(
542 drum_rack::discrete_label(drum_rack::P_KIT, reopened.presets[0].params[drum_rack::P_KIT]),
543 Some("707"),
544 "the fraction no longer names the 707, so this test proves nothing"
545 );
546 let loaded = reopened
548 .params_at(0, InstrumentType::DrumRack, panel.len())
549 .unwrap()
550 .expect("its own panel should load");
551 assert_eq!(
552 drum_rack::discrete_label(drum_rack::P_KIT, loaded.params[drum_rack::P_KIT]),
553 Some("909"),
554 "the preset opened on a different drum machine"
555 );
556 assert!(loaded.clamped.is_empty());
557 assert!(!loaded.legacy_selectors);
558
559 let _ = std::fs::remove_dir_all(&dir);
560 }
561
562 #[test]
566 fn both_dx7_selectors_survive_a_round_trip() {
567 use phosphor_dsp::dx7;
568
569 let mut panel = dx7::PARAM_DEFAULTS.to_vec();
570 let (bank_knob, patch_knob) = dx7::voice_knobs(147);
571 panel[dx7::P_BANK] = bank_knob;
572 panel[dx7::P_PATCH] = patch_knob;
573
574 let mut bank = PresetFile::new(InstrumentType::DX7);
575 bank.store("timpani", InstrumentType::DX7, &panel).unwrap();
576
577 let stored: Vec<usize> = bank.presets[0].discrete.iter().map(|s| s.param).collect();
578 assert!(stored.contains(&dx7::P_BANK), "the cartridge was not stored");
579 assert!(stored.contains(&dx7::P_PATCH), "the voice was not stored");
580
581 bank.presets[0].params[dx7::P_BANK] = 0.0;
584 bank.presets[0].params[dx7::P_PATCH] = 0.0;
585
586 let loaded = bank
587 .params_at(0, InstrumentType::DX7, panel.len())
588 .unwrap()
589 .expect("its own panel should load");
590 assert_eq!(loaded.params[dx7::P_BANK], bank_knob, "the cartridge did not come back");
591 assert_eq!(loaded.params[dx7::P_PATCH], patch_knob, "the voice did not come back");
592 }
593
594 #[test]
598 fn a_selector_past_the_end_of_the_bank_is_clamped_and_reported() {
599 use phosphor_dsp::drum_rack;
600
601 let panel = drum_rack::PARAM_DEFAULTS.to_vec();
602 let mut bank = PresetFile::new(InstrumentType::DrumRack);
603 bank.store("from the future", InstrumentType::DrumRack, &panel).unwrap();
604 let selector = bank.presets[0]
605 .discrete
606 .iter_mut()
607 .find(|s| s.param == drum_rack::P_KIT)
608 .expect("the kit is a selector");
609 selector.index = 900;
610
611 let loaded = bank
612 .params_at(0, InstrumentType::DrumRack, panel.len())
613 .unwrap()
614 .expect("its own panel should load");
615 assert_eq!(
616 loaded.clamped,
617 vec![(drum_rack::P_KIT, 900, drum_rack::KIT_COUNT - 1)],
618 "a position the rack no longer has was not reported"
619 );
620 assert_eq!(
621 loaded.params[drum_rack::P_KIT],
622 drum_rack::kit_knob(drum_rack::KIT_COUNT - 1),
623 "the kit did not land on the last one the rack has"
624 );
625 }
626
627 #[test]
633 fn a_version_1_preset_loads_from_its_fractions_and_says_so() {
634 use phosphor_dsp::drum_rack;
635
636 let dir = scratch("version-1");
637 let params: Vec<String> = drum_rack::PARAM_DEFAULTS
640 .iter()
641 .enumerate()
642 .map(|(i, v)| {
643 if i == drum_rack::P_KIT { (1.5f32 / 10.0).to_string() } else { v.to_string() }
644 })
645 .collect();
646 let json = format!(
647 r#"{{"version":1,"instrument":"drums","presets":[{{"name":"old",
648 "instrument":"drums","layout":"{}","param_count":{},"params":[{}]}}]}}"#,
649 layout_fingerprint(InstrumentType::DrumRack),
650 drum_rack::PARAM_COUNT,
651 params.join(",")
652 );
653 std::fs::create_dir_all(&dir).unwrap();
654 std::fs::write(bank_path(&dir, InstrumentType::DrumRack), json).unwrap();
655
656 let bank = load_bank(&dir, InstrumentType::DrumRack).unwrap();
657 assert_eq!(bank.version, 1);
658 assert_eq!(bank.presets[0].version, LEGACY_VERSION, "a missing version is version 1");
659 assert!(bank.presets[0].discrete.is_empty());
660
661 let loaded = bank
662 .params_at(0, InstrumentType::DrumRack, drum_rack::PARAM_COUNT)
663 .unwrap()
664 .expect("an old preset still loads");
665 assert!(loaded.legacy_selectors, "an old preset loaded without a word said");
666 assert_eq!(
669 drum_rack::discrete_label(drum_rack::P_KIT, loaded.params[drum_rack::P_KIT]),
670 Some("707")
671 );
672
673 let _ = std::fs::remove_dir_all(&dir);
674 }
675
676 #[test]
680 fn every_selector_on_every_instrument_is_stored() {
681 for instrument in InstrumentType::ALL {
682 let count = param_count(*instrument);
683 let panel = vec![0.5f32; count];
684 let mut bank = PresetFile::new(*instrument);
685 bank.store("all", *instrument, &panel).unwrap();
686
687 let stored: Vec<usize> =
688 bank.presets[0].discrete.iter().map(|s| s.param).collect();
689 let wanted: Vec<usize> = (0..count)
690 .filter(|&p| crate::discrete::is_discrete(*instrument, p))
691 .collect();
692 assert_eq!(stored, wanted, "{instrument:?} did not store all of its selectors");
693 assert!(!wanted.is_empty(), "{instrument:?} has no selectors at all");
694 }
695 }
696
697 #[test]
700 fn a_bank_that_does_not_exist_is_empty() {
701 let dir = scratch("missing");
702 let bank = load_bank(&dir, InstrumentType::DX7).unwrap();
703 assert!(bank.presets.is_empty());
704 assert_eq!(bank.instrument, "dx7");
705 }
706
707 #[test]
712 fn a_preset_with_the_wrong_control_count_is_refused() {
713 let dir = scratch("count");
714 let mut bank = PresetFile::new(InstrumentType::Juno60);
715 bank.store("old panel", InstrumentType::Juno60, &juno_panel()).unwrap();
716 bank.presets[0].params.truncate(16);
718 bank.presets[0].param_count = 16;
719 save_bank(&dir, InstrumentType::Juno60, &bank).unwrap();
720
721 let reopened = load_bank(&dir, InstrumentType::Juno60).unwrap();
722 let want = param_count(InstrumentType::Juno60);
723 assert_eq!(
724 reopened.params_at(0, InstrumentType::Juno60, want).unwrap(),
725 Err(PresetError::ParamCountMismatch { saved: 16, wanted: want })
726 );
727
728 let _ = std::fs::remove_dir_all(&dir);
729 }
730
731 #[test]
735 fn a_preset_from_a_reordered_panel_is_refused() {
736 let panel = juno_panel();
737 let mut preset = Preset {
738 name: "reordered".into(),
739 instrument: "juno60".into(),
740 layout: layout_fingerprint(InstrumentType::Juno60),
741 param_count: panel.len(),
742 params: panel,
743 discrete: Vec::new(),
744 version: FORMAT_VERSION,
745 };
746 assert_eq!(preset.check(InstrumentType::Juno60, 25), Ok(()));
747
748 preset.layout = "0000000000000000".into();
750 assert!(matches!(
751 preset.check(InstrumentType::Juno60, 25),
752 Err(PresetError::LayoutMismatch { .. })
753 ));
754 }
755
756 #[test]
759 fn the_fingerprint_separates_every_instrument() {
760 let mut seen = Vec::new();
761 for inst in InstrumentType::ALL {
762 let fp = layout_fingerprint(*inst);
763 assert_eq!(fp.len(), 16, "{fp} is not a 64-bit fingerprint");
764 seen.push((inst, fp));
765 }
766 for (a, fa) in &seen {
769 for (b, fb) in &seen {
770 let shared_panel = matches!(
771 (a, b),
772 (InstrumentType::Synth, InstrumentType::Sampler)
773 | (InstrumentType::Sampler, InstrumentType::Synth)
774 );
775 if a != b && !shared_panel {
776 assert_ne!(fa, fb, "{a:?} and {b:?} fingerprint the same");
777 }
778 }
779 }
780 }
781
782 #[test]
785 fn a_preset_saved_for_another_instrument_is_refused() {
786 let dir = scratch("instrument");
787 let mut dx7 = PresetFile::new(InstrumentType::DX7);
788 dx7.store("e.piano", InstrumentType::DX7, &phosphor_dsp::dx7::PARAM_DEFAULTS).unwrap();
789
790 let mut juno = PresetFile::new(InstrumentType::Juno60);
792 juno.presets.push(dx7.presets[0].clone());
793 save_bank(&dir, InstrumentType::Juno60, &juno).unwrap();
794
795 let reopened = load_bank(&dir, InstrumentType::Juno60).unwrap();
796 assert_eq!(
797 reopened.params_at(0, InstrumentType::Juno60, 9).unwrap(),
798 Err(PresetError::WrongInstrument {
799 saved: "dx7".into(),
800 wanted: "juno60".into()
801 }),
802 "a DX7 preset loaded into a Juno"
803 );
804
805 assert_ne!(
807 bank_path(&dir, InstrumentType::DX7),
808 bank_path(&dir, InstrumentType::Juno60)
809 );
810
811 let _ = std::fs::remove_dir_all(&dir);
812 }
813
814 #[test]
817 fn saving_over_a_name_replaces_it_in_place() {
818 let mut bank = PresetFile::new(InstrumentType::Juno60);
819 let mut first = juno_panel();
820 first[phosphor_dsp::juno::P_CUTOFF] = 0.2;
821 let mut second = juno_panel();
822 second[phosphor_dsp::juno::P_CUTOFF] = 0.9;
823
824 bank.store("brass", InstrumentType::Juno60, &first).unwrap();
825 bank.store("strings", InstrumentType::Juno60, &juno_panel()).unwrap();
826 assert_eq!(
827 bank.store("brass", InstrumentType::Juno60, &second),
828 Ok(StoreOutcome::Replaced)
829 );
830
831 assert_eq!(bank.names(), vec!["brass", "strings"], "the slot moved or duplicated");
832 assert_eq!(bank.presets[0].params[phosphor_dsp::juno::P_CUTOFF], 0.9);
833
834 assert_eq!(
837 bank.store(" brass ", InstrumentType::Juno60, &first),
838 Ok(StoreOutcome::Replaced)
839 );
840 assert_eq!(bank.presets.len(), 2);
841 }
842
843 #[test]
845 fn the_bank_stops_at_its_limit() {
846 let mut bank = PresetFile::new(InstrumentType::Juno60);
847 let panel = juno_panel();
848 for i in 0..MAX_PRESETS {
849 bank.store(&format!("p{i}"), InstrumentType::Juno60, &panel).unwrap();
850 }
851 assert_eq!(
852 bank.store("one more", InstrumentType::Juno60, &panel),
853 Err(PresetError::BankFull { max: MAX_PRESETS })
854 );
855 assert_eq!(
856 bank.store("p0", InstrumentType::Juno60, &panel),
857 Ok(StoreOutcome::Replaced),
858 "a full bank became read-only"
859 );
860
861 bank.remove(0);
862 assert_eq!(bank.presets.len(), MAX_PRESETS - 1);
863 assert_eq!(
864 bank.store("one more", InstrumentType::Juno60, &panel),
865 Ok(StoreOutcome::Added)
866 );
867 }
868
869 #[test]
871 fn names_are_bounded_and_non_empty() {
872 let mut bank = PresetFile::new(InstrumentType::Juno60);
873 let panel = juno_panel();
874 assert_eq!(
875 bank.store(" ", InstrumentType::Juno60, &panel),
876 Err(PresetError::NameEmpty)
877 );
878 let long = "x".repeat(MAX_NAME_LEN + 1);
879 assert_eq!(
880 bank.store(&long, InstrumentType::Juno60, &panel),
881 Err(PresetError::NameTooLong { len: MAX_NAME_LEN + 1, max: MAX_NAME_LEN })
882 );
883 assert!(bank.presets.is_empty());
884 }
885
886 #[test]
889 fn a_preset_that_contradicts_itself_is_refused() {
890 let panel = juno_panel();
891 let preset = Preset {
892 name: "hand edited".into(),
893 instrument: "juno60".into(),
894 layout: layout_fingerprint(InstrumentType::Juno60),
895 param_count: 99,
896 params: panel.clone(),
897 discrete: Vec::new(),
898 version: FORMAT_VERSION,
899 };
900 assert_eq!(
901 preset.check(InstrumentType::Juno60, panel.len()),
902 Err(PresetError::Corrupt { declared: 99, actual: panel.len() })
903 );
904 }
905
906 #[test]
909 fn every_instrument_has_a_panel() {
910 for inst in InstrumentType::ALL {
911 assert!(param_count(*inst) > 0, "{inst:?} has no parameters");
912 }
913 assert_eq!(param_count(InstrumentType::Juno60), phosphor_dsp::juno::PARAM_COUNT);
914 assert_eq!(param_count(InstrumentType::Jupiter8), phosphor_dsp::jupiter::PARAM_COUNT);
915 assert_eq!(param_count(InstrumentType::DX7), phosphor_dsp::dx7::PARAM_COUNT);
916 assert_eq!(param_count(InstrumentType::Odyssey), phosphor_dsp::odyssey::PARAM_COUNT);
917 assert_eq!(param_count(InstrumentType::Rhodes), phosphor_dsp::rhodes::PARAM_COUNT);
918 assert_eq!(param_count(InstrumentType::DrumRack), phosphor_dsp::drum_rack::PARAM_COUNT);
919 assert_eq!(param_count(InstrumentType::Synth), phosphor_dsp::synth::PARAM_COUNT);
920 assert_eq!(param_count(InstrumentType::Sampler), phosphor_dsp::synth::PARAM_COUNT);
921 assert_eq!(param_count(InstrumentType::LittlePhatty), phosphor_dsp::phatty::PARAM_COUNT);
922 assert_eq!(param_count(InstrumentType::Prophet6), phosphor_dsp::prophet6::PARAM_COUNT);
923 }
924}