stratum_dsp/analysis/
result.rs

1//! Analysis result types
2
3use serde::{Deserialize, Serialize};
4
5/// Musical key
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
7pub enum Key {
8    /// Major key (0 = C, 1 = C#, ..., 11 = B)
9    Major(u32),
10    /// Minor key (0 = C, 1 = C#, ..., 11 = B)
11    Minor(u32),
12}
13
14impl Key {
15    /// Get key name in musical notation (e.g., "C", "Am", "F#", "D#m")
16    ///
17    /// Returns standard musical notation:
18    /// - Major keys: note name only (e.g., "C", "C#", "D", "F#")
19    /// - Minor keys: note name + "m" (e.g., "Am", "C#m", "Dm", "F#m")
20    ///
21    /// # Example
22    ///
23    /// ```
24    /// use stratum_dsp::analysis::result::Key;
25    ///
26    /// assert_eq!(Key::Major(0).name(), "C");
27    /// assert_eq!(Key::Major(6).name(), "F#");
28    /// assert_eq!(Key::Minor(9).name(), "Am");
29    /// assert_eq!(Key::Minor(1).name(), "C#m");
30    /// ```
31    pub fn name(&self) -> String {
32        let note_names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
33        match self {
34            Key::Major(i) => note_names[*i as usize % 12].to_string(),
35            Key::Minor(i) => format!("{}m", note_names[*i as usize % 12]),
36        }
37    }
38    
39    /// Get key in DJ standard numerical notation (e.g., "1A", "2B", "12A")
40    ///
41    /// Uses the circle of fifths mapping popularized in DJ software:
42    /// - Major keys: 1A-12A (1A = C, 2A = G, 3A = D, ..., 12A = F)
43    /// - Minor keys: 1B-12B (1B = Am, 2B = Em, 3B = Bm, ..., 12B = Dm)
44    ///
45    /// The numbering follows the circle of fifths (up a fifth each step).
46    /// Keys are displayed in standard musical notation by default (e.g., "C", "Am", "F#").
47    ///
48    /// # Example
49    ///
50    /// ```
51    /// use stratum_dsp::analysis::result::Key;
52    ///
53    /// assert_eq!(Key::Major(0).numerical(), "1A");   // C
54    /// assert_eq!(Key::Major(7).numerical(), "2A");   // G
55    /// assert_eq!(Key::Minor(9).numerical(), "1B");   // Am
56    /// assert_eq!(Key::Minor(4).numerical(), "2B");   // Em
57    /// ```
58    pub fn numerical(&self) -> String {
59        // Circle of fifths mapping: C=0, G=7, D=2, A=9, E=4, B=11, F#=6, C#=1, G#=8, D#=3, A#=10, F=5
60        // This maps to: 1A, 2A, 3A, 4A, 5A, 6A, 7A, 8A, 9A, 10A, 11A, 12A
61        let circle_of_fifths_major = [0, 7, 2, 9, 4, 11, 6, 1, 8, 3, 10, 5]; // C, G, D, A, E, B, F#, C#, G#, D#, A#, F
62        
63        match self {
64            Key::Major(i) => {
65                let key_idx = *i as usize % 12;
66                // Find position in circle of fifths
67                let position = circle_of_fifths_major.iter()
68                    .position(|&x| x == key_idx)
69                    .unwrap_or(0);
70                format!("{}A", position + 1)
71            }
72            Key::Minor(i) => {
73                let key_idx = *i as usize % 12;
74                // Minor keys follow the same circle of fifths pattern
75                // Relative minor of 1A (C) is 1B (Am), relative minor of 2A (G) is 2B (Em), etc.
76                let circle_of_fifths_minor = [9, 4, 11, 6, 1, 8, 3, 10, 5, 0, 7, 2]; // Am, Em, Bm, F#m, C#m, G#m, D#m, A#m, Fm, Cm, Gm, Dm
77                let position = circle_of_fifths_minor.iter()
78                    .position(|&x| x == key_idx)
79                    .unwrap_or(0);
80                format!("{}B", position + 1)
81            }
82        }
83    }
84    
85    /// Get key from DJ standard numerical notation
86    ///
87    /// Converts numerical notation (e.g., "1A", "2B", "12A") back to a Key.
88    ///
89    /// # Arguments
90    ///
91    /// * `notation` - Numerical key notation (e.g., "1A", "2B", "12A")
92    ///
93    /// # Returns
94    ///
95    /// `Some(Key)` if valid, `None` if invalid format
96    ///
97    /// # Example
98    ///
99    /// ```
100    /// use stratum_dsp::analysis::result::Key;
101    ///
102    /// assert_eq!(Key::from_numerical("1A"), Some(Key::Major(0)));   // C
103    /// assert_eq!(Key::from_numerical("2A"), Some(Key::Major(7)));   // G
104    /// assert_eq!(Key::from_numerical("1B"), Some(Key::Minor(9)));   // Am
105    /// assert_eq!(Key::from_numerical("2B"), Some(Key::Minor(4)));   // Em
106    /// assert_eq!(Key::from_numerical("0A"), None);  // Invalid
107    /// assert_eq!(Key::from_numerical("13A"), None); // Invalid
108    /// ```
109    pub fn from_numerical(notation: &str) -> Option<Self> {
110        if notation.len() < 2 {
111            return None;
112        }
113        
114        let (num_str, suffix) = notation.split_at(notation.len() - 1);
115        let num: u32 = num_str.parse().ok()?;
116        
117        if num < 1 || num > 12 {
118            return None;
119        }
120        
121        // Circle of fifths mapping
122        let circle_of_fifths_major = [0, 7, 2, 9, 4, 11, 6, 1, 8, 3, 10, 5]; // C, G, D, A, E, B, F#, C#, G#, D#, A#, F
123        let circle_of_fifths_minor = [9, 4, 11, 6, 1, 8, 3, 10, 5, 0, 7, 2]; // Am, Em, Bm, F#m, C#m, G#m, D#m, A#m, Fm, Cm, Gm, Dm
124        
125        match suffix {
126            "A" => {
127                let key_idx = circle_of_fifths_major[(num - 1) as usize];
128                Some(Key::Major(key_idx))
129            }
130            "B" => {
131                let key_idx = circle_of_fifths_minor[(num - 1) as usize];
132                Some(Key::Minor(key_idx))
133            }
134            _ => None,
135        }
136    }
137}
138
139/// Beat grid structure
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct BeatGrid {
142    /// Downbeat times (beat 1) in seconds
143    pub downbeats: Vec<f32>,
144    
145    /// All beat times in seconds
146    pub beats: Vec<f32>,
147    
148    /// Bar boundaries in seconds
149    pub bars: Vec<f32>,
150}
151
152/// Analysis flags
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub enum AnalysisFlag {
155    /// Multiple BPM peaks equally strong
156    MultimodalBpm,
157    /// Low key clarity (atonal/ambiguous)
158    WeakTonality,
159    /// Track has tempo drift
160    TempoVariation,
161    /// Multiple onset interpretations
162    OnsetDetectionAmbiguous,
163}
164
165/// Tempogram candidate diagnostics (for validation/tuning)
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct TempoCandidateDebug {
168    /// Candidate tempo in BPM.
169    pub bpm: f32,
170    /// Combined score used for ranking (blended FFT+autocorr, with mild priors).
171    pub score: f32,
172    /// FFT-method normalized support in [0, 1].
173    pub fft_norm: f32,
174    /// Autocorrelation-method normalized support in [0, 1].
175    pub autocorr_norm: f32,
176    /// True if this candidate was selected as the final BPM.
177    pub selected: bool,
178}
179
180/// Complete analysis result
181#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct AnalysisResult {
183    /// BPM estimate
184    pub bpm: f32,
185    
186    /// BPM confidence (0.0-1.0)
187    pub bpm_confidence: f32,
188    
189    /// Detected key
190    pub key: Key,
191    
192    /// Key confidence (0.0-1.0)
193    pub key_confidence: f32,
194    
195    /// Key clarity (0.0-1.0)
196    /// 
197    /// Measures how "tonal" vs "atonal" the track is:
198    /// - High (>0.5): Strong tonality, reliable key detection
199    /// - Medium (0.2-0.5): Moderate tonality
200    /// - Low (<0.2): Weak tonality, key detection may be unreliable
201    pub key_clarity: f32,
202    
203    /// Beat grid
204    pub beat_grid: BeatGrid,
205    
206    /// Grid stability (0.0-1.0)
207    pub grid_stability: f32,
208    
209    /// Analysis metadata
210    pub metadata: AnalysisMetadata,
211}
212
213/// Analysis metadata
214#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct AnalysisMetadata {
216    /// Audio duration in seconds
217    pub duration_seconds: f32,
218    
219    /// Sample rate in Hz
220    pub sample_rate: u32,
221    
222    /// Processing time in milliseconds
223    pub processing_time_ms: f32,
224    
225    /// Algorithm version
226    pub algorithm_version: String,
227    
228    /// Onset method consensus score
229    pub onset_method_consensus: f32,
230    
231    /// Methods used
232    pub methods_used: Vec<String>,
233    
234    /// Analysis flags
235    pub flags: Vec<AnalysisFlag>,
236    
237    /// Confidence warnings (low confidence, ambiguous results, etc.)
238    pub confidence_warnings: Vec<String>,
239
240    /// Optional: tempogram candidate list (top-N) for diagnostics.
241    #[serde(skip_serializing_if = "Option::is_none")]
242    pub tempogram_candidates: Option<Vec<TempoCandidateDebug>>,
243
244    /// Tempogram multi-resolution escalation was triggered for this track (ambiguous base estimate).
245    #[serde(skip_serializing_if = "Option::is_none")]
246    pub tempogram_multi_res_triggered: Option<bool>,
247
248    /// Tempogram multi-resolution result was selected over the base single-resolution estimate.
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub tempogram_multi_res_used: Option<bool>,
251
252    /// Tempogram percussive-only fallback was triggered (ambiguous estimate + HPSS enabled).
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub tempogram_percussive_triggered: Option<bool>,
255
256    /// Tempogram percussive-only fallback was selected over the current estimate.
257    #[serde(skip_serializing_if = "Option::is_none")]
258    pub tempogram_percussive_used: Option<bool>,
259}
260
261// Re-export for convenience
262pub use Key as KeyType;
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267    
268    #[test]
269    fn test_key_name_major() {
270        assert_eq!(Key::Major(0).name(), "C");
271        assert_eq!(Key::Major(1).name(), "C#");
272        assert_eq!(Key::Major(2).name(), "D");
273        assert_eq!(Key::Major(6).name(), "F#");
274        assert_eq!(Key::Major(11).name(), "B");
275    }
276    
277    #[test]
278    fn test_key_name_minor() {
279        assert_eq!(Key::Minor(0).name(), "Cm");
280        assert_eq!(Key::Minor(1).name(), "C#m");
281        assert_eq!(Key::Minor(2).name(), "Dm");
282        assert_eq!(Key::Minor(9).name(), "Am");
283        assert_eq!(Key::Minor(11).name(), "Bm");
284    }
285    
286    #[test]
287    fn test_key_numerical_major() {
288        // Circle of fifths: C=1A, G=2A, D=3A, A=4A, E=5A, B=6A, F#=7A, C#=8A, G#=9A, D#=10A, A#=11A, F=12A
289        assert_eq!(Key::Major(0).numerical(), "1A");   // C
290        assert_eq!(Key::Major(7).numerical(), "2A");   // G
291        assert_eq!(Key::Major(2).numerical(), "3A");   // D
292        assert_eq!(Key::Major(9).numerical(), "4A");   // A
293        assert_eq!(Key::Major(4).numerical(), "5A");   // E
294        assert_eq!(Key::Major(11).numerical(), "6A");  // B
295        assert_eq!(Key::Major(6).numerical(), "7A");   // F#
296        assert_eq!(Key::Major(1).numerical(), "8A");   // C#
297        assert_eq!(Key::Major(8).numerical(), "9A");   // G#
298        assert_eq!(Key::Major(3).numerical(), "10A");  // D#
299        assert_eq!(Key::Major(10).numerical(), "11A"); // A#
300        assert_eq!(Key::Major(5).numerical(), "12A"); // F
301    }
302    
303    #[test]
304    fn test_key_numerical_minor() {
305        // Circle of fifths: Am=1B, Em=2B, Bm=3B, F#m=4B, C#m=5B, G#m=6B, D#m=7B, A#m=8B, Fm=9B, Cm=10B, Gm=11B, Dm=12B
306        assert_eq!(Key::Minor(9).numerical(), "1B");   // Am
307        assert_eq!(Key::Minor(4).numerical(), "2B");   // Em
308        assert_eq!(Key::Minor(11).numerical(), "3B");  // Bm
309        assert_eq!(Key::Minor(6).numerical(), "4B");   // F#m
310        assert_eq!(Key::Minor(1).numerical(), "5B");   // C#m
311        assert_eq!(Key::Minor(8).numerical(), "6B");   // G#m
312        assert_eq!(Key::Minor(3).numerical(), "7B");   // D#m
313        assert_eq!(Key::Minor(10).numerical(), "8B");  // A#m
314        assert_eq!(Key::Minor(5).numerical(), "9B");   // Fm
315        assert_eq!(Key::Minor(0).numerical(), "10B");  // Cm
316        assert_eq!(Key::Minor(7).numerical(), "11B");  // Gm
317        assert_eq!(Key::Minor(2).numerical(), "12B");  // Dm
318    }
319    
320    #[test]
321    fn test_key_from_numerical() {
322        // Test major keys
323        assert_eq!(Key::from_numerical("1A"), Some(Key::Major(0)));   // C
324        assert_eq!(Key::from_numerical("2A"), Some(Key::Major(7)));   // G
325        assert_eq!(Key::from_numerical("7A"), Some(Key::Major(6)));   // F#
326        assert_eq!(Key::from_numerical("12A"), Some(Key::Major(5)));  // F
327        
328        // Test minor keys
329        assert_eq!(Key::from_numerical("1B"), Some(Key::Minor(9)));   // Am
330        assert_eq!(Key::from_numerical("2B"), Some(Key::Minor(4)));   // Em
331        assert_eq!(Key::from_numerical("10B"), Some(Key::Minor(0)));  // Cm
332        
333        // Test invalid inputs
334        assert_eq!(Key::from_numerical("0A"), None);
335        assert_eq!(Key::from_numerical("13A"), None);
336        assert_eq!(Key::from_numerical("1C"), None);
337        assert_eq!(Key::from_numerical(""), None);
338        assert_eq!(Key::from_numerical("A"), None);
339    }
340    
341    #[test]
342    fn test_key_numerical_roundtrip() {
343        // Test that numerical() and from_numerical() are inverses
344        for i in 0..12 {
345            let major = Key::Major(i);
346            let num = major.numerical();
347            assert_eq!(Key::from_numerical(&num), Some(major), "Failed roundtrip for major key {}: {}", i, num);
348            
349            let minor = Key::Minor(i);
350            let num = minor.numerical();
351            assert_eq!(Key::from_numerical(&num), Some(minor), "Failed roundtrip for minor key {}: {}", i, num);
352        }
353    }
354}
355