Skip to main content

qualia_core_db/audio/
stft_bake.rs

1//! Cold-path STFT/CQT sidecar bake — preview bins → mmap-ready `AudioSpectralSidecarHeader` payload.
2//!
3//! Runs at ingest only (heap OK in caller buffer). Hot U3 reads mmap or preview bins.
4
5use 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/// Synthesize one STFT frame from preview bins with temporal phase evolution.
16#[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
32/// Bake STFT sidecar bytes into caller `out` (header + `frame_count` × `bin_count` f32 raster).
33pub 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/// Bake from `Tensor10D` preview channels (cold ingest linker).
66#[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}