Skip to main content

max / audiofiles

device profiles: add category and notes fields Add optional `category` (e.g. "sampler", "groovebox") and `notes` (free-form per-device hints) to DeviceProfile, DeviceProfileSummary, and the plugin manifest schema. Plumbed through manifest_to_profile and the registry. Backfill the bundled SP-404 MKII manifest with the new fields to anchor the format. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-21 21:52 UTC
Commit: 17c31e708e23524ab623333e278bc2983ddccda7
Parent: de992af
4 files changed, +55 insertions, -0 deletions
@@ -16,6 +16,12 @@ pub struct DeviceProfile {
16 16 pub name: String,
17 17 /// Device manufacturer (e.g. "Roland").
18 18 pub manufacturer: String,
19 + /// Device category (e.g. "sampler", "groovebox", "drum machine").
20 + #[serde(default)]
21 + pub category: Option<String>,
22 + /// Free-form notes about the device (e.g. *"Mounts as USB drive — drag-and-drop"*).
23 + #[serde(default)]
24 + pub notes: Option<String>,
19 25 /// Audio format constraints.
20 26 pub audio: AudioConstraints,
21 27 /// Filename rules (case, separator, length, character stripping).
@@ -93,6 +99,12 @@ pub struct DeviceProfileSummary {
93 99 /// to refetch full profiles to surface the values (M-6).
94 100 #[serde(default)]
95 101 pub format_summary: Option<String>,
102 + /// Device category from the manifest (p-5).
103 + #[serde(default)]
104 + pub category: Option<String>,
105 + /// Free-form notes from the manifest (p-5).
106 + #[serde(default)]
107 + pub notes: Option<String>,
96 108 }
97 109
98 110 /// Format an `AudioConstraints` set as a single-line summary suitable for
@@ -157,6 +169,8 @@ mod tests {
157 169 let profile = DeviceProfile {
158 170 name: "SP-404 MKII".to_string(),
159 171 manufacturer: "Roland".to_string(),
172 + category: Some("sampler".to_string()),
173 + notes: Some("Mounts as USB drive".to_string()),
160 174 audio: AudioConstraints {
161 175 formats: vec![ExportFormat::Wav],
162 176 sample_rates: vec![44100, 48000],
@@ -179,6 +193,8 @@ mod tests {
179 193 let deserialized: DeviceProfile = serde_json::from_str(&json).unwrap();
180 194 assert_eq!(deserialized.name, "SP-404 MKII");
181 195 assert_eq!(deserialized.audio.sample_rates, vec![44100, 48000]);
196 + assert_eq!(deserialized.category.as_deref(), Some("sampler"));
197 + assert_eq!(deserialized.notes.as_deref(), Some("Mounts as USB drive"));
182 198 assert_eq!(
183 199 deserialized.naming.as_ref().unwrap().case,
184 200 NamingCase::Upper
@@ -190,6 +206,8 @@ mod tests {
190 206 let profile = DeviceProfile {
191 207 name: "Generic".to_string(),
192 208 manufacturer: "Unknown".to_string(),
209 + category: None,
210 + notes: None,
193 211 audio: AudioConstraints {
194 212 formats: vec![ExportFormat::Wav],
195 213 sample_rates: vec![44100],
@@ -2,6 +2,8 @@
2 2 name = "SP-404 MKII"
3 3 manufacturer = "Roland"
4 4 version = "1.0"
5 + category = "sampler"
6 + notes = "Hold MIDI when powering on to mount as USB drive."
5 7
6 8 [audio]
7 9 formats = ["wav"]
@@ -26,6 +26,10 @@ pub struct DeviceSection {
26 26 pub name: String,
27 27 pub manufacturer: String,
28 28 pub version: String,
29 + #[serde(default)]
30 + pub category: Option<String>,
31 + #[serde(default)]
32 + pub notes: Option<String>,
29 33 }
30 34
31 35 #[derive(Debug, Deserialize)]
@@ -90,6 +94,8 @@ pub fn manifest_to_profile(manifest: &PluginManifest) -> Result<DeviceProfile, P
90 94 Ok(DeviceProfile {
91 95 name: manifest.device.name.clone(),
92 96 manufacturer: manifest.device.manufacturer.clone(),
97 + category: manifest.device.category.clone(),
98 + notes: manifest.device.notes.clone(),
93 99 audio: AudioConstraints {
94 100 formats,
95 101 sample_rates: manifest.audio.sample_rates.clone(),
@@ -224,6 +230,31 @@ max_file_size_bytes = 134217728
224 230 }
225 231
226 232 #[test]
233 + fn manifest_with_category_and_notes() {
234 + let toml = r#"
235 + [device]
236 + name = "OP-1"
237 + manufacturer = "Teenage Engineering"
238 + version = "1.0"
239 + category = "synthesizer"
240 + notes = "Mounts as USB drive when held with shift on boot."
241 +
242 + [audio]
243 + formats = ["aiff"]
244 + sample_rates = [44100]
245 + bit_depths = [16]
246 + channels = "mono"
247 + "#;
248 + let manifest = parse_manifest(toml).unwrap();
249 + let profile = manifest_to_profile(&manifest).unwrap();
250 + assert_eq!(profile.category.as_deref(), Some("synthesizer"));
251 + assert_eq!(
252 + profile.notes.as_deref(),
253 + Some("Mounts as USB drive when held with shift on boot.")
254 + );
255 + }
256 +
257 + #[test]
227 258 fn minimal_manifest_no_naming_no_limits() {
228 259 let toml = r#"
229 260 [device]
@@ -88,6 +88,8 @@ impl PluginRegistry {
88 88 format_summary: Some(audiofiles_core::export::profile::format_audio_constraints(
89 89 &p.profile.audio,
90 90 )),
91 + category: p.profile.category.clone(),
92 + notes: p.profile.notes.clone(),
91 93 })
92 94 .collect();
93 95 summaries.sort_by(|a, b| a.name.cmp(&b.name));
@@ -117,6 +119,8 @@ mod tests {
117 119 DeviceProfile {
118 120 name: name.to_string(),
119 121 manufacturer: "Test".to_string(),
122 + category: None,
123 + notes: None,
120 124 audio: AudioConstraints {
121 125 formats: vec![ExportFormat::Wav],
122 126 sample_rates: vec![44100],