1use crate::core::metadata::WaveformData;
4use crate::dsp::{Complex32, DefaultPlanner, FftDirection, FftPlanner};
5use alloc::vec;
6use alloc::vec::Vec;
7use serde::{Deserialize, Serialize};
8
9pub type FloSample = f32;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct SpectralFingerprint {
13 pub hash: [u8; 32],
15 pub duration_ms: u32,
17 pub sample_rate: u32,
19 pub channels: u8,
21 pub frequency_peaks: [u8; 8],
23 pub energy_profile: [u8; 16],
25 pub avg_loudness: u8,
27}
28
29pub fn extract_waveform_peaks(
40 samples: &[FloSample],
41 channels: u8,
42 sample_rate: u32,
43 peaks_per_second: u32,
44) -> WaveformData {
45 if samples.is_empty() || channels == 0 || peaks_per_second == 0 || sample_rate == 0 {
46 return WaveformData {
47 peaks_per_second,
48 peaks: Vec::new(),
49 channels,
50 };
51 }
52
53 let samples_per_peak = (sample_rate as f64) / (peaks_per_second as f64);
54 let total_peaks =
55 libm::ceil((samples.len() as f64) / (samples_per_peak * (channels as f64))) as usize;
56
57 let mut peaks = Vec::with_capacity(total_peaks);
58
59 for peak_idx in 0..total_peaks {
60 let start_sample = ((peak_idx as f64) * samples_per_peak) as usize;
61 let end_sample = (((peak_idx as f64) + 1.0) * samples_per_peak) as usize;
62
63 let start_sample = start_sample * (channels as usize);
64 let end_sample = (end_sample * (channels as usize)).min(samples.len());
65
66 if start_sample >= samples.len() {
67 break;
68 }
69
70 let window_samples = &samples[start_sample..end_sample];
71
72 match channels {
73 1 => {
74 let peak = window_samples
76 .iter()
77 .map(|&s| s.abs())
78 .fold(0.0f32, f32::max);
79 peaks.push(peak);
80 }
81 2 => {
82 let (left_peak, right_peak) = window_samples
84 .as_chunks::<2>()
85 .0
86 .iter()
87 .map(|chunk| (chunk[0].abs(), chunk[1].abs()))
88 .fold((0.0f32, 0.0f32), |(l_max, r_max), (l, r)| {
89 (l_max.max(l), r_max.max(r))
90 });
91
92 peaks.push((left_peak + right_peak) / 2.0);
94 }
95 _ => {
96 let peak = window_samples
98 .chunks(channels as usize)
99 .map(|chunk| chunk.iter().copied().sum::<f32>() / (chunk.len() as f32))
100 .fold(0.0f32, f32::max);
101 peaks.push(peak);
102 }
103 }
104 }
105
106 let max_peak = peaks.iter().fold(0.0f32, |max, &peak| max.max(peak));
108 if max_peak > 0.0 {
109 for peak in &mut peaks {
110 *peak /= max_peak;
111 }
112 }
113
114 WaveformData {
115 peaks_per_second,
116 peaks,
117 channels,
118 }
119}
120
121pub fn extract_waveform_rms(
126 samples: &[FloSample],
127 channels: u8,
128 sample_rate: u32,
129 peaks_per_second: u32,
130) -> WaveformData {
131 if samples.is_empty() || channels == 0 || peaks_per_second == 0 || sample_rate == 0 {
132 return WaveformData {
133 peaks_per_second,
134 peaks: Vec::new(),
135 channels,
136 };
137 }
138
139 let samples_per_peak = (sample_rate as f64) / (peaks_per_second as f64);
140 let total_peaks =
141 libm::ceil((samples.len() as f64) / (samples_per_peak * (channels as f64))) as usize;
142
143 let mut peaks = Vec::with_capacity(total_peaks);
144
145 for peak_idx in 0..total_peaks {
146 let start_sample = ((peak_idx as f64) * samples_per_peak) as usize;
147 let end_sample = (((peak_idx as f64) + 1.0) * samples_per_peak) as usize;
148
149 let start_sample = start_sample * (channels as usize);
150 let end_sample = (end_sample * (channels as usize)).min(samples.len());
151
152 if start_sample >= samples.len() {
153 break;
154 }
155
156 let window_samples = &samples[start_sample..end_sample];
157
158 match channels {
159 1 => {
160 let rms = libm::sqrt(
162 window_samples.iter().map(|&s| (s * s) as f64).sum::<f64>()
163 / (window_samples.len() as f64),
164 ) as f32;
165 peaks.push(rms);
166 }
167 2 => {
168 let (left_sum, right_sum, count) = window_samples.as_chunks::<2>().0.iter().fold(
170 (0.0f64, 0.0f64, 0usize),
171 |(l_sum, r_sum, count), chunk| {
172 (
173 l_sum + ((chunk[0] * chunk[0]) as f64),
174 r_sum + ((chunk[1] * chunk[1]) as f64),
175 count + 1,
176 )
177 },
178 );
179
180 let count = count.max(1); let left_rms = libm::sqrt(left_sum / (count as f64)) as f32;
182 let right_rms = libm::sqrt(right_sum / (count as f64)) as f32;
183
184 peaks.push((left_rms + right_rms) / 2.0);
186 }
187 _ => {
188 let rms = libm::sqrt(
190 window_samples
191 .chunks(channels as usize)
192 .map(|chunk| {
193 let avg = chunk.iter().copied().sum::<f32>() / (chunk.len() as f32);
194 (avg * avg) as f64
195 })
196 .sum::<f64>()
197 / ((window_samples.len() / (channels as usize)) as f64),
198 ) as f32;
199 peaks.push(rms);
200 }
201 }
202 }
203
204 let max_peak = peaks.iter().fold(0.0f32, |max, &peak| max.max(peak));
206 if max_peak > 0.0 {
207 for peak in &mut peaks {
208 *peak /= max_peak;
209 }
210 }
211
212 WaveformData {
213 peaks_per_second,
214 peaks,
215 channels,
216 }
217}
218
219pub fn extract_spectral_fingerprint(
233 samples: &[FloSample],
234 channels: u8,
235 sample_rate: u32,
236 fft_size: Option<usize>,
237 hop_size: Option<usize>,
238) -> SpectralFingerprint {
239 if samples.is_empty() || channels == 0 || sample_rate == 0 {
240 return SpectralFingerprint {
241 hash: [0; 32],
242 duration_ms: 0,
243 sample_rate,
244 channels,
245 frequency_peaks: [0; 8],
246 energy_profile: [0; 16],
247 avg_loudness: 0,
248 };
249 }
250
251 let mut planner = DefaultPlanner::new();
252 let max_fft_len = planner.max_supported_len();
253
254 let fft_size = fft_size
256 .unwrap_or(2048)
257 .next_power_of_two()
258 .clamp(4, max_fft_len);
259 let hop_size = hop_size.unwrap_or(fft_size / 2).max(1);
260
261 let samples_per_channel = samples.len() / (channels as usize);
263 let duration_ms =
264 ((((samples_per_channel as f64) / (sample_rate as f64)) * 1000.0) as u32).max(1);
265
266 use blake3::Hasher;
268 let mut hasher = Hasher::new();
269
270 hasher.update(&channels.to_le_bytes());
272 hasher.update(&sample_rate.to_le_bytes());
273 hasher.update(&(samples.len() as u32).to_le_bytes());
274
275 for chunk in samples.chunks(1024) {
277 let chunk_bytes = unsafe {
281 core::slice::from_raw_parts(chunk.as_ptr() as *const u8, core::mem::size_of_val(chunk))
282 };
283 hasher.update(chunk_bytes);
284 }
285 let hash = hasher.finalize().into();
286
287 let fft = planner.plan_fft(fft_size, FftDirection::Forward);
290 let mut fft_buffer = vec![Complex32 { re: 0.0, im: 0.0 }; fft_size];
291
292 let window: Vec<f32> = (0..fft_size)
294 .map(|i| {
295 0.5 - 0.5
296 * libm::cosf((2.0 * core::f32::consts::PI * i as f32) / ((fft_size - 1) as f32))
297 })
298 .collect();
299
300 let mut frequency_bands = [0.0f32; 16];
301 let mut peak_bands = [0u8; 8];
302
303 if samples_per_channel >= fft_size {
305 let num_frames = (samples_per_channel - fft_size) / hop_size + 1;
306
307 for frame_idx in 0..num_frames {
308 let sample_idx = frame_idx * hop_size;
309
310 for i in 0..fft_size {
312 let mut sample = 0.0;
313 for ch in 0..channels {
314 let idx = (sample_idx + i) * (channels as usize) + (ch as usize);
315 if idx < samples.len() {
316 sample += samples[idx];
317 }
318 }
319 sample /= channels as f32;
320 fft_buffer[i] = Complex32 {
321 re: sample * window[i],
322 im: 0.0,
323 };
324 }
325
326 fft.process(&mut fft_buffer);
328
329 for band in 0..16 {
331 let start_bin = (band * fft_size) / 32;
332 let end_bin = (((band + 1) * fft_size) / 32).min(fft_size / 2);
333 let mut energy = 0.0;
334 for bin in start_bin..end_bin {
335 energy += fft_buffer[bin].re * fft_buffer[bin].re
336 + fft_buffer[bin].im * fft_buffer[bin].im;
337 }
338 frequency_bands[band] += libm::sqrtf(energy);
339 }
340
341 for band in 0..8 {
343 let start_bin = (band * fft_size) / 16;
344 let end_bin = (((band + 1) * fft_size) / 16).min(fft_size / 2);
345
346 let (peak_bin, _) = (start_bin..end_bin)
347 .map(|bin| {
348 (
349 bin,
350 libm::sqrtf(
351 fft_buffer[bin].re * fft_buffer[bin].re
352 + fft_buffer[bin].im * fft_buffer[bin].im,
353 ),
354 )
355 })
356 .max_by(|a, b| a.1.total_cmp(&b.1))
357 .unwrap_or((0, 0.0));
358
359 let peak_value = (((peak_bin as f32) / (fft_size as f32)) * 255.0) as u8;
361 peak_bands[band] = peak_bands[band].max(peak_value);
362 }
363 }
364 }
365
366 let max_energy = frequency_bands.iter().cloned().fold(0.0f32, f32::max);
368 let energy_profile = if max_energy > 0.0 {
369 frequency_bands.map(|e| ((e / max_energy) * 255.0) as u8)
370 } else {
371 [0; 16]
372 };
373
374 let rms: f32 = samples.iter().map(|&s| s * s).sum::<f32>() / (samples.len() as f32);
376 let avg_loudness_db = (20.0 * libm::log10f(rms + 1e-10)).clamp(-60.0, 0.0);
377 let avg_loudness = ((avg_loudness_db + 60.0) / 60.0 * 255.0) as u8;
378
379 SpectralFingerprint {
380 hash,
381 duration_ms,
382 sample_rate,
383 channels,
384 frequency_peaks: peak_bands,
385 energy_profile,
386 avg_loudness,
387 }
388}
389
390pub fn extract_dominant_frequencies(
399 fingerprint: &SpectralFingerprint,
400 num_frequencies: usize,
401) -> Vec<Vec<f64>> {
402 let num_frequencies = num_frequencies.min(8); let mut dominant_freqs = Vec::with_capacity(1);
404 let mut frame_dominants = Vec::with_capacity(num_frequencies);
405
406 for i in 0..num_frequencies {
408 let normalized_freq = (fingerprint.frequency_peaks[i] as f64) / 255.0;
410 let frequency = normalized_freq * ((fingerprint.sample_rate as f64) / 2.0);
411 frame_dominants.push(frequency);
412 }
413
414 dominant_freqs.push(frame_dominants);
415 dominant_freqs
416}
417
418pub fn spectral_similarity(
427 fingerprint1: &SpectralFingerprint,
428 fingerprint2: &SpectralFingerprint,
429) -> f32 {
430 let hash_match = fingerprint1.hash == fingerprint2.hash;
432
433 if hash_match {
434 return 1.0;
435 }
436
437 if fingerprint1.sample_rate != fingerprint2.sample_rate
439 || fingerprint1.channels != fingerprint2.channels
440 {
441 return 0.0;
442 }
443
444 let energy_similarity: f32 = fingerprint1
446 .energy_profile
447 .iter()
448 .zip(fingerprint2.energy_profile.iter())
449 .map(|(a, b)| 1.0 - ((*a as f32) - (*b as f32)).abs() / 255.0)
450 .sum::<f32>()
451 / 16.0;
452
453 let peak_similarity: f32 = fingerprint1
455 .frequency_peaks
456 .iter()
457 .zip(fingerprint2.frequency_peaks.iter())
458 .map(|(a, b)| 1.0 - ((*a as f32) - (*b as f32)).abs() / 255.0)
459 .sum::<f32>()
460 / 8.0;
461
462 let loudness_similarity = 1.0
464 - ((fingerprint1.avg_loudness as f32) - (fingerprint2.avg_loudness as f32)).abs() / 255.0;
465
466 energy_similarity * 0.5 + peak_similarity * 0.3 + loudness_similarity * 0.2
468}