如何在Rust编译时嵌入目录下所有文件(含二级目录)并实现跨平台无依赖的MP3随机播放功能
如何在Rust编译时嵌入目录下所有文件(含二级目录)并实现跨平台无依赖的MP3随机播放功能
嗨,我来帮你搞定这个需求!你现在的代码只能播放单个嵌入的MP3,要实现多级目录MP3的编译时嵌入+随机播放,咱们可以分三步走:用build.rs批量嵌入文件、构建文件名到内容的映射、实现随机循环播放逻辑。下面是具体步骤和代码:
第一步:添加必要依赖
先在Cargo.toml里补充需要的库:
[package] name = "mp3_player" version = "0.1.0" edition = "2021" [dependencies] rodio = "0.17" rand = "0.8" walkdir = "2" # 用于build.rs里遍历目录 [build-dependencies] walkdir = "2"
第二步:编写build.rs实现批量文件嵌入
在项目根目录创建build.rs脚本,它会在编译前遍历你的MP3目录(比如./music,你可以改成自己的目录路径),自动生成包含所有文件嵌入代码的embedded_mp3s.rs文件:
use std::fs::File; use std::io::Write; use std::path::{Path, PathBuf}; use walkdir::WalkDir; fn main() { // 要遍历的MP3目录,改成你自己的路径 let music_dir = "./music"; // 生成的代码文件路径 let out_file = Path::new(&std::env::var("OUT_DIR").unwrap()).join("embedded_mp3s.rs"); let mut file = File::create(out_file).unwrap(); // 写入HashMap的定义和初始化代码 writeln!(file, "use std::collections::HashMap;").unwrap(); writeln!(file, "pub fn get_embedded_mp3s() -> HashMap<String, &'static [u8]> {{").unwrap(); writeln!(file, " let mut map = HashMap::new();").unwrap(); // 遍历目录下所有.mp3文件(包含二级目录) for entry in WalkDir::new(music_dir) .into_iter() .filter_map(|e| e.ok()) .filter(|e| e.file_type().is_file()) .filter(|e| { e.path() .extension() .map(|ext| ext.to_ascii_lowercase() == "mp3") .unwrap_or(false) }) { let file_path = entry.path(); // 获取相对路径作为HashMap的键(避免文件名重复的问题) let relative_path = file_path .strip_prefix(music_dir) .unwrap() .to_str() .unwrap(); // 把文件路径转成适合include_bytes的格式(处理Windows路径分隔符) let include_path = file_path.to_str().unwrap().replace("\\", "/"); // 写入HashMap的插入语句 writeln!( file, " map.insert(\"{}\".to_string(), include_bytes!(\"{}\"));", relative_path, include_path ) .unwrap(); } writeln!(file, " map").unwrap(); writeln!(file, "}}").unwrap(); // 告诉Cargo目录变化时重新编译 println!("cargo:rerun-if-changed={}", music_dir); }
这个脚本会遍历指定目录下所有MP3文件,生成一个get_embedded_mp3s函数,返回一个HashMap<String, &'static [u8]>,键是文件的相对路径(比如subdir/song1.mp3),值是嵌入的文件内容。
第三步:编写主程序实现随机播放
修改你的src/main.rs,引入生成的代码,然后实现随机循环播放:
use rand::seq::IteratorRandom; use rodio::{Decoder, OutputStream, source::Source}; use std::collections::HashMap; use std::io::Cursor; // 引入build.rs生成的代码 include!(concat!(env!("OUT_DIR"), "/embedded_mp3s.rs")); fn main() -> Result<(), Box<dyn std::error::Error>> { // 获取所有嵌入的MP3文件 let mp3_map: HashMap<String, &[u8]> = get_embedded_mp3s(); if mp3_map.is_empty() { eprintln!("Error: No MP3 files found in the directory!"); return Ok(()); } // 初始化音频输出流 let (_stream, stream_handle) = OutputStream::try_default()?; let mut rng = rand::thread_rng(); println!("Playing random MP3s... Press Ctrl+C to stop."); loop { // 随机选择一个MP3文件 let (file_name, file_data) = mp3_map.iter().choose(&mut rng).unwrap(); println!("Now playing: {}", file_name); // 解码MP3并播放 match Decoder::new_mp3(Cursor::new(file_data)) { Ok(mut source) => { // 播放音频,等待播放完成(不用固定sleep) stream_handle.play_raw(source.convert_samples())?; while source.next().is_some() { std::thread::sleep(std::time::Duration::from_millis(100)); } } Err(e) => { eprintln!("Failed to decode {}: {}", file_name, e); } } } }
关键说明:
- 编译时嵌入:通过
build.rs批量处理多级目录,避免手动写无数个include_bytes,而且目录内容变化时Cargo会自动重新编译。 - 跨平台支持:
rodio本身是无依赖的音频库,walkdir支持跨平台目录遍历,整个程序不需要安装外部播放器。 - 随机播放逻辑:用
rand库的choose方法随机选文件,播放时等待音频源结束(而不是固定休眠时间),确保一首歌放完再换下一首。 - 文件名处理:用相对路径作为HashMap的键,避免二级目录里文件名重复导致覆盖的问题。
你只需要把你的MP3文件放到指定的目录(比如./music),然后运行cargo run就能看到效果啦!
备注:内容来源于stack exchange,提问作者Laurent FAVOLE
相关产品推荐
相关产品推荐

