1pub mod ableton;
23pub mod fl_studio;
24pub mod logic;
25pub mod project_data;
26pub mod reaper;
27
28use serde::{Deserialize, Serialize};
29use std::path::{Path, PathBuf};
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
33pub enum Daw {
34 AbletonLive,
35 LogicPro,
36 FlStudio,
37 Reaper,
38}
39
40impl Daw {
41 pub fn as_str(&self) -> &'static str {
43 match self {
44 Daw::AbletonLive => "ableton",
45 Daw::LogicPro => "logic",
46 Daw::FlStudio => "fl_studio",
47 Daw::Reaper => "reaper",
48 }
49 }
50
51 pub fn label(&self) -> &'static str {
53 match self {
54 Daw::AbletonLive => "Ableton Live",
55 Daw::LogicPro => "Logic Pro",
56 Daw::FlStudio => "FL Studio",
57 Daw::Reaper => "Reaper",
58 }
59 }
60
61 pub fn from_ext(ext: &str) -> Option<Self> {
63 match ext.to_lowercase().as_str() {
64 "als" | "ableton" => Some(Daw::AbletonLive),
65 "logicx" | "logic" => Some(Daw::LogicPro),
66 "flp" | "fl_studio" => Some(Daw::FlStudio),
67 "rpp" | "reaper" => Some(Daw::Reaper),
68 _ => None,
69 }
70 }
71
72 pub fn detect(path: &Path) -> Option<Self> {
74 path.extension()
75 .and_then(|e| e.to_str())
76 .and_then(Self::from_ext)
77 }
78}
79
80impl std::fmt::Display for Daw {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 f.write_str(self.label())
83 }
84}
85
86#[derive(Debug)]
88pub enum Error {
89 UnknownFormat(PathBuf),
91 Parse {
93 daw: Daw,
94 path: PathBuf,
95 message: String,
96 },
97}
98
99impl std::fmt::Display for Error {
100 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101 match self {
102 Error::UnknownFormat(path) => {
103 write!(f, "unrecognized project format: {}", path.display())
104 }
105 Error::Parse { daw, path, message } => {
106 write!(
107 f,
108 "{} parse failed for {}: {}",
109 daw,
110 path.display(),
111 message
112 )
113 }
114 }
115 }
116}
117
118impl std::error::Error for Error {}
119
120#[derive(Debug, Clone, Serialize, Deserialize, Default)]
122pub struct ParsedIntelligence {
123 pub daw: String,
124 #[serde(skip_serializing_if = "Option::is_none")]
125 pub daw_version: Option<String>,
126 #[serde(skip_serializing_if = "Option::is_none")]
127 pub bpm: Option<f64>,
128 #[serde(skip_serializing_if = "Option::is_none")]
129 pub key: Option<String>,
130 #[serde(skip_serializing_if = "Option::is_none")]
131 pub time_signature: Option<String>,
132 #[serde(skip_serializing_if = "Option::is_none")]
133 pub duration_seconds: Option<f64>,
134 #[serde(skip_serializing_if = "Option::is_none")]
135 pub swing_percentage: Option<f64>,
136 pub tracks: Vec<TrackInfo>,
137 pub plugins: Vec<PluginInfo>,
138 pub samples: Vec<SampleInfo>,
139 pub midi_tracks: Vec<MidiTrackInfo>,
140 pub markers: Vec<MarkerInfo>,
141 pub mixer: MixerInfo,
142 pub automated_params: Vec<String>,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize, Default)]
146pub struct TrackInfo {
147 pub name: String,
148 #[serde(rename = "type")]
149 pub track_type: String, pub plugins: Vec<String>,
151 #[serde(skip_serializing_if = "Option::is_none")]
152 pub color: Option<String>,
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize, Default)]
156pub struct PluginInfo {
157 pub name: String,
158 #[serde(rename = "type")]
159 pub plugin_type: String, pub count: u32,
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize, Default)]
164pub struct SampleInfo {
165 pub name: String,
166 #[serde(skip_serializing_if = "Option::is_none")]
167 pub path: Option<String>,
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize, Default)]
171pub struct MidiTrackInfo {
172 pub track_name: String,
173 pub note_count: u32,
174 #[serde(skip_serializing_if = "Option::is_none")]
175 pub pitch_low: Option<u8>,
176 #[serde(skip_serializing_if = "Option::is_none")]
177 pub pitch_high: Option<u8>,
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize, Default)]
181pub struct MarkerInfo {
182 pub name: String,
183 pub position_seconds: f64,
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize, Default)]
187pub struct MixerInfo {
188 pub total_tracks: u32,
189 pub audio_tracks: u32,
190 pub midi_tracks: u32,
191 pub return_tracks: u32,
192 pub group_tracks: u32,
193 pub has_sidechain: bool,
194}
195
196pub fn parse<P: AsRef<Path>>(path: P) -> Result<ParsedIntelligence, Error> {
198 let path = path.as_ref();
199 let daw = Daw::detect(path).ok_or_else(|| Error::UnknownFormat(path.to_path_buf()))?;
200 parse_as(path, daw)
201}
202
203pub fn parse_as<P: AsRef<Path>>(path: P, daw: Daw) -> Result<ParsedIntelligence, Error> {
205 let path = path.as_ref();
206 let result = match daw {
207 Daw::AbletonLive => ableton::parse(path),
208 Daw::LogicPro => logic::parse(path),
209 Daw::FlStudio => fl_studio::parse(path),
210 Daw::Reaper => reaper::parse(path),
211 };
212 result.map_err(|message| Error::Parse {
213 daw,
214 path: path.to_path_buf(),
215 message,
216 })
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 #[test]
224 fn parsed_intelligence_serializes_to_json() {
225 let intel = ParsedIntelligence {
226 daw: "Ableton Live".to_string(),
227 bpm: Some(128.0),
228 time_signature: Some("4/4".to_string()),
229 tracks: vec![TrackInfo {
230 name: "Drums".to_string(),
231 track_type: "audio".to_string(),
232 plugins: vec!["Drum Rack".to_string()],
233 color: None,
234 }],
235 plugins: vec![PluginInfo {
236 name: "Serum".to_string(),
237 plugin_type: "vst".to_string(),
238 count: 2,
239 }],
240 mixer: MixerInfo {
241 total_tracks: 14,
242 audio_tracks: 10,
243 midi_tracks: 3,
244 return_tracks: 1,
245 ..Default::default()
246 },
247 ..Default::default()
248 };
249 let json = serde_json::to_string(&intel).unwrap();
250 assert!(json.contains("\"daw\":\"Ableton Live\""));
251 assert!(json.contains("\"bpm\":128.0"));
252 assert!(json.contains("\"type\":\"audio\""));
253 assert!(json.contains("\"type\":\"vst\""));
254 assert!(!json.contains("\"key\""));
256 assert!(!json.contains("\"daw_version\""));
257 }
258
259 #[test]
260 fn mixer_info_defaults() {
261 let m = MixerInfo::default();
262 assert_eq!(m.total_tracks, 0);
263 assert!(!m.has_sidechain);
264 }
265
266 #[test]
267 fn detect_by_extension() {
268 assert_eq!(Daw::detect(Path::new("x.als")), Some(Daw::AbletonLive));
269 assert_eq!(Daw::detect(Path::new("x.FLP")), Some(Daw::FlStudio));
270 assert_eq!(Daw::detect(Path::new("x.logicx")), Some(Daw::LogicPro));
271 assert_eq!(Daw::detect(Path::new("x.rpp")), Some(Daw::Reaper));
272 assert_eq!(Daw::detect(Path::new("x.wav")), None);
273 assert_eq!(Daw::detect(Path::new("noext")), None);
274 }
275
276 #[test]
277 fn parse_unknown_format_errors() {
278 let err = parse("/nonexistent/song.wav").unwrap_err();
279 assert!(matches!(err, Error::UnknownFormat(_)));
280 }
281
282 #[test]
283 fn parse_nonexistent_file_errors() {
284 let err = parse("/nonexistent/project.als").unwrap_err();
285 assert!(matches!(
286 err,
287 Error::Parse {
288 daw: Daw::AbletonLive,
289 ..
290 }
291 ));
292 }
293
294 #[test]
295 fn parse_corrupt_file_errors() {
296 let dir = tempfile::tempdir().unwrap();
298 let path = dir.path().join("fake.als");
299 std::fs::write(&path, b"not a gzip file").unwrap();
300 assert!(parse(&path).is_err());
301 }
302}