mirror of
https://github.com/OPSnet/audioinfo.git
synced 2026-11-07 06:15:59 -10:00
Init
This commit is contained in:
commit
5543be8661
5 files changed
+345
No files matched your search
@@ -0,0 +1,3 @@
|
||||
/target
|
||||
/Cargo.lock
|
||||
*.txt
|
||||
+19
@@ -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"
|
||||
@@ -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```
|
||||
@@ -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
@@ -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()))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user