qualia_core_db/audio/
stft_bake.rs1use crate::audio::audio_spectral_sheet::{
6 AudioSpectralSidecarHeader, SIDECAR_KIND_STFT, SPECTRAL_PREVIEW_BINS, SPECTRAL_SIDECAR_MAGIC,
7};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum StftBakeError {
11 OutputTooSmall,
12 InvalidFrameCount,
13}
14
15#[inline]
17pub fn synthesize_stft_frame(
18 preview: &[f32; SPECTRAL_PREVIEW_BINS],
19 frame_index: u32,
20 frame_count: u32,
21) -> [f32; SPECTRAL_PREVIEW_BINS] {
22 let mut out = [0.0_f32; SPECTRAL_PREVIEW_BINS];
23 let phase = (frame_index as f32 / frame_count.max(1) as f32) * std::f32::consts::TAU;
24 for (i, o) in out.iter_mut().enumerate() {
25 let base = preview[i];
26 let wobble = (phase * (i as f32 + 1.0) * 0.07).sin() * 0.12;
27 *o = (base * (1.0 + wobble)).max(0.0);
28 }
29 out
30}
31
32pub fn bake_stft_sidecar_from_preview(
34 preview: &[f32; SPECTRAL_PREVIEW_BINS],
35 frame_count: u32,
36 sample_rate: u32,
37 out: &mut [u8],
38) -> Result<usize, StftBakeError> {
39 if frame_count == 0 || frame_count > 4096 {
40 return Err(StftBakeError::InvalidFrameCount);
41 }
42 let header = AudioSpectralSidecarHeader {
43 magic: SPECTRAL_SIDECAR_MAGIC,
44 version: AudioSpectralSidecarHeader::VERSION,
45 _pad: SIDECAR_KIND_STFT,
46 bin_count: SPECTRAL_PREVIEW_BINS as u32,
47 frame_count,
48 sample_rate,
49 };
50 let need = std::mem::size_of::<AudioSpectralSidecarHeader>() + header.payload_bytes();
51 if out.len() < need {
52 return Err(StftBakeError::OutputTooSmall);
53 }
54 out[..std::mem::size_of::<AudioSpectralSidecarHeader>()]
55 .copy_from_slice(bytemuck::bytes_of(&header));
56 let payload_off = std::mem::size_of::<AudioSpectralSidecarHeader>();
57 for f in 0..frame_count {
58 let frame = synthesize_stft_frame(preview, f, frame_count);
59 let off = payload_off + f as usize * SPECTRAL_PREVIEW_BINS * 4;
60 out[off..off + SPECTRAL_PREVIEW_BINS * 4].copy_from_slice(bytemuck::cast_slice(&frame));
61 }
62 Ok(need)
63}
64
65#[inline]
67pub fn bake_tensor_stft_sidecar(
68 preview: &[f32; SPECTRAL_PREVIEW_BINS],
69 frame_count: u32,
70 out: &mut [u8],
71) -> Result<usize, StftBakeError> {
72 bake_stft_sidecar_from_preview(preview, frame_count, 48_000, out)
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78 use crate::audio::audio_spectral_sheet::parse_sidecar_header;
79 use crate::audio::audio_spectral_sheet::preview_bins_from_tensor;
80 use crate::tensor::Tensor10D;
81
82 #[test]
83 fn bake_produces_valid_header() {
84 let t = Tensor10D::new(0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.55);
85 let preview = preview_bins_from_tensor(&t);
86 const HEADER_BYTES: usize = 20;
87 let mut buf = [0u8; HEADER_BYTES + 64 * 8 * 4];
88 let n = bake_tensor_stft_sidecar(&preview, 8, &mut buf).unwrap();
89 assert_eq!(n, HEADER_BYTES + 64 * 8 * 4);
90 let h = parse_sidecar_header(&buf).unwrap();
91 assert_eq!(h.frame_count, 8);
92 assert_eq!(h.bin_count, 64);
93 }
94
95 #[test]
96 fn frame_synthesis_nonzero() {
97 let preview = [0.5_f32; SPECTRAL_PREVIEW_BINS];
98 let f = synthesize_stft_frame(&preview, 3, 16);
99 assert!(f.iter().any(|&v| v > 0.0));
100 }
101}