audio_engine_core/decoder/
metadata.rs1use symphonia::core::meta::{MetadataRevision, StandardTagKey, Value};
2use symphonia::core::probe::ProbeResult;
3
4#[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#[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}