use crate::errors::{Result, TokenizerError};
use base64::Engine;
use ndarray::Array1;
use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AudioSpectrogramConfig {
pub num_mel_bins: usize,
pub hop_length: usize,
pub window_size: usize,
}
impl AudioSpectrogramConfig {
pub fn new(num_mel_bins: usize, hop_length: usize, window_size: usize) -> Result<Self> {
if num_mel_bins == 0 {
return Err(TokenizerError::InvalidConfig(
"num_mel_bins must be > 0".to_string(),
));
}
if hop_length == 0 {
return Err(TokenizerError::InvalidConfig(
"hop_length must be > 0".to_string(),
));
}
if window_size == 0 {
return Err(TokenizerError::InvalidConfig(
"window_size must be > 0".to_string(),
));
}
Ok(Self {
num_mel_bins,
hop_length,
window_size,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AudioConfig {
pub sampling_rate: usize,
pub frame_rate: f64,
pub audio_encoding_config: AudioSpectrogramConfig,
pub chunk_length_s: Option<f64>,
}
impl AudioConfig {
pub fn new(
sampling_rate: usize,
frame_rate: f64,
encoding_config: AudioSpectrogramConfig,
chunk_length_s: Option<f64>,
) -> Result<Self> {
if sampling_rate == 0 {
return Err(TokenizerError::InvalidConfig(
"sampling_rate must be > 0".to_string(),
));
}
if frame_rate <= 0.0 {
return Err(TokenizerError::InvalidConfig(
"frame_rate must be > 0".to_string(),
));
}
if let Some(chunk_length) = chunk_length_s {
if chunk_length <= 0.0 {
return Err(TokenizerError::InvalidConfig(
"chunk_length_s must be > 0".to_string(),
));
}
}
Ok(Self {
sampling_rate,
frame_rate,
audio_encoding_config: encoding_config,
chunk_length_s,
})
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
pub fn chunk_frames(&self) -> Result<usize> {
match self.chunk_length_s {
Some(chunk_length) =>
{
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
Ok((chunk_length * self.sampling_rate as f64) as usize)
}
None => Err(TokenizerError::InvalidConfig(
"chunk_length_s not set".to_string(),
)),
}
}
#[must_use]
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
pub fn audio_length_per_tok(&self) -> usize {
#[allow(clippy::cast_precision_loss)]
let mut downsample_factor = self.sampling_rate as f64 / self.frame_rate;
#[allow(clippy::cast_precision_loss)]
{
downsample_factor /= self.audio_encoding_config.hop_length as f64;
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
{
downsample_factor as usize
}
}
}
#[derive(Debug, Clone)]
pub struct Audio {
pub audio_array: Array1<f32>,
pub sampling_rate: usize,
pub format: String,
}
impl Audio {
#[must_use]
pub fn new(audio_array: Array1<f32>, sampling_rate: usize, format: String) -> Self {
Self {
audio_array,
sampling_rate,
format,
}
}
#[allow(clippy::cast_precision_loss)]
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let mut reader = hound::WavReader::open(path)
.map_err(|e| TokenizerError::Audio(format!("Failed to open audio file: {e}")))?;
let spec = reader.spec();
let sampling_rate = spec.sample_rate as usize;
let samples: std::result::Result<Vec<f32>, _> = match spec.sample_format {
hound::SampleFormat::Float => reader.samples::<f32>().collect(),
hound::SampleFormat::Int => reader
.samples::<i32>()
.map(|s| {
s.map(|v| {
#[allow(clippy::cast_precision_loss)]
{
v as f32 / i32::MAX as f32
}
})
})
.collect(),
};
let samples =
samples.map_err(|e| TokenizerError::Audio(format!("Failed to read samples: {e}")))?;
let audio_array = if spec.channels == 1 {
Array1::from_vec(samples)
} else {
let mono_samples: Vec<f32> = samples
.chunks(spec.channels as usize)
.map(|chunk| {
#[allow(clippy::cast_precision_loss)]
{
chunk.iter().sum::<f32>() / chunk.len() as f32
}
})
.collect();
Array1::from_vec(mono_samples)
};
Ok(Self::new(audio_array, sampling_rate, "wav".to_string()))
}
pub fn from_base64(data: &str) -> Result<Self> {
let audio_bytes = base64::engine::general_purpose::STANDARD.decode(data)?;
Self::from_bytes(&audio_bytes)
}
#[allow(clippy::cast_precision_loss)]
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
let cursor = std::io::Cursor::new(bytes);
let mut reader = hound::WavReader::new(cursor)
.map_err(|e| TokenizerError::Audio(format!("Failed to parse audio bytes: {e}")))?;
let spec = reader.spec();
let sampling_rate = spec.sample_rate as usize;
let samples: std::result::Result<Vec<f32>, _> = match spec.sample_format {
hound::SampleFormat::Float => reader.samples::<f32>().collect(),
hound::SampleFormat::Int => reader
.samples::<i32>()
.map(|s| {
s.map(|v| {
#[allow(clippy::cast_precision_loss)]
{
v as f32 / i32::MAX as f32
}
})
})
.collect(),
};
let samples =
samples.map_err(|e| TokenizerError::Audio(format!("Failed to read samples: {e}")))?;
let audio_array = if spec.channels == 1 {
Array1::from_vec(samples)
} else {
let mono_samples: Vec<f32> = samples
.chunks(spec.channels as usize)
.map(|chunk| {
#[allow(clippy::cast_precision_loss)]
{
chunk.iter().sum::<f32>() / chunk.len() as f32
}
})
.collect();
Array1::from_vec(mono_samples)
};
Ok(Self::new(audio_array, sampling_rate, "wav".to_string()))
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn duration(&self) -> f64 {
#[allow(clippy::cast_precision_loss)]
{
self.audio_array.len() as f64 / self.sampling_rate as f64
}
}
pub fn resample(&mut self, target_rate: usize) -> Result<()> {
if self.sampling_rate == target_rate {
return Ok(());
}
Err(TokenizerError::Audio(
"Resampling not yet implemented".to_string(),
))
}
pub fn pad(&mut self, config: &AudioConfig) -> Result<()> {
let current_length = self.audio_array.len();
let target_length = if let Some(_chunk_length_s) = config.chunk_length_s {
let chunk_frames = config.chunk_frames()?;
current_length.div_ceil(chunk_frames) * chunk_frames
} else if current_length < config.audio_encoding_config.window_size {
config.audio_encoding_config.window_size
} else {
return Ok(());
};
if target_length > current_length {
let padding_length = target_length - current_length;
let _ = padding_length; let mut padded = Array1::zeros(target_length);
padded
.slice_mut(ndarray::s![..current_length])
.assign(&self.audio_array);
self.audio_array = padded;
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct AudioEncoding {
pub tokens: Vec<u32>,
pub audio: Audio,
}
#[derive(Debug, Clone)]
pub struct AudioEncoder {
pub config: AudioConfig,
pub audio_token_id: u32,
pub begin_audio_token_id: u32,
}
impl AudioEncoder {
#[must_use]
pub fn new(config: AudioConfig, audio_token_id: u32, begin_audio_token_id: u32) -> Self {
Self {
config,
audio_token_id,
begin_audio_token_id,
}
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
pub fn encode(&self, mut audio: Audio) -> Result<AudioEncoding> {
audio.resample(self.config.sampling_rate)?;
audio.pad(&self.config)?;
let signal_length = audio.audio_array.len();
let signal_length = if signal_length % self.config.audio_encoding_config.hop_length != 0 {
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
{
(signal_length as f64 / self.config.audio_encoding_config.hop_length as f64 - 1.0)
.ceil() as usize
}
} else {
signal_length / self.config.audio_encoding_config.hop_length
};
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
let num_audio_tokens =
(signal_length as f64 / self.config.audio_length_per_tok() as f64).ceil() as usize;
let mut tokens = vec![self.begin_audio_token_id];
tokens.extend(vec![self.audio_token_id; num_audio_tokens]);
Ok(AudioEncoding { tokens, audio })
}
}
#[must_use]
pub fn hertz_to_mel(freq: f64) -> f64 {
let min_log_hertz = 1000.0;
let min_log_mel = 15.0;
let logstep = 27.0 / 6.4_f64.ln();
if freq >= min_log_hertz {
min_log_mel + (freq / min_log_hertz).ln() * logstep
} else {
3.0 * freq / 200.0
}
}
#[must_use]
pub fn mel_to_hertz(mel: f64) -> f64 {
let min_log_hertz = 1000.0;
let min_log_mel = 15.0;
let logstep = 6.4_f64.ln() / 27.0;
if mel >= min_log_mel {
min_log_hertz * ((mel - min_log_mel) * logstep).exp()
} else {
200.0 * mel / 3.0
}
}
#[allow(clippy::cast_precision_loss)]
pub fn mel_filter_bank(
num_frequency_bins: usize,
num_mel_bins: usize,
min_frequency: f64,
max_frequency: f64,
sampling_rate: usize,
) -> Result<ndarray::Array2<f64>> {
if num_frequency_bins < 2 {
return Err(TokenizerError::InvalidConfig(format!(
"num_frequency_bins must be >= 2, got {num_frequency_bins}"
)));
}
if min_frequency > max_frequency {
return Err(TokenizerError::InvalidConfig(format!(
"min_frequency ({min_frequency}) must be <= max_frequency ({max_frequency})"
)));
}
let mel_min = hertz_to_mel(min_frequency);
let mel_max = hertz_to_mel(max_frequency);
#[allow(clippy::cast_precision_loss)]
let mel_freqs: Vec<f64> = (0..=num_mel_bins + 1)
.map(|i| mel_min + (mel_max - mel_min) * i as f64 / (num_mel_bins + 1) as f64)
.collect();
let filter_freqs: Vec<f64> = mel_freqs.iter().map(|&mel| mel_to_hertz(mel)).collect();
#[allow(clippy::cast_precision_loss)]
let fft_freqs: Vec<f64> = (0..num_frequency_bins)
.map(|i| i as f64 * sampling_rate as f64 / 2.0 / (num_frequency_bins - 1) as f64)
.collect();
let mut filter_bank = ndarray::Array2::zeros((num_frequency_bins, num_mel_bins));
for mel_idx in 0..num_mel_bins {
let left_freq = filter_freqs[mel_idx];
let center_freq = filter_freqs[mel_idx + 1];
let right_freq = filter_freqs[mel_idx + 2];
for (freq_idx, &fft_freq) in fft_freqs.iter().enumerate() {
let value = if fft_freq >= left_freq && fft_freq <= center_freq {
(fft_freq - left_freq) / (center_freq - left_freq)
} else if fft_freq > center_freq && fft_freq <= right_freq {
(right_freq - fft_freq) / (right_freq - center_freq)
} else {
0.0
};
filter_bank[[freq_idx, mel_idx]] = value.max(0.0);
}
}
for mel_idx in 0..num_mel_bins {
let enorm = 2.0 / (filter_freqs[mel_idx + 2] - filter_freqs[mel_idx]);
for freq_idx in 0..num_frequency_bins {
filter_bank[[freq_idx, mel_idx]] *= enorm;
}
}
Ok(filter_bank)
}