mirror of
https://github.com/Serial-ATA/lofty-rs
synced 2024-11-10 06:34:18 +00:00
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:
parent
e59a41420b
commit
4cc33b5e53
13 changed files with 425 additions and 10 deletions
|
@ -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"]
|
10
src/components/logic/constants.rs
Normal file
10
src/components/logic/constants.rs
Normal 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];
|
|
@ -1,2 +1,3 @@
|
|||
mod constants;
|
||||
pub(crate) mod read;
|
||||
pub(crate) mod write;
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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!()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
|
|
14
src/tag.rs
14
src/tag.rs
|
@ -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())),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>>,
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
Loading…
Reference in a new issue