1use std::ffi::{CStr, CString};
27use std::os::raw::{c_char, c_int, c_uint};
28use std::path::Path;
29use std::sync::Mutex;
30use std::time::Duration;
31
32const STR: usize = 512;
33const COMMENT: usize = 1024;
34const GENRE: usize = 256;
35const SMALL: usize = 64;
36const CODEC: usize = 32;
37
38#[repr(C)]
40struct RawTags {
41 codec: [u8; CODEC],
42 title: [u8; STR],
43 artist: [u8; STR],
44 album: [u8; STR],
45 albumartist: [u8; STR],
46 composer: [u8; STR],
47 grouping: [u8; STR],
48 comment: [u8; COMMENT],
49 genre: [u8; GENRE],
50 year_string: [u8; SMALL],
51 track_string: [u8; SMALL],
52 disc_string: [u8; SMALL],
53 mb_track_id: [u8; SMALL],
54
55 codectype: i32,
56 tracknum: i32,
57 discnum: i32,
58 year: i32,
59 layer: i32,
60 id3version: i32,
61 vbr: i32,
62 bitrate: i32,
63
64 frequency: u32,
65 reserved_: u32,
66
67 filesize: u64,
68 length_ms: u64,
69 samples: u64,
70 frame_count: u64,
71 first_frame_offset: u64,
72
73 track_level: i64,
74 album_level: i64,
75 track_gain: i64,
76 album_gain: i64,
77 track_peak: i64,
78 album_peak: i64,
79
80 has_albumart: i32,
81 albumart_type: i32,
82 albumart_pos: i64,
83 albumart_size: i32,
84
85 has_cuesheet: i32,
86 cuesheet_pos: i64,
87 cuesheet_size: i32,
88 cuesheet_encoding: i32,
89}
90
91extern "C" {
92 fn rbmeta_read(path: *const c_char, out: *mut RawTags) -> c_int;
93 fn rbmeta_codec_label(codectype: c_uint) -> *const c_char;
94 fn probe_file_format(filename: *const c_char) -> c_uint;
95}
96
97static META_LOCK: Mutex<()> = Mutex::new(());
99
100#[derive(Debug, Clone, Copy, Default, PartialEq)]
107pub struct ReplayGain {
108 pub track_gain_db: Option<f32>,
110 pub album_gain_db: Option<f32>,
112 pub track_peak: Option<f32>,
114 pub album_peak: Option<f32>,
116 pub raw_track_gain: i64,
118 pub raw_album_gain: i64,
120 pub raw_track_peak: i64,
122 pub raw_album_peak: i64,
124}
125
126impl ReplayGain {
127 pub fn is_empty(&self) -> bool {
129 self.raw_track_gain == 0
130 && self.raw_album_gain == 0
131 && self.raw_track_peak == 0
132 && self.raw_album_peak == 0
133 }
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub enum AlbumArtKind {
139 Bmp,
140 Png,
141 Jpeg,
142 Unknown,
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
150pub struct AlbumArt {
151 pub kind: AlbumArtKind,
152 pub offset: u64,
154 pub size: u32,
156 pub id3_unsync: bool,
159 pub vorbis_base64: bool,
161}
162
163#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165pub enum CueEncoding {
166 Latin1,
167 Utf8,
168 Utf16Le,
169 Utf16Be,
170 Unknown,
171}
172
173#[derive(Debug, Clone, Copy, PartialEq, Eq)]
175pub struct Cuesheet {
176 pub offset: u64,
177 pub size: u32,
178 pub encoding: CueEncoding,
179}
180
181#[derive(Debug, Clone, Default)]
183pub struct Metadata {
184 pub codec: String,
186 pub codec_id: u32,
188
189 pub title: String,
191 pub artist: String,
192 pub album: String,
193 pub albumartist: String,
194 pub composer: String,
195 pub grouping: String,
196 pub comment: String,
197 pub genre: String,
198 pub year_string: String,
200 pub track_string: String,
201 pub disc_string: String,
202 pub mb_track_id: String,
204
205 pub track_number: Option<u32>,
206 pub disc_number: Option<u32>,
207 pub year: Option<u32>,
208
209 pub duration: Duration,
211 pub bitrate: u32,
213 pub sample_rate: u32,
215 pub filesize: u64,
217 pub samples: u64,
219 pub vbr: bool,
221 pub first_frame_offset: u64,
223
224 pub replaygain: ReplayGain,
225 pub album_art: Option<AlbumArt>,
226 pub cuesheet: Option<Cuesheet>,
227}
228
229#[derive(Debug)]
231pub enum Error {
232 Open(std::path::PathBuf),
234 Parse(std::path::PathBuf),
236 InvalidPath,
238 OutOfMemory,
240}
241
242impl std::fmt::Display for Error {
243 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
244 match self {
245 Error::Open(p) => write!(f, "cannot open {}", p.display()),
246 Error::Parse(p) => write!(f, "unrecognized or corrupt audio file {}", p.display()),
247 Error::InvalidPath => write!(f, "path contains a NUL byte"),
248 Error::OutOfMemory => write!(f, "out of memory"),
249 }
250 }
251}
252
253impl std::error::Error for Error {}
254
255fn cstr_field(bytes: &[u8]) -> String {
256 let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
257 String::from_utf8_lossy(&bytes[..end]).into_owned()
258}
259
260fn q12_db(v: i64) -> f32 {
262 v as f32 / 4096.0
263}
264
265fn q24(v: i64) -> f32 {
267 v as f32 / (1u32 << 24) as f32
268}
269
270impl Metadata {
271 fn from_raw(raw: &RawTags) -> Self {
272 let opt_u32 = |v: i32| if v > 0 { Some(v as u32) } else { None };
273
274 Metadata {
275 codec: cstr_field(&raw.codec),
276 codec_id: raw.codectype as u32,
277 title: cstr_field(&raw.title),
278 artist: cstr_field(&raw.artist),
279 album: cstr_field(&raw.album),
280 albumartist: cstr_field(&raw.albumartist),
281 composer: cstr_field(&raw.composer),
282 grouping: cstr_field(&raw.grouping),
283 comment: cstr_field(&raw.comment),
284 genre: cstr_field(&raw.genre),
285 year_string: cstr_field(&raw.year_string),
286 track_string: cstr_field(&raw.track_string),
287 disc_string: cstr_field(&raw.disc_string),
288 mb_track_id: cstr_field(&raw.mb_track_id),
289 track_number: opt_u32(raw.tracknum),
290 disc_number: opt_u32(raw.discnum),
291 year: opt_u32(raw.year),
292 duration: Duration::from_millis(raw.length_ms),
293 bitrate: raw.bitrate.max(0) as u32,
294 sample_rate: raw.frequency,
295 filesize: raw.filesize,
296 samples: raw.samples,
297 vbr: raw.vbr != 0,
298 first_frame_offset: raw.first_frame_offset,
299 replaygain: ReplayGain {
300 track_gain_db: (raw.track_gain != 0).then(|| q12_db(raw.track_level)),
301 album_gain_db: (raw.album_gain != 0).then(|| q12_db(raw.album_level)),
302 track_peak: (raw.track_peak != 0).then(|| q24(raw.track_peak)),
303 album_peak: (raw.album_peak != 0).then(|| q24(raw.album_peak)),
304 raw_track_gain: raw.track_gain,
305 raw_album_gain: raw.album_gain,
306 raw_track_peak: raw.track_peak,
307 raw_album_peak: raw.album_peak,
308 },
309 album_art: (raw.has_albumart != 0 && raw.albumart_size > 0).then(|| AlbumArt {
310 kind: match raw.albumart_type & 0xF {
311 1 => AlbumArtKind::Bmp,
312 2 => AlbumArtKind::Png,
313 3 => AlbumArtKind::Jpeg,
314 _ => AlbumArtKind::Unknown,
315 },
316 offset: raw.albumart_pos.max(0) as u64,
317 size: raw.albumart_size as u32,
318 id3_unsync: raw.albumart_type & (1 << 4) != 0,
319 vorbis_base64: raw.albumart_type & (1 << 5) != 0,
320 }),
321 cuesheet: (raw.has_cuesheet != 0 && raw.cuesheet_size > 0).then(|| Cuesheet {
322 offset: raw.cuesheet_pos.max(0) as u64,
323 size: raw.cuesheet_size as u32,
324 encoding: match raw.cuesheet_encoding {
325 1 => CueEncoding::Latin1,
326 2 => CueEncoding::Utf8,
327 3 => CueEncoding::Utf16Le,
328 4 => CueEncoding::Utf16Be,
329 _ => CueEncoding::Unknown,
330 },
331 }),
332 }
333 }
334}
335
336pub fn read<P: AsRef<Path>>(path: P) -> Result<Metadata, Error> {
338 let path = path.as_ref();
339 let c_path = CString::new(path.to_string_lossy().as_bytes()).map_err(|_| Error::InvalidPath)?;
340
341 let mut raw: Box<RawTags> = unsafe { Box::new(std::mem::zeroed()) };
343
344 let rc = {
345 let _guard = META_LOCK.lock().unwrap_or_else(|e| e.into_inner());
346 unsafe { rbmeta_read(c_path.as_ptr(), &mut *raw) }
347 };
348
349 match rc {
350 0 => Ok(Metadata::from_raw(&raw)),
351 -1 => Err(Error::Open(path.to_path_buf())),
352 -3 => Err(Error::OutOfMemory),
353 _ => Err(Error::Parse(path.to_path_buf())),
354 }
355}
356
357pub fn probe(filename: &str) -> Option<String> {
360 let c = CString::new(filename).ok()?;
361 unsafe {
362 let afmt = probe_file_format(c.as_ptr());
363 let label = rbmeta_codec_label(afmt);
364 if afmt == 0 || label.is_null() {
365 return None;
366 }
367 Some(CStr::from_ptr(label).to_string_lossy().into_owned())
368 }
369}