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