Start work on wav decoding

Very few IDs supported right now, test doesn't work yet, and this only supports RIFF INFO chunks, ID3 has to wait for https://github.com/polyfloyd/rust-id3/pull/64

Signed-off-by: Serial <69764315+Serial-ATA@users.noreply.github.com>
This commit is contained in:
Serial 2021-04-17 15:42:06 -04:00
parent e59a41420b
commit 4cc33b5e53
13 changed files with 425 additions and 10 deletions

View file

@ -15,7 +15,8 @@ categories = ["accessiblity", "multimedia::audio"]
# Ape
ape = {version = "0.3.0", optional = true}
# Wav
hound = {version = "3.4.0", optional = true}
sndfile-sys = {version = "0.2.2", optional = true}
riff = {version = "1.0.1", optional = true}
# Mp3
id3 = {version = "0.6.2", optional = true} # De/Encoding
mp3-duration = {version = "0.1.10", optional = true} # Duration
@ -30,6 +31,7 @@ metaflac = {version = "0.2.4", optional = true}
opus_headers = {version = "0.1.2", optional = true}
# Errors
thiserror = "1.0.24"
byteorder = "1.4.3"
filepath = "0.1.1"
[features]
@ -37,7 +39,7 @@ default = ["full"]
full = ["all_tags", "duration"]
mp4 = ["mp4ameta"]
mp3 = ["id3"]
wav = ["hound"]
wav = ["sndfile-sys", "riff"]
vorbis = ["lewton", "metaflac", "opus_headers", "ogg"]
all_tags = ["vorbis", "mp4", "mp3", "wav", "ape"]
duration = ["mp3-duration"]

View file

@ -0,0 +1,10 @@
// Used to determine the WAV metadata format
pub const LIST_ID: &[u8; 4] = b"LIST";
pub const ID3_ID: &[u8; 4] = b"ID3 "; // TODO
// FourCC
pub const IART: [u8; 4] = [73, 65, 82, 84];
pub const ICMT: [u8; 4] = [73, 67, 77, 84];
pub const ICRD: [u8; 4] = [73, 67, 82, 68];
pub const INAM: [u8; 4] = [73, 78, 65, 77];
pub const ISFT: [u8; 4] = [73, 83, 70, 84];

View file

@ -1,2 +1,3 @@
mod constants;
pub(crate) mod read;
pub(crate) mod write;

View file

@ -1 +1,93 @@
use super::constants::{ID3_ID, LIST_ID};
use crate::{AnyTag, Error, Id3v2Tag, Result, ToAnyTag};
use byteorder::{BigEndian, ByteOrder, LittleEndian, ReadBytesExt};
use std::collections::HashMap;
use std::io::{Cursor, Read, Seek, SeekFrom};
pub(crate) fn wav<T>(mut data: T) -> Result<Option<HashMap<String, String>>>
where
T: Read + Seek,
{
let chunk = riff::Chunk::read(&mut data, 0)?;
let mut list: Option<riff::Chunk> = None;
for child in chunk.iter(&mut data) {
println!("{}", child.id());
let chunk_id = child.id();
let value_upper = std::str::from_utf8(&chunk_id.value)?.to_uppercase();
let value_bytes = value_upper.as_bytes();
if value_bytes == LIST_ID {
list = Some(child);
break;
}
if value_bytes == ID3_ID {
#[cfg(feature = "mp3")]
{
list = Some(child);
break;
}
#[cfg(not(feature = "mp3"))]
return Err(Error::Wav(
"WAV file has an id3 tag, but `mp3` feature is not enabled.",
));
}
}
return if let Some(list) = list {
let mut content = list.read_contents(&mut data)?;
#[cfg(feature = "mp3")]
if &list.id().value == ID3_ID {
// TODO
}
println!("{:?}", content);
content.drain(0..4); // Get rid of the chunk ID
let mut cursor = Cursor::new(&*content);
let chunk_len = list.len();
let mut metadata: HashMap<String, String> = HashMap::with_capacity(chunk_len as usize);
for _ in 0..chunk_len {
let fourcc = cursor.read_u32::<LittleEndian>()? as u32;
let size = cursor.read_u32::<LittleEndian>()? as u32;
match create_wav_key(&fourcc.to_le_bytes()) {
Some(key) => {
let mut buf = vec![0; size as usize];
cursor.read_exact(&mut buf)?;
let val = std::str::from_utf8(&*buf)?;
metadata.insert(key, val.trim_matches(char::from(0)).to_string());
// Skip null byte
if size as usize % 2 != 0 {
cursor.set_position(cursor.position() + 1)
}
},
None => cursor.set_position(cursor.position() + u64::from(size)),
}
}
Ok(Some(metadata))
} else {
Err(Error::Wav(
"This file does not contain an INFO chunk".to_string(),
))
};
}
fn create_wav_key(fourcc: &[u8]) -> Option<String> {
match fourcc {
fcc if fcc == super::constants::IART => Some("Artist".to_string()),
fcc if fcc == super::constants::ICMT => Some("Comment".to_string()),
fcc if fcc == super::constants::ICRD => Some("Date".to_string()),
fcc if fcc == super::constants::INAM => Some("Title".to_string()),
fcc if fcc == super::constants::ISFT => Some("Title".to_string()),
_ => None,
}
}

