commit fc759a04e0b72ab6dffe3130f17c19e176cf55cb
parent e8b88ee0b6be4b86c16733712bd09eee04d31ba3
Author: mtmn <miro@haravara.org>
Date: Mon, 11 May 2026 20:44:28 +0200
feat: perf improvements
Diffstat:
4 files changed, 93 insertions(+), 63 deletions(-)
diff --git a/src/audio_processing.rs b/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<Array1<f64>> {
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<Array1<f64>> {
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) -> Array1<f64> {
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)
}
}
diff --git a/src/constants.rs b/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",
+ ])
+});
diff --git a/src/file_io.rs b/src/file_io.rs
@@ -38,9 +38,9 @@ fn build_track_info(path: &Path, metadata: FileMetadata, bpm: f32, key: String)
///
/// 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, force: bool) -> Result<TrackI
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(),
- }
-}
-
-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 bpm = bpm_str.parse::<f32>().ok()?;
- Some((bpm, key.to_string()))
+ let Some(t) = tag else {
+ return (FileMetadata::default(), None);
+ };
+
+ 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 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),
}
}
diff --git a/src/main.rs b/src/main.rs
@@ -19,8 +19,8 @@ fn is_supported_audio_file(path: &std::path::Path) -> bool {
}
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()