Skip to main content

max / audiofiles

7.6 KB · 227 lines History Blame Raw
1 //! Device profile types for device-aware export.
2 //!
3 //! These types describe the constraints of a hardware sampler — supported formats,
4 //! sample rates, bit depths, channel counts, filename rules, and size limits.
5 //! Profiles are parsed from TOML manifests by the `audiofiles-rhai` crate;
6 //! `audiofiles-core` stays Rhai-independent.
7
8 use serde::{Deserialize, Serialize};
9
10 use super::ExportFormat;
11
12 /// A device profile describing the constraints of a hardware sampler.
13 #[derive(Debug, Clone, Serialize, Deserialize)]
14 pub struct DeviceProfile {
15 /// Human-readable device name (e.g. "SP-404 MKII").
16 pub name: String,
17 /// Device manufacturer (e.g. "Roland").
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>,
25 /// Audio format constraints.
26 pub audio: AudioConstraints,
27 /// Filename rules (case, separator, length, character stripping).
28 pub naming: Option<NamingRules>,
29 /// Export limits (file size, sample count).
30 pub limits: Option<ExportConstraints>,
31 }
32
33 /// Audio format constraints for a device.
34 #[derive(Debug, Clone, Serialize, Deserialize)]
35 pub struct AudioConstraints {
36 /// Supported output formats (e.g. WAV, AIFF).
37 pub formats: Vec<ExportFormat>,
38 /// Supported sample rates in Hz (e.g. [44100, 48000]).
39 pub sample_rates: Vec<u32>,
40 /// Supported bit depths (e.g. [16, 24]).
41 pub bit_depths: Vec<u16>,
42 /// Channel constraint: mono only, stereo only, or both.
43 pub channels: ChannelConstraint,
44 }
45
46 /// Whether a device supports mono, stereo, or both.
47 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48 pub enum ChannelConstraint {
49 Mono,
50 Stereo,
51 Both,
52 }
53
54 /// Filename rules enforced by a device.
55 #[derive(Debug, Clone, Serialize, Deserialize)]
56 pub struct NamingRules {
57 /// Case transformation: lower, upper, or keep original.
58 pub case: NamingCase,
59 /// Separator character for word boundaries (e.g. '_', '-').
60 pub separator: char,
61 /// Maximum filename length (stem only, not including extension).
62 pub max_length: usize,
63 /// Strip characters outside [A-Za-z0-9] and the separator.
64 pub strip_special: bool,
65 }
66
67 /// Filename case transformation.
68 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69 pub enum NamingCase {
70 Lower,
71 Upper,
72 Original,
73 }
74
75 /// Export-level constraints (file size, sample count).
76 #[derive(Debug, Clone, Serialize, Deserialize)]
77 pub struct ExportConstraints {
78 /// Maximum file size in bytes (after encoding).
79 pub max_file_size_bytes: Option<u64>,
80 /// Maximum number of samples that can be loaded on the device.
81 pub max_sample_count: Option<usize>,
82 }
83
84 /// Summary info for listing available profiles in the UI.
85 #[derive(Debug, Clone, Serialize, Deserialize)]
86 pub struct DeviceProfileSummary {
87 /// Device name.
88 pub name: String,
89 /// Manufacturer.
90 pub manufacturer: String,
91 /// Whether this is a bundled (built-in) or user plugin.
92 pub bundled: bool,
93 /// Maximum file size in bytes (from profile limits), if any.
94 #[serde(default)]
95 pub max_file_size_bytes: Option<u64>,
96 /// Human-readable summary of the device's audio constraints, e.g.
97 /// *"WAV \u{00B7} 44.1k \u{00B7} 16-bit \u{00B7} Mono"*. Computed by the
98 /// registry from the underlying `AudioConstraints` so the UI doesn't have
99 /// to refetch full profiles to surface the values (M-6).
100 #[serde(default)]
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>,
108 }
109
110 /// Format an `AudioConstraints` set as a single-line summary suitable for
111 /// muted display under the profile picker. Joins formats / rates / depths /
112 /// channels with middle dots; collapses multi-element lists with `/`.
113 pub fn format_audio_constraints(c: &AudioConstraints) -> String {
114 let fmt = c
115 .formats
116 .iter()
117 .map(|f| match f {
118 ExportFormat::Wav => "WAV",
119 ExportFormat::Aiff => "AIFF",
120 ExportFormat::Original => "Original",
121 })
122 .collect::<Vec<_>>()
123 .join("/");
124 let rates = c
125 .sample_rates
126 .iter()
127 .map(|r| {
128 if *r >= 1000 && r % 1000 == 0 {
129 format!("{}k", r / 1000)
130 } else if *r >= 1000 {
131 format!("{:.1}k", *r as f64 / 1000.0)
132 } else {
133 r.to_string()
134 }
135 })
136 .collect::<Vec<_>>()
137 .join("/");
138 let depths = c
139 .bit_depths
140 .iter()
141 .map(|d| d.to_string())
142 .collect::<Vec<_>>()
143 .join("/");
144 let channels = match c.channels {
145 ChannelConstraint::Mono => "Mono",
146 ChannelConstraint::Stereo => "Stereo",
147 ChannelConstraint::Both => "Mono+Stereo",
148 };
149 let mut parts: Vec<String> = Vec::new();
150 if !fmt.is_empty() {
151 parts.push(fmt);
152 }
153 if !rates.is_empty() {
154 parts.push(rates);
155 }
156 if !depths.is_empty() {
157 parts.push(format!("{depths}-bit"));
158 }
159 parts.push(channels.to_string());
160 parts.join(" \u{00B7} ")
161 }
162
163 #[cfg(test)]
164 mod tests {
165 use super::*;
166
167 #[test]
168 fn profile_serializes_roundtrip() {
169 let profile = DeviceProfile {
170 name: "SP-404 MKII".to_string(),
171 manufacturer: "Roland".to_string(),
172 category: Some("sampler".to_string()),
173 notes: Some("Mounts as USB drive".to_string()),
174 audio: AudioConstraints {
175 formats: vec![ExportFormat::Wav],
176 sample_rates: vec![44100, 48000],
177 bit_depths: vec![16, 24],
178 channels: ChannelConstraint::Both,
179 },
180 naming: Some(NamingRules {
181 case: NamingCase::Upper,
182 separator: '_',
183 max_length: 12,
184 strip_special: true,
185 }),
186 limits: Some(ExportConstraints {
187 max_file_size_bytes: Some(134_217_728),
188 max_sample_count: None,
189 }),
190 };
191
192 let json = serde_json::to_string(&profile).unwrap();
193 let deserialized: DeviceProfile = serde_json::from_str(&json).unwrap();
194 assert_eq!(deserialized.name, "SP-404 MKII");
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"));
198 assert_eq!(
199 deserialized.naming.as_ref().unwrap().case,
200 NamingCase::Upper
201 );
202 }
203
204 #[test]
205 fn profile_without_optional_fields() {
206 let profile = DeviceProfile {
207 name: "Generic".to_string(),
208 manufacturer: "Unknown".to_string(),
209 category: None,
210 notes: None,
211 audio: AudioConstraints {
212 formats: vec![ExportFormat::Wav],
213 sample_rates: vec![44100],
214 bit_depths: vec![16],
215 channels: ChannelConstraint::Stereo,
216 },
217 naming: None,
218 limits: None,
219 };
220
221 let json = serde_json::to_string(&profile).unwrap();
222 let deserialized: DeviceProfile = serde_json::from_str(&json).unwrap();
223 assert!(deserialized.naming.is_none());
224 assert!(deserialized.limits.is_none());
225 }
226 }
227