use crate::core::metadata::WaveformData;
use crate::dsp::{Complex32, DefaultPlanner, FftDirection, FftPlanner};
use alloc::vec;
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
pub type FloSample = f32;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpectralFingerprint {
pub hash: [u8; 32],
pub duration_ms: u32,
pub sample_rate: u32,
pub channels: u8,
pub frequency_peaks: [u8; 8],
pub energy_profile: [u8; 16],
pub avg_loudness: u8,
}
pub fn extract_waveform_peaks(
samples: &[FloSample],
channels: u8,
sample_rate: u32,
peaks_per_second: u32,
) -> WaveformData {
if samples.is_empty() || channels == 0 || peaks_per_second == 0 || sample_rate == 0 {
return WaveformData {
peaks_per_second,
peaks: Vec::new(),
channels,
};
}
let samples_per_peak = (sample_rate as f64) / (peaks_per_second as f64);
let total_peaks =
libm::ceil((samples.len() as f64) / (samples_per_peak * (channels as f64))) as usize;
let mut peaks = Vec::with_capacity(total_peaks);
for peak_idx in 0..total_peaks {
let start_sample = ((peak_idx as f64) * samples_per_peak) as usize;
let end_sample = (((peak_idx as f64) + 1.0) * samples_per_peak) as usize;
let start_sample = start_sample * (channels as usize);
let end_sample = (end_sample * (channels as usize)).min(samples.len());
if start_sample >= samples.len() {
break;
}
let window_samples = &samples[start_sample..end_sample];
match channels {
1 => {
let peak = window_samples
.iter()
.map(|&s| s.abs())
.fold(0.0f32, f32::max);
peaks.push(peak);
}
2 => {
let (left_peak, right_peak) = window_samples
.as_chunks::<2>()
.0
.iter()
.map(|chunk| (chunk[0].abs(), chunk[1].abs()))
.fold((0.0f32, 0.0f32), |(l_max, r_max), (l, r)| {
(l_max.max(l), r_max.max(r))
});
peaks.push((left_peak + right_peak) / 2.0);
}
_ => {
let peak = window_samples
.chunks(channels as usize)
.map(|chunk| chunk.iter().copied().sum::<f32>() / (chunk.len() as f32))
.fold(0.0f32, f32::max);
peaks.push(peak);
}
}
}
let max_peak = peaks.iter().fold(0.0f32, |max, &peak| max.max(peak));
if max_peak > 0.0 {
for peak in &mut peaks {
*peak /= max_peak;
}
}
WaveformData {
peaks_per_second,
peaks,
channels,
}
}
pub fn extract_waveform_rms(
samples: &[FloSample],
channels: u8,
sample_rate: u32,
peaks_per_second: u32,
) -> WaveformData {
if samples.is_empty() || channels == 0 || peaks_per_second == 0 || sample_rate == 0 {
return WaveformData {
peaks_per_second,
peaks: Vec::new(),
channels,
};
}
let samples_per_peak = (sample_rate as f64) / (peaks_per_second as f64);
let total_peaks =
libm::ceil((samples.len() as f64) / (samples_per_peak * (channels as f64))) as usize;
let mut peaks = Vec::with_capacity(total_peaks);
for peak_idx in 0..total_peaks {
let start_sample = ((peak_idx as f64) * samples_per_peak) as usize;
let end_sample = (((peak_idx as f64) + 1.0) * samples_per_peak) as usize;
let start_sample = start_sample * (channels as usize);
let end_sample = (end_sample * (channels as usize)).min(samples.len());
if start_sample >= samples.len() {
break;
}
let window_samples = &samples[start_sample..end_sample];
match channels {
1 => {
let rms = libm::sqrt(
window_samples.iter().map(|&s| (s * s) as f64).sum::<f64>()
/ (window_samples.len() as f64),
) as f32;
peaks.push(rms);
}
2 => {
let (left_sum, right_sum, count) = window_samples.as_chunks::<2>().0.iter().fold(
(0.0f64, 0.0f64, 0usize),
|(l_sum, r_sum, count), chunk| {
(
l_sum + ((chunk[0] * chunk[0]) as f64),
r_sum + ((chunk[1] * chunk[1]) as f64),
count + 1,
)
},
);
let count = count.max(1); let left_rms = libm::sqrt(left_sum / (count as f64)) as f32;
let right_rms = libm::sqrt(right_sum / (count as f64)) as f32;
peaks.push((left_rms + right_rms) / 2.0);
}
_ => {
let rms = libm::sqrt(
window_samples
.chunks(channels as usize)
.map(|chunk| {
let avg = chunk.iter().copied().sum::<f32>() / (chunk.len() as f32);
(avg * avg) as f64
})
.sum::<f64>()
/ ((window_samples.len() / (channels as usize)) as f64),
) as f32;
peaks.push(rms);
}
}
}
let max_peak = peaks.iter().fold(0.0f32, |max, &peak| max.max(peak));
if max_peak > 0.0 {
for peak in &mut peaks {
*peak /= max_peak;
}
}
WaveformData {
peaks_per_second,
peaks,
channels,
}
}
pub fn extract_spectral_fingerprint(
samples: &[FloSample],
channels: u8,
sample_rate: u32,
fft_size: Option<usize>,
hop_size: Option<usize>,
) -> SpectralFingerprint {
if samples.is_empty() || channels == 0 || sample_rate == 0 {
return SpectralFingerprint {
hash: [0; 32],
duration_ms: 0,
sample_rate,
channels,
frequency_peaks: [0; 8],
energy_profile: [0; 16],
avg_loudness: 0,
};
}
let mut planner = DefaultPlanner::new();
let max_fft_len = planner.max_supported_len();
let fft_size = fft_size
.unwrap_or(2048)
.next_power_of_two()
.clamp(4, max_fft_len);
let hop_size = hop_size.unwrap_or(fft_size / 2).max(1);
let samples_per_channel = samples.len() / (channels as usize);
let duration_ms =
((((samples_per_channel as f64) / (sample_rate as f64)) * 1000.0) as u32).max(1);
use blake3::Hasher;
let mut hasher = Hasher::new();
hasher.update(&channels.to_le_bytes());
hasher.update(&sample_rate.to_le_bytes());
hasher.update(&(samples.len() as u32).to_le_bytes());
for chunk in samples.chunks(1024) {
let chunk_bytes = unsafe {
core::slice::from_raw_parts(chunk.as_ptr() as *const u8, core::mem::size_of_val(chunk))
};
hasher.update(chunk_bytes);
}
let hash = hasher.finalize().into();
let fft = planner.plan_fft(fft_size, FftDirection::Forward);
let mut fft_buffer = vec![Complex32 { re: 0.0, im: 0.0 }; fft_size];
let window: Vec<f32> = (0..fft_size)
.map(|i| {
0.5 - 0.5
* libm::cosf((2.0 * core::f32::consts::PI * i as f32) / ((fft_size - 1) as f32))
})
.collect();
let mut frequency_bands = [0.0f32; 16];
let mut peak_bands = [0u8; 8];
if samples_per_channel >= fft_size {
let num_frames = (samples_per_channel - fft_size) / hop_size + 1;
for frame_idx in 0..num_frames {
let sample_idx = frame_idx * hop_size;
for i in 0..fft_size {
let mut sample = 0.0;
for ch in 0..channels {
let idx = (sample_idx + i) * (channels as usize) + (ch as usize);
if idx < samples.len() {
sample += samples[idx];
}
}
sample /= channels as f32;
fft_buffer[i] = Complex32 {
re: sample * window[i],
im: 0.0,
};
}
fft.process(&mut fft_buffer);
for band in 0..16 {
let start_bin = (band * fft_size) / 32;
let end_bin = (((band + 1) * fft_size) / 32).min(fft_size / 2);
let mut energy = 0.0;
for bin in start_bin..end_bin {
energy += fft_buffer[bin].re * fft_buffer[bin].re
+ fft_buffer[bin].im * fft_buffer[bin].im;
}
frequency_bands[band] += libm::sqrtf(energy);
}
for band in 0..8 {
let start_bin = (band * fft_size) / 16;
let end_bin = (((band + 1) * fft_size) / 16).min(fft_size / 2);
let (peak_bin, _) = (start_bin..end_bin)
.map(|bin| {
(
bin,
libm::sqrtf(
fft_buffer[bin].re * fft_buffer[bin].re
+ fft_buffer[bin].im * fft_buffer[bin].im,
),
)
})
.max_by(|a, b| a.1.total_cmp(&b.1))
.unwrap_or((0, 0.0));
let peak_value = (((peak_bin as f32) / (fft_size as f32)) * 255.0) as u8;
peak_bands[band] = peak_bands[band].max(peak_value);
}
}
}
let max_energy = frequency_bands.iter().cloned().fold(0.0f32, f32::max);
let energy_profile = if max_energy > 0.0 {
frequency_bands.map(|e| ((e / max_energy) * 255.0) as u8)
} else {
[0; 16]
};
let rms: f32 = samples.iter().map(|&s| s * s).sum::<f32>() / (samples.len() as f32);
let avg_loudness_db = (20.0 * libm::log10f(rms + 1e-10)).clamp(-60.0, 0.0);
let avg_loudness = ((avg_loudness_db + 60.0) / 60.0 * 255.0) as u8;
SpectralFingerprint {
hash,
duration_ms,
sample_rate,
channels,
frequency_peaks: peak_bands,
energy_profile,
avg_loudness,
}
}
pub fn extract_dominant_frequencies(
fingerprint: &SpectralFingerprint,
num_frequencies: usize,
) -> Vec<Vec<f64>> {
let num_frequencies = num_frequencies.min(8); let mut dominant_freqs = Vec::with_capacity(1);
let mut frame_dominants = Vec::with_capacity(num_frequencies);
for i in 0..num_frequencies {
let normalized_freq = (fingerprint.frequency_peaks[i] as f64) / 255.0;
let frequency = normalized_freq * ((fingerprint.sample_rate as f64) / 2.0);
frame_dominants.push(frequency);
}
dominant_freqs.push(frame_dominants);
dominant_freqs
}
pub fn spectral_similarity(
fingerprint1: &SpectralFingerprint,
fingerprint2: &SpectralFingerprint,
) -> f32 {
let hash_match = fingerprint1.hash == fingerprint2.hash;
if hash_match {
return 1.0;
}
if fingerprint1.sample_rate != fingerprint2.sample_rate
|| fingerprint1.channels != fingerprint2.channels
{
return 0.0;
}
let energy_similarity: f32 = fingerprint1
.energy_profile
.iter()
.zip(fingerprint2.energy_profile.iter())
.map(|(a, b)| 1.0 - ((*a as f32) - (*b as f32)).abs() / 255.0)
.sum::<f32>()
/ 16.0;
let peak_similarity: f32 = fingerprint1
.frequency_peaks
.iter()
.zip(fingerprint2.frequency_peaks.iter())
.map(|(a, b)| 1.0 - ((*a as f32) - (*b as f32)).abs() / 255.0)
.sum::<f32>()
/ 8.0;
let loudness_similarity = 1.0
- ((fingerprint1.avg_loudness as f32) - (fingerprint2.avg_loudness as f32)).abs() / 255.0;
energy_similarity * 0.5 + peak_similarity * 0.3 + loudness_similarity * 0.2
}