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 InstrumentType::Sequencer => &[],
184 }
185}
186
187pub fn layout_fingerprint(instrument: InstrumentType) -> String {
205 const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
206 const PRIME: u64 = 0x0000_0100_0000_01b3;
207
208 let mut hash = OFFSET;
209 for name in param_names(instrument) {
210 for byte in name.bytes().chain(std::iter::once(0xff)) {
212 hash ^= u64::from(byte);
213 hash = hash.wrapping_mul(PRIME);
214 }
215 }
216 format!("{hash:016x}")
217}
218
219pub fn param_count(instrument: InstrumentType) -> usize {
221 param_names(instrument).len()
222}
223
224pub fn defaults(instrument: InstrumentType) -> Vec<f32> {
233 match instrument {
234 InstrumentType::Synth | InstrumentType::Sampler => {
235 phosphor_dsp::synth::PARAM_DEFAULTS.to_vec()
236 }
237 InstrumentType::DrumRack => phosphor_dsp::drum_rack::PARAM_DEFAULTS.to_vec(),
238 InstrumentType::DX7 => phosphor_dsp::dx7::PARAM_DEFAULTS.to_vec(),
239 InstrumentType::Jupiter8 => phosphor_dsp::jupiter::PARAM_DEFAULTS.to_vec(),
240 InstrumentType::Odyssey => phosphor_dsp::odyssey::PARAM_DEFAULTS.to_vec(),
241 InstrumentType::Juno60 => phosphor_dsp::juno::PARAM_DEFAULTS.to_vec(),
242 InstrumentType::Rhodes => phosphor_dsp::rhodes::PARAM_DEFAULTS.to_vec(),
243 InstrumentType::LittlePhatty => phosphor_dsp::phatty::PARAM_DEFAULTS.to_vec(),
244 InstrumentType::Prophet6 => phosphor_dsp::prophet6::param_defaults().to_vec(),
245 InstrumentType::Sequencer => Vec::new(),
248 }
249}
250
251pub fn default_dir() -> Option<PathBuf> {
265 crate::paths::preset_dir()
266}
267
268pub fn bank_path(dir: &Path, instrument: InstrumentType) -> PathBuf {
270 dir.join(format!("{}.json", instrument_key(instrument)))
271}
272
273pub fn load_bank(dir: &Path, instrument: InstrumentType) -> Result<PresetFile> {
280 let path = bank_path(dir, instrument);
281 if !path.exists() {
282 return Ok(PresetFile::new(instrument));
283 }
284 let json = std::fs::read_to_string(&path)?;
285 let bank: PresetFile = serde_json::from_str(&json)?;
286 Ok(bank)
287}
288
289pub fn save_bank(dir: &Path, instrument: InstrumentType, bank: &PresetFile) -> Result<()> {
292 std::fs::create_dir_all(dir)?;
293 let path = bank_path(dir, instrument);
294 let json = serde_json::to_string_pretty(bank)?;
295
296 let tmp = path.with_extension("json.tmp");
297 std::fs::write(&tmp, &json)?;
298 std::fs::rename(&tmp, &path)?;
299
300 tracing::debug!("preset bank saved: {} ({} presets)", path.display(), bank.presets.len());
301 Ok(())
302}
303
304impl PresetFile {
307 pub fn new(instrument: InstrumentType) -> Self {
308 Self {
309 version: FORMAT_VERSION,
310 instrument: instrument_key(instrument).to_string(),
311 presets: Vec::new(),
312 }
313 }
314
315 pub fn names(&self) -> Vec<&str> {
317 self.presets.iter().map(|p| p.name.as_str()).collect()
318 }
319
320 pub fn find(&self, name: &str) -> Option<usize> {
325 let name = name.trim();
326 self.presets.iter().position(|p| p.name == name)
327 }
328
329 pub fn store(
337 &mut self,
338 name: &str,
339 instrument: InstrumentType,
340 params: &[f32],
341 ) -> Result<StoreOutcome, PresetError> {
342 let name = name.trim();
343 if name.is_empty() {
344 return Err(PresetError::NameEmpty);
345 }
346 let len = name.chars().count();
347 if len > MAX_NAME_LEN {
348 return Err(PresetError::NameTooLong { len, max: MAX_NAME_LEN });
349 }
350
351 let preset = Preset {
352 name: name.to_string(),
353 instrument: instrument_key(instrument).to_string(),
354 layout: layout_fingerprint(instrument),
355 param_count: params.len(),
356 params: params.to_vec(),
357 discrete: crate::session::selectors_of(instrument, params),
361 version: FORMAT_VERSION,
362 };
363
364 let outcome = match self.find(name) {
365 Some(idx) => {
366 self.presets[idx] = preset;
367 StoreOutcome::Replaced
368 }
369 None => {
370 if self.presets.len() >= MAX_PRESETS {
374 return Err(PresetError::BankFull { max: MAX_PRESETS });
375 }
376 self.presets.push(preset);
377 StoreOutcome::Added
378 }
379 };
380
381 self.version = FORMAT_VERSION;
385 Ok(outcome)
386 }
387
388 pub fn remove(&mut self, index: usize) -> Option<Preset> {
390 (index < self.presets.len()).then(|| self.presets.remove(index))
391 }
392
393 pub fn params_at(
401 &self,
402 index: usize,
403 instrument: InstrumentType,
404 want_count: usize,
405 ) -> Option<Result<LoadedPreset, PresetError>> {
406 let preset = self.presets.get(index)?;
407 Some(preset.check(instrument, want_count).map(|()| preset.resolve(instrument)))
408 }
409}
410
411impl Preset {
412 pub fn check(&self, instrument: InstrumentType, want_count: usize) -> Result<(), PresetError> {
418 let wanted_key = instrument_key(instrument);
419 if self.instrument != wanted_key {
420 return Err(PresetError::WrongInstrument {
421 saved: self.instrument.clone(),
422 wanted: wanted_key.to_string(),
423 });
424 }
425 if self.param_count != self.params.len() {
426 return Err(PresetError::Corrupt {
427 declared: self.param_count,
428 actual: self.params.len(),
429 });
430 }
431 if self.params.len() != want_count {
432 return Err(PresetError::ParamCountMismatch {
433 saved: self.params.len(),
434 wanted: want_count,
435 });
436 }
437 let wanted_layout = layout_fingerprint(instrument);
438 if self.layout != wanted_layout {
439 return Err(PresetError::LayoutMismatch {
440 saved: self.layout.clone(),
441 wanted: wanted_layout,
442 });
443 }
444 Ok(())
445 }
446
447 #[must_use]
455 pub fn resolve(&self, instrument: InstrumentType) -> LoadedPreset {
456 let mut params = self.params.clone();
457 let clamped = crate::session::apply_selectors(instrument, &mut params, &self.discrete);
458 LoadedPreset {
459 params,
460 clamped,
461 legacy_selectors: self.version < FORMAT_VERSION,
462 }
463 }
464}
465
466#[cfg(test)]
467mod tests {
468 use super::*;
469
470 fn scratch(tag: &str) -> PathBuf {
473 let dir = std::env::temp_dir()
474 .join(format!("phosphor-presets-{}-{tag}", std::process::id()));
475 let _ = std::fs::remove_dir_all(&dir);
476 dir
477 }
478
479 fn juno_panel() -> Vec<f32> {
480 phosphor_dsp::juno::PARAM_DEFAULTS.to_vec()
481 }
482
483 #[test]
487 fn a_preset_round_trips_through_the_file() {
488 let dir = scratch("round-trip");
489 let mut panel = juno_panel();
490 panel[phosphor_dsp::juno::P_CUTOFF] = 0.317_25;
491 panel[phosphor_dsp::juno::P_RESO] = 0.812_5;
492 panel[phosphor_dsp::juno::P_PATCH] = phosphor_dsp::juno::patch_knob(24);
497
498 let mut bank = PresetFile::new(InstrumentType::Juno60);
499 assert_eq!(
500 bank.store("evening pad", InstrumentType::Juno60, &panel),
501 Ok(StoreOutcome::Added)
502 );
503 save_bank(&dir, InstrumentType::Juno60, &bank).unwrap();
504
505 let reopened = load_bank(&dir, InstrumentType::Juno60).unwrap();
506 assert_eq!(reopened.names(), vec!["evening pad"]);
507 let loaded = reopened
508 .params_at(0, InstrumentType::Juno60, panel.len())
509 .unwrap()
510 .expect("its own panel should load");
511 for (index, (before, after)) in panel.iter().zip(loaded.params.iter()).enumerate() {
518 if crate::discrete::is_discrete(InstrumentType::Juno60, index) {
519 assert_eq!(
520 crate::discrete::index_of(InstrumentType::Juno60, index, *after),
521 crate::discrete::index_of(InstrumentType::Juno60, index, *before),
522 "control {index} came back on a different position"
523 );
524 } else {
525 assert_eq!(before, after, "control {index} came back changed");
526 }
527 }
528 assert!(loaded.clamped.is_empty());
529 assert!(!loaded.legacy_selectors, "a preset written now is not an old one");
530
531 let _ = std::fs::remove_dir_all(&dir);
532 }
533
534 #[test]
543 fn a_selector_survives_the_bank_growing() {
544 use phosphor_dsp::drum_rack;
545
546 let dir = scratch("bank-grew");
547 let mut panel = drum_rack::PARAM_DEFAULTS.to_vec();
548 panel[drum_rack::P_KIT] = drum_rack::kit_knob(1);
549 assert_eq!(
550 drum_rack::discrete_label(drum_rack::P_KIT, panel[drum_rack::P_KIT]),
551 Some("909"),
552 "this test is pinned to the 909 being position 1"
553 );
554
555 let mut bank = PresetFile::new(InstrumentType::DrumRack);
556 bank.store("my kit", InstrumentType::DrumRack, &panel).unwrap();
557 assert_eq!(
558 bank.presets[0].discrete.iter().find(|s| s.param == drum_rack::P_KIT),
559 Some(&SessionSelector { param: drum_rack::P_KIT, index: 1 }),
560 "the kit was not stored by position"
561 );
562
563 bank.presets[0].params[drum_rack::P_KIT] = 1.5 / 10.0;
567 save_bank(&dir, InstrumentType::DrumRack, &bank).unwrap();
568
569 let reopened = load_bank(&dir, InstrumentType::DrumRack).unwrap();
571 assert_eq!(
572 drum_rack::discrete_label(drum_rack::P_KIT, reopened.presets[0].params[drum_rack::P_KIT]),
573 Some("707"),
574 "the fraction no longer names the 707, so this test proves nothing"
575 );
576 let loaded = reopened
578 .params_at(0, InstrumentType::DrumRack, panel.len())
579 .unwrap()
580 .expect("its own panel should load");
581 assert_eq!(
582 drum_rack::discrete_label(drum_rack::P_KIT, loaded.params[drum_rack::P_KIT]),
583 Some("909"),
584 "the preset opened on a different drum machine"
585 );
586 assert!(loaded.clamped.is_empty());
587 assert!(!loaded.legacy_selectors);
588
589 let _ = std::fs::remove_dir_all(&dir);
590 }
591
592 #[test]
596 fn both_dx7_selectors_survive_a_round_trip() {
597 use phosphor_dsp::dx7;
598
599 let mut panel = dx7::PARAM_DEFAULTS.to_vec();
600 let (bank_knob, patch_knob) = dx7::voice_knobs(147);
601 panel[dx7::P_BANK] = bank_knob;
602 panel[dx7::P_PATCH] = patch_knob;
603
604 let mut bank = PresetFile::new(InstrumentType::DX7);
605 bank.store("timpani", InstrumentType::DX7, &panel).unwrap();
606
607 let stored: Vec<usize> = bank.presets[0].discrete.iter().map(|s| s.param).collect();
608 assert!(stored.contains(&dx7::P_BANK), "the cartridge was not stored");
609 assert!(stored.contains(&dx7::P_PATCH), "the voice was not stored");
610
611 bank.presets[0].params[dx7::P_BANK] = 0.0;
614 bank.presets[0].params[dx7::P_PATCH] = 0.0;
615
616 let loaded = bank
617 .params_at(0, InstrumentType::DX7, panel.len())
618 .unwrap()
619 .expect("its own panel should load");
620 assert_eq!(loaded.params[dx7::P_BANK], bank_knob, "the cartridge did not come back");
621 assert_eq!(loaded.params[dx7::P_PATCH], patch_knob, "the voice did not come back");
622 }
623
624 #[test]
628 fn a_selector_past_the_end_of_the_bank_is_clamped_and_reported() {
629 use phosphor_dsp::drum_rack;
630
631 let panel = drum_rack::PARAM_DEFAULTS.to_vec();
632 let mut bank = PresetFile::new(InstrumentType::DrumRack);
633 bank.store("from the future", InstrumentType::DrumRack, &panel).unwrap();
634 let selector = bank.presets[0]
635 .discrete
636 .iter_mut()
637 .find(|s| s.param == drum_rack::P_KIT)
638 .expect("the kit is a selector");
639 selector.index = 900;
640
641 let loaded = bank
642 .params_at(0, InstrumentType::DrumRack, panel.len())
643 .unwrap()
644 .expect("its own panel should load");
645 assert_eq!(
646 loaded.clamped,
647 vec![(drum_rack::P_KIT, 900, drum_rack::KIT_COUNT - 1)],
648 "a position the rack no longer has was not reported"
649 );
650 assert_eq!(
651 loaded.params[drum_rack::P_KIT],
652 drum_rack::kit_knob(drum_rack::KIT_COUNT - 1),
653 "the kit did not land on the last one the rack has"
654 );
655 }
656
657 #[test]
663 fn a_version_1_preset_loads_from_its_fractions_and_says_so() {
664 use phosphor_dsp::drum_rack;
665
666 let dir = scratch("version-1");
667 let params: Vec<String> = drum_rack::PARAM_DEFAULTS
670 .iter()
671 .enumerate()
672 .map(|(i, v)| {
673 if i == drum_rack::P_KIT { (1.5f32 / 10.0).to_string() } else { v.to_string() }
674 })
675 .collect();
676 let json = format!(
677 r#"{{"version":1,"instrument":"drums","presets":[{{"name":"old",
678 "instrument":"drums","layout":"{}","param_count":{},"params":[{}]}}]}}"#,
679 layout_fingerprint(InstrumentType::DrumRack),
680 drum_rack::PARAM_COUNT,
681 params.join(",")
682 );
683 std::fs::create_dir_all(&dir).unwrap();
684 std::fs::write(bank_path(&dir, InstrumentType::DrumRack), json).unwrap();
685
686 let bank = load_bank(&dir, InstrumentType::DrumRack).unwrap();
687 assert_eq!(bank.version, 1);
688 assert_eq!(bank.presets[0].version, LEGACY_VERSION, "a missing version is version 1");
689 assert!(bank.presets[0].discrete.is_empty());
690
691 let loaded = bank
692 .params_at(0, InstrumentType::DrumRack, drum_rack::PARAM_COUNT)
693 .unwrap()
694 .expect("an old preset still loads");
695 assert!(loaded.legacy_selectors, "an old preset loaded without a word said");
696 assert_eq!(
699 drum_rack::discrete_label(drum_rack::P_KIT, loaded.params[drum_rack::P_KIT]),
700 Some("707")
701 );
702
703 let _ = std::fs::remove_dir_all(&dir);
704 }
705
706 #[test]
710 fn every_selector_on_every_instrument_is_stored() {
711 for instrument in InstrumentType::ALL {
712 let count = param_count(*instrument);
713 let panel = vec![0.5f32; count];
714 let mut bank = PresetFile::new(*instrument);
715 bank.store("all", *instrument, &panel).unwrap();
716
717 let stored: Vec<usize> =
718 bank.presets[0].discrete.iter().map(|s| s.param).collect();
719 let wanted: Vec<usize> = (0..count)
720 .filter(|&p| crate::discrete::is_discrete(*instrument, p))
721 .collect();
722 assert_eq!(stored, wanted, "{instrument:?} did not store all of its selectors");
723 assert!(
724 !wanted.is_empty() || instrument.is_sequencer(),
725 "{instrument:?} has no selectors at all"
726 );
727 }
728 }
729
730 #[test]
733 fn a_bank_that_does_not_exist_is_empty() {
734 let dir = scratch("missing");
735 let bank = load_bank(&dir, InstrumentType::DX7).unwrap();
736 assert!(bank.presets.is_empty());
737 assert_eq!(bank.instrument, "dx7");
738 }
739
740 #[test]
745 fn a_preset_with_the_wrong_control_count_is_refused() {
746 let dir = scratch("count");
747 let mut bank = PresetFile::new(InstrumentType::Juno60);
748 bank.store("old panel", InstrumentType::Juno60, &juno_panel()).unwrap();
749 bank.presets[0].params.truncate(16);
751 bank.presets[0].param_count = 16;
752 save_bank(&dir, InstrumentType::Juno60, &bank).unwrap();
753
754 let reopened = load_bank(&dir, InstrumentType::Juno60).unwrap();
755 let want = param_count(InstrumentType::Juno60);
756 assert_eq!(
757 reopened.params_at(0, InstrumentType::Juno60, want).unwrap(),
758 Err(PresetError::ParamCountMismatch { saved: 16, wanted: want })
759 );
760
761 let _ = std::fs::remove_dir_all(&dir);
762 }
763
764 #[test]
768 fn a_preset_from_a_reordered_panel_is_refused() {
769 let panel = juno_panel();
770 let mut preset = Preset {
771 name: "reordered".into(),
772 instrument: "juno60".into(),
773 layout: layout_fingerprint(InstrumentType::Juno60),
774 param_count: panel.len(),
775 params: panel,
776 discrete: Vec::new(),
777 version: FORMAT_VERSION,
778 };
779 assert_eq!(preset.check(InstrumentType::Juno60, 25), Ok(()));
780
781 preset.layout = "0000000000000000".into();
783 assert!(matches!(
784 preset.check(InstrumentType::Juno60, 25),
785 Err(PresetError::LayoutMismatch { .. })
786 ));
787 }
788
789 #[test]
792 fn the_fingerprint_separates_every_instrument() {
793 let mut seen = Vec::new();
794 for inst in InstrumentType::ALL {
795 let fp = layout_fingerprint(*inst);
796 assert_eq!(fp.len(), 16, "{fp} is not a 64-bit fingerprint");
797 seen.push((inst, fp));
798 }
799 for (a, fa) in &seen {
802 for (b, fb) in &seen {
803 let shared_panel = matches!(
804 (a, b),
805 (InstrumentType::Synth, InstrumentType::Sampler)
806 | (InstrumentType::Sampler, InstrumentType::Synth)
807 );
808 if a != b && !shared_panel {
809 assert_ne!(fa, fb, "{a:?} and {b:?} fingerprint the same");
810 }
811 }
812 }
813 }
814
815 #[test]
818 fn a_preset_saved_for_another_instrument_is_refused() {
819 let dir = scratch("instrument");
820 let mut dx7 = PresetFile::new(InstrumentType::DX7);
821 dx7.store("e.piano", InstrumentType::DX7, &phosphor_dsp::dx7::PARAM_DEFAULTS).unwrap();
822
823 let mut juno = PresetFile::new(InstrumentType::Juno60);
825 juno.presets.push(dx7.presets[0].clone());
826 save_bank(&dir, InstrumentType::Juno60, &juno).unwrap();
827
828 let reopened = load_bank(&dir, InstrumentType::Juno60).unwrap();
829 assert_eq!(
830 reopened.params_at(0, InstrumentType::Juno60, 9).unwrap(),
831 Err(PresetError::WrongInstrument {
832 saved: "dx7".into(),
833 wanted: "juno60".into()
834 }),
835 "a DX7 preset loaded into a Juno"
836 );
837
838 assert_ne!(
840 bank_path(&dir, InstrumentType::DX7),
841 bank_path(&dir, InstrumentType::Juno60)
842 );
843
844 let _ = std::fs::remove_dir_all(&dir);
845 }
846
847 #[test]
850 fn saving_over_a_name_replaces_it_in_place() {
851 let mut bank = PresetFile::new(InstrumentType::Juno60);
852 let mut first = juno_panel();
853 first[phosphor_dsp::juno::P_CUTOFF] = 0.2;
854 let mut second = juno_panel();
855 second[phosphor_dsp::juno::P_CUTOFF] = 0.9;
856
857 bank.store("brass", InstrumentType::Juno60, &first).unwrap();
858 bank.store("strings", InstrumentType::Juno60, &juno_panel()).unwrap();
859 assert_eq!(
860 bank.store("brass", InstrumentType::Juno60, &second),
861 Ok(StoreOutcome::Replaced)
862 );
863
864 assert_eq!(bank.names(), vec!["brass", "strings"], "the slot moved or duplicated");
865 assert_eq!(bank.presets[0].params[phosphor_dsp::juno::P_CUTOFF], 0.9);
866
867 assert_eq!(
870 bank.store(" brass ", InstrumentType::Juno60, &first),
871 Ok(StoreOutcome::Replaced)
872 );
873 assert_eq!(bank.presets.len(), 2);
874 }
875
876 #[test]
878 fn the_bank_stops_at_its_limit() {
879 let mut bank = PresetFile::new(InstrumentType::Juno60);
880 let panel = juno_panel();
881 for i in 0..MAX_PRESETS {
882 bank.store(&format!("p{i}"), InstrumentType::Juno60, &panel).unwrap();
883 }
884 assert_eq!(
885 bank.store("one more", InstrumentType::Juno60, &panel),
886 Err(PresetError::BankFull { max: MAX_PRESETS })
887 );
888 assert_eq!(
889 bank.store("p0", InstrumentType::Juno60, &panel),
890 Ok(StoreOutcome::Replaced),
891 "a full bank became read-only"
892 );
893
894 bank.remove(0);
895 assert_eq!(bank.presets.len(), MAX_PRESETS - 1);
896 assert_eq!(
897 bank.store("one more", InstrumentType::Juno60, &panel),
898 Ok(StoreOutcome::Added)
899 );
900 }
901
902 #[test]
904 fn names_are_bounded_and_non_empty() {
905 let mut bank = PresetFile::new(InstrumentType::Juno60);
906 let panel = juno_panel();
907 assert_eq!(
908 bank.store(" ", InstrumentType::Juno60, &panel),
909 Err(PresetError::NameEmpty)
910 );
911 let long = "x".repeat(MAX_NAME_LEN + 1);
912 assert_eq!(
913 bank.store(&long, InstrumentType::Juno60, &panel),
914 Err(PresetError::NameTooLong { len: MAX_NAME_LEN + 1, max: MAX_NAME_LEN })
915 );
916 assert!(bank.presets.is_empty());
917 }
918
919 #[test]
922 fn a_preset_that_contradicts_itself_is_refused() {
923 let panel = juno_panel();
924 let preset = Preset {
925 name: "hand edited".into(),
926 instrument: "juno60".into(),
927 layout: layout_fingerprint(InstrumentType::Juno60),
928 param_count: 99,
929 params: panel.clone(),
930 discrete: Vec::new(),
931 version: FORMAT_VERSION,
932 };
933 assert_eq!(
934 preset.check(InstrumentType::Juno60, panel.len()),
935 Err(PresetError::Corrupt { declared: 99, actual: panel.len() })
936 );
937 }
938
939 #[test]
946 fn every_instrument_has_a_panel() {
947 for inst in InstrumentType::ALL {
948 if inst.is_sequencer() {
949 assert_eq!(param_count(*inst), 0, "the sequencer grew a panel");
950 continue;
951 }
952 assert!(param_count(*inst) > 0, "{inst:?} has no parameters");
953 assert_eq!(
954 crate::preset::defaults(*inst).len(),
955 param_count(*inst),
956 "{inst:?} is born with a block of the wrong size"
957 );
958 }
959 assert_eq!(param_count(InstrumentType::Juno60), phosphor_dsp::juno::PARAM_COUNT);
960 assert_eq!(param_count(InstrumentType::Jupiter8), phosphor_dsp::jupiter::PARAM_COUNT);
961 assert_eq!(param_count(InstrumentType::DX7), phosphor_dsp::dx7::PARAM_COUNT);
962 assert_eq!(param_count(InstrumentType::Odyssey), phosphor_dsp::odyssey::PARAM_COUNT);
963 assert_eq!(param_count(InstrumentType::Rhodes), phosphor_dsp::rhodes::PARAM_COUNT);
964 assert_eq!(param_count(InstrumentType::DrumRack), phosphor_dsp::drum_rack::PARAM_COUNT);
965 assert_eq!(param_count(InstrumentType::Synth), phosphor_dsp::synth::PARAM_COUNT);
966 assert_eq!(param_count(InstrumentType::Sampler), phosphor_dsp::synth::PARAM_COUNT);
967 assert_eq!(param_count(InstrumentType::LittlePhatty), phosphor_dsp::phatty::PARAM_COUNT);
968 assert_eq!(param_count(InstrumentType::Prophet6), phosphor_dsp::prophet6::PARAM_COUNT);
969 }
970}