This commit is contained in:
Spider committed 2023-07-28 16:45:07 +00:00
commit 5543be8661
5 files changed
+345

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
/target
/Cargo.lock
*.txt
+19
View File
@@ -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"
+23
View File
@@ -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 <DIRECTORY> [--output <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```
+71
View File
@@ -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::<String>("directory").expect("required");
let output = matches.get_one::<String>("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");
}
}
+229
View File
@@ -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<FlacLoadError> for ProcessError {
fn from(err: FlacLoadError) -> Self {
ProcessError::FlacLoadError(err)
}
}
impl From<std::io::Error> for FlacLoadError {
fn from(err: std::io::Error) -> Self {
FlacLoadError::FileOpenError(err)
}
}
impl From<claxon::Error> 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<AudioFile>,
}
#[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<AudioFile> {
let mut songs: Vec<AudioFile> = 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<AudioFile, ProcessError> {
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<i16>) -> 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<AudioFile>) -> 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<i16>, 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<i16> = reader
.samples()
.map(|sample| sample.unwrap_or(0) as i16)
.collect();
Ok((samples, reader.streaminfo()))
}
}