use std::{fs, io::Cursor, path::Path};
|
|
use anyhow::{Context, Result, anyhow};
|
use hound::{SampleFormat, WavReader};
|
use minimp3::{Decoder as Mp3Decoder, Error as Mp3Error, Frame as Mp3Frame};
|
use reqwest::Client;
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
pub struct PcmFrame {
|
pub data: Vec<i16>,
|
pub sample_rate: u32,
|
pub num_channels: u32,
|
pub samples_per_channel: u32,
|
}
|
|
impl PcmFrame {
|
pub fn new(
|
data: Vec<i16>,
|
sample_rate: u32,
|
num_channels: u32,
|
samples_per_channel: u32,
|
) -> Self {
|
Self {
|
data,
|
sample_rate,
|
num_channels,
|
samples_per_channel,
|
}
|
}
|
}
|
|
pub async fn load_pre_recorded_frames(
|
http: &Client,
|
audio_file: Option<&str>,
|
audio_url: Option<&str>,
|
target_sample_rate_hz: u32,
|
target_num_channels: u16,
|
) -> Result<Option<Vec<PcmFrame>>> {
|
if let Some(path) = audio_file.filter(|value| !value.trim().is_empty()) {
|
let audio_bytes = fs::read(Path::new(path))
|
.with_context(|| format!("failed to read greeting audio file {path}"))?;
|
return decode_audio_frames(&audio_bytes, target_sample_rate_hz, target_num_channels)
|
.map(Some);
|
}
|
if let Some(url) = audio_url.filter(|value| !value.trim().is_empty()) {
|
let response = http
|
.get(url)
|
.send()
|
.await
|
.with_context(|| format!("failed to fetch greeting audio {url}"))?;
|
if !response.status().is_success() {
|
return Err(anyhow!(
|
"greeting audio fetch failed for {url} with status {}",
|
response.status()
|
));
|
}
|
let bytes = response
|
.bytes()
|
.await
|
.with_context(|| format!("failed to read greeting audio body {url}"))?;
|
return decode_audio_frames(&bytes, target_sample_rate_hz, target_num_channels).map(Some);
|
}
|
Ok(None)
|
}
|
|
fn decode_audio_frames(
|
audio_bytes: &[u8],
|
target_sample_rate_hz: u32,
|
target_num_channels: u16,
|
) -> Result<Vec<PcmFrame>> {
|
if is_wav(audio_bytes) {
|
return decode_wav_frames(audio_bytes, target_sample_rate_hz, target_num_channels);
|
}
|
if is_mp3(audio_bytes) {
|
return decode_mp3_frames(audio_bytes, target_sample_rate_hz, target_num_channels);
|
}
|
|
let wav_result = decode_wav_frames(audio_bytes, target_sample_rate_hz, target_num_channels);
|
if wav_result.is_ok() {
|
return wav_result;
|
}
|
let wav_error = wav_result.err();
|
decode_mp3_frames(audio_bytes, target_sample_rate_hz, target_num_channels).map_err(
|
|mp3_error| {
|
anyhow!(
|
"failed to decode greeting audio as wav or mp3: wav={}, mp3={}",
|
wav_error
|
.map(|error| error.to_string())
|
.unwrap_or_else(|| "unknown".to_string()),
|
mp3_error
|
)
|
},
|
)
|
}
|
|
fn decode_wav_frames(
|
wav_bytes: &[u8],
|
target_sample_rate_hz: u32,
|
target_num_channels: u16,
|
) -> Result<Vec<PcmFrame>> {
|
let cursor = Cursor::new(wav_bytes.to_vec());
|
let mut reader = WavReader::new(cursor).context("failed to open greeting wav bytes")?;
|
let spec = reader.spec();
|
let src_channels = spec.channels.max(1);
|
let mut samples = match (spec.sample_format, spec.bits_per_sample) {
|
(SampleFormat::Int, 16) => reader
|
.samples::<i16>()
|
.collect::<std::result::Result<Vec<_>, _>>()
|
.context("failed to decode 16-bit wav samples")?,
|
(SampleFormat::Float, 32) => reader
|
.samples::<f32>()
|
.map(|sample| sample.map(|value| (value.clamp(-1.0, 1.0) * i16::MAX as f32) as i16))
|
.collect::<std::result::Result<Vec<_>, _>>()
|
.context("failed to decode float wav samples")?,
|
_ => {
|
return Err(anyhow!(
|
"unsupported greeting wav format: {:?} {}-bit",
|
spec.sample_format,
|
spec.bits_per_sample
|
));
|
}
|
};
|
|
samples = remap_channels(samples, src_channels, target_num_channels);
|
samples = resample_linear(
|
samples,
|
spec.sample_rate,
|
target_sample_rate_hz,
|
target_num_channels,
|
);
|
|
Ok(chunk_pcm_samples(
|
samples,
|
target_sample_rate_hz,
|
target_num_channels,
|
))
|
}
|
|
fn decode_mp3_frames(
|
mp3_bytes: &[u8],
|
target_sample_rate_hz: u32,
|
target_num_channels: u16,
|
) -> Result<Vec<PcmFrame>> {
|
let cursor = Cursor::new(mp3_bytes.to_vec());
|
let mut decoder = Mp3Decoder::new(cursor);
|
let mut samples = Vec::new();
|
|
loop {
|
match decoder.next_frame() {
|
Ok(Mp3Frame {
|
data,
|
sample_rate,
|
channels,
|
..
|
}) => {
|
let src_rate = u32::try_from(sample_rate)
|
.map_err(|_| anyhow!("unsupported mp3 sample rate {sample_rate}"))?;
|
let src_channels = u16::try_from(channels)
|
.map_err(|_| anyhow!("unsupported mp3 channel count {channels}"))?;
|
let mut frame_samples = remap_channels(data, src_channels, target_num_channels);
|
frame_samples = resample_linear(
|
frame_samples,
|
src_rate,
|
target_sample_rate_hz,
|
target_num_channels,
|
);
|
samples.extend(frame_samples);
|
}
|
Err(Mp3Error::Eof) => break,
|
Err(Mp3Error::SkippedData) | Err(Mp3Error::InsufficientData) => continue,
|
Err(error) => return Err(anyhow!("failed to decode mp3 frame: {error:?}")),
|
}
|
}
|
|
if samples.is_empty() {
|
return Err(anyhow!("decoded mp3 contains no audio samples"));
|
}
|
|
Ok(chunk_pcm_samples(
|
samples,
|
target_sample_rate_hz,
|
target_num_channels,
|
))
|
}
|
|
fn chunk_pcm_samples(
|
samples: Vec<i16>,
|
target_sample_rate_hz: u32,
|
target_num_channels: u16,
|
) -> Vec<PcmFrame> {
|
const CHUNK_MS: u32 = 20;
|
let samples_per_chunk = ((target_sample_rate_hz / 1000) * CHUNK_MS).max(1) as usize
|
* usize::from(target_num_channels);
|
let mut frames = Vec::new();
|
for chunk in samples.chunks(samples_per_chunk) {
|
let mut frame_data = chunk.to_vec();
|
if frame_data.len() < samples_per_chunk {
|
frame_data.resize(samples_per_chunk, 0);
|
}
|
let samples_per_channel =
|
(frame_data.len() / usize::from(target_num_channels.max(1))) as u32;
|
frames.push(PcmFrame::new(
|
frame_data,
|
target_sample_rate_hz,
|
u32::from(target_num_channels),
|
samples_per_channel,
|
));
|
}
|
frames
|
}
|
|
fn is_wav(bytes: &[u8]) -> bool {
|
bytes.len() >= 12 && bytes.starts_with(b"RIFF") && &bytes[8..12] == b"WAVE"
|
}
|
|
fn is_mp3(bytes: &[u8]) -> bool {
|
bytes.starts_with(b"ID3") || (bytes.len() >= 2 && bytes[0] == 0xff && bytes[1] & 0xe0 == 0xe0)
|
}
|
|
fn remap_channels(samples: Vec<i16>, src_channels: u16, dst_channels: u16) -> Vec<i16> {
|
if src_channels == dst_channels {
|
return samples;
|
}
|
|
let src_channels = usize::from(src_channels.max(1));
|
let dst_channels = usize::from(dst_channels.max(1));
|
let frames = samples.chunks(src_channels);
|
let mut output = Vec::new();
|
for frame in frames {
|
match (src_channels, dst_channels) {
|
(1, 2) => {
|
let sample = frame.first().copied().unwrap_or_default();
|
output.push(sample);
|
output.push(sample);
|
}
|
(2, 1) => {
|
let left = frame.first().copied().unwrap_or_default() as i32;
|
let right = frame.get(1).copied().unwrap_or_default() as i32;
|
output.push(((left + right) / 2) as i16);
|
}
|
_ => {
|
for channel in 0..dst_channels {
|
output.push(
|
frame
|
.get(channel % src_channels)
|
.copied()
|
.unwrap_or_default(),
|
);
|
}
|
}
|
}
|
}
|
output
|
}
|
|
fn resample_linear(samples: Vec<i16>, src_rate: u32, dst_rate: u32, channels: u16) -> Vec<i16> {
|
if src_rate == dst_rate {
|
return samples;
|
}
|
|
let channels = usize::from(channels.max(1));
|
let src_frames = samples.len() / channels;
|
if src_frames <= 1 {
|
return samples;
|
}
|
let ratio = dst_rate as f64 / src_rate as f64;
|
let dst_frames = ((src_frames as f64) * ratio).round().max(1.0) as usize;
|
let mut output = Vec::with_capacity(dst_frames * channels);
|
|
for dst_frame in 0..dst_frames {
|
let src_pos = (dst_frame as f64) / ratio;
|
let src_index = src_pos.floor() as usize;
|
let next_index = (src_index + 1).min(src_frames - 1);
|
let frac = (src_pos - src_index as f64) as f32;
|
for channel in 0..channels {
|
let a = samples[src_index * channels + channel] as f32;
|
let b = samples[next_index * channels + channel] as f32;
|
let mixed = a + (b - a) * frac;
|
output.push(mixed.round().clamp(i16::MIN as f32, i16::MAX as f32) as i16);
|
}
|
}
|
|
output
|
}
|
|
#[cfg(test)]
|
mod tests {
|
use super::load_pre_recorded_frames;
|
|
#[tokio::test]
|
async fn load_pre_recorded_frames_reads_local_wav() {
|
let dir = std::env::temp_dir().join("cv-runtime-helper-tests");
|
std::fs::create_dir_all(&dir).expect("create temp dir");
|
let path = dir.join("fixture.wav");
|
let spec = hound::WavSpec {
|
channels: 1,
|
sample_rate: 16_000,
|
bits_per_sample: 16,
|
sample_format: hound::SampleFormat::Int,
|
};
|
let mut writer = hound::WavWriter::create(&path, spec).expect("create wav");
|
for _ in 0..16_000 {
|
writer.write_sample(512_i16).expect("write sample");
|
}
|
writer.finalize().expect("finalize wav");
|
|
let http = reqwest::Client::new();
|
let frames = load_pre_recorded_frames(&http, path.to_str(), None, 48_000, 1)
|
.await
|
.expect("load frames")
|
.expect("frames");
|
|
assert!(!frames.is_empty());
|
assert_eq!(frames[0].sample_rate, 48_000);
|
assert_eq!(frames[0].num_channels, 1);
|
}
|
}
|