Skip to main content

audio_engine_core/decoder/
metadata.rs

1use symphonia::core::meta::{MetadataRevision, StandardTagKey, Value};
2use symphonia::core::probe::ProbeResult;
3
4/// Track metadata extracted from audio file tags.
5#[derive(Debug, Clone, Default)]
6pub struct TrackMetadata {
7    pub title: Option<String>,
8    pub artist: Option<String>,
9    pub album: Option<String>,
10    pub track_number: Option<u32>,
11    pub disc_number: Option<u32>,
12    pub genre: Option<String>,
13    pub year: Option<u32>,
14    pub cover_art: Option<Vec<u8>>,
15    pub cover_art_mime: Option<String>,
16    pub lyrics: Option<String>,
17    pub rg_track_gain: Option<f64>,
18    pub rg_track_peak: Option<f64>,
19    pub rg_album_gain: Option<f64>,
20    pub rg_album_peak: Option<f64>,
21}
22
23/// Audio format information extracted from file.
24#[derive(Debug, Clone)]
25pub struct AudioInfo {
26    pub sample_rate: u32,
27    pub channels: usize,
28    pub bits_per_sample: Option<u32>,
29    pub total_frames: Option<u64>,
30    pub duration_secs: Option<f64>,
31    pub encoder_delay: u32,
32    pub end_padding: u32,
33    pub metadata: TrackMetadata,
34}
35
36pub(super) fn extract_metadata(probed: &mut ProbeResult) -> TrackMetadata {
37    let mut metadata = TrackMetadata::default();
38
39    if let Some(meta) = probed.metadata.get() {
40        if let Some(revision) = meta.current() {
41            merge_metadata_revision(&mut metadata, revision);
42        }
43    }
44
45    if metadata.title.is_some() || metadata.artist.is_some() {
46        log::debug!(
47            "Extracted metadata: {:?} by {:?} from {:?}",
48            metadata.title,
49            metadata.artist,
50            metadata.album
51        );
52    }
53
54    metadata
55}
56
57pub(super) fn merge_metadata_revision(metadata: &mut TrackMetadata, revision: &MetadataRevision) {
58    for tag in revision.tags() {
59        match tag.std_key {
60            Some(StandardTagKey::TrackTitle) => {
61                metadata.title = metadata
62                    .title
63                    .take()
64                    .or_else(|| tag_value_to_string(&tag.value));
65            }
66            Some(StandardTagKey::Artist) => {
67                metadata.artist = metadata
68                    .artist
69                    .take()
70                    .or_else(|| tag_value_to_string(&tag.value));
71            }
72            Some(StandardTagKey::Album) => {
73                metadata.album = metadata
74                    .album
75                    .take()
76                    .or_else(|| tag_value_to_string(&tag.value));
77            }
78            Some(StandardTagKey::TrackNumber) => {
79                metadata.track_number = metadata
80                    .track_number
81                    .or_else(|| tag_value_to_u32(&tag.value));
82            }
83            Some(StandardTagKey::DiscNumber) => {
84                metadata.disc_number = metadata
85                    .disc_number
86                    .or_else(|| tag_value_to_u32(&tag.value));
87            }
88            Some(StandardTagKey::Genre) => {
89                metadata.genre = metadata
90                    .genre
91                    .take()
92                    .or_else(|| tag_value_to_string(&tag.value));
93            }
94            Some(StandardTagKey::Date) => {
95                metadata.year = metadata.year.or_else(|| tag_value_to_u32(&tag.value));
96            }
97            Some(StandardTagKey::Lyrics) => {
98                metadata.lyrics = metadata
99                    .lyrics
100                    .take()
101                    .or_else(|| tag_value_to_non_empty_string(&tag.value));
102            }
103            _ => merge_non_standard_tag(metadata, &tag.key, &tag.value),
104        }
105    }
106
107    if metadata.cover_art.is_none() {
108        if let Some(visual) = revision.visuals().first() {
109            metadata.cover_art = Some(visual.data.to_vec());
110            metadata.cover_art_mime = Some(visual.media_type.clone());
111        }
112    }
113}
114
115fn merge_non_standard_tag(metadata: &mut TrackMetadata, key: &str, value: &Value) {
116    match key.to_lowercase().as_str() {
117        "title" => {
118            metadata.title = metadata.title.take().or_else(|| tag_value_to_string(value));
119        }
120        "artist" | "albumartist" | "album_artist" => {
121            metadata.artist = metadata
122                .artist
123                .take()
124                .or_else(|| tag_value_to_string(value));
125        }
126        "album" => {
127            metadata.album = metadata.album.take().or_else(|| tag_value_to_string(value));
128        }
129        "tracknumber" | "track_number" => {
130            metadata.track_number = metadata.track_number.or_else(|| tag_value_to_u32(value));
131        }
132        "discnumber" | "disc_number" => {
133            metadata.disc_number = metadata.disc_number.or_else(|| tag_value_to_u32(value));
134        }
135        "genre" => {
136            metadata.genre = metadata.genre.take().or_else(|| tag_value_to_string(value));
137        }
138        "date" | "year" => {
139            metadata.year = metadata.year.or_else(|| tag_value_to_u32(value));
140        }
141        "lyrics"
142        | "lyric"
143        | "unsyncedlyrics"
144        | "unsynced lyrics"
145        | "unsynchronisedlyrics"
146        | "unsynchronised lyrics"
147        | "unsynchronizedlyrics"
148        | "unsynchronized lyrics"
149        | "syncedlyrics"
150        | "synced lyrics" => {
151            metadata.lyrics = metadata
152                .lyrics
153                .take()
154                .or_else(|| tag_value_to_non_empty_string(value));
155        }
156        "replaygain_track_gain" => {
157            metadata.rg_track_gain = metadata
158                .rg_track_gain
159                .or_else(|| parse_rg_gain_from_value(value));
160        }
161        "replaygain_track_peak" => {
162            metadata.rg_track_peak = metadata
163                .rg_track_peak
164                .or_else(|| parse_rg_peak_from_value(value));
165        }
166        "replaygain_album_gain" => {
167            metadata.rg_album_gain = metadata
168                .rg_album_gain
169                .or_else(|| parse_rg_gain_from_value(value));
170        }
171        "replaygain_album_peak" => {
172            metadata.rg_album_peak = metadata
173                .rg_album_peak
174                .or_else(|| parse_rg_peak_from_value(value));
175        }
176        _ => {}
177    }
178}
179
180fn tag_value_to_string(value: &Value) -> Option<String> {
181    match value {
182        Value::String(s) => Some(s.clone()),
183        Value::UnsignedInt(n) => Some(n.to_string()),
184        Value::SignedInt(n) => Some(n.to_string()),
185        _ => None,
186    }
187}
188
189fn tag_value_to_non_empty_string(value: &Value) -> Option<String> {
190    tag_value_to_string(value).and_then(non_empty_string)
191}
192
193fn non_empty_string(value: String) -> Option<String> {
194    let trimmed = value.trim();
195    if trimmed.is_empty() {
196        None
197    } else if trimmed.len() == value.len() {
198        Some(value)
199    } else {
200        Some(trimmed.to_string())
201    }
202}
203
204fn tag_value_to_u32(value: &Value) -> Option<u32> {
205    match value {
206        Value::String(s) => s.parse().ok(),
207        Value::UnsignedInt(n) => Some(*n as u32),
208        Value::SignedInt(n) => Some(*n as u32),
209        _ => None,
210    }
211}
212
213fn parse_rg_gain_from_value(value: &Value) -> Option<f64> {
214    let s = tag_value_to_string(value)?;
215    parse_rg_gain_str(&s)
216}
217
218fn parse_rg_peak_from_value(value: &Value) -> Option<f64> {
219    let s = tag_value_to_string(value)?;
220    parse_rg_peak_str(&s)
221}
222
223fn parse_rg_gain_str(s: &str) -> Option<f64> {
224    s.trim()
225        .trim_end_matches("dB")
226        .trim()
227        .trim_end_matches("db")
228        .trim()
229        .parse::<f64>()
230        .ok()
231}
232
233fn parse_rg_peak_str(s: &str) -> Option<f64> {
234    s.split_whitespace()
235        .next()
236        .and_then(|p| p.parse::<f64>().ok())
237}