commit 5543be8661d5d96c5c9ed83ef3124f3667f75bc7 Author: Spider Date: Fri Jul 28 16:45:07 2023 +0000 Init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f1b6781 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +/Cargo.lock +*.txt \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..bc5deb2 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "audioinfo" +version = "0.0.1" +edition = "2021" + +[dependencies] +serde = { version = "1.0.152", features = ["derive"] } +serde_yaml = "0.9.25" +walkdir = "2" +crc32fast = "1.3.2" +claxon = "0.4.3" +byteorder = "1.4.3" +hex = "0.4.3" +tracing = "0.1.37" +clap = "4.3.19" +tracing-subscriber = "0.3" +[[bin]] +name = "audioinfo" +path = "src/bin/main.rs" diff --git a/README.md b/README.md new file mode 100644 index 0000000..b0f53ed --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# AudioInfo Generator +The AudioInfo Generator is a command-line tool that generates an audioinfo file for a given directory containing FLAC files. This README provides an overview of the tool's functionality, usage, and command-line options. + +# Usage + +```audioinfo-generator [FLAGS] --directory [--output ] [--print]``` + +* `--directory: Sets the directory to scan for FLAC files (required).` + +* `--output: Sets the output directory for the generated audioinfo file. If not provided, the audioinfo file will be saved in the current working directory with the name "audioinfo.txt".` + +* `--print: If specified, the generated audioinfo content will be printed to the standard output instead of saving it to a file.` + +* `--verbose or -v: Enables verbose (debug) output. This option can be used to get more detailed information during the execution of the tool.` + + +# Examples +## Generate an audioinfo file for FLAC files in a specified directory and save it to a custom output file: +```audioinfo --directory /path/to/your/flac_files --output /path/to/output/audioinfo.txt``` +## Generate an audioinfo file for FLAC files in a specified directory and print the content to the standard output: +```audioinfo-generator --directory /path/to/your/flac_files --print``` +## Enable verbose (debug) output: +```audioinfo --directory /path/to/your/flac_files --verbose``` \ No newline at end of file diff --git a/src/bin/main.rs b/src/bin/main.rs new file mode 100644 index 0000000..fcbdb09 --- /dev/null +++ b/src/bin/main.rs @@ -0,0 +1,71 @@ +use audioinfo::AudioInfo; +use clap::{Arg, ArgAction, Command}; +use std::fs; +use std::io::Write; +use std::path::Path; + +fn main() { + let matches = Command::new("AudioInfo Generator") + .version("1.0") + .about("Generates an audioinfo file for the given directory") + .arg( + Arg::new("directory") + .help("Sets the directory to scan for FLAC files") + .required(true), + ) + .arg(Arg::new("output").help("Sets the output directory for audioinfo")) + .arg( + Arg::new("verbose") + .short('v') + .long("verbose") + .action(ArgAction::SetTrue) + .help("Enables verbose (debug) output"), + ) + .arg( + Arg::new("print") + .short('p') + .long("print") + .action(ArgAction::SetTrue) + .help("Print AudioInfo to std"), + ) + .get_matches(); + + let directory = matches.get_one::("directory").expect("required"); + let output = matches.get_one::("output"); + let print = matches.get_flag("print"); + + let verbose = matches.get_flag("verbose"); + if verbose { + tracing::subscriber::set_global_default( + tracing_subscriber::FmtSubscriber::builder() + .with_max_level(tracing::Level::DEBUG) + .finish(), + ) + .expect("Failed to set global default tracing subscriber"); + } + let audio_info_string = AudioInfo::generate_audio_info_from_path(directory.to_string()); + + if print { + print!("{:}", audio_info_string); + } else { + match output { + Some(output) => { + save_file(output.to_string(), audio_info_string); + } + None => { + save_file(String::from("./audioinfo.txt"), audio_info_string); + } + } + } +} + +fn save_file(path: String, audio_info_string: String) { + let audioinfo_file_path = Path::new(&path); + if let Ok(mut file) = fs::File::create(audioinfo_file_path) { + if let Err(e) = file.write_all(audio_info_string.as_bytes()) { + println!("Error writing to audioinfo file: {}", e); + } + } else { + println!("Error creating audioinfo file"); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..9dbb4c2 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,229 @@ +use claxon::metadata::StreamInfo; +use crc32fast::Hasher; +use hex; +use serde::{Deserialize, Serialize}; +use std::cmp::min; +use std::fmt; +use std::fs::File; +use std::io::Read; +use std::path::Path; +use walkdir::WalkDir; +static AUDIOINFO_VERSION: i32 = 0; +static AUDIOINFO_FILETYPE: &str = "audioinfo"; +static AUDIOINFO_CREATED_BY: &str = "audioinfo"; + +#[derive(Debug)] +enum ProcessError { + NonFlacError, + FlacLoadError(FlacLoadError), + NoSamplesFound, +} + +#[derive(Debug)] +enum FlacLoadError { + FileOpenError(std::io::Error), + FlacReaderError(claxon::Error), +} +impl fmt::Display for ProcessError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ProcessError::FlacLoadError(err) => write!(f, "Flac load error: {}", err), + ProcessError::NoSamplesFound => write!(f, "Total samples not found"), + ProcessError::NonFlacError => write!(f, "Non flac file found"), + } + } +} +impl fmt::Display for FlacLoadError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + FlacLoadError::FileOpenError(err) => write!(f, "Error opening file: {}", err), + FlacLoadError::FlacReaderError(err) => { + write!(f, "Error creating claxon FLAC reader: {}", err) + } + } + } +} +impl From for ProcessError { + fn from(err: FlacLoadError) -> Self { + ProcessError::FlacLoadError(err) + } +} + +impl From for FlacLoadError { + fn from(err: std::io::Error) -> Self { + FlacLoadError::FileOpenError(err) + } +} + +impl From for FlacLoadError { + fn from(err: claxon::Error) -> Self { + FlacLoadError::FlacReaderError(err) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct AudioInfo { + #[serde(rename = r#"type"#)] + type_: String, + version: i32, + created_by: String, + summary: Summary, + files: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +struct Summary { + total_files: usize, + total_duration: String, + sample_rate: i32, + bit_depth: i32, + channels: i32, +} +#[derive(Debug, Serialize, Deserialize)] +struct AudioFile { + file_name: String, + duration: String, + total_samples: u64, + sample_rate: u32, + bit_depth: u32, + channels: u32, + peak_level: f32, + rms_db_level: f32, + crc32: String, + md5: String, +} +impl AudioInfo { + pub fn generate_audio_info_from_path(path: String) -> String { + let songs = Self::walk_dir(&path); + let total_duration = Self::add_durations(&songs); + let audio_info = AudioInfo { + type_: AUDIOINFO_FILETYPE.to_owned(), + version: AUDIOINFO_VERSION, + created_by: AUDIOINFO_CREATED_BY.to_owned(), + summary: Summary { + total_files: songs.len(), + total_duration, + sample_rate: 44100, + bit_depth: 16, + channels: 2, + }, + files: songs, + }; + serde_yaml::to_string(&audio_info).unwrap() + } + + fn walk_dir(dir: &str) -> Vec { + let mut songs: Vec = Vec::new(); + for entry in WalkDir::new(dir.trim_end_matches("\\").trim_end_matches("/")) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|f| { + !f.path() + .iter() + .any(|s| s.to_str().map(|x| x.starts_with('.')).unwrap_or(false)) + }) + { + if entry.file_type().is_file() && entry.path().extension() == Some("flac".as_ref()) { + match Self::process_audio_file(&entry) { + Ok(audio_file) => { + tracing::debug!("{:?}", audio_file); + songs.push(audio_file); + } + Err(ProcessError::NonFlacError) => { + tracing::debug!("Non flac file found: {:?}", &entry.path()); + continue; + } + Err(err) => { + tracing::error!("Error processing file: {}", err); + break; + } + } + } + } + songs + } + + fn process_audio_file(entry: &walkdir::DirEntry) -> Result { + if entry.file_type().is_file() && entry.path().extension() == Some("flac".as_ref()) { + let (samples, stream_info) = Self::load_flac(entry.path())?; + + let total_samples = stream_info.samples.ok_or(ProcessError::NoSamplesFound)?; + + let crc32_checksum = Self::generate_crc32(samples); + let duration = total_samples as f32 / stream_info.sample_rate as f32; + + let audio_info = AudioFile { + file_name: entry.file_name().to_string_lossy().to_string(), + duration: Self::format_duration(duration), + total_samples, + sample_rate: stream_info.sample_rate, + bit_depth: stream_info.bits_per_sample, + channels: stream_info.channels, + peak_level: 0.0, + rms_db_level: 0.0, + crc32: crc32_checksum, + md5: hex::encode(stream_info.md5sum), + }; + + Ok(audio_info) + } else { + Err(ProcessError::NonFlacError) + } + } + fn generate_crc32(samples: Vec) -> String { + let mut crc32 = Hasher::new(); + + for sample in samples { + crc32.update(&sample.to_le_bytes()); + } + let crc32_hash = crc32.finalize(); + format!("{:X}", crc32_hash) + } + fn add_durations(audio_files: &Vec) -> String { + let total_duration_secs: f32 = audio_files + .iter() + .map(|audio_file| Self::parse_duration(&audio_file.duration)) + .sum(); + + Self::format_duration(total_duration_secs) + } + fn parse_duration(duration: &str) -> f32 { + let components: Vec<&str> = duration.split(':').collect(); + let hours: f32 = components.get(0).unwrap_or(&"0").parse().unwrap_or(0.0); + let minutes: f32 = components.get(1).unwrap_or(&"0").parse().unwrap_or(0.0); + let seconds: f32 = components.get(2).unwrap_or(&"0").parse().unwrap_or(0.0); + let milliseconds: f32 = components.get(3).unwrap_or(&"00").parse().unwrap_or(0.0); + + hours * 3600.0 + minutes * 60.0 + seconds + milliseconds / 100.0 + } + + fn format_duration(duration_secs: f32) -> String { + let hours = (duration_secs / 3600.0) as u32; + let minutes = ((duration_secs - (hours as f32 * 3600.0)) / 60.0) as u32; + let seconds = (duration_secs - (hours as f32 * 3600.0) - (minutes as f32 * 60.0)) as u32; + + let milliseconds = (duration_secs.fract() * 100.0 + 0.5) as u32; + + let capped_milliseconds = min(milliseconds, 99); + + format!( + "{:02}:{:02}:{:02}.{:02}", + hours, minutes, seconds, capped_milliseconds + ) + } + + fn load_flac(path: &Path) -> Result<(Vec, StreamInfo), FlacLoadError> { + let mut flac_file = File::open(path)?; + let mut flac_data = Vec::new(); + flac_file.read_to_end(&mut flac_data)?; + + // Decode FLAC data to PCM samples + let mut reader = claxon::FlacReader::new(&flac_data[..])?; + let samples: Vec = reader + .samples() + .map(|sample| sample.unwrap_or(0) as i16) + .collect(); + + Ok((samples, reader.streaminfo())) + } +}