ftts-core 0.1.6

Engine core for franken_tts: admission, budgets, health, streaming audio buffers
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
//! The audio output tail: decoded PCM to a playable file.
//!
//! The codec hands back `f32` samples in `[-1, 1]`; everything downstream of that is this module.
//! It is deliberately small and pure-Rust (AGENTS.md toolchain: no libsndfile, no FFmpeg FFI), and
//! it owns exactly two conversions that are easy to get subtly wrong:
//!
//! 1. **f32 to 16-bit PCM.** Clamping happens *before* scaling. A sample of `1.2` scaled first and
//!    clamped second wraps to a large negative value — an audible click that a duration check and a
//!    byte-count check both call success.
//! 2. **The RIFF header's two length fields.** They must describe the bytes actually present. A
//!    header claiming more data than the file holds is corrupt, and players disagree about how to
//!    fail on it, so [`WavWriter::finish`] patches both from the real sample count. The same rule
//!    is what makes a disk-full partial file still playable.
//!
//! # Format
//!
//! 16-bit signed PCM, mono, 24 kHz — [`SAMPLE_RATE_HZ`], the codec's native rate (plan §2.7:
//! 24 kHz, 1,920 samples per 80 ms frame). Nothing here resamples: emitting the codec's own rate
//! keeps the output bit-faithful to what the model produced, and resampling is a separate,
//! explicitly-requested operation.

use std::io::{self, Seek, SeekFrom, Write};

/// The codec's native output rate.
pub const SAMPLE_RATE_HZ: u32 = 24_000;

/// Samples the codec emits per 80 ms frame: `SAMPLE_RATE_HZ / 12.5`.
pub const SAMPLES_PER_FRAME: usize = 1_920;

/// Channel count. The model is mono; stereo would be a fabrication.
pub const CHANNELS: u16 = 1;

/// Bits per sample in the emitted WAV.
pub const BITS_PER_SAMPLE: u16 = 16;

/// Bytes in a canonical 44-byte RIFF/WAVE header for uncompressed PCM.
pub const WAV_HEADER_BYTES: usize = 44;

/// Samples produced by `frames` codec frames.
#[must_use]
pub const fn samples_for_frames(frames: usize) -> usize {
    frames * SAMPLES_PER_FRAME
}

/// Convert one `f32` sample in `[-1, 1]` to signed 16-bit PCM.
///
/// Clamp first, then scale. The reverse order wraps on overshoot: `1.2 * 32767.0` is `39320`,
/// which truncates to a large negative `i16` and produces a click exactly where the audio was
/// loudest. Non-finite input becomes silence rather than an arbitrary bit pattern — a NaN reaching
/// here is a bug upstream, and the runtime-health `NonFinite` seam is what reports it; this
/// conversion's job is to not turn it into noise.
///
/// The scale is `32767.0` (not `32768.0`) so that `+1.0` maps to `i16::MAX` exactly and the
/// mapping stays symmetric about zero.
#[must_use]
pub fn sample_to_i16(sample: f32) -> i16 {
    if !sample.is_finite() {
        return 0;
    }
    let clamped = sample.clamp(-1.0, 1.0);
    // `round()` gives round-half-away-from-zero, matching the reference converters; truncation
    // would bias every sample toward zero and quietly lower the output level.
    (clamped * 32_767.0).round() as i16
}

/// Convert a decoded `f32` buffer to 16-bit PCM.
#[must_use]
pub fn pcm_f32_to_i16(pcm: &[f32]) -> Vec<i16> {
    pcm.iter().copied().map(sample_to_i16).collect()
}

/// Mean square energy of a PCM buffer, in `[0, 1]`.
///
/// Used to answer "did we actually produce audio?". A silent result is a failure that every
/// byte-count and duration check reports as success, so energy is the check that distinguishes
/// them. Returns `0.0` for an empty buffer rather than dividing by zero.
#[must_use]
pub fn mean_square_energy(pcm: &[f32]) -> f64 {
    if pcm.is_empty() {
        return 0.0;
    }
    let total: f64 = pcm.iter().map(|s| f64::from(*s) * f64::from(*s)).sum();
    total / pcm.len() as f64
}

/// Build a 44-byte RIFF/WAVE header for `sample_count` mono 16-bit samples.
///
/// Both length fields are derived from `sample_count` so they can never disagree with each other
/// or with the payload.
#[must_use]
pub fn wav_header(sample_rate: u32, sample_count: usize) -> [u8; WAV_HEADER_BYTES] {
    let data_bytes = (sample_count * usize::from(BITS_PER_SAMPLE / 8)) as u32;
    let byte_rate = sample_rate * u32::from(CHANNELS) * u32::from(BITS_PER_SAMPLE / 8);
    let block_align = CHANNELS * (BITS_PER_SAMPLE / 8);

    let mut header = [0u8; WAV_HEADER_BYTES];
    header[0..4].copy_from_slice(b"RIFF");
    // RIFF size counts everything after this field: 36 header bytes plus the payload.
    header[4..8].copy_from_slice(&(36u32.saturating_add(data_bytes)).to_le_bytes());
    header[8..12].copy_from_slice(b"WAVE");
    header[12..16].copy_from_slice(b"fmt ");
    header[16..20].copy_from_slice(&16u32.to_le_bytes()); // PCM fmt chunk size
    header[20..22].copy_from_slice(&1u16.to_le_bytes()); // format 1 = uncompressed PCM
    header[22..24].copy_from_slice(&CHANNELS.to_le_bytes());
    header[24..28].copy_from_slice(&sample_rate.to_le_bytes());
    header[28..32].copy_from_slice(&byte_rate.to_le_bytes());
    header[32..34].copy_from_slice(&block_align.to_le_bytes());
    header[34..36].copy_from_slice(&BITS_PER_SAMPLE.to_le_bytes());
    header[36..40].copy_from_slice(b"data");
    header[40..44].copy_from_slice(&data_bytes.to_le_bytes());
    header
}

