Commit Diff


commit - e8b88ee0b6be4b86c16733712bd09eee04d31ba3
commit + fc759a04e0b72ab6dffe3130f17c19e176cf55cb
blob - bd5f7d44d827b82979fa5033891a243819f17d6c
blob + 2261306ee72c936c393c110fc4ce6717fc9cc532
--- src/audio_processing.rs
+++ src/audio_processing.rs
@@ -1,3 +1,5 @@
+use std::sync::LazyLock;
+
 use crate::constants::{
     BPM_HOP_SIZE, BPM_MAX, BPM_MIN, BPM_OFFSET, BPM_WINDOW_SIZE, CHROMA_BINS, KEY_HOP_SIZE,
     KEY_WINDOW_SIZE, SAMPLE_RATE, TUNING_PRECISION,
@@ -20,7 +22,7 @@ pub fn calculate_bpm(samples: &[f32]) -> f32 {
     let hop_size = BPM_HOP_SIZE;
     let mut tempo = Tempo::new(OnsetMode::SpecFlux, window_size, hop_size, SAMPLE_RATE)
         .expect("Failed to create tempo object");
-    let mut bpms = Vec::new();
+    let mut bpms = Vec::with_capacity(samples.len() / hop_size);
 
     for chunk in samples.chunks(hop_size) {
         let mut padded = chunk.to_vec();
@@ -91,10 +93,10 @@ fn calculate_global_chroma(samples: &[f32]) -> Result<
     let n_chroma = CHROMA_BINS as u32;
     let chroma = chroma_stft(SAMPLE_RATE, &mut spectrum, window_size, n_chroma, tuning)?;
 
-    let shape = chroma.shape();
-    let mut result = vec![0.0; shape[0]];
-    for i in 0..shape[0] {
-        for j in 0..shape[1] {
+    let (n_bins, n_frames) = (chroma.shape()[0], chroma.shape()[1]);
+    let mut result = vec![0.0f64; n_bins];
+    for i in 0..n_bins {
+        for j in 0..n_frames {
             result[i] += chroma[[i, j]];
         }
     }
@@ -102,30 +104,38 @@ fn calculate_global_chroma(samples: &[f32]) -> Result<
     Ok(Array1::from_vec(result))
 }
 
-fn find_best_key(global_chroma: &Array1<f64>) -> (usize, &'static str) {
-    let major_profile = ndarray::arr1(&[
+static MAJOR_ROTATIONS: LazyLock<Vec<Array1<f64>>> = LazyLock::new(|| {
+    let base = ndarray::arr1(&[
         6.35, 2.23, 3.48, 2.33, 4.38, 4.09, 2.52, 5.19, 2.39, 3.66, 2.29, 2.88,
     ]);
-    let minor_profile = ndarray::arr1(&[
+    (0..CHROMA_BINS).map(|i| rotate_array(&base, i)).collect()
+});
+
+static MINOR_ROTATIONS: LazyLock<Vec<Array1<f64>>> = LazyLock::new(|| {
+    let base = ndarray::arr1(&[
         6.33, 2.68, 3.52, 5.38, 2.60, 3.53, 2.54, 4.75, 3.98, 2.69, 3.34, 3.17,
     ]);
+    (0..CHROMA_BINS).map(|i| rotate_array(&base, i)).collect()
+});
 
+fn find_best_key(global_chroma: &Array1<f64>) -> (usize, &'static str) {
     let mut max_corr = -1.0;
     let mut best_key_idx = 0;
     let mut best_mode = "Major";
+    let (major_mean, major_stddev) = *MAJOR_PROFILE_STATS;
+    let (minor_mean, minor_stddev) = *MINOR_PROFILE_STATS;
 
     for i in 0..CHROMA_BINS {
-        let rotated_major = rotate_array(&major_profile, i);
-        let rotated_minor = rotate_array(&minor_profile, i);
-
-        let corr_major = pearson_correlation(global_chroma, &rotated_major);
+        let corr_major =
+            pearson_correlation(global_chroma, &MAJOR_ROTATIONS[i], major_mean, major_stddev);
         if corr_major > max_corr {
             max_corr = corr_major;
             best_key_idx = i;
             best_mode = "Major";
         }
 
-        let corr_minor = pearson_correlation(global_chroma, &rotated_minor);
+        let corr_minor =
+            pearson_correlation(global_chroma, &MINOR_ROTATIONS[i], minor_mean, minor_stddev);
         if corr_minor > max_corr {
             max_corr = corr_minor;
             best_key_idx = i;
@@ -141,19 +151,38 @@ fn rotate_array(arr: &Array1<f64>, shift: usize) -> Ar
     Array1::from_iter((0..n).map(|j| arr[(j + n - shift) % n]))
 }
 
-fn pearson_correlation(v1: &Array1<f64>, v2: &Array1<f64>) -> f64 {
-    let mean1 = v1.mean().expect("Failed to calculate mean");
-    let mean2 = v2.mean().expect("Failed to calculate mean");
-    let num: f64 = v1
+/// Pre-computed profile stats: (mean, stddev) for major and minor profiles.
+/// Since rotation is a cyclic permutation, all rotations share the same mean and stddev.
+static MAJOR_PROFILE_STATS: LazyLock<(f64, f64)> = LazyLock::new(|| {
+    let v = &MAJOR_ROTATIONS[0];
+    let mean = v.mean().expect("Failed to calculate mean");
+    let denom: f64 = v.iter().map(|x| (x - mean).powi(2)).sum();
+    (mean, denom.sqrt())
+});
+
+static MINOR_PROFILE_STATS: LazyLock<(f64, f64)> = LazyLock::new(|| {
+    let v = &MINOR_ROTATIONS[0];
+    let mean = v.mean().expect("Failed to calculate mean");
+    let denom: f64 = v.iter().map(|x| (x - mean).powi(2)).sum();
+    (mean, denom.sqrt())
+});
+
+fn pearson_correlation(
+    global_chroma: &Array1<f64>,
+    profile: &Array1<f64>,
+    profile_mean: f64,
+    profile_stddev: f64,
+) -> f64 {
+    let mean1 = global_chroma.mean().expect("Failed to calculate mean");
+    let num: f64 = global_chroma
         .iter()
-        .zip(v2.iter())
-        .map(|(x, y)| (x - mean1) * (y - mean2))
+        .zip(profile.iter())
+        .map(|(x, y)| (x - mean1) * (y - profile_mean))
         .sum();
-    let den1: f64 = v1.iter().map(|x| (x - mean1).powi(2)).sum();
-    let den2: f64 = v2.iter().map(|y| (y - mean2).powi(2)).sum();
-    if den1 == 0.0 || den2 == 0.0 {
+    let den1: f64 = global_chroma.iter().map(|x| (x - mean1).powi(2)).sum();
+    if den1 == 0.0 || profile_stddev == 0.0 {
         0.0
     } else {
-        num / (den1.sqrt() * den2.sqrt())
+        num / (den1.sqrt() * profile_stddev)
     }
 }
blob - 28dc15a9279aa9879c479cdc4e3234ef30a88381
blob + 4b74b37f4eba4e5199922c49023bb306eb4f73a8
--- src/constants.rs
+++ src/constants.rs
@@ -16,7 +16,12 @@ pub const TUNING_PRECISION: f64 = 0.01;
 pub const CHROMA_BINS: usize = 12;
 
 // Supported audio file extensions
-pub const SUPPORTED_EXTENSIONS: &[&str] = &[
-    "aac", "ape", "aif", "aiff", "flac", "mp3", "mp4", "m4a", "mpc", "opus", "ogg", "spx", "wav",
-    "wv",
-];
+use std::collections::HashSet;
+use std::sync::LazyLock;
+
+pub static SUPPORTED_EXTENSIONS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
+    HashSet::from([
+        "aac", "ape", "aif", "aiff", "flac", "mp3", "mp4", "m4a", "mpc", "opus", "ogg", "spx",
+        "wav", "wv",
+    ])
+});
blob - df5ebd1c2c9850ed62f88d11866e7df4933e7483
blob + 47501d6b9ca68faf776a6effea7117b8c03d879c
--- src/file_io.rs
+++ src/file_io.rs
@@ -38,9 +38,9 @@ fn build_track_info(path: &Path, metadata: FileMetadat
 ///
 /// Returns an error if the file cannot be processed.
 pub fn process_file(path: &Path, write_tags: bool, force: bool) -> Result<TrackInfo, String> {
-    let metadata = read_metadata(path);
+    let (metadata, existing_tags) = read_tags(path);
 
-    if !force && let Some((bpm, key)) = read_existing_tags(path) {
+    if !force && let Some((bpm, key)) = existing_tags {
         println!(
             "File: {}, BPM: {:.1}, Key: {} (existing tags)",
             path.display(),
@@ -65,43 +65,39 @@ pub fn process_file(path: &Path, write_tags: bool, for
     Ok(build_track_info(path, metadata, bpm, key))
 }
 
-pub fn read_metadata(path: &Path) -> FileMetadata {
+fn read_tags(path: &Path) -> (FileMetadata, Option<(f32, String)>) {
     match Probe::open(path).and_then(Probe::read) {
         Ok(tagged_file) => {
             let tag = tagged_file
                 .primary_tag()
                 .or_else(|| tagged_file.first_tag());
-            if let Some(t) = tag {
-                FileMetadata {
-                    artist: t
-                        .get_string(ItemKey::TrackArtist)
-                        .or_else(|| t.get_string(ItemKey::AlbumArtist))
-                        .map(String::from),
-                    album: t.get_string(ItemKey::AlbumTitle).map(String::from),
-                    track: t.get_string(ItemKey::TrackTitle).map(String::from),
-                    label: t.get_string(ItemKey::Label).map(String::from),
-                }
-            } else {
-                FileMetadata::default()
-            }
-        }
-        Err(_) => FileMetadata::default(),
-    }
-}
+            let Some(t) = tag else {
+                return (FileMetadata::default(), None);
+            };
 
-fn read_existing_tags(path: &Path) -> Option<(f32, String)> {
-    match Probe::open(path).and_then(Probe::read) {
-        Ok(tagged_file) => {
-            let tag = tagged_file
-                .primary_tag()
-                .or_else(|| tagged_file.first_tag())?;
-            let bpm_str = tag.get_string(ItemKey::Bpm)?;
-            let key = tag.get_string(ItemKey::InitialKey)?;
+            let metadata = FileMetadata {
+                artist: t
+                    .get_string(ItemKey::TrackArtist)
+                    .or_else(|| t.get_string(ItemKey::AlbumArtist))
+                    .map(String::from),
+                album: t.get_string(ItemKey::AlbumTitle).map(String::from),
+                track: t.get_string(ItemKey::TrackTitle).map(String::from),
+                label: t.get_string(ItemKey::Label).map(String::from),
+            };
 
-            let bpm = bpm_str.parse::<f32>().ok()?;
-            Some((bpm, key.to_string()))
+            let existing_tags = t
+                .get_string(ItemKey::Bpm)
+                .zip(t.get_string(ItemKey::InitialKey))
+                .and_then(|(bpm_str, key)| {
+                    bpm_str
+                        .parse::<f32>()
+                        .ok()
+                        .map(|bpm| (bpm, key.to_string()))
+                });
+
+            (metadata, existing_tags)
         }
-        Err(_) => None,
+        Err(_) => (FileMetadata::default(), None),
     }
 }
 
blob - a43e2ffb51b2630b4de2de890b90dcc752562174
blob + c81d7ae4f5b318246e7b55b70773e90c543531cd
--- src/main.rs
+++ src/main.rs
@@ -19,8 +19,8 @@ fn is_supported_audio_file(path: &std::path::Path) -> 
 }
 
 fn is_supported_extension(ext: &std::ffi::OsStr) -> bool {
-    let ext_str = ext.to_string_lossy().to_lowercase();
-    SUPPORTED_EXTENSIONS.contains(&ext_str.as_ref())
+    let ext_lower = ext.to_string_lossy().to_lowercase();
+    SUPPORTED_EXTENSIONS.contains(&*ext_lower)
 }
 
 fn main() -> Result<()> {
@@ -93,7 +93,7 @@ fn main() -> Result<()> {
                 vec![]
             }
         })
-        .collect();
+        .collect::<Vec<_>>();
 
     let (results, errors): (Vec<_>, Vec<_>) = files_to_process
         .par_iter()