use crate::analysis::beat::detect_beats_from_transients_with_options;
use crate::analysis::tempogram::TempoTrackingOptions;
use crate::analysis::transient::detect_transients;
use crate::core::preanalysis::{hash_samples, PreAnalysisArtifact, PREANALYSIS_VERSION};
const PREANALYSIS_FFT_SIZE: usize = 2048;
const PREANALYSIS_HOP_SIZE: usize = 512;
const PREANALYSIS_SENSITIVITY: f32 = 0.4;
const DJ_TEMPO_HINT_RANGE: (f64, f64) = (100.0, 160.0);
pub fn downmix_to_mid(interleaved: &[f32], channels: usize) -> Vec<f32> {
if channels <= 1 {
return interleaved.to_vec();
}
interleaved
.chunks(channels)
.map(|frame| {
let left = frame.first().copied().unwrap_or(0.0);
let right = frame.get(1).copied().unwrap_or(left);
(left + right) * 0.5
})
.collect()
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct AnalysisReport {
pub odf_median: f32,
pub odf_mad: f32,
pub odf_max: f32,
pub onset_count: usize,
pub onset_rate_per_sec: f64,
pub analysis_elapsed_secs: f64,
}
pub fn analyze_for_dj(samples: &[f32], sample_rate: u32) -> PreAnalysisArtifact {
analyze_for_dj_with_report(samples, sample_rate).0
}
pub fn analyze_for_dj_with_report(
samples: &[f32],
sample_rate: u32,
) -> (PreAnalysisArtifact, AnalysisReport) {
let started = std::time::Instant::now();
let transients = detect_transients(
samples,
sample_rate,
PREANALYSIS_FFT_SIZE,
PREANALYSIS_HOP_SIZE,
PREANALYSIS_SENSITIVITY,
);
let grid = detect_beats_from_transients_with_options(
&transients,
sample_rate,
&TempoTrackingOptions {
hint_range: Some(DJ_TEMPO_HINT_RANGE),
..TempoTrackingOptions::default()
},
);
let bpm = if grid.bpm.is_finite() && grid.bpm > 0.0 {
grid.bpm
} else {
0.0
};
let beat_positions = grid.beats_rounded();
let downbeat_offset_samples = if bpm > 0.0 && !grid.beats.is_empty() {
let beat_interval = 60.0 * sample_rate as f64 / bpm;
grid.beats[0].rem_euclid(beat_interval).round() as usize
} else {
0
};
let confidence =
estimate_confidence(&beat_positions, &transients.onsets, sample_rate).max(grid.confidence);
let transient_strengths = if transients.strengths.len() == transients.onsets.len() {
transients.strengths.clone()
} else {
vec![1.0; transients.onsets.len()]
};
let hop = transients.hop_size.max(1);
let onset_band_flux = transients
.onsets
.iter()
.map(|&onset| {
transients
.per_frame_band_flux
.get(onset / hop)
.copied()
.unwrap_or([0.0; 4])
})
.collect();
let report = AnalysisReport {
odf_median: median_of(&transients.flux),
odf_mad: mad_of(&transients.flux),
odf_max: transients.flux.iter().copied().fold(0.0f32, f32::max),
onset_count: transients.onsets.len(),
onset_rate_per_sec: transients.onsets.len() as f64
/ (samples.len() as f64 / sample_rate.max(1) as f64).max(1e-9),
analysis_elapsed_secs: started.elapsed().as_secs_f64(),
};
let artifact = PreAnalysisArtifact {
version: PREANALYSIS_VERSION,
sample_rate,
bpm,
downbeat_offset_samples,
confidence,
beat_positions,
beat_positions_fractional: grid.beats,
downbeat_beat_indices: grid.downbeats,
tempo_segments: grid.segments,
transient_onsets: transients.onsets,
transient_strengths,
onset_band_flux,
analysis_hop_size: hop,
source_len_samples: samples.len(),
content_hash: hash_samples(samples),
};
(artifact, report)
}
fn median_of(values: &[f32]) -> f32 {
if values.is_empty() {
return 0.0;
}
let mut sorted = values.to_vec();
sorted.sort_by(|a, b| a.total_cmp(b));
sorted[sorted.len() / 2]
}
fn mad_of(values: &[f32]) -> f32 {
if values.is_empty() {
return 0.0;
}
let median = median_of(values);
let deviations: Vec<f32> = values.iter().map(|&v| (v - median).abs()).collect();
median_of(&deviations)
}
fn estimate_confidence(beats: &[usize], onsets: &[usize], sample_rate: u32) -> f32 {
if beats.len() < 3 {
return 0.0;
}
let intervals: Vec<f64> = beats
.windows(2)
.map(|w| w[1].saturating_sub(w[0]) as f64)
.filter(|&v| v > 0.0)
.collect();
if intervals.len() < 2 {
return 0.0;
}
let mean = intervals.iter().sum::<f64>() / intervals.len() as f64;
if mean <= 0.0 {
return 0.0;
}
let variance = intervals
.iter()
.map(|&v| {
let d = v - mean;
d * d
})
.sum::<f64>()
/ intervals.len() as f64;
let std = variance.sqrt();
let cv = std / mean;
let regularity = (1.0 - (cv / 0.25)).clamp(0.0, 1.0);
let seconds = (beats.last().copied().unwrap_or(0) as f64 / sample_rate as f64).max(1.0);
let beat_rate = beats.len() as f64 / seconds;
let onset_rate = onsets.len() as f64 / seconds;
let beat_density_score = (beat_rate / 2.0).clamp(0.0, 1.0); let onset_support_score = (onset_rate / 8.0).clamp(0.0, 1.0);
(0.6 * regularity + 0.25 * beat_density_score + 0.15 * onset_support_score) as f32
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_analyze_for_dj_click_train_has_confidence() {
let sample_rate = 44100u32;
let bpm = 120.0;
let beat_interval = (60.0 * sample_rate as f64 / bpm) as usize;
let len = sample_rate as usize * 4;
let mut signal = vec![0.0f32; len];
for i in (0..len).step_by(beat_interval) {
for j in 0..10.min(len - i) {
signal[i + j] = if j < 5 { 1.0 } else { -0.5 };
}
}
let artifact = analyze_for_dj(&signal, sample_rate);
assert!(artifact.bpm > 0.0);
assert!(artifact.confidence > 0.2);
assert!(!artifact.beat_positions.is_empty());
}
#[test]
fn test_analyze_for_dj_fills_v2_fields() {
let sample_rate = 44100u32;
let beat_interval = (60.0 * sample_rate as f64 / 120.0) as usize;
let len = sample_rate as usize * 4;
let mut signal = vec![0.0f32; len];
for i in (0..len).step_by(beat_interval) {
for j in 0..10.min(len - i) {
signal[i + j] = if j < 5 { 1.0 } else { -0.5 };
}
}
let artifact = analyze_for_dj(&signal, sample_rate);
assert_eq!(artifact.version, PREANALYSIS_VERSION);
assert_eq!(artifact.source_len_samples, len);
assert_eq!(artifact.content_hash, hash_samples(&signal));
assert_eq!(artifact.analysis_hop_size, PREANALYSIS_HOP_SIZE);
assert_eq!(
artifact.transient_strengths.len(),
artifact.transient_onsets.len()
);
assert_eq!(
artifact.onset_band_flux.len(),
artifact.transient_onsets.len()
);
assert!(artifact.matches_source(&signal, sample_rate));
}
#[test]
fn test_downmix_to_mid() {
let mono = vec![0.1, 0.2, 0.3];
assert_eq!(downmix_to_mid(&mono, 1), mono);
let stereo = vec![1.0, 0.0, 0.5, 0.5, -1.0, 1.0];
assert_eq!(downmix_to_mid(&stereo, 2), vec![0.5, 0.5, 0.0]);
}
}