/// Encode a whole PCM buffer as a WAV file in memory.
///
/// For the offline path, where the sample count is known before writing. Streaming synthesis uses
/// [`WavWriter`], which does not need to know it up front.
#[must_use]
pub fn encode_wav(pcm: &[f32], sample_rate: u32) -> Vec<u8> {
    let mut bytes = Vec::with_capacity(WAV_HEADER_BYTES + pcm.len() * 2);
    bytes.extend_from_slice(&wav_header(sample_rate, pcm.len()));
    for sample in pcm {
        bytes.extend_from_slice(&sample_to_i16(*sample).to_le_bytes());
    }
    bytes
}

/// Samples of low-level, high-frequency junk at the very end of an utterance.
///
/// # The artifact
///
/// The model reliably ends an utterance with a short burst of broadband noise after speech has
/// decayed and before its own trailing silence. Measured on real synthesis at 24 kHz, the burst
/// sits around RMS 20-90 against speech at 500-1200, and its first-difference energy ratio (a cheap
/// high-frequency proxy) runs 0.3-0.9 where speech runs 0.02-0.10. It is what a listener hears as
/// "a little noise right at the end".
///
/// It is NOT a quantization artifact: an interleaved A/B against the f32 reference route on the
/// same text and seed showed the reference producing MORE of it (sustained ~90 ms at ratio up to
/// 0.94) than the int8 route. So it comes from the model's own final frames, and no kernel change
/// removes it.
///
/// # What this does, and what it deliberately does not
///
/// Returns how many samples to drop from the end. The rule is conjunctive on purpose, because each
/// condition alone has a false positive that would eat real audio:
///
/// * **quiet** relative to this utterance's own speech level, so a loud ending is never touched;
/// * **high-frequency dominated**, so a soft voiced ending (a low hum, a sustained vowel) is never
///   touched — those are the tonal opposite of this artifact;
/// * **contiguous from the end**, so noise in the middle of a sentence is left alone entirely.
///
/// Trailing pure silence is skipped before analysis and kept afterwards: it is genuine model output
/// (measured runs carry 0-71 ms of it), and trimming it would change utterance timing.
///
/// Returns 0 whenever the input is too short to judge, which keeps every caller total.
#[must_use]
pub fn trailing_noise_samples(pcm: &[f32], sample_rate: u32) -> usize {
    let level = speech_level(pcm, sample_rate);
    trailing_noise_samples_relative_to(pcm, sample_rate, level)
}

/// The loudest 10 ms window in `pcm`, the reference "speech level" the tail rule compares against.
#[must_use]
pub fn speech_level(pcm: &[f32], sample_rate: u32) -> f32 {
    let window = (sample_rate as usize).saturating_mul(10) / 1000;
    if window < 2 || pcm.is_empty() {
        return 0.0;
    }
    pcm.chunks(window)
        .map(|chunk| {
            if chunk.is_empty() {
                0.0
            } else {
                (chunk.iter().map(|s| s * s).sum::<f32>() / chunk.len() as f32).sqrt()
            }
        })
        .fold(0.0_f32, f32::max)
}

/// [`trailing_noise_samples`] against a caller-supplied speech level.
///
/// Exists because the streaming writer only holds the last quarter second when it decides, and the
/// "quiet" test is meaningless against a window that contains nothing but the artifact — the
/// artifact would become its own reference and never look quiet. The writer therefore measures the
/// level across the whole utterance as it goes and passes it in here.
#[must_use]
pub fn trailing_noise_samples_relative_to(
    pcm: &[f32],
    sample_rate: u32,
    speech_level: f32,
) -> usize {
    trailing_noise_range_relative_to(pcm, sample_rate, speech_level).len()
}

