qualia_core_db/audio/
istft.rs1use crate::audio::stft_bake::StftBakeError;
16
17#[inline]
21fn hann(i: usize, n: usize) -> f32 {
22 if n <= 1 {
23 return 1.0;
24 }
25 0.5 * (1.0 - (core::f32::consts::TAU * i as f32 / (n - 1) as f32).cos())
26}
27
28#[cfg(not(target_arch = "wasm32"))]
51pub fn inverse_stft(
52 spec: &[Vec<[f32; 2]>],
53 frame_size: usize,
54 hop: usize,
55) -> Result<Vec<f32>, StftBakeError> {
56 if !frame_size.is_power_of_two() || frame_size == 0 || hop == 0 {
57 return Err(StftBakeError::InvalidFrameCount);
58 }
59 let num_frames = spec.len();
60 if num_frames == 0 {
61 return Ok(Vec::new());
62 }
63 for frame in spec {
65 if frame.len() != frame_size {
66 return Err(StftBakeError::InvalidFrameCount);
67 }
68 }
69
70 let two_n = 2 * frame_size;
71 let mut spectra = vec![0.0_f32; num_frames * two_n];
74 for (f, frame) in spec.iter().enumerate() {
75 let seg = &mut spectra[f * two_n..(f + 1) * two_n];
76 for (k, &[re, im]) in frame.iter().enumerate() {
77 seg[2 * k] = re;
78 seg[2 * k + 1] = im;
79 }
80 }
81
82 let window: Vec<f32> = (0..frame_size).map(|i| hann(i, frame_size)).collect();
84
85 let out_len = (num_frames - 1)
86 .checked_mul(hop)
87 .and_then(|v| v.checked_add(frame_size))
88 .ok_or(StftBakeError::InvalidFrameCount)?;
89
90 let mut ifft_scratch = vec![0.0_f32; frame_size];
91 let mut norm = vec![0.0_f32; out_len];
92 let mut out = vec![0.0_f32; out_len];
93
94 let written = qualia_audio::features::framing::istft(
95 &mut spectra,
96 frame_size,
97 hop,
98 &window,
99 &window,
100 &mut ifft_scratch,
101 &mut norm,
102 &mut out,
103 )
104 .map_err(map_audio_err)?;
105
106 out.truncate(written);
107 Ok(out)
108}
109
110#[cfg(not(target_arch = "wasm32"))]
113#[inline]
114fn map_audio_err(e: qualia_audio::types::AudioError) -> StftBakeError {
115 use qualia_audio::types::AudioError;
116 match e {
117 AudioError::OutputBufferTooSmall | AudioError::WorkspaceTooSmall => {
118 StftBakeError::OutputTooSmall
119 }
120 _ => StftBakeError::InvalidFrameCount,
121 }
122}
123
124#[cfg(all(test, not(target_arch = "wasm32")))]
125mod tests {
126 use super::*;
127 use crate::audio::stft::forward_stft;
128 use core::f32::consts::TAU;
129
130 #[test]
134 fn istft_round_trip_reconstructs_interior() {
135 const N: usize = 256;
136 const HOP: usize = N / 2; let fs = 16_000.0f32;
138 let len = 4096usize;
139 let signal: Vec<f32> = (0..len)
140 .map(|i| {
141 let t = i as f32 / fs;
142 0.6 * (TAU * 440.0 * t).sin() + 0.35 * (TAU * 1234.0 * t).cos()
143 })
144 .collect();
145
146 let spec = forward_stft(&signal, N, HOP).expect("forward stft");
147 let recon = inverse_stft(&spec, N, HOP).expect("inverse stft");
148
149 let out_len = recon.len();
151 assert!(out_len >= 2 * N + 1, "not enough overlap for interior test");
152 let (lo, hi) = (N, out_len - N);
153 let mut mae = 0.0f64;
154 for k in lo..hi {
155 mae += (recon[k] - signal[k]).abs() as f64;
156 }
157 mae /= (hi - lo) as f64;
158 assert!(mae < 1e-2, "round-trip steady-state MAE {mae} exceeds 1e-2");
159 }
160
161 #[test]
162 fn istft_empty_spec_yields_empty() {
163 let empty: Vec<Vec<[f32; 2]>> = Vec::new();
164 let out = inverse_stft(&empty, 256, 128).expect("empty ok");
165 assert!(out.is_empty());
166 }
167
168 #[test]
169 fn istft_output_length_matches_formula() {
170 const N: usize = 64;
171 const HOP: usize = 32;
172 let signal = vec![0.2f32; N * 5];
173 let spec = forward_stft(&signal, N, HOP).expect("forward");
174 let num_frames = spec.len();
175 let recon = inverse_stft(&spec, N, HOP).expect("inverse");
176 assert_eq!(recon.len(), (num_frames - 1) * HOP + N);
177 }
178
179 #[test]
180 fn istft_rejects_non_power_of_two() {
181 let spec = vec![vec![[0.0f32; 2]; 48]];
182 assert_eq!(
183 inverse_stft(&spec, 48, 16),
184 Err(StftBakeError::InvalidFrameCount)
185 );
186 }
187
188 #[test]
189 fn istft_rejects_wrong_frame_width() {
190 let spec = vec![vec![[0.0f32; 2]; 10]];
192 assert_eq!(
193 inverse_stft(&spec, 256, 128),
194 Err(StftBakeError::InvalidFrameCount)
195 );
196 }
197}