use crate::error::{WhisperError, WhisperResult};
#[cfg(test)]
mod tests;
const WAVE_FORMAT_PCM: u16 = 1;
const WAVE_FORMAT_IEEE_FLOAT: u16 = 3;
const WAVE_FORMAT_EXTENSIBLE: u16 = 0xFFFE;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WavError {
TooSmall,
MissingRiff,
MissingWave,
FmtTruncated,
UnsupportedFormat {
format: u16,
bits: u16,
},
UnsupportedChannels(u16),
NoDataChunk,
}
impl std::fmt::Display for WavError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::TooSmall => write!(f, "WAV file too small"),
Self::MissingRiff => write!(f, "Invalid WAV: missing RIFF header"),
Self::MissingWave => write!(f, "Invalid WAV: missing WAVE marker"),
Self::FmtTruncated => write!(f, "Invalid WAV: fmt chunk truncated"),
Self::UnsupportedFormat { format, bits } => {
write!(f, "Unsupported format: {bits} bits, format {format}")
}
Self::UnsupportedChannels(ch) => write!(f, "Unsupported channel count: {ch}"),
Self::NoDataChunk => write!(f, "Invalid WAV: no data chunk found"),
}
}
}
impl std::error::Error for WavError {}
impl From<WavError> for WhisperError {
fn from(e: WavError) -> Self {
Self::Audio(e.to_string())
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct WavData {
pub samples: Vec<f32>,
pub sample_rate: u32,
pub original_channels: u16,
pub bits_per_sample: u16,
}
struct FmtChunk {
audio_format: u16,
channels: u16,
sample_rate: u32,
bits_per_sample: u16,
sub_format: u16,
}
fn parse_fmt_chunk(data: &[u8], pos: usize, chunk_size: usize) -> Result<FmtChunk, WavError> {
if pos + 8 + chunk_size > data.len() {
return Err(WavError::FmtTruncated);
}
let audio_format = u16::from_le_bytes([data[pos + 8], data[pos + 9]]);
let channels = u16::from_le_bytes([data[pos + 10], data[pos + 11]]);
let sample_rate = u32::from_le_bytes([
data[pos + 12],
data[pos + 13],
data[pos + 14],
data[pos + 15],
]);
let bits_per_sample = u16::from_le_bytes([data[pos + 22], data[pos + 23]]);
let sub_format = if audio_format == WAVE_FORMAT_EXTENSIBLE && chunk_size >= 40 {
let sub_format_offset = pos + 8 + 24;
if sub_format_offset + 2 <= data.len() {
u16::from_le_bytes([data[sub_format_offset], data[sub_format_offset + 1]])
} else {
0
}
} else {
0
};
Ok(FmtChunk {
audio_format,
channels,
sample_rate,
bits_per_sample,
sub_format,
})
}
fn convert_audio_samples(audio_data: &[u8], fmt: &FmtChunk) -> Result<Vec<f32>, WavError> {
let effective_format = if fmt.audio_format == WAVE_FORMAT_EXTENSIBLE {
fmt.sub_format
} else {
fmt.audio_format
};
match (effective_format, fmt.bits_per_sample) {
(WAVE_FORMAT_PCM, 16) => Ok(convert_16bit_pcm(audio_data)),
(WAVE_FORMAT_PCM, 8) => Ok(convert_8bit_pcm(audio_data)),
(WAVE_FORMAT_PCM, 24) => Ok(convert_24bit_pcm(audio_data)),
(WAVE_FORMAT_PCM, 32) => Ok(convert_32bit_pcm(audio_data)),
(WAVE_FORMAT_IEEE_FLOAT, 32) => Ok(convert_32bit_float(audio_data)),
_ => Err(WavError::UnsupportedFormat {
format: fmt.audio_format,
bits: fmt.bits_per_sample,
}),
}
}
fn validate_wav_header(data: &[u8]) -> Result<(), WavError> {
if data.len() < 44 {
return Err(WavError::TooSmall);
}
match (data.get(0..4), data.get(8..12)) {
(Some(b"RIFF"), Some(b"WAVE")) => Ok(()),
(Some(b"RIFF"), _) => Err(WavError::MissingWave),
_ => Err(WavError::MissingRiff),
}
}
fn process_data_chunk(
data: &[u8],
pos: usize,
chunk_size: usize,
fmt: &FmtChunk,
) -> Result<WavData, WavError> {
let data_start = pos + 8;
let data_end = (data_start + chunk_size).min(data.len());
let audio_data = &data[data_start..data_end];
let samples = convert_audio_samples(audio_data, fmt)?;
let mono_samples = convert_to_mono(samples, fmt.channels)?;
Ok(WavData {
samples: mono_samples,
sample_rate: fmt.sample_rate,
original_channels: fmt.channels,
bits_per_sample: fmt.bits_per_sample,
})
}
#[cfg_attr(feature = "tracing", tracing::instrument(level = "info", skip(data), fields(data_len = data.len())))]
pub fn parse_wav(data: &[u8]) -> Result<WavData, WavError> {
validate_wav_header(data)?;
let mut pos = 12;
let mut fmt_chunk: Option<FmtChunk> = None;
while pos + 8 <= data.len() {
let chunk_id = &data[pos..pos + 4];
let chunk_size =
u32::from_le_bytes([data[pos + 4], data[pos + 5], data[pos + 6], data[pos + 7]])
as usize;
if chunk_id == b"fmt " {
fmt_chunk = Some(parse_fmt_chunk(data, pos, chunk_size)?);
pos += 8 + chunk_size;
} else if chunk_id == b"data" {
let fmt = fmt_chunk.as_ref().ok_or(WavError::NoDataChunk)?;
return process_data_chunk(data, pos, chunk_size, fmt);
} else {
pos += 8 + chunk_size + (chunk_size % 2);
}
}
Err(WavError::NoDataChunk)
}
pub fn parse_wav_file(data: &[u8]) -> WhisperResult<WavData> {
parse_wav(data).map_err(WhisperError::from)
}
#[inline]
pub(crate) fn convert_16bit_pcm(data: &[u8]) -> Vec<f32> {
data.chunks_exact(2)
.map(|chunk| {
let sample = i16::from_le_bytes([chunk[0], chunk[1]]);
sample as f32 / 32768.0
})
.collect()
}
#[inline]
pub(crate) fn convert_8bit_pcm(data: &[u8]) -> Vec<f32> {
data.iter().map(|&b| (b as f32 - 128.0) / 128.0).collect()
}
#[inline]
pub(crate) fn convert_24bit_pcm(data: &[u8]) -> Vec<f32> {
data.chunks_exact(3)
.map(|chunk| {
let sign_extend = if chunk[2] & 0x80 != 0 { 0xFF } else { 0x00 };
let sample = i32::from_le_bytes([chunk[0], chunk[1], chunk[2], sign_extend]);
sample as f32 / 8_388_608.0
})
.collect()
}
#[inline]
pub(crate) fn convert_32bit_pcm(data: &[u8]) -> Vec<f32> {
data.chunks_exact(4)
.map(|chunk| {
let sample = i32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
sample as f32 / 2_147_483_648.0
})
.collect()
}
#[inline]
fn convert_32bit_float(data: &[u8]) -> Vec<f32> {
data.chunks_exact(4)
.map(|chunk| f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
.collect()
}
#[inline]
pub(crate) fn convert_to_mono(samples: Vec<f32>, channels: u16) -> Result<Vec<f32>, WavError> {
match channels {
1 => Ok(samples),
2 => Ok(aprender::audio::stereo_to_mono(&samples)),
_ => Err(WavError::UnsupportedChannels(channels)),
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(level = "info", skip(samples), fields(samples_len = samples.len())))]
pub fn resample(samples: &[f32], source_rate: u32, target_rate: u32) -> Vec<f32> {
if source_rate == target_rate || samples.is_empty() {
return samples.to_vec();
}
let ratio = source_rate as f64 / target_rate as f64;
let output_len = ((samples.len() as f64) / ratio).ceil() as usize;
let mut output = Vec::with_capacity(output_len);
for i in 0..output_len {
let src_pos = i as f64 * ratio;
let src_idx = src_pos as usize;
let frac = (src_pos - src_idx as f64) as f32;
let s0 = samples.get(src_idx).copied().unwrap_or(0.0);
let s1 = samples.get(src_idx + 1).copied().unwrap_or(s0);
output.push(s0.mul_add(1.0 - frac, s1 * frac));
}
output
}