use crate::Result;
use crate::XbergError;
#[cfg_attr(alef, alef(skip))]
#[derive(Debug, Clone)]
pub struct PcmAudio {
pub samples: Vec<f32>,
pub sample_rate_hz: u32,
pub channels: u16,
pub duration_ms: u64,
}
#[cfg(feature = "transcription")]
#[cfg_attr(alef, alef(skip))]
pub fn decode_audio_to_pcm(bytes: &[u8], max_bytes: Option<u64>) -> Result<PcmAudio> {
use std::io::Cursor;
use symphonia::core::codecs::audio::AudioDecoderOptions;
use symphonia::core::errors::Error as SymphoniaError;
use symphonia::core::formats::FormatOptions;
use symphonia::core::formats::TrackType;
use symphonia::core::formats::probe::Hint;
use symphonia::core::io::MediaSourceStream;
use symphonia::core::meta::MetadataOptions;
if let Some(limit) = max_bytes
&& (bytes.len() as u64) > limit
{
return Err(XbergError::transcription(format!(
"Audio input size {} bytes exceeds configured limit of {} bytes",
bytes.len(),
limit
)));
}
let cursor = Cursor::new(bytes);
let mss = MediaSourceStream::new(Box::new(cursor), Default::default());
let hint = Hint::new();
let fmt_opts: FormatOptions = Default::default();
let meta_opts: MetadataOptions = Default::default();
let mut format = symphonia::default::get_probe()
.probe(&hint, mss, fmt_opts, meta_opts)
.map_err(|e| XbergError::transcription(format!("symphonia probe failed: {e}")))?;
let track = format
.default_track(TrackType::Audio)
.ok_or_else(|| XbergError::transcription("no audio track found in input"))?;
let track_id = track.id;
let audio_codec_params = track
.codec_params
.as_ref()
.and_then(|p| p.audio())
.cloned()
.ok_or_else(|| XbergError::transcription("audio track has no decodable codec parameters"))?;
let src_sample_rate = audio_codec_params.sample_rate.unwrap_or(44_100);
let src_channels = audio_codec_params.channels.as_ref().map(|c| c.count()).unwrap_or(1);
let dec_opts: AudioDecoderOptions = Default::default();
let mut decoder = symphonia::default::get_codecs()
.make_audio_decoder(&audio_codec_params, &dec_opts)
.map_err(|e| XbergError::transcription(format!("unsupported audio codec: {e}")))?;
let mut interleaved: Vec<f32> = Vec::new();
loop {
let packet = match format.next_packet() {
Ok(Some(pkt)) => pkt,
Ok(None) => break,
Err(SymphoniaError::IoError(e)) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
break;
}
Err(SymphoniaError::ResetRequired) => {
decoder.reset();
continue;
}
Err(e) => {
return Err(XbergError::transcription(format!("error reading audio packet: {e}")));
}
};
if packet.track_id != track_id {
continue;
}
let audio_buf = match decoder.decode(&packet) {
Ok(buf) => buf,
Err(SymphoniaError::IoError(_)) | Err(SymphoniaError::DecodeError(_)) => continue,
Err(e) => {
return Err(XbergError::transcription(format!("audio decode error: {e}")));
}
};
let frame_count = audio_buf.frames();
if frame_count == 0 {
continue;
}
let total_samples = audio_buf.samples_interleaved();
let mut chunk = vec![0.0f32; total_samples];
audio_buf.copy_to_slice_interleaved(chunk.as_mut_slice());
interleaved.extend_from_slice(&chunk);
}
let mono = if src_channels <= 1 {
interleaved
} else {
down_mix_to_mono(&interleaved, src_channels)
};
let samples = if src_sample_rate == 16_000 {
mono
} else {
resample_linear_to_16k(&mono, src_sample_rate)
};
let duration_ms = samples.len() as u64 * 1000 / 16_000;
Ok(PcmAudio {
samples,
sample_rate_hz: 16_000,
channels: 1,
duration_ms,
})
}
#[cfg(feature = "transcription")]
fn down_mix_to_mono(interleaved: &[f32], channels: usize) -> Vec<f32> {
if channels == 0 {
return Vec::new();
}
let frames = interleaved.len() / channels;
let mut mono = Vec::with_capacity(frames);
let inv = 1.0_f32 / channels as f32;
for frame in 0..frames {
let mut sum = 0.0_f32;
for ch in 0..channels {
sum += interleaved[frame * channels + ch];
}
mono.push(sum * inv);
}
mono
}
#[cfg(feature = "transcription")]
fn resample_linear_to_16k(samples: &[f32], src_hz: u32) -> Vec<f32> {
const TARGET_HZ: u32 = 16_000;
if samples.is_empty() || src_hz == 0 {
return Vec::new();
}
if src_hz == TARGET_HZ {
return samples.to_vec();
}
let src_len = samples.len();
let out_len = (src_len as u64 * TARGET_HZ as u64).div_ceil(src_hz as u64) as usize;
let mut out = Vec::with_capacity(out_len);
let ratio = src_hz as f64 / TARGET_HZ as f64;
for i in 0..out_len {
let src_pos = i as f64 * ratio;
let lo = src_pos as usize;
let hi = (lo + 1).min(src_len - 1);
let frac = (src_pos - lo as f64) as f32;
out.push(samples[lo] + (samples[hi] - samples[lo]) * frac);
}
out
}
#[cfg(not(feature = "transcription"))]
#[cfg_attr(alef, alef(skip))]
pub fn decode_audio_to_pcm(_bytes: &[u8], _max_bytes: Option<u64>) -> Result<PcmAudio> {
Err(XbergError::transcription(
"Audio decoding requires the `transcription` Cargo feature (symphonia + ORT)",
))
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "transcription")]
#[test]
fn test_size_limit_enforced() {
let result = decode_audio_to_pcm(&[0u8; 20], Some(10));
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(msg.contains("exceed") || msg.contains("limit"), "unexpected: {msg}");
}
#[cfg(feature = "transcription")]
#[test]
fn test_empty_bytes_returns_error() {
let result = decode_audio_to_pcm(&[], None);
assert!(result.is_err());
}
#[cfg(feature = "transcription")]
#[test]
fn test_down_mix_to_mono_stereo() {
let stereo = vec![1.0f32, -1.0, 0.5, 0.5];
let mono = down_mix_to_mono(&stereo, 2);
assert_eq!(mono.len(), 2);
assert!((mono[0]).abs() < 1e-6);
assert!((mono[1] - 0.5).abs() < 1e-6);
}
#[cfg(feature = "transcription")]
#[test]
fn test_resample_linear_passthrough_when_same_rate() {
let samples = vec![0.1f32, 0.2, 0.3];
let out = resample_linear_to_16k(&samples, 16_000);
assert_eq!(out, samples);
}
#[cfg(feature = "transcription")]
#[test]
fn test_resample_linear_halves_rate() {
let samples = vec![0.0f32, 1.0];
let out = resample_linear_to_16k(&samples, 32_000);
assert_eq!(out.len(), 1);
}
}