oxideav_opus/celt_overlap_add.rs
1//! CELT §4.3.7 weighted overlap-add
2//! (RFC 6716 §4.3.7, p. 121).
3//!
4//! The inverse MDCT ([`crate::celt_imdct`]) maps each CELT frame's `N`
5//! denormalised frequency bins to a `2N`-sample time-domain block. A
6//! single such block is *not* the decoded signal: the MDCT is a lapped
7//! transform, and each reconstructed block carries a time-domain aliased
8//! copy of itself folded about its half-block midpoints (the defining
9//! property that lets `N` coefficients represent a `2N`-sample block
10//! without redundancy). RFC 6716 §4.3.7.1 (p. 121) names the step that
11//! removes that aliasing:
12//!
13//! > The output of the inverse MDCT (after weighted overlap-add) is sent
14//! > to the post-filter.
15//!
16//! The "weighted overlap-add" multiplies each `2N` block by the §4.3.7
17//! low-overlap window ([`crate::celt_mdct_window`]) and sums the leading
18//! half of the current block with the trailing half of the previous
19//! block. Because the window is power-complementary
20//! (`W(n)^2 + W(L-1-n)^2 = 1`, the Princen-Bradley condition the §4.3.7
21//! prose requires), the equal-and-opposite aliased halves of neighbouring
22//! blocks cancel exactly on the add, and the result is the aliasing-free
23//! time-domain signal (at the §4.3.7 `1/2` IMDCT scale).
24//!
25//! ## Block layout and the hop
26//!
27//! For a frame of `N` MDCT bins:
28//!
29//! * the inverse MDCT yields a `2N`-sample block;
30//! * the synthesis window touches only the `overlap` samples at each
31//! edge — the leading `overlap` samples are multiplied by the rising
32//! ramp `W(0) .. W(overlap-1)`, the trailing `overlap` samples by the
33//! falling ramp `W(overlap-1) .. W(0)`, and the `2N - 2*overlap`
34//! samples in the middle are unity ("inserting ones in the middle",
35//! §4.3.7);
36//! * the hop between successive frames is `N` samples, so block `i`
37//! overlaps block `i-1` over its first `N` samples.
38//!
39//! The decoder therefore keeps a per-channel **history** buffer holding
40//! the windowed *second half* (samples `N .. 2N`) of the previous block.
41//! Each frame emits `N` time-domain samples:
42//!
43//! ```text
44//! out(n) = windowed_block_i(n) + history(n) for n = 0 .. N
45//! history'(n) = windowed_block_i(N + n) for n = 0 .. N
46//! ```
47//!
48//! On the very first frame after a stream start or a §4.5.2 CELT state
49//! reset the history is all-zero, so the first emitted frame is simply
50//! the leading half of the first windowed block — the aliasing in that
51//! leading half is cancelled by the *next* frame's overlap, exactly as
52//! the trailing aliasing of every block is cancelled by its successor.
53//!
54//! The `overlap` is constrained to `0 < overlap <= N`. The CELT layer
55//! fixes the 48 kHz overlap at 120 samples
56//! ([`crate::celt_mdct_window::CELT_OVERLAP_48K`]); for the shortest CELT
57//! frame the per-MDCT `N` can be as small as the overlap, so equality is
58//! permitted. An even overlap is required so the window centre splits
59//! cleanly (the §4.3.7 low-overlap construction).
60//!
61//! ## Relation to the transform core
62//!
63//! [`crate::celt_imdct`] computes the raw `2N` inverse block already
64//! scaled by `1/2`; this module owns the windowing and the cross-frame
65//! state. The TDAC property — that a windowed forward/inverse pair plus
66//! this overlap-add reconstructs the input at the `1/2` scale — is pinned
67//! both by the transform core's own tests and, end-to-end through this
68//! stateful adder, by the tests below.
69//!
70//! ## Provenance
71//!
72//! Lapped-transform overlap-add narrative + "after weighted overlap-add"
73//! step ordering: RFC 6716 §4.3.7 / §4.3.7.1 (p. 121); fixed-overlap
74//! statement: §1 (p. 9–10); window shape: §4.3.7 (p. 121). All
75//! reproduced from `docs/audio/opus/rfc6716-opus.txt`. The overlap-add
76//! is the textbook synthesis step for a lapped transform with a
77//! power-complementary window; no external library source was consulted.
78
79use crate::celt_mdct_window::{mdct_window, MdctWindowError};
80
81/// Errors returnable by the §4.3.7 weighted overlap-add helpers.
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum OverlapAddError {
84 /// The transform half-length `N` is zero; an empty block has no
85 /// overlap-add.
86 ZeroLength,
87 /// The supplied block was not exactly `2*N` samples long.
88 BlockLenNotEven {
89 /// The block length the caller supplied.
90 got: usize,
91 },
92 /// The block length disagreed with the adder's configured `N`.
93 BlockLenMismatch {
94 /// The block length the caller supplied.
95 got: usize,
96 /// The required length `2 * N`.
97 want: usize,
98 },
99 /// The overlap is zero or exceeds `N` (the §4.3.7 window touches at
100 /// most one half-block at each edge), or is odd (the low-overlap
101 /// construction needs an even overlap so the centre splits cleanly).
102 BadOverlap {
103 /// The overlap the caller passed.
104 overlap: usize,
105 /// The configured `N`.
106 n: usize,
107 },
108 /// The output slice passed to [`WeightedOverlapAdd::process_into`]
109 /// is shorter than the `N` samples a frame emits.
110 OutputTooSmall {
111 /// The required output length `N`.
112 want: usize,
113 /// The length the caller supplied.
114 got: usize,
115 },
116}
117
118impl core::fmt::Display for OverlapAddError {
119 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
120 match *self {
121 OverlapAddError::ZeroLength => {
122 write!(f, "oxideav-opus: CELT §4.3.7 overlap-add requires N >= 1")
123 }
124 OverlapAddError::BlockLenNotEven { got } => write!(
125 f,
126 "oxideav-opus: CELT §4.3.7 overlap-add block length {got} is not 2*N (odd)"
127 ),
128 OverlapAddError::BlockLenMismatch { got, want } => write!(
129 f,
130 "oxideav-opus: CELT §4.3.7 overlap-add block length {got} != required 2*N = {want}"
131 ),
132 OverlapAddError::BadOverlap { overlap, n } => write!(
133 f,
134 "oxideav-opus: CELT §4.3.7 overlap-add overlap {overlap} invalid for N={n} \
135 (require 0 < overlap <= N and overlap even)"
136 ),
137 OverlapAddError::OutputTooSmall { want, got } => write!(
138 f,
139 "oxideav-opus: CELT §4.3.7 overlap-add output length {got} < required N = {want}"
140 ),
141 }
142 }
143}
144
145impl std::error::Error for OverlapAddError {}
146
147/// Apply the §4.3.7 low-overlap synthesis window to a `2N`-sample inverse
148/// MDCT block **in place** (RFC 6716 §4.3.7, p. 121).
149///
150/// The leading `overlap` samples are scaled by the rising ramp
151/// `W(0) .. W(overlap-1)`, the trailing `overlap` samples by the falling
152/// ramp `W(overlap-1) .. W(0)`, and the `2N - 2*overlap` samples in the
153/// middle are left unchanged (unity window).
154///
155/// `block.len()` must be `2 * n`, and `ramp.len()` must be `overlap`
156/// with `0 < overlap <= n`. This is a free function so a caller that has
157/// already built the ramp once can window many blocks without rebuilding
158/// it; [`WeightedOverlapAdd`] caches the ramp and calls this internally.
159///
160/// # Errors
161///
162/// Returns [`OverlapAddError`] if `n == 0`, if `block.len() != 2*n`, or
163/// if the ramp length is not a valid overlap for `n`.
164pub fn apply_synthesis_window(
165 block: &mut [f64],
166 ramp: &[f64],
167 n: usize,
168) -> Result<(), OverlapAddError> {
169 if n == 0 {
170 return Err(OverlapAddError::ZeroLength);
171 }
172 if block.len() != 2 * n {
173 if block.len() % 2 != 0 {
174 return Err(OverlapAddError::BlockLenNotEven { got: block.len() });
175 }
176 return Err(OverlapAddError::BlockLenMismatch {
177 got: block.len(),
178 want: 2 * n,
179 });
180 }
181 let overlap = ramp.len();
182 if overlap == 0 || overlap > n || overlap % 2 != 0 {
183 return Err(OverlapAddError::BadOverlap { overlap, n });
184 }
185 let two_n = 2 * n;
186 for i in 0..overlap {
187 // Rising ramp on the leading edge.
188 block[i] *= ramp[i];
189 // Falling ramp on the trailing edge: W(overlap-1-i).
190 block[two_n - 1 - i] *= ramp[i];
191 }
192 // The middle `2N - 2*overlap` samples keep their unity window.
193 Ok(())
194}
195
196/// The §4.3.7 stateful weighted overlap-add for one channel
197/// (RFC 6716 §4.3.7 / §4.3.7.1, p. 121).
198///
199/// Feed each frame's `2N`-sample inverse MDCT block (the raw output of
200/// [`crate::celt_imdct`], already scaled by `1/2`) to [`Self::process`]
201/// / [`Self::process_into`]; each call returns the `N` aliasing-free
202/// time-domain samples for that frame and carries the windowed trailing
203/// half forward as the overlap history for the next frame.
204///
205/// One instance tracks one channel. A stereo decoder keeps two.
206///
207/// # Examples
208///
209/// ```
210/// use oxideav_opus::celt_overlap_add::WeightedOverlapAdd;
211/// // N = 4, overlap = 2.
212/// let mut ola = WeightedOverlapAdd::new(4, 2).unwrap();
213/// let block = [0.0_f64; 8];
214/// // A silent block stays silent through the overlap-add.
215/// assert_eq!(ola.process(&block).unwrap(), vec![0.0; 4]);
216/// ```
217#[derive(Debug, Clone, PartialEq)]
218pub struct WeightedOverlapAdd {
219 /// The transform half-length: each frame emits `n` samples and
220 /// consumes a `2*n`-sample block.
221 n: usize,
222 /// The `overlap` rising window taps `W(0) .. W(overlap-1)`.
223 ramp: Vec<f64>,
224 /// The windowed trailing half (`n` samples) of the previous block,
225 /// to be added to the leading half of the next block. All-zero at
226 /// stream start / after a §4.5.2 reset.
227 history: Vec<f64>,
228}
229
230impl WeightedOverlapAdd {
231 /// Create a fresh overlap-add for transform half-length `n` and
232 /// `overlap`, with zeroed history (the stream-start / post-reset
233 /// state).
234 ///
235 /// Requires `0 < overlap <= n` and `overlap` even (the §4.3.7
236 /// low-overlap construction).
237 ///
238 /// # Errors
239 ///
240 /// Returns [`OverlapAddError::ZeroLength`] if `n == 0`, or
241 /// [`OverlapAddError::BadOverlap`] if the overlap is out of range or
242 /// odd.
243 pub fn new(n: usize, overlap: usize) -> Result<Self, OverlapAddError> {
244 if n == 0 {
245 return Err(OverlapAddError::ZeroLength);
246 }
247 if overlap == 0 || overlap > n || overlap % 2 != 0 {
248 return Err(OverlapAddError::BadOverlap { overlap, n });
249 }
250 let ramp = mdct_window(overlap).map_err(|e| match e {
251 MdctWindowError::ZeroLength => OverlapAddError::BadOverlap { overlap, n },
252 MdctWindowError::OddOverlap { overlap } => OverlapAddError::BadOverlap { overlap, n },
253 MdctWindowError::PositionOutOfRange { .. } => {
254 OverlapAddError::BadOverlap { overlap, n }
255 }
256 })?;
257 Ok(Self {
258 n,
259 ramp,
260 history: vec![0.0_f64; n],
261 })
262 }
263
264 /// The transform half-length `N`: each frame emits this many samples.
265 #[must_use]
266 pub fn frame_len(&self) -> usize {
267 self.n
268 }
269
270 /// The overlap length (window transition width).
271 #[must_use]
272 pub fn overlap(&self) -> usize {
273 self.ramp.len()
274 }
275
276 /// The current overlap history (the windowed trailing half of the
277 /// last block). All-zero before the first [`Self::process`] call.
278 #[must_use]
279 pub fn history(&self) -> &[f64] {
280 &self.history
281 }
282
283 /// Reset the overlap history to zero, as on a §4.5.2 CELT state
284 /// reset or at the start of a new stream.
285 pub fn reset(&mut self) {
286 for h in self.history.iter_mut() {
287 *h = 0.0;
288 }
289 }
290
291 /// Run the weighted overlap-add for one frame, returning the `N`
292 /// aliasing-free time-domain samples.
293 ///
294 /// `block` is the `2*N`-sample inverse MDCT output for this frame.
295 /// Allocating wrapper for [`Self::process_into`].
296 ///
297 /// # Errors
298 ///
299 /// Returns [`OverlapAddError::BlockLenMismatch`] /
300 /// [`OverlapAddError::BlockLenNotEven`] if `block.len() != 2*N`.
301 pub fn process(&mut self, block: &[f64]) -> Result<Vec<f64>, OverlapAddError> {
302 let mut out = vec![0.0_f64; self.n];
303 self.process_into(block, &mut out)?;
304 Ok(out)
305 }
306
307 /// Run the weighted overlap-add for one frame into a caller-provided
308 /// buffer.
309 ///
310 /// `block` is the `2*N`-sample inverse MDCT output; `out` receives
311 /// the `N` time-domain samples (`out.len()` must be `>= N`). On error
312 /// the adder's history is left unchanged.
313 ///
314 /// # Errors
315 ///
316 /// Returns [`OverlapAddError`] if the block length is wrong or `out`
317 /// is shorter than `N`.
318 pub fn process_into(&mut self, block: &[f64], out: &mut [f64]) -> Result<(), OverlapAddError> {
319 let two_n = 2 * self.n;
320 if block.len() != two_n {
321 if block.len() % 2 != 0 {
322 return Err(OverlapAddError::BlockLenNotEven { got: block.len() });
323 }
324 return Err(OverlapAddError::BlockLenMismatch {
325 got: block.len(),
326 want: two_n,
327 });
328 }
329 if out.len() < self.n {
330 return Err(OverlapAddError::OutputTooSmall {
331 want: self.n,
332 got: out.len(),
333 });
334 }
335
336 // Window the block (a private copy so the caller's buffer is not
337 // mutated). Leading rising ramp, trailing falling ramp, unity
338 // middle.
339 let mut windowed = block.to_vec();
340 // `apply_synthesis_window` re-validates n / overlap, but both are
341 // invariants here, so it cannot fail.
342 apply_synthesis_window(&mut windowed, &self.ramp, self.n)?;
343
344 // Emit the leading half overlap-added with the saved history.
345 for i in 0..self.n {
346 out[i] = windowed[i] + self.history[i];
347 }
348
349 // Save the windowed trailing half as the next frame's history.
350 self.history.copy_from_slice(&windowed[self.n..two_n]);
351
352 Ok(())
353 }
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359 use crate::celt_imdct::{imdct, mdct_forward};
360 use crate::celt_mdct_window::window_tap;
361
362 const EPS: f64 = 1e-9;
363
364 #[test]
365 fn rejects_zero_n() {
366 assert_eq!(
367 WeightedOverlapAdd::new(0, 2),
368 Err(OverlapAddError::ZeroLength)
369 );
370 }
371
372 #[test]
373 fn rejects_bad_overlap() {
374 // Overlap larger than N.
375 assert_eq!(
376 WeightedOverlapAdd::new(4, 6),
377 Err(OverlapAddError::BadOverlap { overlap: 6, n: 4 })
378 );
379 // Zero overlap.
380 assert_eq!(
381 WeightedOverlapAdd::new(4, 0),
382 Err(OverlapAddError::BadOverlap { overlap: 0, n: 4 })
383 );
384 // Odd overlap.
385 assert_eq!(
386 WeightedOverlapAdd::new(8, 3),
387 Err(OverlapAddError::BadOverlap { overlap: 3, n: 8 })
388 );
389 }
390
391 #[test]
392 fn overlap_equal_to_n_is_allowed() {
393 // The shortest CELT MDCT can have N == overlap (full-overlap).
394 let ola = WeightedOverlapAdd::new(4, 4).unwrap();
395 assert_eq!(ola.frame_len(), 4);
396 assert_eq!(ola.overlap(), 4);
397 }
398
399 #[test]
400 fn rejects_wrong_block_length() {
401 let mut ola = WeightedOverlapAdd::new(4, 2).unwrap();
402 // Odd length.
403 assert_eq!(
404 ola.process(&[0.0; 7]),
405 Err(OverlapAddError::BlockLenNotEven { got: 7 })
406 );
407 // Even but != 2N.
408 assert_eq!(
409 ola.process(&[0.0; 6]),
410 Err(OverlapAddError::BlockLenMismatch { got: 6, want: 8 })
411 );
412 }
413
414 #[test]
415 fn rejects_output_too_small() {
416 let mut ola = WeightedOverlapAdd::new(4, 2).unwrap();
417 let block = [0.0; 8];
418 let mut out = [0.0; 3];
419 assert_eq!(
420 ola.process_into(&block, &mut out),
421 Err(OverlapAddError::OutputTooSmall { want: 4, got: 3 })
422 );
423 }
424
425 #[test]
426 fn silence_stays_silent() {
427 let mut ola = WeightedOverlapAdd::new(8, 4).unwrap();
428 for _ in 0..3 {
429 let out = ola.process(&[0.0; 16]).unwrap();
430 assert_eq!(out, vec![0.0; 8]);
431 }
432 }
433
434 #[test]
435 fn first_frame_emits_windowed_leading_half() {
436 // With zero history, frame 0 output is just the windowed leading
437 // half of the block (the trailing-half aliasing is cancelled by
438 // the next frame).
439 let n = 4;
440 let overlap = 2;
441 let mut ola = WeightedOverlapAdd::new(n, overlap).unwrap();
442 let block: Vec<f64> = (0..2 * n).map(|i| (i as f64 + 1.0) * 0.5).collect();
443 let out = ola.process(&block).unwrap();
444 // Expected: leading `overlap` samples scaled by rising ramp,
445 // remaining samples in [overlap, N) unity.
446 let ramp = mdct_window(overlap).unwrap();
447 for i in 0..overlap {
448 assert!((out[i] - block[i] * ramp[i]).abs() < EPS, "i={i}");
449 }
450 for i in overlap..n {
451 assert!((out[i] - block[i]).abs() < EPS, "i={i}");
452 }
453 }
454
455 #[test]
456 fn history_holds_windowed_trailing_half() {
457 let n = 4;
458 let overlap = 2;
459 let mut ola = WeightedOverlapAdd::new(n, overlap).unwrap();
460 assert_eq!(ola.history(), &[0.0; 4]);
461 let block: Vec<f64> = (0..2 * n).map(|i| i as f64 + 1.0).collect();
462 ola.process(&block).unwrap();
463 // History = windowed samples [N, 2N): the trailing `overlap`
464 // samples carry the falling ramp, the rest are unity.
465 let ramp = mdct_window(overlap).unwrap();
466 let mut windowed = block.clone();
467 apply_synthesis_window(&mut windowed, &ramp, n).unwrap();
468 assert_eq!(ola.history(), &windowed[n..2 * n]);
469 }
470
471 #[test]
472 fn reset_zeroes_history() {
473 let mut ola = WeightedOverlapAdd::new(4, 2).unwrap();
474 ola.process(&[1.0; 8]).unwrap();
475 assert!(ola.history().iter().any(|&h| h != 0.0));
476 ola.reset();
477 assert_eq!(ola.history(), &[0.0; 4]);
478 }
479
480 #[test]
481 fn apply_synthesis_window_layout() {
482 // Directly verify the rising/falling/unity layout.
483 let n = 8;
484 let overlap = 4;
485 let ramp = mdct_window(overlap).unwrap();
486 let block: Vec<f64> = (0..2 * n).map(|i| i as f64 + 1.0).collect();
487 let mut w = block.clone();
488 apply_synthesis_window(&mut w, &ramp, n).unwrap();
489 let two_n = 2 * n;
490 for i in 0..overlap {
491 assert!((w[i] - block[i] * ramp[i]).abs() < EPS, "lead i={i}");
492 assert!(
493 (w[two_n - 1 - i] - block[two_n - 1 - i] * ramp[i]).abs() < EPS,
494 "trail i={i}"
495 );
496 }
497 for i in overlap..(two_n - overlap) {
498 assert!((w[i] - block[i]).abs() < EPS, "middle i={i}");
499 }
500 }
501
502 #[test]
503 fn apply_synthesis_window_rejects_bad_args() {
504 let ramp = mdct_window(2).unwrap();
505 assert_eq!(
506 apply_synthesis_window(&mut [0.0; 8], &ramp, 0),
507 Err(OverlapAddError::ZeroLength)
508 );
509 // Block length not 2N.
510 assert_eq!(
511 apply_synthesis_window(&mut [0.0; 6], &ramp, 4),
512 Err(OverlapAddError::BlockLenMismatch { got: 6, want: 8 })
513 );
514 // Odd block length.
515 assert!(matches!(
516 apply_synthesis_window(&mut [0.0; 7], &ramp, 4),
517 Err(OverlapAddError::BlockLenNotEven { got: 7 })
518 ));
519 // Overlap > N (ramp of len 6, N=2).
520 let big = mdct_window(6).unwrap();
521 assert_eq!(
522 apply_synthesis_window(&mut [0.0; 4], &big, 2),
523 Err(OverlapAddError::BadOverlap { overlap: 6, n: 2 })
524 );
525 }
526
527 /// A symmetric Princen-Bradley analysis/synthesis window over the
528 /// *whole* `2N` block: `w(n) = sin( (pi/2N)*(n + 1/2) )`. This is the
529 /// full-overlap window pair (overlap = N) that gives perfect MDCT
530 /// reconstruction; here it lets us drive the stateful overlap-add
531 /// end-to-end against a windowed forward/inverse round-trip.
532 fn pb_window(two_n: usize) -> Vec<f64> {
533 (0..two_n)
534 .map(|i| (core::f64::consts::PI / (two_n as f64) * (i as f64 + 0.5)).sin())
535 .collect()
536 }
537
538 #[test]
539 fn multi_frame_arithmetic_matches_reference() {
540 // Validate the stateful adder's arithmetic across several frames:
541 // for each frame, out_b(i) = windowed_b[i] + windowed_{b-1}[N+i],
542 // and the history carries the windowed trailing half forward. We
543 // compare the adder against an independent hand computation using
544 // the §4.3.7 low-overlap ramp.
545 let n = 6;
546 let overlap = 4;
547 let mut ola = WeightedOverlapAdd::new(n, overlap).unwrap();
548 let ramp = mdct_window(overlap).unwrap();
549 // Sanity: the ramp is the §4.3.7 window over its own length.
550 assert!((ramp[0] - window_tap(0, overlap).unwrap()).abs() < EPS);
551
552 let blocks: Vec<Vec<f64>> = (0..3)
553 .map(|b| {
554 (0..2 * n)
555 .map(|i| ((b * n + i) as f64 * 0.21).sin() - 0.3)
556 .collect()
557 })
558 .collect();
559
560 // Independent reference: window each block, then out_b(i) =
561 // windowed_b[i] + windowed_{b-1}[N+i].
562 let mut prev_tail = vec![0.0_f64; n];
563 for block in &blocks {
564 let mut w = block.clone();
565 apply_synthesis_window(&mut w, &ramp, n).unwrap();
566 let want: Vec<f64> = (0..n).map(|i| w[i] + prev_tail[i]).collect();
567 let got = ola.process(block).unwrap();
568 for i in 0..n {
569 assert!(
570 (got[i] - want[i]).abs() < EPS,
571 "i={i}: {} != {}",
572 got[i],
573 want[i]
574 );
575 }
576 prev_tail.copy_from_slice(&w[n..2 * n]);
577 }
578 }
579
580 #[test]
581 fn end_to_end_tdac_via_imdct_full_overlap() {
582 // True TDAC: with the symmetric Princen-Bradley window applied on
583 // BOTH analysis and synthesis (overlap = N), a windowed forward
584 // MDCT, the §4.3.7 inverse MDCT, and a hop-N overlap-add
585 // reconstruct the input at the §4.3.7 `1/2` scale. This pins the
586 // aliasing-cancellation property the §4.3.7 overlap-add provides,
587 // using the canonical symmetric MDCT window (independent of the
588 // CELT low-overlap layout, which is exercised arithmetically
589 // above).
590 let n = 8;
591 let two_n = 2 * n;
592 let win = pb_window(two_n);
593 // hop = N; produce three overlapping analysis blocks of a signal.
594 let total = 4 * n;
595 let signal: Vec<f64> = (0..total)
596 .map(|i| (i as f64 * 0.37).sin() * 1.5 - (i as f64 * 0.11).cos())
597 .collect();
598
599 // Synthesise each block: window (analysis), forward, inverse,
600 // window (synthesis) -> these are the per-block windowed IMDCT
601 // outputs the overlap-add sums.
602 let synth = |start: usize| -> Vec<f64> {
603 let blk: Vec<f64> = (0..two_n).map(|i| signal[start + i] * win[i]).collect();
604 let coeffs = mdct_forward(&blk).unwrap();
605 let rec = imdct(&coeffs).unwrap();
606 rec.iter().zip(win.iter()).map(|(r, w)| r * w).collect()
607 };
608
609 // Overlap-add by hand (the symmetric window is already applied):
610 // out covering global [N, 2N) = synth(0)[N..2N] + synth(N)[0..N].
611 let f0 = synth(0);
612 let f1 = synth(n);
613 for j in 0..n {
614 let recon = f0[n + j] + f1[j];
615 let want = 0.5 * signal[n + j];
616 assert!(
617 (recon - want).abs() < 1e-9,
618 "overlap j={j}: {recon} != 0.5*signal {want}"
619 );
620 }
621 }
622
623 #[test]
624 fn error_display_messages() {
625 assert!(OverlapAddError::ZeroLength.to_string().contains("N >= 1"));
626 assert!(OverlapAddError::BlockLenNotEven { got: 7 }
627 .to_string()
628 .contains("not 2*N"));
629 assert!(OverlapAddError::BlockLenMismatch { got: 6, want: 8 }
630 .to_string()
631 .contains("!= required 2*N"));
632 assert!(OverlapAddError::BadOverlap { overlap: 6, n: 4 }
633 .to_string()
634 .contains("invalid"));
635 assert!(OverlapAddError::OutputTooSmall { want: 4, got: 3 }
636 .to_string()
637 .contains("< required N"));
638 }
639}