/// As [`trailing_noise_samples_relative_to`], but returning WHERE the noise run sits.
///
/// The count alone is not enough to remove the artifact: the noise ends at the last
/// audible sample, and any sub-epsilon silence after it is genuine model output to be
/// kept. A caller that removes `count` samples from the buffer's literal end instead
/// keeps the audible artifact and deletes the harmless silence — the exact inversion
/// this range exists to prevent.
#[must_use]
pub fn trailing_noise_range_relative_to(
    pcm: &[f32],
    sample_rate: u32,
    speech_level: f32,
) -> std::ops::Range<usize> {
    /// Analysis window. 10 ms is short enough to localize the burst and long enough that a
    /// single glottal pulse does not dominate the statistic.
    const WINDOW_MILLIS: usize = 10;
    /// Never remove more than this. Measured artifact runs: 30 ms (nz5), 80 ms (f32 reference),
    /// and just over 200 ms on the preset voice samples. 250 ms covers the observed range while
    /// still bounding the damage if the rule ever misfires.
    const MAX_TRIM_MILLIS: usize = 250;
    /// A window must be under this fraction of the utterance's speech level to be a candidate.
    /// Artifact windows measured at most 0.08 of the utterance peak, so 0.15 leaves headroom
    /// without reaching the level a real final consonant occupies.
    const QUIET_FRACTION: f32 = 0.15;
    /// First-difference energy ratio above which a window is high-frequency dominated. Voiced
    /// speech measured 0.005-0.10; the artifact measured 0.31-1.92. 0.25 sits in the empty gap
    /// between those two populations.
    const HF_RATIO: f32 = 0.25;
    /// The whole trimmed run must also be this quiet on average. A sustained final fricative is
    /// high-frequency too, and this is what separates it from the artifact: /s/ carries real
    /// level, the artifact does not.
    const RUN_MEAN_FRACTION: f32 = 0.10;

    let window = (sample_rate as usize).saturating_mul(WINDOW_MILLIS) / 1000;
    if window < 2 || pcm.len() < window * 4 {
        return 0..0;
    }

    // Trailing silence is model output, not artifact; keep it, and analyze what precedes it.
    //
    // "Silence" is defined by AUDIBILITY, not by an exact zero bit pattern, and that distinction is
    // load-bearing. This runs on the engine's raw f32, where the quiet tail is a run of tiny
    // non-zero values that only become zeros when written as 16-bit PCM. Testing `!= 0.0` therefore
    // found the last sample of the buffer, started the walk inside inaudible noise, and bailed
    // immediately — the detector matched every WAV file it was validated against and did nothing at
    // all in the live path. Half an i16 step is the honest threshold: below it, the sample rounds
    // to zero in the file the listener actually gets.
    const SILENCE_EPSILON: f32 = 0.5 / 32_767.0;
    let voiced_end = pcm
        .iter()
        .rposition(|sample| sample.abs() >= SILENCE_EPSILON)
        .map_or(0, |i| i + 1);
    if voiced_end < window * 4 {
        return 0..0;
    }

    let rms = |seg: &[f32]| -> f32 {
        if seg.is_empty() {
            return 0.0;
        }
        (seg.iter().map(|s| s * s).sum::<f32>() / seg.len() as f32).sqrt()
    };
    // First-difference energy over signal energy: high for broadband noise, low for voiced speech.
    let hf = |seg: &[f32]| -> f32 {
        let energy: f32 = seg.iter().map(|s| s * s).sum();
        if energy <= f32::MIN_POSITIVE {
            return 0.0;
        }
        let diff: f32 = seg.windows(2).map(|p| (p[1] - p[0]) * (p[1] - p[0])).sum();
        diff / energy
    };

    // The CALLER'S speech level is authoritative, and must not be recomputed from `pcm` here.
    //
    // A local `let speech_level = ...` used to shadow the parameter, which silently defeated this
    // function's entire reason to exist: the streaming writer holds only the last quarter second
    // when it decides, so recomputing from `pcm` measures the artifact against ITSELF and nothing
    // ever looks quiet. It compiled, it passed every whole-buffer test (where the two values
    // coincide), and it made the live path behave differently from the tested one.
    if speech_level <= 0.0 {
        return 0..0;
    }
    let quiet_ceiling = speech_level * QUIET_FRACTION;

    let max_trim = (sample_rate as usize).saturating_mul(MAX_TRIM_MILLIS) / 1000;
    let mut trimmed = 0_usize;
    let mut end = voiced_end;
    while end >= window && trimmed + window <= max_trim {
        let start = end - window;
        let segment = &pcm[start..end];
        if rms(segment) < quiet_ceiling && hf(segment) > HF_RATIO {
            trimmed += window;
            end = start;
        } else {
            break;
        }
    }

    // Final guard on the run as a whole. Each window passing individually is not enough: a
    // sustained final fricative is quiet-ish AND high-frequency window by window, and would walk
    // the loop above backwards through real speech. The artifact's run mean sits far below a
    // fricative's, so this is the condition that separates them.
    if trimmed > 0 {
        let run = &pcm[voiced_end - trimmed..voiced_end];
        if rms(run) >= speech_level * RUN_MEAN_FRACTION {
            return 0..0;
        }
    }
    voiced_end - trimmed..voiced_end
}

/// A streaming WAV writer that finalises a correct header.
///
/// Streaming synthesis does not know the sample count until the run ends, so a provisional header
/// is written first and patched by [`WavWriter::finish`]. That seek-back is why the sink must be
/// `Seek`: an unseekable sink cannot carry a correct length, and silently emitting a wrong one is
/// the corruption this type exists to prevent.
///
/// If the run is cut short — cancellation, a full disk — calling `finish` still yields a valid
/// file describing the samples that made it, which is the partial-output promise in plan §9.6.
/// Milliseconds of audio held back when tail trimming is armed: the detector's own
/// ceiling, so the buffer always holds every sample the trim could possibly want. The
/// sample count derives from the writer's OWN rate — a constant sized for 24 kHz would
/// silently halve the trim window at 48 kHz.
const TAIL_HOLDBACK_MILLIS: usize = 250;

