use crate::tags::{PaddingInfo, Tags};
use crate::{
AudexError, FileType, Result, StreamInfo,
id3::{ID3Tags, specs, tags::ID3Header},
};
use std::io::{Read, Seek, SeekFrom};
use std::path::Path;
use std::time::Duration;
#[cfg(feature = "async")]
use crate::iff::{
IffChunkAsync, RiffFileAsync, resize_riff_chunk_async, update_riff_file_size_async,
};
#[cfg(feature = "async")]
use crate::util::{
delete_bytes_async, insert_bytes_async, loadfile_read_async, loadfile_write_async,
};
#[cfg(feature = "async")]
use tokio::fs::File as TokioFile;
#[cfg(feature = "async")]
use tokio::io::{AsyncSeekExt, AsyncWriteExt};
#[derive(Debug, Clone)]
pub struct RiffChunk {
pub id: String,
pub size: u32,
pub offset: u64,
pub data_offset: u64,
pub data_size: u32,
}
impl RiffChunk {
pub fn read_data<R: Read + Seek>(&self, reader: &mut R) -> Result<Vec<u8>> {
crate::limits::ParseLimits::default()
.check_tag_size(self.data_size as u64, "RIFF chunk")?;
reader.seek(SeekFrom::Start(self.data_offset))?;
let mut data = vec![0u8; self.data_size as usize];
reader.read_exact(&mut data)?;
Ok(data)
}
}
#[derive(Debug, Clone)]
pub struct RiffFile {
pub file_type: String,
pub chunks: Vec<RiffChunk>,
pub file_size: u32,
}
impl RiffFile {
pub fn parse<R: Read + Seek + ?Sized>(reader: &mut R) -> Result<Self> {
let mut header = [0u8; 12];
reader.read_exact(&mut header)?;
if &header[0..4] != b"RIFF" {
return Err(AudexError::WAVError("Expected RIFF signature".to_string()));
}
let file_size = u32::from_le_bytes([header[4], header[5], header[6], header[7]]);
let file_type = String::from_utf8_lossy(&header[8..12]).into_owned();
if file_type != "WAVE" {
return Err(AudexError::WAVError("Expected WAVE format".to_string()));
}
let mut chunks = Vec::new();
let mut offset = 12u64;
let actual_end = reader.seek(SeekFrom::End(0)).unwrap_or(u64::MAX);
reader.seek(SeekFrom::Start(offset))?;
let end_bound = (file_size as u64 + 8).min(actual_end);
let mut consecutive_zero_chunks = 0u32;
while offset < end_bound {
reader.seek(SeekFrom::Start(offset))?;
let mut chunk_header = [0u8; 8];
if reader.read_exact(&mut chunk_header).is_err() {
break; }
let chunk_id = String::from_utf8_lossy(&chunk_header[0..4]).into_owned();
let chunk_size = u32::from_le_bytes([
chunk_header[4],
chunk_header[5],
chunk_header[6],
chunk_header[7],
]);
if chunk_size == 0 {
consecutive_zero_chunks += 1;
if consecutive_zero_chunks > 64 {
break; }
offset += 8; continue;
}
consecutive_zero_chunks = 0;
let chunk = RiffChunk {
id: chunk_id,
size: chunk_size,
offset,
data_offset: offset + 8,
data_size: chunk_size,
};
chunks.push(chunk);
let advance = 8u64 + chunk_size as u64 + if chunk_size % 2 == 1 { 1 } else { 0 };
offset = match offset.checked_add(advance) {
Some(next) => next,
None => break, };
}
Ok(RiffFile {
file_type,
chunks,
file_size,
})
}
pub fn find_chunk(&self, id: &str) -> Option<&RiffChunk> {
self.chunks.iter().find(|chunk| {
if chunk.id.eq_ignore_ascii_case(id) {
return true;
}
let padded_id = if id.len() < 4 {
format!("{:<4}", id) } else {
id.to_string()
};
chunk.id.eq_ignore_ascii_case(&padded_id)
|| chunk.id.trim_end().eq_ignore_ascii_case(id.trim_end())
})
}
pub fn has_chunk(&self, id: &str) -> bool {
self.find_chunk(id).is_some()
}
}
#[derive(Debug, Default)]
pub struct WAVEStreamInfo {
pub length: Option<Duration>,
pub bitrate: Option<u32>,
pub channels: u16,
pub sample_rate: u32,
pub bits_per_sample: u16,
pub audio_format: u16,
pub number_of_samples: u64,
}
impl StreamInfo for WAVEStreamInfo {
fn length(&self) -> Option<Duration> {
self.length
}
fn bitrate(&self) -> Option<u32> {
self.bitrate
}
fn sample_rate(&self) -> Option<u32> {
Some(self.sample_rate)
}
fn channels(&self) -> Option<u16> {
Some(self.channels)
}
fn bits_per_sample(&self) -> Option<u16> {
Some(self.bits_per_sample)
}
fn pprint(&self) -> String {
let mut output = String::new();
if let Some(length) = self.length {
output.push_str(&format!("{:.2} seconds\n", length.as_secs_f64()));
}
if let Some(bitrate) = self.bitrate {
output.push_str(&format!("{} bps\n", bitrate));
}
output.push_str(&format!("{} Hz\n", self.sample_rate));
let channel_text = if self.channels == 1 {
"channel"
} else {
"channels"
};
output.push_str(&format!("{} {}\n", self.channels, channel_text));
output.push_str(&format!("{} bit\n", self.bits_per_sample));
output.trim_end().to_string()
}
}
impl WAVEStreamInfo {
pub fn from_riff_file<R: Read + Seek>(riff: &RiffFile, reader: &mut R) -> Result<Self> {
let fmt_chunk = riff
.find_chunk("fmt")
.ok_or_else(|| AudexError::WAVError("No 'fmt' chunk found".to_string()))?;
if fmt_chunk.data_size < 16 {
return Err(AudexError::WAVInvalidChunk(
"Format chunk too small".to_string(),
));
}
let fmt_data = fmt_chunk.read_data(reader)?;
let audio_format = u16::from_le_bytes([fmt_data[0], fmt_data[1]]);
let channels = u16::from_le_bytes([fmt_data[2], fmt_data[3]]);
let sample_rate = u32::from_le_bytes([fmt_data[4], fmt_data[5], fmt_data[6], fmt_data[7]]);
let _byte_rate = u32::from_le_bytes([fmt_data[8], fmt_data[9], fmt_data[10], fmt_data[11]]);
let block_align = u16::from_le_bytes([fmt_data[12], fmt_data[13]]);
let bits_per_sample = u16::from_le_bytes([fmt_data[14], fmt_data[15]]);
if block_align == 0 {
return Err(AudexError::WAVInvalidChunk(
"block_align must be nonzero".to_string(),
));
}
if channels == 0 {
warn_event!("WAVE: channels is zero in fmt chunk — bitrate will be reported as 0");
}
if bits_per_sample == 0 {
warn_event!(
"WAVE: bits_per_sample is zero in fmt chunk — bitrate will be reported as 0"
);
}
let bitrate = if sample_rate == 0 || channels == 0 || bits_per_sample == 0 {
None
} else {
Some(
(channels as u32)
.saturating_mul(bits_per_sample as u32)
.saturating_mul(sample_rate),
)
};
let mut number_of_samples = 0u64;
let mut length = None;
if let Some(data_chunk) = riff.find_chunk("data") {
if sample_rate > 0 {
number_of_samples = data_chunk.data_size as u64 / block_align as u64;
let duration_secs = number_of_samples as f64 / sample_rate as f64;
length = Some(Duration::from_secs_f64(duration_secs));
}
}
Ok(WAVEStreamInfo {
length,
bitrate,
channels,
sample_rate,
bits_per_sample,
audio_format,
number_of_samples,
})
}
}
#[derive(Debug)]
pub struct WAVE {
pub info: WAVEStreamInfo,
pub tags: Option<ID3Tags>,
pub filename: Option<String>,
riff_file: Option<RiffFile>,
}
impl WAVE {
pub fn new() -> Self {
Self {
info: WAVEStreamInfo::default(),
tags: None,
filename: None,
riff_file: None,
}
}
fn parse_file<R: Read + Seek>(&mut self, reader: &mut R) -> Result<()> {
reader.seek(SeekFrom::Start(0))?;
let riff_file = RiffFile::parse(reader)?;
for _chunk in &riff_file.chunks {
trace_event!(chunk_id = %_chunk.id, chunk_size = _chunk.size, "WAVE chunk");
}
self.info = WAVEStreamInfo::from_riff_file(&riff_file, reader)?;
self.tags = if let Some(id3_chunk) = riff_file
.find_chunk("id3")
.or_else(|| riff_file.find_chunk("ID3"))
{
let id3_data = id3_chunk.read_data(reader)?;
if id3_data.len() >= 10 {
match specs::ID3Header::from_bytes(&id3_data) {
Ok(specs_header) => {
let header = ID3Header::from_specs_header(&specs_header);
ID3Tags::from_data(&id3_data, &header).ok()
}
Err(_) => None, }
} else {
None }
} else {
None
};
self.riff_file = Some(riff_file);
Ok(())
}
pub fn clear(&mut self) -> Result<()> {
self.tags = None;
let has_id3_chunk = if let Some(ref riff_file) = self.riff_file {
riff_file
.chunks
.iter()
.any(|chunk| chunk.id == "id3 " || chunk.id == "ID3 ")
} else {
false
};
if has_id3_chunk {
if let Some(filename) = self.filename.clone() {
self.remove_id3_chunk(&filename)?;
}
}
Ok(())
}
fn remove_id3_chunk(&mut self, filename: &str) -> Result<()> {
use std::fs::OpenOptions;
let mut file = OpenOptions::new().read(true).write(true).open(filename)?;
let id3_total_size = self.riff_file.as_ref().and_then(|rf| {
rf.chunks
.iter()
.find(|c| c.id == "id3 " || c.id == "ID3 ")
.map(|chunk| {
let pad = if chunk.data_size % 2 == 1 { 1u64 } else { 0 };
8 + chunk.data_size as u64 + pad
})
});
let size_before = file.seek(SeekFrom::End(0))?;
self.remove_id3_chunk_writer(&mut file)?;
if let Some(removed) = id3_total_size {
let new_size = size_before.saturating_sub(removed);
file.set_len(new_size)?;
}
Ok(())
}
fn remove_id3_chunk_writer(&mut self, file: &mut dyn crate::ReadWriteSeek) -> Result<()> {
if let Some(ref riff_file) = self.riff_file {
if let Some(chunk) = riff_file
.chunks
.iter()
.find(|c| c.id == "id3 " || c.id == "ID3 ")
{
let pad = if chunk.data_size % 2 == 1 { 1u64 } else { 0 };
let total_size = 8 + chunk.data_size as u64 + pad; let chunk_offset = chunk.offset;
let old_riff_size = riff_file.file_size;
Self::delete_bytes_dyn(file, total_size, chunk_offset)?;
let new_riff_size = (old_riff_size as u64)
.checked_sub(total_size)
.and_then(|v| u32::try_from(v).ok())
.ok_or_else(|| {
AudexError::InvalidData(
"ID3 chunk size exceeds RIFF container size".to_string(),
)
})?;
file.seek(SeekFrom::Start(4))?;
file.write_all(&new_riff_size.to_le_bytes())?;
file.flush()?;
}
}
if let Some(ref mut riff_file) = self.riff_file {
riff_file
.chunks
.retain(|chunk| chunk.id != "id3 " && chunk.id != "ID3 ");
}
Ok(())
}
pub fn add_tags(&mut self) -> Result<()> {
if self.tags.is_some() {
return Err(AudexError::WAVError("ID3 tag already exists".to_string()));
}
use crate::id3::ID3Tags;
self.tags = Some(ID3Tags::new());
Ok(())
}
pub fn mime(&self) -> &'static [&'static str] {
WAVE::mime_types()
}
pub fn pprint(&self) -> String {
let mut output = String::new();
output.push_str(&format!(
"WAVE File: {}\n",
self.filename.as_deref().unwrap_or("<unnamed>")
));
output.push_str(&self.info.pprint());
if let Some(tags) = &self.tags {
output.push_str("\n\nID3v2 Tags:\n");
for key in tags.keys() {
if let Some(values) = tags.get(&key) {
for value in values {
output.push_str(&format!("{}: {}\n", key, value));
}
}
}
}
output
}
pub fn save_with_options(
&mut self,
file_path: Option<&str>,
v2_version: Option<u8>,
v23_sep: Option<&str>,
) -> Result<()> {
let v2_version_option = v2_version.unwrap_or(3); let v23_sep_string = v23_sep.unwrap_or("/").to_string();
let target_path = match file_path {
Some(path) => path.to_string(),
None => self.filename.clone().ok_or_else(|| {
AudexError::InvalidData("No file path provided and no filename stored".to_string())
})?,
};
self.save_to_file_with_options(target_path, v2_version_option, Some(v23_sep_string))
}
pub fn save_to_file<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
self.save_to_file_with_options(path.as_ref(), 3, Some("/".to_string()))
}
fn save_to_file_with_options<P: AsRef<Path>>(
&mut self,
path: P,
v2_version: u8,
v23_sep: Option<String>,
) -> Result<()> {
use std::fs::OpenOptions;
let file_path = path.as_ref();
let mut file = OpenOptions::new().read(true).write(true).open(file_path)?;
let size_before = file.metadata()?.len();
self.save_to_writer_impl(&mut file, v2_version, v23_sep)?;
if let Some(ref rf) = self.riff_file {
let logical_size = rf.file_size as u64 + 8;
if logical_size < size_before {
file.set_len(logical_size)?;
}
}
Ok(())
}
fn save_to_writer_impl(
&mut self,
file: &mut dyn crate::ReadWriteSeek,
v2_version: u8,
v23_sep: Option<String>,
) -> Result<()> {
let mut riff_file = RiffFile::parse(file)?;
let id3_chunk = riff_file
.find_chunk("id3")
.or_else(|| riff_file.find_chunk("ID3"));
let new_id3_data = if let Some(ref tags) = self.tags {
let minimal_data = self.generate_id3_data(tags, v2_version, v23_sep.clone(), 0)?;
let needed = minimal_data.len();
let available = id3_chunk.as_ref().map_or(0, |c| c.data_size as usize);
let file_size = file.seek(SeekFrom::End(0))?;
let trailing_size = match id3_chunk.as_ref() {
Some(chunk) => file_size as i64 - chunk.data_offset as i64,
None => 0,
};
let info = PaddingInfo::new(available as i64 - needed as i64, trailing_size);
let padding = info.get_default_padding().max(0) as usize;
self.generate_id3_data(tags, v2_version, v23_sep, padding)?
} else {
Vec::new() };
if let Some(existing_chunk) = id3_chunk {
let old_size = existing_chunk.data_size as u64;
let new_size = new_id3_data.len() as u64;
let padded_new_size = if new_size % 2 == 1 {
new_size + 1
} else {
new_size
};
let old_padded_size = if old_size % 2 == 1 {
old_size + 1
} else {
old_size
};
Self::resize_bytes_dyn(
file,
old_padded_size,
padded_new_size,
existing_chunk.data_offset,
)?;
file.seek(SeekFrom::Start(existing_chunk.data_offset))?;
file.write_all(&new_id3_data)?;
if new_size % 2 == 1 {
file.write_all(&[0])?;
}
let chunk_size_u32 = u32::try_from(new_size).map_err(|_| {
AudexError::InvalidData("chunk size exceeds u32::MAX (> 4 GB)".to_string())
})?;
file.seek(SeekFrom::Start(existing_chunk.data_offset - 4))?;
file.write_all(&chunk_size_u32.to_le_bytes())?;
if padded_new_size != old_padded_size {
let size_diff = padded_new_size as i64 - old_padded_size as i64;
let computed = (riff_file.file_size as i64)
.checked_add(size_diff)
.ok_or_else(|| {
AudexError::InvalidData("RIFF file size arithmetic overflow".to_string())
})?;
let new_riff_size = u32::try_from(computed).map_err(|_| {
AudexError::InvalidData("RIFF file size does not fit in u32".to_string())
})?;
file.seek(SeekFrom::Start(4))?; file.write_all(&new_riff_size.to_le_bytes())?; riff_file.file_size = new_riff_size;
}
} else if !new_id3_data.is_empty() {
self.insert_id3_chunk(file, &mut riff_file, new_id3_data)?;
}
self.riff_file = Some(riff_file);
Self::zero_stale_tail(file)?;
if let Some(ref rf) = self.riff_file {
let logical_end = rf.file_size as u64 + 8;
file.seek(SeekFrom::Start(logical_end))?;
}
Ok(())
}
fn generate_id3_data(
&self,
tags: &ID3Tags,
v2_version: u8,
v23_sep: Option<String>,
padding: usize,
) -> Result<Vec<u8>> {
let default = crate::id3::tags::ID3SaveConfig::default();
let config = crate::id3::tags::ID3SaveConfig {
v2_version,
v23_sep: v23_sep.unwrap_or(default.v23_sep),
padding: if padding > 0 { Some(padding) } else { None },
..default
};
let tag_data = tags.write_with_config(&config)?;
if tag_data.is_empty() {
return Ok(Vec::new());
}
let mut id3v2_data = Vec::new();
id3v2_data.extend_from_slice(b"ID3"); id3v2_data.push(v2_version); id3v2_data.push(0); id3v2_data.push(0);
let size = u32::try_from(tag_data.len()).map_err(|_| {
AudexError::InvalidData("ID3 tag data length exceeds u32::MAX".to_string())
})?;
if size > 0x0FFF_FFFF {
return Err(AudexError::InvalidData(
"ID3 tag data exceeds synchsafe size limit (268,435,455 bytes)".to_string(),
));
}
let synchsafe = [
((size >> 21) & 0x7F) as u8,
((size >> 14) & 0x7F) as u8,
((size >> 7) & 0x7F) as u8,
(size & 0x7F) as u8,
];
id3v2_data.extend_from_slice(&synchsafe);
id3v2_data.extend_from_slice(&tag_data);
Ok(id3v2_data)
}
fn insert_id3_chunk(
&self,
file: &mut dyn crate::ReadWriteSeek,
riff_file: &mut RiffFile,
id3_data: Vec<u8>,
) -> Result<()> {
let insert_offset = if let Some(data_chunk) = riff_file.find_chunk("data") {
data_chunk.offset } else {
file.seek(SeekFrom::End(0))?;
file.stream_position()?
};
let data_size = id3_data.len();
let padding_size = if data_size % 2 == 1 { 1 } else { 0 };
let total_chunk_size = 8 + data_size + padding_size;
Self::insert_bytes_dyn(file, total_chunk_size as u64, insert_offset)?;
file.seek(SeekFrom::Start(insert_offset))?;
file.write_all(b"id3 ")?; let data_size_u32 = u32::try_from(data_size)
.map_err(|_| AudexError::InvalidData("ID3 chunk data size exceeds u32::MAX".into()))?;
file.write_all(&data_size_u32.to_le_bytes())?; file.write_all(&id3_data)?;
if padding_size > 0 {
file.write_all(&[0])?;
}
let new_file_size = riff_file
.file_size
.checked_add(u32::try_from(total_chunk_size).map_err(|_| {
AudexError::InvalidData("ID3 chunk total size exceeds u32::MAX".into())
})?)
.ok_or_else(|| {
AudexError::InvalidData(
"RIFF file size would exceed u32::MAX after inserting ID3 chunk".to_string(),
)
})?;
file.seek(SeekFrom::Start(4))?; file.write_all(&new_file_size.to_le_bytes())?;
riff_file.file_size = new_file_size;
let new_chunk = RiffChunk {
id: "id3 ".to_string(),
size: data_size as u32,
offset: insert_offset,
data_offset: insert_offset + 8,
data_size: data_size as u32,
};
let insert_index = riff_file
.chunks
.iter()
.position(|chunk| chunk.offset >= insert_offset)
.unwrap_or(riff_file.chunks.len());
riff_file.chunks.insert(insert_index, new_chunk);
Ok(())
}
}
const WAVE_IO_BUF: usize = 64 * 1024;
impl WAVE {
fn move_bytes_dyn(
file: &mut dyn crate::ReadWriteSeek,
dest: u64,
src: u64,
count: u64,
) -> Result<()> {
if count == 0 || src == dest {
return Ok(());
}
let chunk_size = std::cmp::min(WAVE_IO_BUF as u64, count) as usize;
let mut buf = vec![0u8; chunk_size];
if src < dest {
let mut remaining = count;
while remaining > 0 {
let cur = std::cmp::min(chunk_size as u64, remaining) as usize;
let s = src + remaining - cur as u64;
let d = dest + remaining - cur as u64;
file.seek(SeekFrom::Start(s))?;
file.read_exact(&mut buf[..cur])?;
file.seek(SeekFrom::Start(d))?;
file.write_all(&buf[..cur])?;
remaining -= cur as u64;
}
} else {
let mut moved = 0u64;
while moved < count {
let cur = std::cmp::min(chunk_size as u64, count - moved) as usize;
file.seek(SeekFrom::Start(src + moved))?;
file.read_exact(&mut buf[..cur])?;
file.seek(SeekFrom::Start(dest + moved))?;
file.write_all(&buf[..cur])?;
moved += cur as u64;
}
}
Ok(())
}
fn delete_bytes_dyn(file: &mut dyn crate::ReadWriteSeek, size: u64, offset: u64) -> Result<()> {
if size == 0 {
return Ok(());
}
let file_size = file.seek(SeekFrom::End(0))?;
let delete_end = offset + size;
let trailing = file_size.saturating_sub(delete_end);
if trailing > 0 {
Self::move_bytes_dyn(file, offset, delete_end, trailing)?;
}
let new_size = file_size.saturating_sub(size);
Self::truncate_stream(file, new_size);
Ok(())
}
fn insert_bytes_dyn(file: &mut dyn crate::ReadWriteSeek, size: u64, offset: u64) -> Result<()> {
if size == 0 {
return Ok(());
}
let file_size = file.seek(SeekFrom::End(0))?;
let zero_buf = vec![0u8; WAVE_IO_BUF];
let mut remaining = size;
file.seek(SeekFrom::End(0))?;
while remaining > 0 {
let chunk = std::cmp::min(remaining, WAVE_IO_BUF as u64) as usize;
file.write_all(&zero_buf[..chunk])?;
remaining -= chunk as u64;
}
let bytes_to_move = file_size - offset;
if bytes_to_move > 0 {
Self::move_bytes_dyn(file, offset + size, offset, bytes_to_move)?;
file.seek(SeekFrom::Start(offset))?;
let mut rem = size;
while rem > 0 {
let chunk = std::cmp::min(rem, WAVE_IO_BUF as u64) as usize;
file.write_all(&vec![0u8; chunk])?;
rem -= chunk as u64;
}
}
Ok(())
}
fn resize_bytes_dyn(
file: &mut dyn crate::ReadWriteSeek,
old_size: u64,
new_size: u64,
offset: u64,
) -> Result<()> {
if old_size == new_size {
return Ok(());
}
if new_size > old_size {
let diff = new_size - old_size;
Self::insert_bytes_dyn(file, diff, offset + old_size)
} else {
let diff = old_size - new_size;
Self::delete_bytes_dyn(file, diff, offset + new_size)
}
}
fn truncate_stream(file: &mut dyn crate::ReadWriteSeek, new_size: u64) {
let _ = file.seek(SeekFrom::Start(new_size));
let _ = file.flush();
}
fn zero_stale_tail(writer: &mut dyn crate::ReadWriteSeek) -> Result<()> {
writer.seek(SeekFrom::Start(4))?;
let mut size_buf = [0u8; 4];
writer.read_exact(&mut size_buf)?;
let riff_data_size = u32::from_le_bytes(size_buf) as u64;
let logical_end = riff_data_size + 8;
let physical_end = writer.seek(SeekFrom::End(0))?;
if logical_end < physical_end {
writer.seek(SeekFrom::Start(logical_end))?;
let mut remaining = (physical_end - logical_end) as usize;
const CHUNK_SIZE: usize = 64 * 1024;
let zeroes = [0u8; CHUNK_SIZE];
while remaining > 0 {
let n = remaining.min(CHUNK_SIZE);
writer.write_all(&zeroes[..n])?;
remaining -= n;
}
writer.flush()?;
}
Ok(())
}
}
impl Default for WAVE {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "async")]
impl WAVE {
pub async fn load_async<P: AsRef<Path>>(path: P) -> Result<Self> {
let mut file = loadfile_read_async(&path).await?;
let mut wave = WAVE::new();
wave.filename = Some(path.as_ref().to_string_lossy().to_string());
wave.parse_file_async(&mut file).await?;
Ok(wave)
}
async fn parse_file_async(&mut self, file: &mut TokioFile) -> Result<()> {
file.seek(SeekFrom::Start(0)).await?;
let riff_file = RiffFileAsync::parse(file).await?;
if riff_file.file_type != "WAVE" {
return Err(AudexError::WAVError("Expected WAVE format".to_string()));
}
self.info = Self::parse_stream_info_async(&riff_file, file).await?;
self.tags = if let Some(id3_chunk) = riff_file
.find_chunk("id3 ")
.or_else(|| riff_file.find_chunk("ID3 "))
{
let id3_data = id3_chunk.read_data(file).await?;
if id3_data.len() >= 10 {
match specs::ID3Header::from_bytes(&id3_data) {
Ok(specs_header) => {
let header = ID3Header::from_specs_header(&specs_header);
ID3Tags::from_data(&id3_data, &header).ok()
}
Err(_) => None,
}
} else {
None
}
} else {
None
};
self.riff_file = Some(Self::convert_riff_to_riff_file(&riff_file));
Ok(())
}
fn convert_riff_to_riff_file(riff: &RiffFileAsync) -> RiffFile {
let chunks = riff
.chunks
.iter()
.map(|chunk| RiffChunk {
id: chunk.id.clone(),
size: chunk.size,
offset: chunk.offset,
data_offset: chunk.data_offset,
data_size: chunk.data_size,
})
.collect();
RiffFile {
file_type: riff.file_type.clone(),
chunks,
file_size: riff.file_size,
}
}
async fn parse_stream_info_async(
riff: &RiffFileAsync,
file: &mut TokioFile,
) -> Result<WAVEStreamInfo> {
let fmt_chunk = riff
.find_chunk("fmt ")
.ok_or_else(|| AudexError::WAVError("No 'fmt' chunk found".to_string()))?;
if fmt_chunk.data_size < 16 {
return Err(AudexError::WAVInvalidChunk(
"Format chunk too small".to_string(),
));
}
let fmt_data = fmt_chunk.read_data(file).await?;
let audio_format = u16::from_le_bytes([fmt_data[0], fmt_data[1]]);
let channels = u16::from_le_bytes([fmt_data[2], fmt_data[3]]);
let sample_rate = u32::from_le_bytes([fmt_data[4], fmt_data[5], fmt_data[6], fmt_data[7]]);
let _byte_rate = u32::from_le_bytes([fmt_data[8], fmt_data[9], fmt_data[10], fmt_data[11]]);
let block_align = u16::from_le_bytes([fmt_data[12], fmt_data[13]]);
let bits_per_sample = u16::from_le_bytes([fmt_data[14], fmt_data[15]]);
if block_align == 0 {
return Err(AudexError::WAVInvalidChunk(
"block_align must be nonzero".to_string(),
));
}
if channels == 0 {
warn_event!("WAVE: channels is zero in fmt chunk — bitrate will be reported as 0");
}
if bits_per_sample == 0 {
warn_event!(
"WAVE: bits_per_sample is zero in fmt chunk — bitrate will be reported as 0"
);
}
let bitrate = if sample_rate == 0 || channels == 0 || bits_per_sample == 0 {
None
} else {
Some(
(channels as u32)
.saturating_mul(bits_per_sample as u32)
.saturating_mul(sample_rate),
)
};
let mut number_of_samples = 0u64;
let mut length = None;
if let Some(data_chunk) = riff.find_chunk("data") {
if sample_rate > 0 {
number_of_samples = data_chunk.data_size as u64 / block_align as u64;
let duration_secs = number_of_samples as f64 / sample_rate as f64;
length = Some(Duration::from_secs_f64(duration_secs));
}
}
Ok(WAVEStreamInfo {
length,
bitrate,
channels,
sample_rate,
bits_per_sample,
audio_format,
number_of_samples,
})
}
pub async fn save_async(&mut self) -> Result<()> {
let filename = self
.filename
.clone()
.ok_or(AudexError::InvalidData("No filename set".to_string()))?;
self.save_to_file_async(&filename).await
}
pub async fn save_to_file_async<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
self.save_to_file_with_options_async(path.as_ref(), 3, Some("/".to_string()))
.await
}
pub async fn save_with_options_async(
&mut self,
file_path: Option<&str>,
v2_version: Option<u8>,
v23_sep: Option<&str>,
) -> Result<()> {
let version = v2_version.unwrap_or(3);
let sep = v23_sep.unwrap_or("/").to_string();
let target_path = match file_path {
Some(path) => path.to_string(),
None => self.filename.clone().ok_or_else(|| {
AudexError::InvalidData("No file path provided and no filename stored".to_string())
})?,
};
self.save_to_file_with_options_async(target_path, version, Some(sep))
.await
}
async fn save_to_file_with_options_async<P: AsRef<Path>>(
&mut self,
path: P,
v2_version: u8,
v23_sep: Option<String>,
) -> Result<()> {
let mut file = loadfile_write_async(&path).await?;
let mut riff_file = RiffFileAsync::parse(&mut file).await?;
if riff_file.file_type != "WAVE" {
return Err(AudexError::WAVError("Expected WAVE format".to_string()));
}
let id3_chunk = riff_file
.find_chunk("id3 ")
.or_else(|| riff_file.find_chunk("ID3 "))
.cloned();
let new_id3_data = if let Some(ref tags) = self.tags {
let minimal_data = self.generate_id3_data(tags, v2_version, v23_sep.clone(), 0)?;
let needed = minimal_data.len();
let available = id3_chunk.as_ref().map_or(0, |c| c.data_size as usize);
let file_size = file.seek(SeekFrom::End(0)).await?;
let trailing_size = match id3_chunk.as_ref() {
Some(chunk) => file_size as i64 - chunk.data_offset as i64,
None => 0,
};
let info = PaddingInfo::new(available as i64 - needed as i64, trailing_size);
let padding = info.get_default_padding().max(0) as usize;
self.generate_id3_data(tags, v2_version, v23_sep, padding)?
} else {
Vec::new()
};
if let Some(existing_chunk) = id3_chunk {
let old_size = existing_chunk.data_size;
let new_size = new_id3_data.len() as u32;
if old_size != new_size {
resize_riff_chunk_async(&mut file, &existing_chunk, new_size).await?;
let old_padded = old_size + (old_size % 2);
let new_padded = new_size + (new_size % 2);
let size_diff = new_padded as i64 - old_padded as i64;
let computed = (riff_file.file_size as i64)
.checked_add(size_diff)
.ok_or_else(|| {
AudexError::InvalidData("RIFF file size arithmetic overflow".to_string())
})?;
let new_riff_size = u32::try_from(computed).map_err(|_| {
AudexError::InvalidData("RIFF file size does not fit in u32".to_string())
})?;
update_riff_file_size_async(&mut file, new_riff_size).await?;
riff_file.file_size = new_riff_size;
}
file.seek(SeekFrom::Start(existing_chunk.data_offset))
.await?;
file.write_all(&new_id3_data).await?;
if new_size % 2 == 1 {
file.write_all(&[0]).await?;
}
} else if !new_id3_data.is_empty() {
let insert_offset = if let Some(data_chunk) = riff_file.find_chunk("data") {
data_chunk.offset
} else {
file.seek(SeekFrom::End(0)).await?
};
let data_size = new_id3_data.len() as u32;
let padding = if data_size % 2 == 1 { 1 } else { 0 };
let total_chunk_size = 8 + data_size + padding;
insert_bytes_async(&mut file, total_chunk_size as u64, insert_offset, None).await?;
file.seek(SeekFrom::Start(insert_offset)).await?;
file.write_all(b"id3 ").await?;
file.write_all(&data_size.to_le_bytes()).await?;
file.write_all(&new_id3_data).await?;
if padding > 0 {
file.write_all(&[0]).await?;
}
let new_riff_size = riff_file
.file_size
.checked_add(total_chunk_size)
.ok_or_else(|| {
AudexError::InvalidData(
"RIFF file size would exceed u32::MAX after inserting ID3 chunk"
.to_string(),
)
})?;
update_riff_file_size_async(&mut file, new_riff_size).await?;
riff_file.file_size = new_riff_size;
let new_chunk = IffChunkAsync::new("id3 ".to_string(), data_size, insert_offset)?;
let insert_index = riff_file
.chunks
.iter()
.position(|c| c.offset >= insert_offset)
.unwrap_or(riff_file.chunks.len());
riff_file.chunks.insert(insert_index, new_chunk);
}
file.flush().await.map_err(AudexError::Io)?;
self.riff_file = Some(Self::convert_riff_to_riff_file(&riff_file));
Ok(())
}
pub async fn clear_async(&mut self) -> Result<()> {
self.tags = None;
let has_id3_chunk = if let Some(ref riff_file) = self.riff_file {
riff_file
.chunks
.iter()
.any(|chunk| chunk.id == "id3 " || chunk.id == "ID3 ")
} else {
false
};
if has_id3_chunk {
if let Some(filename) = self.filename.clone() {
self.remove_id3_chunk_async(&filename).await?;
}
}
Ok(())
}
async fn remove_id3_chunk_async(&mut self, filename: &str) -> Result<()> {
use tokio::fs::OpenOptions;
if let Some(ref riff_file) = self.riff_file {
if let Some(chunk) = riff_file
.chunks
.iter()
.find(|c| c.id == "id3 " || c.id == "ID3 ")
{
let pad = if chunk.data_size % 2 == 1 { 1u64 } else { 0 };
let total_size = 8 + chunk.data_size as u64 + pad;
let chunk_offset = chunk.offset;
let old_riff_size = riff_file.file_size;
let mut file = OpenOptions::new()
.read(true)
.write(true)
.open(filename)
.await?;
delete_bytes_async(&mut file, total_size, chunk_offset, None).await?;
let new_riff_size = (old_riff_size as u64)
.checked_sub(total_size)
.and_then(|v| u32::try_from(v).ok())
.ok_or_else(|| {
AudexError::InvalidData(
"ID3 chunk size exceeds RIFF container size".to_string(),
)
})?;
file.seek(SeekFrom::Start(4)).await?;
file.write_all(&new_riff_size.to_le_bytes()).await?;
file.flush().await?;
}
}
if let Some(ref mut riff_file) = self.riff_file {
riff_file
.chunks
.retain(|chunk| chunk.id != "id3 " && chunk.id != "ID3 ");
}
Ok(())
}
pub async fn delete_async<P: AsRef<Path>>(path: P) -> Result<()> {
tokio::fs::remove_file(path).await?;
Ok(())
}
}
pub fn clear<P: AsRef<Path>>(path: P) -> Result<()> {
let mut wave = WAVE::load(path)?;
wave.clear()
}
#[cfg(feature = "async")]
pub async fn clear_async<P: AsRef<Path>>(path: P) -> Result<()> {
let mut wave = WAVE::load_async(path).await?;
wave.clear_async().await
}
#[cfg(feature = "async")]
pub async fn open_async<P: AsRef<Path>>(path: P) -> Result<WAVE> {
WAVE::load_async(path).await
}
impl FileType for WAVE {
type Tags = ID3Tags;
type Info = WAVEStreamInfo;
fn format_id() -> &'static str {
"WAVE"
}
fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
debug_event!("parsing WAVE file");
let mut file = std::fs::File::open(&path)?;
let mut wave = WAVE::new();
wave.filename = Some(path.as_ref().to_string_lossy().to_string());
wave.parse_file(&mut file)?;
Ok(wave)
}
fn load_from_reader(reader: &mut dyn crate::ReadSeek) -> Result<Self> {
debug_event!("parsing WAVE file from reader");
let mut wave = Self::new();
let mut reader = reader;
wave.parse_file(&mut reader)?;
Ok(wave)
}
fn save(&mut self) -> Result<()> {
debug_event!("saving WAVE metadata");
let filename = self
.filename
.clone()
.ok_or(AudexError::InvalidData("No filename set".to_string()))?;
self.save_to_file(&filename)
}
fn clear(&mut self) -> Result<()> {
self.tags = None;
let has_id3_chunk = if let Some(ref riff_file) = self.riff_file {
riff_file
.chunks
.iter()
.any(|chunk| chunk.id == "id3 " || chunk.id == "ID3 ")
} else {
false
};
if has_id3_chunk {
if let Some(filename) = self.filename.clone() {
self.remove_id3_chunk(&filename)?;
}
}
Ok(())
}
fn save_to_writer(&mut self, writer: &mut dyn crate::ReadWriteSeek) -> Result<()> {
self.save_to_writer_impl(writer, 3, Some("/".to_string()))
}
fn clear_writer(&mut self, writer: &mut dyn crate::ReadWriteSeek) -> Result<()> {
self.tags = None;
writer.seek(SeekFrom::Start(0))?;
self.riff_file = Some(RiffFile::parse(writer)?);
let has_id3_chunk = if let Some(ref riff_file) = self.riff_file {
riff_file
.chunks
.iter()
.any(|chunk| chunk.id == "id3 " || chunk.id == "ID3 ")
} else {
false
};
if has_id3_chunk {
self.remove_id3_chunk_writer(writer)?;
Self::zero_stale_tail(writer)?;
if let Some(ref rf) = self.riff_file {
let logical_end = rf.file_size as u64 + 8;
writer.seek(SeekFrom::Start(logical_end))?;
}
}
Ok(())
}
fn save_to_path(&mut self, path: &Path) -> Result<()> {
self.save_to_file(path)
}
fn tags(&self) -> Option<&Self::Tags> {
self.tags.as_ref()
}
fn tags_mut(&mut self) -> Option<&mut Self::Tags> {
self.tags.as_mut()
}
fn info(&self) -> &Self::Info {
&self.info
}
fn add_tags(&mut self) -> Result<()> {
if self.tags.is_some() {
return Err(AudexError::WAVError("ID3 tag already exists".to_string()));
}
let mut tags = ID3Tags::new();
if let Some(ref filename) = self.filename {
tags.filename = Some(std::path::PathBuf::from(filename));
}
self.tags = Some(tags);
Ok(())
}
fn get(&self, key: &str) -> Option<Vec<String>> {
self.tags.as_ref()?.get_text_values(key)
}
fn score(filename: &str, header: &[u8]) -> i32 {
let mut score = 0;
if header.len() >= 12 && &header[0..4] == b"RIFF" && &header[8..12] == b"WAVE" {
score += 10;
}
let lower_filename = filename.to_lowercase();
if lower_filename.ends_with(".wav") {
score += 3;
} else if lower_filename.ends_with(".wave") {
score += 2;
}
score
}
fn mime_types() -> &'static [&'static str] {
&["audio/wav", "audio/wave"]
}
}