virittaa

Log | Files | Refs | README | LICENSE

commit fc759a04e0b72ab6dffe3130f17c19e176cf55cb
parent e8b88ee0b6be4b86c16733712bd09eee04d31ba3
Author: mtmn <miro@haravara.org>
Date:   Mon, 11 May 2026 20:44:28 +0200

feat: perf improvements

Diffstat:
Msrc/audio_processing.rs | 75++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Msrc/constants.rs | 13+++++++++----
Msrc/file_io.rs | 62+++++++++++++++++++++++++++++---------------------------------
Msrc/main.rs | 6+++---
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()