qualia_core_db/audio/
cqt_bake.rs1use crate::audio::audio_spectral_sheet::{
4 AudioSpectralSidecarHeader, SIDECAR_KIND_CQT, SPECTRAL_PREVIEW_BINS, SPECTRAL_SIDECAR_MAGIC,
5};
6use crate::audio::stft_bake::StftBakeError;
7
8#[inline]
10pub fn preview_to_cqt_frame(
11 preview: &[f32; SPECTRAL_PREVIEW_BINS],
12 frame_index: u32,
13 frame_count: u32,
14) -> [f32; SPECTRAL_PREVIEW_BINS] {
15 let mut out = [0.0_f32; SPECTRAL_PREVIEW_BINS];
16 let phase = (frame_index as f32 / frame_count.max(1) as f32) * core::f32::consts::TAU;
17 for (i, o) in out.iter_mut().enumerate() {
18 let log_i = (i as f32 + 1.0).ln() / (SPECTRAL_PREVIEW_BINS as f32 + 1.0).ln();
19 let src_idx = (log_i * (SPECTRAL_PREVIEW_BINS - 1) as f32).round() as usize;
20 let base = preview[src_idx.min(SPECTRAL_PREVIEW_BINS - 1)];
21 let shimmer = (phase * (i as f32 + 1.0) * 0.05).sin() * 0.08;
22 *o = (base * (1.0 + shimmer)).max(0.0);
23 }
24 out
25}
26
27pub fn bake_cqt_sidecar_from_preview(
29 preview: &[f32; SPECTRAL_PREVIEW_BINS],
30 frame_count: u32,
31 sample_rate: u32,
32 out: &mut [u8],
33) -> Result<usize, StftBakeError> {
34 if frame_count == 0 || frame_count > 4096 {
35 return Err(StftBakeError::InvalidFrameCount);
36 }
37 let header = AudioSpectralSidecarHeader {
38 magic: SPECTRAL_SIDECAR_MAGIC,
39 version: AudioSpectralSidecarHeader::VERSION,
40 _pad: SIDECAR_KIND_CQT,
41 bin_count: SPECTRAL_PREVIEW_BINS as u32,
42 frame_count,
43 sample_rate,
44 };
45 let need = std::mem::size_of::<AudioSpectralSidecarHeader>() + header.payload_bytes();
46 if out.len() < need {
47 return Err(StftBakeError::OutputTooSmall);
48 }
49 out[..std::mem::size_of::<AudioSpectralSidecarHeader>()]
50 .copy_from_slice(bytemuck::bytes_of(&header));
51 let payload_off = std::mem::size_of::<AudioSpectralSidecarHeader>();
52 for f in 0..frame_count {
53 let frame = preview_to_cqt_frame(preview, f, frame_count);
54 let off = payload_off + f as usize * SPECTRAL_PREVIEW_BINS * 4;
55 out[off..off + SPECTRAL_PREVIEW_BINS * 4].copy_from_slice(bytemuck::cast_slice(&frame));
56 }
57 Ok(need)
58}
59
60pub fn forward_cqt(
73 samples: &[f32],
74 sample_rate: f32,
75 f_min: f32,
76 bins_per_octave: usize,
77 n_bins: usize,
78) -> Vec<f32> {
79 let mut out = Vec::with_capacity(n_bins);
80 if samples.is_empty() || bins_per_octave == 0 || sample_rate <= 0.0 || f_min <= 0.0 {
81 out.resize(n_bins, 0.0);
82 return out;
83 }
84 let q = 1.0_f32 / (2.0_f32.powf(1.0 / bins_per_octave as f32) - 1.0);
86
87 for k in 0..n_bins {
88 let f_k = f_min * 2.0_f32.powf(k as f32 / bins_per_octave as f32);
89 let nk = ((q * sample_rate / f_k).round() as usize).clamp(1, samples.len());
92
93 let mut acc_re = 0.0_f32;
94 let mut acc_im = 0.0_f32;
95 for j in 0..nk {
96 let w = hann(j, nk);
97 let theta = -core::f32::consts::TAU * f_k * j as f32 / sample_rate;
99 let s = samples[j] * w;
100 acc_re += s * theta.cos();
101 acc_im += s * theta.sin();
102 }
103 let inv = 1.0 / nk as f32;
104 acc_re *= inv;
105 acc_im *= inv;
106 out.push((acc_re * acc_re + acc_im * acc_im).sqrt());
107 }
108 out
109}
110
111#[inline]
113fn hann(i: usize, n: usize) -> f32 {
114 if n <= 1 {
115 return 1.0;
116 }
117 0.5 * (1.0 - (core::f32::consts::TAU * i as f32 / (n - 1) as f32).cos())
118}
119
120fn cqt_to_preview(mags: &[f32]) -> [f32; SPECTRAL_PREVIEW_BINS] {
123 let mut out = [0.0_f32; SPECTRAL_PREVIEW_BINS];
124 if mags.is_empty() {
125 return out;
126 }
127 if mags.len() == SPECTRAL_PREVIEW_BINS {
128 out.copy_from_slice(mags);
129 return out;
130 }
131 let n = mags.len();
132 for (b, slot) in out.iter_mut().enumerate() {
133 let lo = b * n / SPECTRAL_PREVIEW_BINS;
134 let hi = ((b + 1) * n / SPECTRAL_PREVIEW_BINS).max(lo + 1).min(n);
135 let mut sum = 0.0_f32;
136 let mut cnt = 0u32;
137 for &m in &mags[lo..hi] {
138 sum += m;
139 cnt += 1;
140 }
141 *slot = if cnt > 0 { sum / cnt as f32 } else { 0.0 };
142 }
143 out
144}
145
146pub fn bake_cqt_sidecar_from_samples(
152 samples: &[f32],
153 sample_rate: f32,
154 f_min: f32,
155 bins_per_octave: usize,
156 n_bins: usize,
157 out: &mut [u8],
158) -> Result<usize, StftBakeError> {
159 let mags = forward_cqt(samples, sample_rate, f_min, bins_per_octave, n_bins);
160 let preview = cqt_to_preview(&mags);
161
162 let header = AudioSpectralSidecarHeader {
163 magic: SPECTRAL_SIDECAR_MAGIC,
164 version: AudioSpectralSidecarHeader::VERSION,
165 _pad: SIDECAR_KIND_CQT,
166 bin_count: SPECTRAL_PREVIEW_BINS as u32,
167 frame_count: 1,
168 sample_rate: sample_rate.round().max(0.0) as u32,
169 };
170 let need = std::mem::size_of::<AudioSpectralSidecarHeader>() + header.payload_bytes();
171 if out.len() < need {
172 return Err(StftBakeError::OutputTooSmall);
173 }
174 out[..std::mem::size_of::<AudioSpectralSidecarHeader>()]
175 .copy_from_slice(bytemuck::bytes_of(&header));
176 let payload_off = std::mem::size_of::<AudioSpectralSidecarHeader>();
177 out[payload_off..payload_off + SPECTRAL_PREVIEW_BINS * 4]
178 .copy_from_slice(bytemuck::cast_slice(&preview));
179 Ok(need)
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185 use crate::audio::audio_spectral_sheet::parse_sidecar_header;
186
187 #[test]
188 fn cqt_header_kind_flag() {
189 let preview = [0.4_f32; SPECTRAL_PREVIEW_BINS];
190 let mut buf = [0u8; 20 + 64 * 4 * 4];
191 let n = bake_cqt_sidecar_from_preview(&preview, 4, 48_000, &mut buf).unwrap();
192 assert!(n > 20);
193 let h = parse_sidecar_header(&buf).unwrap();
194 assert_eq!(h._pad, SIDECAR_KIND_CQT);
195 }
196
197 #[test]
198 fn cqt_bins_log_spread_nonzero() {
199 let mut preview = [0.0_f32; SPECTRAL_PREVIEW_BINS];
200 preview[0] = 1.0;
201 preview[63] = 0.5;
202 let cqt = preview_to_cqt_frame(&preview, 0, 8);
203 assert!(cqt[0] > 0.0, "low CQT bins sample preview[0]");
204 assert!(
205 cqt[63] > 0.0,
206 "high CQT bins sample preview[63] via log map"
207 );
208 }
209
210 #[test]
212 fn forward_cqt_tone_lands_on_expected_bin() {
213 let sample_rate = 44_100.0_f32;
214 let f_min = 55.0_f32;
215 let bins_per_octave = 12usize;
216 let n_bins = 48usize;
217 let freq = 440.0_f32;
218 let n = 11_025usize;
220 let samples: Vec<f32> = (0..n)
221 .map(|i| (core::f32::consts::TAU * freq * i as f32 / sample_rate).sin())
222 .collect();
223 let mags = forward_cqt(&samples, sample_rate, f_min, bins_per_octave, n_bins);
224 assert_eq!(mags.len(), n_bins);
225 let (peak, _) = mags
226 .iter()
227 .enumerate()
228 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
229 .unwrap();
230 let expected = (bins_per_octave as f32 * (freq / f_min).log2()).round() as usize; assert!(
232 peak.abs_diff(expected) <= 1,
233 "expected CQT peak near bin {expected}, got {peak}"
234 );
235 }
236
237 #[test]
238 fn bake_cqt_from_samples_valid_header() {
239 let sample_rate = 44_100.0_f32;
240 let n = 8_192usize;
241 let samples: Vec<f32> = (0..n)
242 .map(|i| (core::f32::consts::TAU * 220.0 * i as f32 / sample_rate).sin())
243 .collect();
244 let mut buf = [0u8; 20 + 64 * 4];
245 let written = bake_cqt_sidecar_from_samples(&samples, sample_rate, 55.0, 12, 48, &mut buf)
246 .expect("bake cqt");
247 let h = parse_sidecar_header(&buf).expect("valid header");
248 assert_eq!(h._pad, SIDECAR_KIND_CQT);
249 assert_eq!(h.bin_count, SPECTRAL_PREVIEW_BINS as u32);
250 assert_eq!(h.frame_count, 1);
251 assert_eq!(h.sample_rate, 44_100);
252 assert_eq!(
253 written,
254 std::mem::size_of::<AudioSpectralSidecarHeader>() + SPECTRAL_PREVIEW_BINS * 4
255 );
256 }
257
258 #[test]
260 fn cqt_preview_passthrough_when_64_bins() {
261 let mags: Vec<f32> = (0..SPECTRAL_PREVIEW_BINS)
262 .map(|i| i as f32 * 0.01)
263 .collect();
264 let preview = cqt_to_preview(&mags);
265 for (a, b) in preview.iter().zip(mags.iter()) {
266 assert!((a - b).abs() < 1e-9);
267 }
268 }
269}