pub struct WavWriter<W: Write + Seek> {
    /// `None` once [`WavWriter::finish`] has handed the sink back.
    ///
    /// An `Option` rather than a bare `W` because a type with a `Drop` impl cannot be moved out
    /// of, and `ftts-core` forbids the `unsafe` that `ManuallyDrop` would need. The `None` state
    /// also tells `Drop` that finalisation already happened, so it is not attempted twice.
    sink: Option<W>,
    sample_rate: u32,
    samples_written: usize,
    /// Samples withheld from the file so the end-of-utterance trim can still see them.
    ///
    /// Writing is delayed by at most the holdback window, which is why this is opt-in: for a
    /// file that delay is invisible, but on `--stream raw` it would add latency to a path whose
    /// whole contract is time-to-first-audio. `None` means trimming is off and every sample goes
    /// straight through.
    holdback: Option<Vec<f32>>,
    /// Loudest 10 ms window seen across the WHOLE utterance.
    ///
    /// Tracked as samples arrive because the tail decision happens when only the last quarter
    /// second is still in hand, and a "quiet relative to speech" rule needs the speech.
    speech_level: f32,
}

impl<W: Write + Seek> WavWriter<W> {
    /// Begin a file, writing a provisional header.
    ///
    /// # Errors
    ///
    /// If the provisional header cannot be written.
    pub fn new(mut sink: W, sample_rate: u32) -> io::Result<Self> {
        sink.write_all(&wav_header(sample_rate, 0))?;
        Ok(Self {
            sink: Some(sink),
            sample_rate,
            samples_written: 0,
            holdback: None,
            speech_level: 0.0,
        })
    }

    /// As [`WavWriter::new`], but drops the model's end-of-utterance noise burst.
    ///
    /// See [`trailing_noise_samples`] for what is removed and why it is safe. The cost is that the
    /// last quarter second is held in memory until [`WavWriter::finish`], so this is for file
    /// output only; a raw PCM stream must keep its latency and stays untrimmed.
    ///
    /// # Errors
    ///
    /// If the provisional header cannot be written.
    pub fn new_trimming_tail(sink: W, sample_rate: u32) -> io::Result<Self> {
        let mut writer = Self::new(sink, sample_rate)?;
        writer.holdback = Some(Vec::with_capacity(writer.holdback_samples() * 2));
        Ok(writer)
    }

    /// Samples the trimming writer withholds, derived from this writer's own rate.
    fn holdback_samples(&self) -> usize {
        (self.sample_rate as usize).saturating_mul(TAIL_HOLDBACK_MILLIS) / 1000
    }

    /// Append one packet of decoded `f32` samples.
    ///
    /// # Errors
    ///
    /// If the sink rejects the write. The count of samples already accepted stays accurate, so a
    /// later [`WavWriter::finish`] still describes the file truthfully.
    pub fn write_samples(&mut self, pcm: &[f32]) -> io::Result<()> {
        // With trimming armed, keep the newest holdback window back and emit only what has
        // aged out. Those held samples are the only ones the trim can ever remove, so nothing that
        // reaches the file here can need taking back.
        if self.holdback.is_some() {
            self.speech_level = self.speech_level.max(speech_level(pcm, self.sample_rate));
            let mut pending = self.holdback.take().unwrap_or_default();
            pending.extend_from_slice(pcm);
            let releasable = pending.len().saturating_sub(self.holdback_samples());
            let released: Vec<f32> = pending.drain(..releasable).collect();
            self.holdback = Some(pending);
            if released.is_empty() {
                return Ok(());
            }
            return self.write_through(&released);
        }
        self.write_through(pcm)
    }

    /// Writes samples straight to the sink, bypassing the hold-back.
    fn write_through(&mut self, pcm: &[f32]) -> io::Result<()> {
        // Buffer the packet so one short write cannot leave half a sample in the file, which
        // would desynchronise every subsequent frame by one byte.
        let mut bytes = Vec::with_capacity(pcm.len() * 2);
        for sample in pcm {
            bytes.extend_from_slice(&sample_to_i16(*sample).to_le_bytes());
        }
        let sink = self
            .sink
            .as_mut()
            .ok_or_else(|| io::Error::other("WavWriter already finished"))?;
        sink.write_all(&bytes)?;
        self.samples_written += pcm.len();
        Ok(())
    }

    /// Samples accepted so far.
    #[must_use]
    pub const fn samples_written(&self) -> usize {
        self.samples_written
    }

    /// Duration of the audio written so far, in milliseconds.
    #[must_use]
    pub const fn duration_millis(&self) -> u64 {
        if self.sample_rate == 0 {
            return 0;
        }
        (self.samples_written as u64) * 1000 / (self.sample_rate as u64)
    }

    /// Patch the header to the real length and flush.
    ///
    /// # Errors
    ///
    /// If seeking back to the header, rewriting it, or flushing fails.
    pub fn finish(self) -> io::Result<W> {
        self.finish_reporting().map(|(sink, _)| sink)
    }

