本系列教程对应的代码已开源在 Github zeedle
目的是从.mp3/.flac/.wav/...
文件中提取歌曲名称/艺术家/音频时长信息/歌词信息/专辑封面
添加依赖
使用lofty这个全能解析库,将其添加到Cargo.toml中:
toml
lofty = "0.22.4"
解析元信息
解析歌名/歌手/时长
这些信息在应用启动后,即刻被加载到音乐列表面板中(SongInfo已于前几篇文章中定义):
rust
/// Read meta info from audio file `fp`, return a SongInfo
pub fn read_meta_info(fp: &PathBuf) -> Option<SongInfo> {
if let Ok(tagged) = lofty::read_from_path(fp) {
let dura = tagged.properties().duration().as_secs_f32();
if let Some(tag) = tagged.primary_tag() {
let song_name = tag.title();
let song_name = song_name.as_deref().unwrap_or(
fp.file_stem()
.map(|x| x.to_str())
.flatten()
.unwrap_or("unknown"),
);
let singer_name = tag.artist();
let singer_name = singer_name.as_deref().unwrap_or("unknown");
let item = SongInfo {
id: 0,
song_path: fp.display().to_shared_string(),
song_name: song_name.into(),
singer: singer_name.into(),
duration: format!("{:02}:{:02}", (dura as u32) / 60, (dura as u32) % 60)
.to_shared_string(),
};
return Some(item);
}
}
None
}
解析歌词
歌词只有在播放该文件时才应该被加载,所以单独解析:
rust
/// Read lyrics from audio file `p`, return a list of LyricItem
pub fn read_lyrics(p: PathBuf) -> Vec<LyricItem> {
if let Ok(tagged) = lofty::read_from_path(&p) {
if let Some(tag) = tagged.primary_tag() {
if let Some(lyric_item) = tag.get(&ItemKey::Lyrics) {
let mut lyrics = lyric_item
.value()
.text()
.unwrap()
.split("\n")
.map(|line| {
let (time_str, text) = line.split_once(']').unwrap_or(("", ""));
let time_str = time_str.trim_start_matches('[');
let dura = time_str
.split(':')
.map(|x| x.parse::<f32>().unwrap_or(0.))
.rev()
.reduce(|acc, x| acc + x * 60.)
.unwrap_or(0.);
LyricItem {
time: dura,
text: text.to_shared_string(),
duration: 0.0,
}
})
.filter(|ins| ins.time > 0. && !ins.text.is_empty())
.collect::<Vec<_>>();
for i in 0..lyrics.len() - 1 {
lyrics[i].duration = lyrics[i + 1].time - lyrics[i].time;
}
lyrics.last_mut().map(|ins| ins.duration = 100.0);
return lyrics;
}
}
}
return Vec::new();
}
解析专辑封面
同上,该图像只有在播放该文件时才应该被加载,所以单独解析:
rust
/// Read album cover from audio file `p`
pub fn read_album_cover(p: PathBuf) -> Option<(Vec<u8>, u32, u32)> {
if let Ok(tagged) = lofty::read_from_path(&p) {
if let Some(tag) = tagged.primary_tag() {
if let Some(picture) = tag.pictures().iter().find(|pic| {
pic.pic_type() == PictureType::CoverFront
|| pic.pic_type() == PictureType::CoverBack
}) {
if let Ok(img) = image::load_from_memory(picture.data()) {
let rgba = img.into_rgba8();
let (width, height) = rgba.dimensions();
let buffer = rgba.into_vec();
return Some((buffer, width, height));
}
}
}
}
None
}