use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_int, c_uint};
use std::path::Path;
use std::sync::Mutex;
use std::time::Duration;
const STR: usize = 512;
const COMMENT: usize = 1024;
const GENRE: usize = 256;
const SMALL: usize = 64;
const CODEC: usize = 32;
#[repr(C)]
struct RawTags {
codec: [u8; CODEC],
title: [u8; STR],
artist: [u8; STR],
album: [u8; STR],
albumartist: [u8; STR],
composer: [u8; STR],
grouping: [u8; STR],
comment: [u8; COMMENT],
genre: [u8; GENRE],
year_string: [u8; SMALL],
track_string: [u8; SMALL],
disc_string: [u8; SMALL],
mb_track_id: [u8; SMALL],
codectype: i32,
tracknum: i32,
discnum: i32,
year: i32,
layer: i32,
id3version: i32,
vbr: i32,
bitrate: i32,
frequency: u32,
reserved_: u32,
filesize: u64,
length_ms: u64,
samples: u64,
frame_count: u64,
first_frame_offset: u64,
track_level: i64,
album_level: i64,
track_gain: i64,
album_gain: i64,
track_peak: i64,
album_peak: i64,
has_albumart: i32,
albumart_type: i32,
albumart_pos: i64,
albumart_size: i32,
has_cuesheet: i32,
cuesheet_pos: i64,
cuesheet_size: i32,
cuesheet_encoding: i32,
}
extern "C" {
fn rbmeta_read(path: *const c_char, out: *mut RawTags) -> c_int;
fn rbmeta_codec_label(codectype: c_uint) -> *const c_char;
fn probe_file_format(filename: *const c_char) -> c_uint;
}
static META_LOCK: Mutex<()> = Mutex::new(());
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct ReplayGain {
pub track_gain_db: Option<f32>,
pub album_gain_db: Option<f32>,
pub track_peak: Option<f32>,
pub album_peak: Option<f32>,
pub raw_track_gain: i64,
pub raw_album_gain: i64,
pub raw_track_peak: i64,
pub raw_album_peak: i64,
}
impl ReplayGain {
pub fn is_empty(&self) -> bool {
self.raw_track_gain == 0
&& self.raw_album_gain == 0
&& self.raw_track_peak == 0
&& self.raw_album_peak == 0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AlbumArtKind {
Bmp,
Png,
Jpeg,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AlbumArt {
pub kind: AlbumArtKind,
pub offset: u64,
pub size: u32,
pub id3_unsync: bool,
pub vorbis_base64: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CueEncoding {
Latin1,
Utf8,
Utf16Le,
Utf16Be,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Cuesheet {
pub offset: u64,
pub size: u32,
pub encoding: CueEncoding,
}
#[derive(Debug, Clone, Default)]
pub struct Metadata {
pub codec: String,
pub codec_id: u32,
pub title: String,
pub artist: String,
pub album: String,
pub albumartist: String,
pub composer: String,
pub grouping: String,
pub comment: String,
pub genre: String,
pub year_string: String,
pub track_string: String,
pub disc_string: String,
pub mb_track_id: String,
pub track_number: Option<u32>,
pub disc_number: Option<u32>,
pub year: Option<u32>,
pub duration: Duration,
pub bitrate: u32,
pub sample_rate: u32,
pub filesize: u64,
pub samples: u64,
pub vbr: bool,
pub first_frame_offset: u64,
pub replaygain: ReplayGain,
pub album_art: Option<AlbumArt>,
pub cuesheet: Option<Cuesheet>,
}
#[derive(Debug)]
pub enum Error {
Open(std::path::PathBuf),
Parse(std::path::PathBuf),
InvalidPath,
OutOfMemory,
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::Open(p) => write!(f, "cannot open {}", p.display()),
Error::Parse(p) => write!(f, "unrecognized or corrupt audio file {}", p.display()),
Error::InvalidPath => write!(f, "path contains a NUL byte"),
Error::OutOfMemory => write!(f, "out of memory"),
}
}
}
impl std::error::Error for Error {}
fn cstr_field(bytes: &[u8]) -> String {
let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
String::from_utf8_lossy(&bytes[..end]).into_owned()
}
fn q12_db(v: i64) -> f32 {
v as f32 / 4096.0
}
fn q24(v: i64) -> f32 {
v as f32 / (1u32 << 24) as f32
}
impl Metadata {
fn from_raw(raw: &RawTags) -> Self {
let opt_u32 = |v: i32| if v > 0 { Some(v as u32) } else { None };
Metadata {
codec: cstr_field(&raw.codec),
codec_id: raw.codectype as u32,
title: cstr_field(&raw.title),
artist: cstr_field(&raw.artist),
album: cstr_field(&raw.album),
albumartist: cstr_field(&raw.albumartist),
composer: cstr_field(&raw.composer),
grouping: cstr_field(&raw.grouping),
comment: cstr_field(&raw.comment),
genre: cstr_field(&raw.genre),
year_string: cstr_field(&raw.year_string),
track_string: cstr_field(&raw.track_string),
disc_string: cstr_field(&raw.disc_string),
mb_track_id: cstr_field(&raw.mb_track_id),
track_number: opt_u32(raw.tracknum),
disc_number: opt_u32(raw.discnum),
year: opt_u32(raw.year),
duration: Duration::from_millis(raw.length_ms),
bitrate: raw.bitrate.max(0) as u32,
sample_rate: raw.frequency,
filesize: raw.filesize,
samples: raw.samples,
vbr: raw.vbr != 0,
first_frame_offset: raw.first_frame_offset,
replaygain: ReplayGain {
track_gain_db: (raw.track_gain != 0).then(|| q12_db(raw.track_level)),
album_gain_db: (raw.album_gain != 0).then(|| q12_db(raw.album_level)),
track_peak: (raw.track_peak != 0).then(|| q24(raw.track_peak)),
album_peak: (raw.album_peak != 0).then(|| q24(raw.album_peak)),
raw_track_gain: raw.track_gain,
raw_album_gain: raw.album_gain,
raw_track_peak: raw.track_peak,
raw_album_peak: raw.album_peak,
},
album_art: (raw.has_albumart != 0 && raw.albumart_size > 0).then(|| AlbumArt {
kind: match raw.albumart_type & 0xF {
1 => AlbumArtKind::Bmp,
2 => AlbumArtKind::Png,
3 => AlbumArtKind::Jpeg,
_ => AlbumArtKind::Unknown,
},
offset: raw.albumart_pos.max(0) as u64,
size: raw.albumart_size as u32,
id3_unsync: raw.albumart_type & (1 << 4) != 0,
vorbis_base64: raw.albumart_type & (1 << 5) != 0,
}),
cuesheet: (raw.has_cuesheet != 0 && raw.cuesheet_size > 0).then(|| Cuesheet {
offset: raw.cuesheet_pos.max(0) as u64,
size: raw.cuesheet_size as u32,
encoding: match raw.cuesheet_encoding {
1 => CueEncoding::Latin1,
2 => CueEncoding::Utf8,
3 => CueEncoding::Utf16Le,
4 => CueEncoding::Utf16Be,
_ => CueEncoding::Unknown,
},
}),
}
}
}
pub fn read<P: AsRef<Path>>(path: P) -> Result<Metadata, Error> {
let path = path.as_ref();
let c_path = CString::new(path.to_string_lossy().as_bytes()).map_err(|_| Error::InvalidPath)?;
let mut raw: Box<RawTags> = unsafe { Box::new(std::mem::zeroed()) };
let rc = {
let _guard = META_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe { rbmeta_read(c_path.as_ptr(), &mut *raw) }
};
match rc {
0 => Ok(Metadata::from_raw(&raw)),
-1 => Err(Error::Open(path.to_path_buf())),
-3 => Err(Error::OutOfMemory),
_ => Err(Error::Parse(path.to_path_buf())),
}
}
pub fn probe(filename: &str) -> Option<String> {
let c = CString::new(filename).ok()?;
unsafe {
let afmt = probe_file_format(c.as_ptr());
let label = rbmeta_codec_label(afmt);
if afmt == 0 || label.is_null() {
return None;
}
Some(CStr::from_ptr(label).to_string_lossy().into_owned())
}
}