View file

@ -1,6 +1,6 @@
use crate::Result;
use ogg::PacketWriteEndInfo;
use std::io::{Cursor, Read, Seek, SeekFrom};
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
pub(crate) fn ogg<T>(data: T, packet: &[u8]) -> Result<Cursor<Vec<u8>>>
where
@ -51,3 +51,14 @@ where
c.seek(SeekFrom::Start(0))?;
Ok(c)
}
pub(crate) fn wav<T>(mut data: T, packet: Vec<u8>, four_cc: &str) -> Result<()>
where
T: Read + Seek + Write,
{
let contents = riff::ChunkContents::Data(riff::ChunkId::new(four_cc).unwrap(), packet);
contents.write(&mut data)?;
data.seek(SeekFrom::Start(0))?;
Ok(())
}

View file

@ -12,3 +12,5 @@ pub use id3_tag::Id3v2Tag;
pub use mp4_tag::Mp4Tag;
#[cfg(feature = "vorbis")]
pub use vorbis_tag::VorbisTag;
#[cfg(feature = "wav")]
pub use wav_tag::WavTag;

View file

@ -1 +1,280 @@
use crate::{
impl_tag, traits::ReadPath, Album, AnyTag, AudioTag, AudioTagEdit, AudioTagWrite, Id3v2Tag,
Picture, Result, TagType, ToAny, ToAnyTag,
};
use std::borrow::BorrowMut;
use std::{collections::HashMap, fs::File, path::Path};
struct WavInnerTag {
data: Option<HashMap<String, String>>,
}
impl ReadPath for WavInnerTag {
fn from_path<P>(path: P, _tag_type: Option<TagType>) -> Result<Self>
where
P: AsRef<std::path::Path>,
Self: Sized,
{
let data = crate::components::logic::read::wav(File::open(path)?)?;
Ok(Self { data })
}
}
impl Default for WavInnerTag {
fn default() -> Self {
let data: Option<HashMap<String, String>> = Some(HashMap::new());
Self { data }
}
}
impl<'a> From<AnyTag<'a>> for WavTag {
fn from(inp: AnyTag<'a>) -> Self {
let mut tag = WavTag::default();
if let Some(v) = inp.title() {
tag.set_title(v)
}
if let Some(v) = inp.artists_as_string() {
tag.set_artist(&v)
}
if let Some(v) = inp.year {
tag.set_year(v)
}
if let Some(v) = inp.album().title {
tag.set_album_title(v)
}
if let Some(v) = inp.album().artists {
tag.set_album_artists(v.join(", "))
}
if let Some(v) = inp.track_number() {
tag.set_track_number(v)
}
if let Some(v) = inp.total_tracks() {
tag.set_total_tracks(v)
}
if let Some(v) = inp.disc_number() {
tag.set_disc_number(v)
}
if let Some(v) = inp.total_discs() {
tag.set_total_discs(v)
}
tag
}
}
impl<'a> From<&'a WavTag> for AnyTag<'a> {
fn from(inp: &'a WavTag) -> Self {
Self {
title: inp.title(),
artists: inp.artists(),
year: inp.year().map(|y| y as i32),
album: Album::new(inp.album_title(), inp.album_artists(), inp.album_cover()),
track_number: inp.track_number(),
total_tracks: inp.total_tracks(),
disc_number: inp.disc_number(),
total_discs: inp.total_discs(),
..AnyTag::default()
}
}
}
impl_tag!(WavTag, WavInnerTag, TagType::Wav);
impl WavTag {
fn get_value(&self, key: &str) -> Option<&str> {
self.0
.data
.as_ref()
.unwrap()
.get_key_value(key)
.and_then(|pair| {
if pair.1.is_empty() {
None
} else {
Some(pair.1.as_str())
}
})
}
fn set_value<V>(&mut self, key: &str, val: V)
where
V: Into<String>,
{
let mut data = self.0.data.clone().unwrap();
let _ = data.insert(key.to_string(), val.into());
self.0.data = Some(data);
}
fn remove_key(&mut self, key: &str) {
let mut data = self.0.data.clone().unwrap();
data.retain(|k, _| k != key);
self.0.data = Some(data);
}
}
impl AudioTagEdit for WavTag {
fn title(&self) -> Option<&str> {
self.get_value("title")
}
fn set_title(&mut self, title: &str) {
self.set_value("title", title)
}
fn remove_title(&mut self) {
self.remove_key("title")
}
fn artist(&self) -> Option<&str> {
self.get_value("artist")
}
fn set_artist(&mut self, artist: &str) {
self.set_value("artist", artist)
}
fn add_artist(&mut self, artist: &str) {
todo!()
}
fn artists(&self) -> Option<Vec<&str>> {
self.artist().map(|a| a.split(", ").collect())
}
fn remove_artist(&mut self) {
self.remove_key("artist")
}
fn year(&self) -> Option<i32> {
if let Some(Ok(y)) = self.get_value("year").map(str::parse::<i32>) {
Some(y)
} else {
None
}
}
fn set_year(&mut self, year: i32) {
self.set_value("year", year.to_string())
}
fn remove_year(&mut self) {
self.remove_key("year")
}
fn album_title(&self) -> Option<&str> {
self.get_value("album")
}
fn set_album_title(&mut self, v: &str) {
self.remove_key("albumartist")
}
fn remove_album_title(&mut self) {
self.remove_key("albumtitle")
}
fn album_artists(&self) -> Option<Vec<&str>> {
self.get_value("albumartist").map(|a| vec![a]) // TODO
}
fn set_album_artists(&mut self, artists: String) {
self.set_value("albumartist", artists)
}
fn add_album_artist(&mut self, artist: &str) {
todo!()
}
fn remove_album_artists(&mut self) {
self.remove_key("albumartist")
}
fn album_cover(&self) -> Option<Picture> {
todo!()
}
fn set_album_cover(&mut self, cover: Picture<'a>) {
todo!()
}
fn remove_album_cover(&mut self) {
todo!()
}
fn track_number(&self) -> Option<u32> {
if let Some(Ok(y)) = self.get_value("tracknumber").map(str::parse::<u32>) {
Some(y)
} else {
None
}
}
fn set_track_number(&mut self, track_number: u32) {
todo!()
}
fn remove_track_number(&mut self) {
todo!()
}
fn total_tracks(&self) -> Option<u32> {
todo!()
}
fn set_total_tracks(&mut self, total_track: u32) {
todo!()
}
fn remove_total_tracks(&mut self) {
todo!()
}
fn disc_number(&self) -> Option<u32> {
todo!()
}
fn set_disc_number(&mut self, disc_number: u32) {
todo!()
}
fn remove_disc_number(&mut self) {
todo!()
}
fn total_discs(&self) -> Option<u32> {
todo!()
}
fn set_total_discs(&mut self, total_discs: u32) {
todo!()
}
fn remove_total_discs(&mut self) {
todo!()
}
}
impl AudioTagWrite for WavTag {
fn write_to(&self, file: &mut File) -> Result<()> {
// let (tag, data) = {
// let mut data = Vec::new();
//
// let tag = if self.0.id3.is_some() {
// "ID3 "
// } else {
// "INFO"
// };
//
// };
//
// crate::components::logic::write::wav(file, )
Ok(())
}
fn write_to_path(&self, path: &str) -> Result<()> {
todo!()
}
}