    /// As [`WavWriter::finish`], also reporting how many samples the file actually contains.
    ///
    /// Callers that publish a sample count need this rather than their own tally: tail trimming
    /// (and any short write) makes "samples handed to the writer" differ from "samples in the
    /// file", and a reported count that describes audio the file does not hold is a false number
    /// in a machine-readable stream.
    ///
    /// # Errors
    ///
    /// If flushing the held tail, seeking back to the header, rewriting it, or flushing fails.
    pub fn finish_reporting(mut self) -> io::Result<(W, usize)> {
        // Release the held tail, minus whatever the detector identifies as the model's end-of-
        // utterance noise. Done before the header is patched so the length describes what landed.
        if let Some(pending) = self.holdback.take() {
            // The noise run ends at the last AUDIBLE sample; sub-epsilon silence after
            // it is genuine model output and is kept, per the detector's contract.
            // Removing `count` samples from the buffer's literal end instead kept the
            // audible artifact and deleted the harmless silence whenever the burst was
            // followed by a quiet tail — the live shape that motivated this feature.
            let noise =
                trailing_noise_range_relative_to(&pending, self.sample_rate, self.speech_level);
            self.write_through(&pending[..noise.start])?;
            if noise.end < pending.len() {
                self.write_through(&pending[noise.end..])?;
            }
        }
        self.finalize_header()?;
        let written = self.samples_written;
        let sink = self
            .sink
            .take()
            .ok_or_else(|| io::Error::other("WavWriter already finished"))?;
        Ok((sink, written))
    }

    fn finalize_header(&mut self) -> io::Result<()> {
        let header = wav_header(self.sample_rate, self.samples_written);
        let Some(sink) = self.sink.as_mut() else {
            return Ok(());
        };
        sink.seek(SeekFrom::Start(0))?;
        sink.write_all(&header)?;
        sink.seek(SeekFrom::End(0))?;
        sink.flush()
    }
}

