use std::io::{self, Seek, SeekFrom, Write};
pub const SAMPLE_RATE_HZ: u32 = 24_000;
pub const SAMPLES_PER_FRAME: usize = 1_920;
pub const CHANNELS: u16 = 1;
pub const BITS_PER_SAMPLE: u16 = 16;
pub const WAV_HEADER_BYTES: usize = 44;
#[must_use]
pub const fn samples_for_frames(frames: usize) -> usize {
frames * SAMPLES_PER_FRAME
}
#[must_use]
pub fn sample_to_i16(sample: f32) -> i16 {
if !sample.is_finite() {
return 0;
}
let clamped = sample.clamp(-1.0, 1.0);
(clamped * 32_767.0).round() as i16
}
#[must_use]
pub fn pcm_f32_to_i16(pcm: &[f32]) -> Vec<i16> {
pcm.iter().copied().map(sample_to_i16).collect()
}
#[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
}
#[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");
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()); header[20..22].copy_from_slice(&1u16.to_le_bytes()); 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
}
#[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
}
#[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)
}
#[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)
}
#[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()
}
#[must_use]
pub fn trailing_noise_range_relative_to(
pcm: &[f32],
sample_rate: u32,
speech_level: f32,
) -> std::ops::Range<usize> {
const WINDOW_MILLIS: usize = 10;
const MAX_TRIM_MILLIS: usize = 250;
const QUIET_FRACTION: f32 = 0.15;
const HF_RATIO: f32 = 0.25;
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;
}
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()
};
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
};
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;
}
}
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
}
const TAIL_HOLDBACK_MILLIS: usize = 250;
pub struct WavWriter<W: Write + Seek> {
sink: Option<W>,
sample_rate: u32,
samples_written: usize,
holdback: Option<Vec<f32>>,
speech_level: f32,
}
impl<W: Write + Seek> WavWriter<W> {
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,
})
}
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)
}
fn holdback_samples(&self) -> usize {
(self.sample_rate as usize).saturating_mul(TAIL_HOLDBACK_MILLIS) / 1000
}
pub fn write_samples(&mut self, pcm: &[f32]) -> io::Result<()> {
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)
}
fn write_through(&mut self, pcm: &[f32]) -> io::Result<()> {
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(())
}
#[must_use]
pub const fn samples_written(&self) -> usize {
self.samples_written
}
#[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)
}
pub fn finish(self) -> io::Result<W> {
self.finish_reporting().map(|(sink, _)| sink)
}
pub fn finish_reporting(mut self) -> io::Result<(W, usize)> {
if let Some(pending) = self.holdback.take() {
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> {
fn drop(&mut self) {
if self.sink.is_some() {
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() {
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() {
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() {
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() {
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() {
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() {
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");
}
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> {
(0..len)
.map(|i| if i % 2 == 0 { amplitude } else { -amplitude })
.collect()
}
fn tone(len: usize, amplitude: f32) -> Vec<f32> {
(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() {
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() {
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"
);
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"
);
}
#[test]
fn the_supplied_speech_level_is_the_one_used() {
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"
);
assert!(
trailing_noise_samples_relative_to(tail, SR, utterance_level) > 0,
"the supplied utterance level was ignored"
);
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()
}
#[test]
fn the_trim_removes_the_burst_not_the_silence_after_it() {
let mut pcm = tone(24_000, 0.6);
for i in 0..2_400 {
pcm.push(if i % 2 == 0 { 0.02 } else { -0.02 });
}
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"
);
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()
}
#[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"
);
}
}
#[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"
);
}
#[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");
}
}
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");
}
#[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::*;
#[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();
pcm.extend((0..2_400).map(|i| if i % 2 == 0 { 0.01 } else { -0.01 }));
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"
);
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"
);
}
}