View file

@ -25,10 +25,16 @@ pub enum Error {
Lewton(#[from] lewton::VorbisError),
#[error(transparent)]
Ogg(#[from] ogg::OggReadError),
#[error("{0}")]
Wav(String),
#[error("")]
NotAPicture,
#[error(transparent)]
Utf8(#[from] std::str::Utf8Error),
#[error(transparent)]
FromUtf8(#[from] std::string::FromUtf8Error),
/// Represents all cases of `std::io::Error`.
#[error(transparent)]
IO(#[from] std::io::Error),

View file

@ -68,6 +68,7 @@
//#![forbid(unused_crate_dependencies, unused_import_braces)]
#![feature(box_into_boxed_slice)]
#![feature(in_band_lifetimes)]
#![warn(clippy::pedantic)]
#![allow(
clippy::too_many_lines,

View file

@ -1,5 +1,6 @@
#[allow(clippy::wildcard_imports)]
use super::{components::tags::*, AudioTag, Error, Result};
use crate::{Id3v2Tag, WavTag};
use std::path::Path;
/// A builder for `Box<dyn AudioTag>`. If you do not want a trait object, you can use individual types.
@ -42,6 +43,8 @@ impl Tag {
TagType::Id3v2 => Ok(Box::new(Id3v2Tag::read_from_path(path, None)?)),
#[cfg(feature = "mp4")]
TagType::Mp4 => Ok(Box::new(Mp4Tag::read_from_path(path, None)?)),
#[cfg(feature = "wav")]
TagType::Wav => Ok(Box::new(WavTag::read_from_path(path, None)?)),
#[cfg(feature = "vorbis")] // TODO: this isn't ideal, make this better somehow
id => Ok(Box::new(VorbisTag::read_from_path(path, Some(id.clone()))?)),
}
@ -57,6 +60,9 @@ pub enum TagType {
#[cfg(feature = "mp3")]
/// Common file extensions: `.mp3`
Id3v2,
#[cfg(feature = "mp4")]
/// Common file extensions: `.mp4, .m4a, .m4p, .m4b, .m4r, .m4v`
Mp4,
#[cfg(feature = "vorbis")]
/// Common file extensions: `.ogg, .oga`
Ogg,
@ -66,9 +72,9 @@ pub enum TagType {
#[cfg(feature = "vorbis")]
/// Common file extensions: `.flac`
Flac,
#[cfg(feature = "mp4")]
/// Common file extensions: `.mp4, .m4a, .m4p, .m4b, .m4r, .m4v`
Mp4,
#[cfg(feature = "wav")]
/// Common file extensions: `.wav, .wave`
Wav,
}
impl TagType {
@ -86,6 +92,8 @@ impl TagType {
"ogg" | "oga" => Ok(Self::Ogg),
#[cfg(feature = "mp4")]
"m4a" | "m4b" | "m4p" | "m4v" | "isom" | "mp4" => Ok(Self::Mp4),
#[cfg(feature = "wav")]
"wav" | "wave" => Ok(Self::Wav),
_ => Err(Error::UnsupportedFormat(ext.to_owned())),
}
}

View file

@ -1,3 +1,4 @@
use crate::WavTag;
#[allow(clippy::wildcard_imports)]
use crate::{components::tags::*, Album, AnyTag, Picture, Result, TagType};
use std::fs::File;
@ -110,10 +111,12 @@ pub trait ToAnyTag: ToAny {
TagType::Ape => Box::new(ApeTag::from(self.to_anytag())),
#[cfg(feature = "mp3")]
TagType::Id3v2 => Box::new(Id3v2Tag::from(self.to_anytag())),
#[cfg(feature = "vorbis")]
TagType::Ogg | TagType::Opus | TagType::Flac => Box::new(VorbisTag::from(self.to_anytag())),
#[cfg(feature = "mp4")]
TagType::Mp4 => Box::new(Mp4Tag::from(self.to_anytag())),
#[cfg(feature = "vorbis")]
TagType::Ogg | TagType::Opus | TagType::Flac => Box::new(VorbisTag::from(self.to_anytag())),
#[cfg(feature = "wav")]
TagType::Wav => Box::new(WavTag::from(self.to_anytag())),
}
}
}

View file

@ -1,7 +1,7 @@
use crate::Album;
/// The tag returned from `read_from_path`
#[derive(Default)]
#[derive(Default, Debug)]
pub struct AnyTag<'a> {
pub title: Option<&'a str>,
pub artists: Option<Vec<&'a str>>,

View file

@ -26,7 +26,7 @@ macro_rules! add_tags {
tag.set_artist("foo artist");
assert_eq!(tag.artist(), Some("foo artist"));
println!("Setting artist");
println!("Setting year");
tag.set_year(2020);
assert_eq!(tag.year(), Some(2020));