impl<W: Write + Seek> Drop for WavWriter<W> {
    /// Best-effort finalisation for a writer dropped without [`WavWriter::finish`].
    ///
    /// A dropped writer means an abnormal end — a panic, an early return, a cancelled run. Leaving
    /// the provisional zero-length header would make the file claim it contains no audio while
    /// holding a megabyte of it. The error is deliberately swallowed because `Drop` cannot report,
    /// which is exactly why `finish` exists and should be called explicitly.
    fn drop(&mut self) {
        // `finish` takes the sink, so a still-present sink means an abnormal end.
        if self.sink.is_some() {
            // Release the held tail FIRST, and deliberately without trimming.
            //
            // Two reasons. Losing it would silently shorten the file by up to a quarter second and
            // break this type's partial-output promise: a run cut short must still describe every
            // sample that made it. And an abnormal end means the tail is not an end-of-utterance
            // artifact at all — it is wherever synthesis happened to stop, most likely mid-word —
            // so the trim's premise does not hold and applying it would remove real audio.
            if let Some(pending) = self.holdback.take()
                && !pending.is_empty()
            {
                let _ = self.write_through(&pending);
            }
            let _ = self.finalize_header();
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Cursor;

    #[test]
    fn frame_count_maps_to_the_codec_sample_rate() {
        // 24 kHz at 12.5 frames/s is 1,920 samples per 80 ms frame; the two constants must agree
        // or every duration we report is wrong.
        assert_eq!(SAMPLES_PER_FRAME * 25, SAMPLE_RATE_HZ as usize * 2);
        assert_eq!(samples_for_frames(1), 1_920);
        assert_eq!(samples_for_frames(125), SAMPLE_RATE_HZ as usize * 10);
    }

    #[test]
    fn conversion_clamps_before_scaling() {
        // The bug this prevents: scaling 1.2 first gives 39_320, which wraps to a large negative
        // i16 — a click at the loudest moment, which every byte-count check calls success.
        assert_eq!(sample_to_i16(1.2), i16::MAX);
        assert_eq!(sample_to_i16(-1.2), -i16::MAX);
        assert_eq!(sample_to_i16(1.0), i16::MAX);
        assert_eq!(sample_to_i16(-1.0), -i16::MAX);
        assert_eq!(sample_to_i16(0.0), 0);
    }

    #[test]
    fn conversion_rounds_rather_than_truncates() {
        // Truncation biases every sample toward zero and quietly lowers the output level.
        let half_step = 0.5 / 32_767.0;
        assert_eq!(sample_to_i16(half_step), 1);
        assert_eq!(sample_to_i16(-half_step), -1);
    }

    #[test]
    fn non_finite_becomes_silence_not_noise() {
        assert_eq!(sample_to_i16(f32::NAN), 0);
        assert_eq!(sample_to_i16(f32::INFINITY), 0);
        assert_eq!(sample_to_i16(f32::NEG_INFINITY), 0);
    }

    #[test]
    fn energy_separates_silence_from_audio() {
        assert_eq!(mean_square_energy(&[]), 0.0);
        assert_eq!(mean_square_energy(&[0.0; 64]), 0.0);
        assert!(mean_square_energy(&[0.5; 64]) > 0.2);
    }

    #[test]
    fn the_header_describes_exactly_the_payload() {
        let pcm = vec![0.25f32; 1_920];
        let wav = encode_wav(&pcm, SAMPLE_RATE_HZ);
        assert_eq!(wav.len(), WAV_HEADER_BYTES + pcm.len() * 2);
        assert_eq!(&wav[0..4], b"RIFF");
        assert_eq!(&wav[8..12], b"WAVE");
        assert_eq!(&wav[36..40], b"data");

        let declared_data = u32::from_le_bytes(wav[40..44].try_into().expect("data size"));
        let actual_data = (wav.len() - WAV_HEADER_BYTES) as u32;
        assert_eq!(
            declared_data, actual_data,
            "data size must match the payload"
        );

        let declared_riff = u32::from_le_bytes(wav[4..8].try_into().expect("riff size"));
        assert_eq!(declared_riff, 36 + actual_data, "RIFF size must agree");

        let rate = u32::from_le_bytes(wav[24..28].try_into().expect("rate"));
        assert_eq!(rate, SAMPLE_RATE_HZ);
        let channels = u16::from_le_bytes(wav[22..24].try_into().expect("channels"));
        assert_eq!(channels, 1, "the model is mono");
        let bits = u16::from_le_bytes(wav[34..36].try_into().expect("bits"));
        assert_eq!(bits, 16);
    }

    #[test]
    fn a_streamed_file_is_byte_identical_to_the_offline_encoding() {
        // Streaming and offline must produce the same file for the same samples, or "streaming ==
        // batch" fails at the very last stage of the pipeline.
        let pcm: Vec<f32> = (0..1_920)
            .map(|i| (i as f32 / 1_920.0 * std::f32::consts::TAU).sin() * 0.5)
            .collect();

        let mut writer = WavWriter::new(Cursor::new(Vec::new()), SAMPLE_RATE_HZ).expect("header");
        for packet in pcm.chunks(480) {
            writer.write_samples(packet).expect("packet");
        }
        assert_eq!(writer.samples_written(), pcm.len());
        assert_eq!(writer.duration_millis(), 80);
        let streamed = writer.finish().expect("finish").into_inner();

        assert_eq!(streamed, encode_wav(&pcm, SAMPLE_RATE_HZ));
    }

    #[test]
    fn a_truncated_run_still_finalises_a_valid_header() {
        // The partial-output promise: a run cut short produces a file describing the samples that
        // actually landed, not the zero-length provisional header.
        let mut writer = WavWriter::new(Cursor::new(Vec::new()), SAMPLE_RATE_HZ).expect("header");
        writer.write_samples(&[0.5f32; 960]).expect("packet");
        let file = writer.finish().expect("finish").into_inner();

        let declared = u32::from_le_bytes(file[40..44].try_into().expect("data size"));
        assert_eq!(declared, 960 * 2);
        assert_eq!(file.len(), WAV_HEADER_BYTES + 960 * 2);
    }

    #[test]
    fn dropping_without_finish_still_patches_the_length() {
        // A panic or early return must not leave a file claiming it holds no audio.
        let mut sink = Cursor::new(Vec::new());
        {
            let mut writer = WavWriter::new(&mut sink, SAMPLE_RATE_HZ).expect("header");
            writer.write_samples(&[0.25f32; 128]).expect("packet");
            // dropped here without finish()
        }
        let file = sink.into_inner();
        let declared = u32::from_le_bytes(file[40..44].try_into().expect("data size"));
        assert_eq!(declared, 128 * 2, "Drop must finalise the length");
    }

    #[test]
    fn an_empty_run_is_a_valid_zero_length_wav() {
        let wav = encode_wav(&[], SAMPLE_RATE_HZ);
        assert_eq!(wav.len(), WAV_HEADER_BYTES);
        assert_eq!(u32::from_le_bytes(wav[40..44].try_into().expect("size")), 0);
        assert_eq!(u32::from_le_bytes(wav[4..8].try_into().expect("riff")), 36);
    }
}

#[cfg(test)]
mod tail_tests {
    use super::*;

    const SR: u32 = 24_000;

    fn noise(len: usize, amplitude: f32) -> Vec<f32> {
        // Alternating sign: maximal first-difference energy, i.e. the broadband shape the
        // artifact has.
        (0..len)
            .map(|i| if i % 2 == 0 { amplitude } else { -amplitude })
            .collect()
    }

    fn tone(len: usize, amplitude: f32) -> Vec<f32> {
        // 200 Hz: voiced, low first-difference energy.
        (0..len)
            .map(|i| amplitude * (i as f32 * 2.0 * std::f32::consts::PI * 200.0 / SR as f32).sin())
            .collect()
    }

    #[test]
    fn a_quiet_high_frequency_tail_is_trimmed() {
        let mut pcm = tone(SR as usize / 2, 0.5);
        pcm.extend(noise(SR as usize * 40 / 1000, 0.02));
        let trimmed = trailing_noise_samples(&pcm, SR);
        assert!(trimmed > 0, "the artifact shape must be detected");
        assert!(
            trimmed <= SR as usize * 40 / 1000 + SR as usize / 100,
            "trim {trimmed} reached past the noise into speech"
        );
    }

    #[test]
    fn a_loud_ending_is_never_trimmed() {
        // Speech that simply stops at full level: nothing to remove, however abrupt.
        let pcm = tone(SR as usize / 2, 0.5);
        assert_eq!(trailing_noise_samples(&pcm, SR), 0);
    }

    #[test]
    fn a_quiet_voiced_ending_is_never_trimmed() {
        // The dangerous false positive: a soft sustained vowel is quiet but TONAL, so the
        // high-frequency condition must save it.
        let mut pcm = tone(SR as usize / 2, 0.5);
        pcm.extend(tone(SR as usize * 60 / 1000, 0.02));
        assert_eq!(
            trailing_noise_samples(&pcm, SR),
            0,
            "a soft voiced ending must survive"
        );
    }

    #[test]
    fn trailing_silence_is_preserved_and_the_noise_before_it_is_found() {
        let mut pcm = tone(SR as usize / 2, 0.5);
        pcm.extend(noise(SR as usize * 30 / 1000, 0.02));
        let silence = SR as usize * 50 / 1000;
        pcm.extend(std::iter::repeat_n(0.0_f32, silence));
        let trimmed = trailing_noise_samples(&pcm, SR);
        assert!(
            trimmed > 0,
            "silence after the burst must not hide the burst"
        );
        // The reported count covers only the noise; the caller keeps the silence.
        assert!(trimmed <= SR as usize * 40 / 1000);
    }

    #[test]
    fn noise_in_the_middle_is_left_alone() {
        let mut pcm = tone(SR as usize / 4, 0.5);
        pcm.extend(noise(SR as usize * 30 / 1000, 0.02));
        pcm.extend(tone(SR as usize / 4, 0.5));
        assert_eq!(
            trailing_noise_samples(&pcm, SR),
            0,
            "only a tail contiguous with the end is in scope"
        );
    }

    /// The caller's speech level must actually be honored.
    ///
    /// This is the test that was missing when a local binding shadowed the parameter: every
    /// whole-buffer test still passed, because there the supplied level and the recomputed one are
    /// the same number. The bug only appeared where it mattered — the streaming writer, which holds
    /// just the tail. Feeding the same tail with the utterance's level versus the tail's own level
    /// must give different answers, or the parameter is being ignored.
    #[test]
    fn the_supplied_speech_level_is_the_one_used() {
        // The noise run must be at least as long as the writer's hold-back, or the 250 ms tail
        // still contains loud speech and the two levels coincide — which is precisely the
        // condition under which the shadowing bug stayed invisible.
        let mut pcm = tone(SR as usize / 2, 0.5);
        pcm.extend(noise(SR as usize * 300 / 1000, 0.02));

        let utterance_level = speech_level(&pcm, SR);
        let tail = &pcm[pcm.len() - SR as usize * 250 / 1000..];
        let tail_level = speech_level(tail, SR);
        assert!(
            tail_level < utterance_level,
            "the fixture must have a tail quieter than the utterance"
        );

        // Against the utterance's level the tail is quiet, so it is trimmed.
        assert!(
            trailing_noise_samples_relative_to(tail, SR, utterance_level) > 0,
            "the supplied utterance level was ignored"
        );
        // Against the tail's OWN level nothing looks quiet, so nothing is trimmed. If the function
        // recomputed internally it would always take this branch and never trim in the live path.
        assert_eq!(
            trailing_noise_samples_relative_to(tail, SR, tail_level),
            0,
            "a self-referential level must find nothing quiet"
        );
    }

    #[test]
    fn short_and_empty_inputs_are_total() {
        assert_eq!(trailing_noise_samples(&[], SR), 0);
        assert_eq!(trailing_noise_samples(&[0.1; 16], SR), 0);
        assert_eq!(trailing_noise_samples(&[0.0; 4096], SR), 0);
        assert_eq!(trailing_noise_samples(&tone(4096, 0.5), 0), 0);
    }
}

#[cfg(test)]
mod holdback_tests {
    use super::*;
    use std::io::Cursor;

    fn tone(len: usize, amplitude: f32) -> Vec<f32> {
        (0..len)
            .map(|i| amplitude * (i as f32 * 2.0 * std::f32::consts::PI * 200.0 / 24_000.0).sin())
            .collect()
    }

    /// The trim must remove the AUDIBLE noise burst and keep the inaudible tail after
    /// it — not shave an equal count off the buffer's literal end. An earlier writer
    /// did exactly that: identical sample COUNTS, completely inverted sample CHOICE,
    /// so this asserts on content where the counts cannot distinguish right from wrong.
    #[test]
    fn the_trim_removes_the_burst_not_the_silence_after_it() {
        let mut pcm = tone(24_000, 0.6);
        // 100 ms audible broadband burst (alternating sign, quiet, high-frequency)...
        for i in 0..2_400 {
            pcm.push(if i % 2 == 0 { 0.02 } else { -0.02 });
        }
        // ...then 100 ms of sub-epsilon silence, the live f32 shape.
        pcm.extend(std::iter::repeat_n(1.0e-6_f32, 2_400));

        let mut writer =
            WavWriter::new_trimming_tail(Cursor::new(Vec::new()), 24_000).expect("writer");
        writer.write_samples(&pcm).expect("write");
        let (sink, written) = writer.finish_reporting().expect("finish");
        let bytes = sink.into_inner();

        let payload: Vec<i16> = bytes[WAV_HEADER_BYTES..]
            .as_chunks::<2>()
            .0
            .iter()
            .map(|pair| i16::from_le_bytes(*pair))
            .collect();
        assert_eq!(written, payload.len(), "header count describes the payload");
        assert_eq!(
            written,
            pcm.len() - 2_400,
            "exactly the burst's length is gone"
        );
        // Content check the counts cannot fake: everything after the tone must be the
        // (quantized-to-zero) silence, with no audible burst sample surviving.
        let audible_after_tone = payload[24_000..].iter().filter(|s| s.abs() > 1).count();
        assert_eq!(
            audible_after_tone, 0,
            "audible burst samples survived the trim: {audible_after_tone}"
        );
    }

    fn write(pcm: &[f32], trimming: bool, chunk: usize) -> Vec<u8> {
        let sink = Cursor::new(Vec::new());
        let mut writer = if trimming {
            WavWriter::new_trimming_tail(sink, SAMPLE_RATE_HZ).expect("header")
        } else {
            WavWriter::new(sink, SAMPLE_RATE_HZ).expect("header")
        };
        for packet in pcm.chunks(chunk) {
            writer.write_samples(packet).expect("write");
        }
        writer.finish().expect("finish").into_inner()
    }

    /// Audio with no artifact must survive the hold-back path completely unchanged — same bytes,
    /// same header — however it is packetized. The hold-back must be a delay, never a filter.
    #[test]
    fn clean_audio_is_byte_identical_through_the_holdback() {
        let pcm = tone(24_000, 0.4);
        let plain = write(&pcm, false, 1_920);
        for chunk in [240, 1_920, 4_096, 24_000] {
            assert_eq!(
                write(&pcm, true, chunk),
                plain,
                "packet size {chunk} changed the bytes"
            );
        }
    }

    /// The header must describe what actually landed after a trim, or the file is corrupt in the
    /// exact way this module exists to prevent.
    #[test]
    fn a_trimmed_file_has_a_header_matching_its_payload() {
        let mut pcm = tone(24_000, 0.5);
        let noise: Vec<f32> = (0..2_400)
            .map(|i| if i % 2 == 0 { 0.01 } else { -0.01 })
            .collect();
        pcm.extend_from_slice(&noise);

        let bytes = write(&pcm, true, 1_920);
        let payload = bytes.len() - WAV_HEADER_BYTES;
        let declared = u32::from_le_bytes([bytes[40], bytes[41], bytes[42], bytes[43]]) as usize;
        assert_eq!(
            declared, payload,
            "data chunk size disagrees with the payload"
        );
        let riff = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]) as usize;
        assert_eq!(riff, 36 + payload, "RIFF size disagrees with the payload");
        assert!(
            payload / 2 < pcm.len(),
            "the artifact tail should have been removed"
        );
    }

