用【rust】实现命令行音乐播放器
一、项目简介
本项目实现了一个基于 Rust 的 命令行音乐播放器 —— RustTune。 它支持播放本地 .mp3 / .flac / .wav 文件, 在终端中实时显示播放信息,并通过键盘快捷键进行控制。
主要特性:
- 支持多格式音乐播放(mp3、flac、wav)
- 实时暂停 / 恢复播放(无延迟)
- 切换下一首歌曲
- 自动扫描
music/目录 - 终端界面(TUI)显示当前播放状态
这不仅是一个好玩的命令行小工具, 更是一次完整练习 Rust 多线程、音频流处理、消息通信、终端 UI 控制 的绝佳项目。
二、项目结构
rusttune/
├── Cargo.toml
└── src/├── main.rs├── player.rs├── tui_app.rs└── utils.rs
三、依赖配置(Cargo.toml)
[package]
name = "rusttune"
version = "0.1.0"
edition = "2021"[dependencies]
rodio = "0.17" # 音频播放
walkdir = "2.5" # 扫描文件目录
crossterm = "0.26" # 终端事件控制
tui = "0.19" # 终端 UI 渲染
chrono = "0.4"
rand = "0.8"
crossbeam-channel = "0.5" # 线程间通信
四、核心实现思路
播放器的架构由两部分组成:
- 播放引擎(player.rs)
-
- 管理播放线程;
- 使用
rodio::Sink播放音频; - 通过
crossbeam-channel接收控制命令(暂停、下一首、退出)。
- 终端界面(tui_app.rs)
-
- 用
tui-rs绘制播放界面; - 用
crossterm监听键盘事件; - 发送用户命令到播放线程。
- 用
两部分通过共享状态和消息通道协同工作,实现即时响应。
五、完整源码
src/main.rs
mod player;
mod tui_app;
mod utils;use std::sync::{Arc, Mutex};
use player::Player;
use tui_app::run_tui;fn main() {let player = Arc::new(Mutex::new(Player::new("music")));run_tui(player);
}
src/player.rs
use rodio::{Decoder, OutputStream, Sink};
use std::{fs::File,io::BufReader,path::PathBuf,thread,time::Duration,
};
use crossbeam_channel::{unbounded, Sender, Receiver};
use crate::utils::scan_music_dir;pub enum PlayerCommand {TogglePause,Next,Exit,
}pub struct Player {pub playlist: Vec<PathBuf>,pub current: usize,pub cmd_tx: Sender<PlayerCommand>,
}impl Player {pub fn new(folder: &str) -> Self {let playlist = scan_music_dir(folder);let (tx, rx) = unbounded();let player = Self { playlist, current: 0, cmd_tx: tx.clone() };
let playlist_clone = player.playlist.clone();thread::spawn(move || player_loop(playlist_clone, rx));player}
pub fn toggle_pause(&self) {let _ = self.cmd_tx.send(PlayerCommand::TogglePause);}
pub fn skip(&self) {let _ = self.cmd_tx.send(PlayerCommand::Next);}
pub fn stop(&self) {let _ = self.cmd_tx.send(PlayerCommand::Exit);}
}fn player_loop(playlist: Vec<PathBuf>, rx: Receiver<PlayerCommand>) {if playlist.is_empty() {println!("⚠️ 没有找到音乐文件,请放到 ./music 文件夹中");return;}
let (stream, handle) = OutputStream::try_default().unwrap();let mut sink = Sink::try_new(&handle).unwrap();let mut current = 0;
loop {let path = &playlist[current];println!("▶️ 正在播放: {}", path.file_name().unwrap().to_string_lossy());
let file = BufReader::new(File::open(path).unwrap());let source = Decoder::new(file).unwrap();sink = Sink::try_new(&handle).unwrap();sink.append(source);sink.play();
loop {if let Ok(cmd) = rx.try_recv() {match cmd {PlayerCommand::TogglePause => {if sink.is_paused() {println!("▶️ 继续播放");sink.play();} else {println!("⏸ 暂停播放");sink.pause();}}PlayerCommand::Next => {println!("⏭ 下一首");sink.stop();current = (current + 1) % playlist.len();break;}PlayerCommand::Exit => {println!("退出播放器");sink.stop();return;}}}
if sink.empty() {current = (current + 1) % playlist.len();break;}thread::sleep(Duration::from_millis(200));}}
}
src/tui_app.rs
use crate::player::Player;
use std::io::{self, stdout};
use std::sync::{Arc, Mutex};
use crossterm::{event::{self, Event, KeyCode},terminal::{enable_raw_mode, disable_raw_mode, Clear, ClearType},execute,
};
use tui::{backend::CrosstermBackend,Terminal,widgets::{Block, Borders, Paragraph},layout::{Layout, Constraint, Direction},text::Span,
};pub fn run_tui(player: Arc<Mutex<Player>>) {enable_raw_mode().unwrap();execute!(stdout(), Clear(ClearType::All)).unwrap();let backend = CrosstermBackend::new(io::stdout());let mut terminal = Terminal::new(backend).unwrap();
loop {terminal.draw(|f| {let size = f.size();let chunks = Layout::default().direction(Direction::Vertical).constraints([Constraint::Percentage(100)].as_ref()).split(size);
let para = Paragraph::new(Span::raw("🎶 RustTune 播放中(Space暂停/继续,n下一首,Esc退出)",)).block(Block::default().borders(Borders::ALL).title("RustTune"));f.render_widget(para, chunks[0]);}).unwrap();
if event::poll(std::time::Duration::from_millis(200)).unwrap() {if let Event::Key(key) = event::read().unwrap() {let p = player.lock().unwrap();match key.code {KeyCode::Char(' ') => p.toggle_pause(),KeyCode::Char('n') => p.skip(),KeyCode::Esc => {p.stop();break;}_ => {}}}}}
disable_raw_mode().unwrap();
}
src/utils.rs
use std::path::PathBuf;
use walkdir::WalkDir;pub fn scan_music_dir(folder: &str) -> Vec<PathBuf> {let mut files = Vec::new();for entry in WalkDir::new(folder).into_iter().filter_map(|e| e.ok()) {if let Some(ext) = entry.path().extension() {if ext == "mp3" || ext == "flac" || ext == "wav" {files.push(entry.path().to_path_buf());}}}files
}
六、运行项目
- 在项目根目录创建一个
music/文件夹 放入一些.mp3、.flac或.wav文件:
rusttune/
├── music/
│ ├── song1.mp3
│ ├── song2.flac
│ └── song3.wav
- 运行项目:
cargo run- 进入命令行界面后使用快捷键控制:
| 键 | 功能 |
| Space | 暂停 / 继续 |
| n | 下一首 |
| Esc | 退出播放器 |


该项目最终将落地为一款功能完备的命令行音乐播放器,以简洁直观的交互形态,全面覆盖音乐播放核心功能。项目深度发挥 Rust 语言的技术优势,既凭借其内存安全特性筑牢程序稳定性基石,又以零成本抽象与高效编译能力保障流畅的播放体验,最终呈现出一款兼具实用性与技术质感的轻量工具,充分彰显了 Rust 编程在性能与安全性上的卓越平衡。
想了解更多关于Rust语言的知识及应用,可前往华为开放原子旋武开源社区(https://xuanwu.openatom.cn/),了解更多资讯~
