//! Device profile types for device-aware export. //! //! These types describe the constraints of a hardware sampler — supported formats, //! sample rates, bit depths, channel counts, filename rules, and size limits. //! Profiles are parsed from TOML manifests by the `audiofiles-rhai` crate; //! `audiofiles-core` stays Rhai-independent. use serde::{Deserialize, Serialize}; use super::ExportFormat; /// A device profile describing the constraints of a hardware sampler. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DeviceProfile { /// Human-readable device name (e.g. "SP-404 MKII"). pub name: String, /// Device manufacturer (e.g. "Roland"). pub manufacturer: String, /// Device category (e.g. "sampler", "groovebox", "drum machine"). #[serde(default)] pub category: Option, /// Free-form notes about the device (e.g. *"Mounts as USB drive — drag-and-drop"*). #[serde(default)] pub notes: Option, /// Audio format constraints. pub audio: AudioConstraints, /// Filename rules (case, separator, length, character stripping). pub naming: Option, /// Export limits (file size, sample count). pub limits: Option, } /// Audio format constraints for a device. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AudioConstraints { /// Supported output formats (e.g. WAV, AIFF). pub formats: Vec, /// Supported sample rates in Hz (e.g. [44100, 48000]). pub sample_rates: Vec, /// Supported bit depths (e.g. [16, 24]). pub bit_depths: Vec, /// Channel constraint: mono only, stereo only, or both. pub channels: ChannelConstraint, } /// Whether a device supports mono, stereo, or both. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum ChannelConstraint { Mono, Stereo, Both, } /// Filename rules enforced by a device. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NamingRules { /// Case transformation: lower, upper, or keep original. pub case: NamingCase, /// Separator character for word boundaries (e.g. '_', '-'). pub separator: char, /// Maximum filename length (stem only, not including extension). pub max_length: usize, /// Strip characters outside [A-Za-z0-9] and the separator. pub strip_special: bool, } /// Filename case transformation. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum NamingCase { Lower, Upper, Original, } /// Export-level constraints (file size, sample count). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExportConstraints { /// Maximum file size in bytes (after encoding). pub max_file_size_bytes: Option, /// Maximum number of samples that can be loaded on the device. pub max_sample_count: Option, } /// Summary info for listing available profiles in the UI. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DeviceProfileSummary { /// Device name. pub name: String, /// Manufacturer. pub manufacturer: String, /// Whether this is a bundled (built-in) or user plugin. pub bundled: bool, /// Maximum file size in bytes (from profile limits), if any. #[serde(default)] pub max_file_size_bytes: Option, /// Human-readable summary of the device's audio constraints, e.g. /// *"WAV \u{00B7} 44.1k \u{00B7} 16-bit \u{00B7} Mono"*. Computed by the /// registry from the underlying `AudioConstraints` so the UI doesn't have /// to refetch full profiles to surface the values (M-6). #[serde(default)] pub format_summary: Option, /// Device category from the manifest (p-5). #[serde(default)] pub category: Option, /// Free-form notes from the manifest (p-5). #[serde(default)] pub notes: Option, } /// Format an `AudioConstraints` set as a single-line summary suitable for /// muted display under the profile picker. Joins formats / rates / depths / /// channels with middle dots; collapses multi-element lists with `/`. pub fn format_audio_constraints(c: &AudioConstraints) -> String { let fmt = c .formats .iter() .map(|f| match f { ExportFormat::Wav => "WAV", ExportFormat::Aiff => "AIFF", ExportFormat::Original => "Original", }) .collect::>() .join("/"); let rates = c .sample_rates .iter() .map(|r| { if *r >= 1000 && r % 1000 == 0 { format!("{}k", r / 1000) } else if *r >= 1000 { format!("{:.1}k", *r as f64 / 1000.0) } else { r.to_string() } }) .collect::>() .join("/"); let depths = c .bit_depths .iter() .map(|d| d.to_string()) .collect::>() .join("/"); let channels = match c.channels { ChannelConstraint::Mono => "Mono", ChannelConstraint::Stereo => "Stereo", ChannelConstraint::Both => "Mono+Stereo", }; let mut parts: Vec = Vec::new(); if !fmt.is_empty() { parts.push(fmt); } if !rates.is_empty() { parts.push(rates); } if !depths.is_empty() { parts.push(format!("{depths}-bit")); } parts.push(channels.to_string()); parts.join(" \u{00B7} ") } #[cfg(test)] mod tests { use super::*; #[test] fn profile_serializes_roundtrip() { let profile = DeviceProfile { name: "SP-404 MKII".to_string(), manufacturer: "Roland".to_string(), category: Some("sampler".to_string()), notes: Some("Mounts as USB drive".to_string()), audio: AudioConstraints { formats: vec![ExportFormat::Wav], sample_rates: vec![44100, 48000], bit_depths: vec![16, 24], channels: ChannelConstraint::Both, }, naming: Some(NamingRules { case: NamingCase::Upper, separator: '_', max_length: 12, strip_special: true, }), limits: Some(ExportConstraints { max_file_size_bytes: Some(134_217_728), max_sample_count: None, }), }; let json = serde_json::to_string(&profile).unwrap(); let deserialized: DeviceProfile = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.name, "SP-404 MKII"); assert_eq!(deserialized.audio.sample_rates, vec![44100, 48000]); assert_eq!(deserialized.category.as_deref(), Some("sampler")); assert_eq!(deserialized.notes.as_deref(), Some("Mounts as USB drive")); assert_eq!( deserialized.naming.as_ref().unwrap().case, NamingCase::Upper ); } #[test] fn profile_without_optional_fields() { let profile = DeviceProfile { name: "Generic".to_string(), manufacturer: "Unknown".to_string(), category: None, notes: None, audio: AudioConstraints { formats: vec![ExportFormat::Wav], sample_rates: vec![44100], bit_depths: vec![16], channels: ChannelConstraint::Stereo, }, naming: None, limits: None, }; let json = serde_json::to_string(&profile).unwrap(); let deserialized: DeviceProfile = serde_json::from_str(&json).unwrap(); assert!(deserialized.naming.is_none()); assert!(deserialized.limits.is_none()); } }