    /// A writer dropped without `finish` must still contain every sample it accepted.
    ///
    /// This is the partial-output promise, and the hold-back is exactly what threatens it: the last
    /// quarter second lives in memory, so a `Drop` that only patched the header would silently
    /// shorten a cancelled run's file by up to 250 ms. Nothing else in the type would notice —
    /// the header would agree with the payload, and the file would play.
    #[test]
    fn a_dropped_writer_keeps_every_sample_it_accepted() {
        let path = std::env::temp_dir().join(format!(
            "ftts-drop-holdback-{}-{}.wav",
            std::process::id(),
            line!()
        ));
        let pcm = tone(24_000, 0.5);
        {
            let file = std::fs::File::create(&path).expect("create");
            let mut writer = WavWriter::new_trimming_tail(file, SAMPLE_RATE_HZ).expect("header");
            for packet in pcm.chunks(1_920) {
                writer.write_samples(packet).expect("write");
            }
            // Deliberately NOT calling finish: this models a panic or a cancelled run.
        }
        let written = std::fs::read(&path).expect("read back");
        let _ = std::fs::remove_file(&path);

        let payload = written.len() - WAV_HEADER_BYTES;
        assert_eq!(
            payload / 2,
            pcm.len(),
            "the dropped writer lost {} held samples",
            pcm.len() - payload / 2
        );
        let declared =
            u32::from_le_bytes([written[40], written[41], written[42], written[43]]) as usize;
        assert_eq!(declared, payload, "header disagrees with the payload");
    }

