use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
pub const SAMPLE_RATE: u32 = 16_000;
pub const SAMPLES_PER_MS: u64 = SAMPLE_RATE as u64 / 1000;
pub fn ms_to_samples(ms: u64) -> usize {
(ms * SAMPLES_PER_MS) as usize
}
pub fn samples_to_ms(samples: usize) -> u64 {
samples as u64 / SAMPLES_PER_MS
}
pub fn level_dbfs(samples: &[i16]) -> f32 {
if samples.is_empty() {
return -90.0;
}
let sum: f64 = samples.iter().map(|&s| (s as f64) * (s as f64)).sum();
let rms = (sum / samples.len() as f64).sqrt();
if rms < 1.0 {
return -90.0;
}
(20.0 * (rms / i16::MAX as f64).log10()) as f32
}
pub fn f32_to_i16(samples: &[f32]) -> Vec<i16> {
samples
.iter()
.map(|&s| (s.clamp(-1.0, 1.0) * i16::MAX as f32) as i16)
.collect()
}
pub fn to_native(samples: &[f32], channels: usize, rate: u32) -> Vec<i16> {
let channels = channels.max(1);
let mono: Vec<f32> = samples
.chunks_exact(channels)
.map(|f| f.iter().sum::<f32>() / channels as f32)
.collect();
let out: Vec<f32> = if rate == SAMPLE_RATE {
mono
} else if rate.is_multiple_of(SAMPLE_RATE) {
let k = (rate / SAMPLE_RATE) as usize;
mono.chunks_exact(k)
.map(|c| c.iter().sum::<f32>() / k as f32)
.collect()
} else {
let step = rate as f64 / SAMPLE_RATE as f64;
let n = (mono.len() as f64 / step) as usize;
(0..n)
.map(|i| {
let x = i as f64 * step;
let j = x as usize;
let t = (x - j as f64) as f32;
let a = mono[j];
let b = *mono.get(j + 1).unwrap_or(&a);
a + (b - a) * t
})
.collect()
};
f32_to_i16(&out)
}
pub struct Resampler {
channels: usize,
rate: u32,
acc: f32,
acc_n: usize,
pos: f64,
index: u64,
prev: Option<f32>,
}
impl Resampler {
pub fn new(channels: usize, rate: u32) -> Self {
Self {
channels: channels.max(1),
rate,
acc: 0.0,
acc_n: 0,
pos: 0.0,
index: 0,
prev: None,
}
}
pub fn push(&mut self, interleaved: &[f32]) -> Vec<i16> {
let c = self.channels;
let mono = interleaved
.chunks_exact(c)
.map(|f| f.iter().sum::<f32>() / c as f32);
let mut out = Vec::new();
if self.rate == SAMPLE_RATE {
out.extend(mono);
} else if self.rate.is_multiple_of(SAMPLE_RATE) {
let k = (self.rate / SAMPLE_RATE) as usize;
for s in mono {
self.acc += s;
self.acc_n += 1;
if self.acc_n == k {
out.push(self.acc / k as f32);
self.acc = 0.0;
self.acc_n = 0;
}
}
} else {
let step = self.rate as f64 / SAMPLE_RATE as f64;
for s in mono {
if let Some(a) = self.prev {
while self.pos < self.index as f64 {
let t = (self.pos - (self.index - 1) as f64) as f32;
out.push(a + (s - a) * t);
self.pos += step;
}
}
self.prev = Some(s);
self.index += 1;
}
}
f32_to_i16(&out)
}
}
fn header(data_bytes: u32) -> [u8; 44] {
let mut h = [0u8; 44];
h[0..4].copy_from_slice(b"RIFF");
h[4..8].copy_from_slice(&(36 + data_bytes).to_le_bytes());
h[8..12].copy_from_slice(b"WAVE");
h[12..16].copy_from_slice(b"fmt ");
h[16..20].copy_from_slice(&16u32.to_le_bytes());
h[20..22].copy_from_slice(&1u16.to_le_bytes()); h[22..24].copy_from_slice(&1u16.to_le_bytes()); h[24..28].copy_from_slice(&SAMPLE_RATE.to_le_bytes());
h[28..32].copy_from_slice(&(SAMPLE_RATE * 2).to_le_bytes());
h[32..34].copy_from_slice(&2u16.to_le_bytes());
h[34..36].copy_from_slice(&16u16.to_le_bytes());
h[36..40].copy_from_slice(b"data");
h[40..44].copy_from_slice(&data_bytes.to_le_bytes());
h
}
pub fn encode_wav(samples: &[i16]) -> Vec<u8> {
let data_bytes = (samples.len() * 2) as u32;
let mut out = Vec::with_capacity(44 + samples.len() * 2);
out.extend_from_slice(&header(data_bytes));
for s in samples {
out.extend_from_slice(&s.to_le_bytes());
}
out
}
pub fn decode_wav(bytes: &[u8]) -> io::Result<Vec<i16>> {
let bad = |m: &str| io::Error::new(io::ErrorKind::InvalidData, m.to_string());
if bytes.len() < 12 || &bytes[0..4] != b"RIFF" || &bytes[8..12] != b"WAVE" {
return Err(bad("не WAV"));
}
let mut pos = 12;
let mut format_ok = false;
while pos + 8 <= bytes.len() {
let id = &bytes[pos..pos + 4];
let size = u32::from_le_bytes(bytes[pos + 4..pos + 8].try_into().unwrap()) as usize;
let body = pos + 8;
let end = (body + size).min(bytes.len());
if id == b"fmt " {
let f = &bytes[body..end];
if f.len() < 16 {
return Err(bad("короткий fmt"));
}
let fmt = u16::from_le_bytes([f[0], f[1]]);
let channels = u16::from_le_bytes([f[2], f[3]]);
let rate = u32::from_le_bytes(f[4..8].try_into().unwrap());
let bits = u16::from_le_bytes([f[14], f[15]]);
format_ok = fmt == 1 && channels == 1 && rate == SAMPLE_RATE && bits == 16;
if !format_ok {
return Err(bad("нужен WAV 16 кГц, моно, 16 бит"));
}
} else if id == b"data" {
if !format_ok {
return Err(bad("data раньше fmt"));
}
return Ok(bytes[body..end]
.as_chunks::<2>()
.0
.iter()
.map(|c| i16::from_le_bytes(*c))
.collect());
}
pos = body + size + (size & 1);
}
Err(bad("нет блока data"))
}
#[derive(Debug, Clone)]
pub struct WavFile {
path: PathBuf,
data_start: u64,
samples: usize,
}
impl WavFile {
pub fn open(path: &Path) -> io::Result<Self> {
let bad = |m: &str| io::Error::new(io::ErrorKind::InvalidData, m.to_string());
let mut file = File::open(path)?;
let file_len = file.metadata()?.len();
let mut riff = [0u8; 12];
file.read_exact(&mut riff).map_err(|_| bad("не WAV"))?;
if &riff[0..4] != b"RIFF" || &riff[8..12] != b"WAVE" {
return Err(bad("не WAV"));
}
let mut pos = 12u64;
let mut format_ok = false;
while pos + 8 <= file_len {
let mut head = [0u8; 8];
file.seek(SeekFrom::Start(pos))?;
file.read_exact(&mut head)?;
let size = u32::from_le_bytes(head[4..8].try_into().unwrap()) as u64;
let body = pos + 8;
if &head[0..4] == b"fmt " {
let mut f = [0u8; 16];
file.read_exact(&mut f).map_err(|_| bad("короткий fmt"))?;
let fmt = u16::from_le_bytes([f[0], f[1]]);
let channels = u16::from_le_bytes([f[2], f[3]]);
let rate = u32::from_le_bytes(f[4..8].try_into().unwrap());
let bits = u16::from_le_bytes([f[14], f[15]]);
format_ok = fmt == 1 && channels == 1 && rate == SAMPLE_RATE && bits == 16;
if !format_ok {
return Err(bad("нужен WAV 16 кГц, моно, 16 бит"));
}
} else if &head[0..4] == b"data" {
if !format_ok {
return Err(bad("data раньше fmt"));
}
let bytes = size.min(file_len - body);
return Ok(Self {
path: path.to_path_buf(),
data_start: body,
samples: (bytes / 2) as usize,
});
}
pos = body + size + (size & 1);
}
Err(bad("нет блока data"))
}
pub fn len(&self) -> usize {
self.samples
}
pub fn is_empty(&self) -> bool {
self.samples == 0
}
pub fn read(&self, start: usize, n: usize) -> io::Result<Vec<i16>> {
let n = n.min(self.samples.saturating_sub(start));
let mut file = File::open(&self.path)?;
file.seek(SeekFrom::Start(self.data_start + start as u64 * 2))?;
let mut bytes = vec![0u8; n * 2];
file.read_exact(&mut bytes)?;
Ok(bytes
.as_chunks::<2>()
.0
.iter()
.map(|c| i16::from_le_bytes(*c))
.collect())
}
}
pub struct WavWriter {
file: File,
samples: u64,
}
impl WavWriter {
pub fn create(path: &Path) -> io::Result<Self> {
let mut file = File::create(path)?;
file.write_all(&header(0))?;
Ok(Self { file, samples: 0 })
}
pub fn write(&mut self, samples: &[i16]) -> io::Result<()> {
let mut buf = Vec::with_capacity(samples.len() * 2);
for s in samples {
buf.extend_from_slice(&s.to_le_bytes());
}
self.file.write_all(&buf)?;
self.samples += samples.len() as u64;
Ok(())
}
pub fn flush(&mut self) -> io::Result<()> {
let bytes = (self.samples * 2).min(u32::MAX as u64 - 36) as u32;
self.file.seek(SeekFrom::Start(0))?;
self.file.write_all(&header(bytes))?;
self.file.seek(SeekFrom::End(0))?;
self.file.flush()
}
pub fn duration_ms(&self) -> u64 {
self.samples / SAMPLES_PER_MS
}
}
impl Drop for WavWriter {
fn drop(&mut self) {
let _ = self.flush();
}
}
#[cfg(test)]
mod tests {
use super::*;
pub fn sine(ms: u64, amplitude: f32) -> Vec<i16> {
(0..ms_to_samples(ms))
.map(|i| {
let t = i as f32 / SAMPLE_RATE as f32;
(amplitude * (2.0 * std::f32::consts::PI * 440.0 * t).sin() * i16::MAX as f32) as i16
})
.collect()
}
#[test]
fn wav_round_trip() {
let s = sine(250, 0.5);
let wav = encode_wav(&s);
assert_eq!(wav.len(), 44 + s.len() * 2);
assert_eq!(decode_wav(&wav).unwrap(), s);
}
#[test]
fn foreign_wav_is_refused() {
let mut wav = encode_wav(&[1, 2, 3]);
wav[24..28].copy_from_slice(&44_100u32.to_le_bytes());
assert!(decode_wav(&wav).is_err());
assert!(decode_wav(b"not a wav at all").is_err());
}
#[test]
fn writer_keeps_file_readable_after_each_flush() {
let dir = std::env::temp_dir().join(format!("meet-wav-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("t.wav");
let mut w = WavWriter::create(&path).unwrap();
w.write(&sine(100, 0.3)).unwrap();
w.flush().unwrap();
assert_eq!(decode_wav(&std::fs::read(&path).unwrap()).unwrap().len(), 1600);
w.write(&sine(100, 0.3)).unwrap();
drop(w);
assert_eq!(decode_wav(&std::fs::read(&path).unwrap()).unwrap().len(), 3200);
std::fs::remove_dir_all(dir).unwrap();
}
#[test]
fn levels() {
assert_eq!(level_dbfs(&[]), -90.0);
assert_eq!(level_dbfs(&[0; 100]), -90.0);
let loud = level_dbfs(&sine(100, 1.0));
assert!(
(loud + 3.0).abs() < 0.2,
"синус полной амплитуды ≈ −3 dBFS, а не {loud}"
);
assert!(level_dbfs(&sine(100, 0.01)) < -40.0);
}
#[test]
fn stereo_48k_becomes_mono_16k() {
let stereo: Vec<f32> = (0..1440).flat_map(|_| [0.5f32, 0.1]).collect();
let out = to_native(&stereo, 2, 48_000);
assert_eq!(out.len(), 480);
assert!(out
.iter()
.all(|&s| (s - (0.3 * i16::MAX as f32) as i16).abs() <= 1));
}
#[test]
fn odd_rate_is_interpolated() {
let mono = vec![0.25f32; 44_100];
let out = to_native(&mono, 1, 44_100);
assert_eq!(out.len(), 16_000);
assert!(out
.iter()
.all(|&s| (s - (0.25 * i16::MAX as f32) as i16).abs() <= 1));
assert_eq!(to_native(&[0.5, 0.5], 1, 16_000).len(), 2);
}
#[test]
fn streaming_resampler_matches_whole_signal() {
for (rate, channels) in [(48_000u32, 2usize), (44_100, 2), (16_000, 1), (22_050, 1)] {
let n = rate as usize; let signal: Vec<f32> = (0..n * channels)
.map(|i| ((i / channels) as f32 / rate as f32 * std::f32::consts::TAU * 220.0).sin() * 0.5)
.collect();
let whole = to_native(&signal, channels, rate);
let mut r = Resampler::new(channels, rate);
let mut streamed = Vec::new();
for part in signal.chunks(1151 * channels) {
streamed.extend(r.push(part));
}
assert!(
(streamed.len() as i64 - whole.len() as i64).abs() <= 1,
"{rate}/{channels}: {} против {}",
streamed.len(),
whole.len()
);
let worst = streamed
.iter()
.zip(&whole)
.map(|(a, b)| (*a as i32 - *b as i32).abs())
.max()
.unwrap();
assert!(worst <= 2, "{rate}/{channels}: расхождение {worst}");
}
}
#[test]
fn float_conversion_saturates() {
assert_eq!(
f32_to_i16(&[0.0, 1.0, -1.0, 2.0, -2.0]),
vec![0, 32767, -32767, 32767, -32767]
);
}
#[test]
fn wav_file_reads_slices_from_disk() {
let path = std::env::temp_dir().join(format!("meet-wavfile-{}.wav", std::process::id()));
let samples: Vec<i16> = (0..50_000).map(|i| (i % 3000) as i16 - 1500).collect();
{
let mut w = WavWriter::create(&path).unwrap();
w.write(&samples).unwrap();
}
let f = WavFile::open(&path).unwrap();
assert_eq!(f.len(), samples.len());
assert_eq!(f.read(0, 10).unwrap(), samples[..10]);
assert_eq!(f.read(12_345, 4_000).unwrap(), samples[12_345..16_345]);
assert_eq!(
f.read(49_990, 100).unwrap(),
samples[49_990..],
"у конца — сколько есть"
);
assert!(f.read(60_000, 10).unwrap().is_empty());
std::fs::write(&path, b"RIFF....WAVEjunk").unwrap();
assert!(WavFile::open(&path).is_err());
let _ = std::fs::remove_file(&path);
}
}