    /// An empty run must still produce a valid, playable, zero-length file.
    #[test]
    fn an_empty_run_still_finalizes() {
        let bytes = write(&[], true, 1_920);
        assert_eq!(bytes.len(), WAV_HEADER_BYTES);
        assert_eq!(
            u32::from_le_bytes([bytes[40], bytes[41], bytes[42], bytes[43]]),
            0
        );
    }
}

#[cfg(test)]
mod f32_silence_tests {
    use super::*;

    /// Trailing silence in the ENGINE's f32 output is not exact zeros: it is tiny values that only
    /// become zeros when written as 16-bit PCM. A detector that tests `!= 0.0` matches every WAV
    /// file it is validated against and then does nothing at all in the live path, which is exactly
    /// what happened. This pins the audibility-based definition instead.
    #[test]
    fn inaudible_f32_tail_counts_as_silence() {
        let sr = 24_000;
        let mut pcm: Vec<f32> = (0..sr / 2)
            .map(|i| 0.5 * (i as f32 * 2.0 * std::f32::consts::PI * 200.0 / sr as f32).sin())
            .collect();
        // The artifact: quiet, alternating-sign, high-frequency.
        pcm.extend((0..2_400).map(|i| if i % 2 == 0 { 0.01 } else { -0.01 }));
        // A tail that rounds to zero in i16 but is nowhere near 0.0 in f32.
        pcm.extend((0..2_400).map(|i| if i % 2 == 0 { 1.0e-6 } else { -1.0e-6 }));

        let trimmed = trailing_noise_samples(&pcm, sr as u32);
        assert!(
            trimmed > 0,
            "an inaudible f32 tail must not hide the artifact behind it"
        );

        // And the same buffer rounded through i16 must agree, so the WAV-file validation and the
        // live f32 path can never diverge again.
        let rounded: Vec<f32> = pcm
            .iter()
            .map(|s| f32::from(sample_to_i16(*s)) / 32_767.0)
            .collect();
        let trimmed_rounded = trailing_noise_samples(&rounded, sr as u32);
        assert_eq!(
            trimmed, trimmed_rounded,
            "f32 and i16-rounded views of the same audio must trim identically"
        );
    }
}