Building a Command-Line Music Player in Rust
Introduction
I built a Rust CLI Music Player that allows users to play, pause, stop, and manage their music files directly from the terminal. It supports basic playback controls, volume adjustment, and song listing.
This walkthrough will cover:
- The dependencies used
- How the music player was implemented
- How to run the application
- A guide on available commands
Dependencies
I used the following Rust crates:
- clap – For command-line argument parsing.
- colored – For colored terminal output.
- rodio – For audio playback.
-
ctrlc – To handle
Ctrl+C
for graceful exit.
To install them, run:
cargo add clap colored rodio ctrlc
How the CLI Music Player Works
The music player follows a command-line workflow where users can:
- Load songs from a specified directory.
- List available songs.
- Play, pause, resume, stop playback.
- Adjust volume.
1. Command-Line Interface (CLI) Configuration
The application uses clap
to handle command-line arguments. Users must specify a music directory:
fn cli_config() -> Command {
Command::new("musicplayer")
.version("0.1.0")
.author("ojalla")
.about("Command-line music player")
.arg(
Arg::new("music-dir")
.short('d')
.long("dir")
.value_name("DIRECTORY")
.help("Sets the music directory")
.required_unless_present("how-to"),
)
.arg(
Arg::new("how-to")
.long("how-to")
.help("Shows operation commands and how to use the application.")
.action(clap::ArgAction::SetTrue),
)
}
This ensures that users provide a valid music directory or request help using --how-to
.
2. Handling User Input
The player reads user commands in a loop:
fn input() -> String {
let mut user_input = String::new();
print!("{}", "musicplayer> ".cyan().bold());
io::stdout().flush().expect("Failed To Flush Output");
io::stdin()
.read_line(&mut user_input)
.expect("Error Getting User Input");
user_input.trim().to_string()
}
This function displays a colored prompt and waits for user input.
3. Implementing the Music Player
I created a CliPlayer
struct to store the player’s state:
struct CliPlayer {
sink: rodio::Sink,
stream: rodio::OutputStream,
stream_handle: OutputStreamHandle,
is_playing: bool,
is_paused: bool,
main_dir: Option<String>,
current_file: Option<String>,
last_input: Option<String>,
available_songs: Option<HashMap<i32, DirEntry>>,
start_time: Option<Instant>,
}
This struct maintains:
- The audio sink and output stream (
rodio
). - The directory containing music files.
- The current playing song and status.
4. Loading Songs from a Directory
When the application starts, it loads music files from the given directory:
fn load_songs(&mut self) -> io::Result<()> {
let mut index = 1;
if let Some(dir) = &self.main_dir {
if let Some(sound_map) = &mut self.available_songs {
for entry in read_dir(dir)? {
let entry = entry?;
if entry.path().is_file() {
sound_map.insert(index, entry);
index += 1;
}
}
}
}
Ok(())
}
This function reads all files in the specified directory and maps them to numbers for easy selection.
5. Playing a Song
To play a song, the application reads the file, decodes it, and plays it using rodio
:
pub fn play(&mut self, sound_index: i32) -> Result<(), Box<dyn std::error::Error>> {
if self.is_playing {
self.sink.stop();
self.sink = Sink::try_new(&self.stream_handle)?;
}
if let Some(sound_map) = &self.available_songs {
if let Some(song) = sound_map.get(&sound_index) {
let file = BufReader::new(File::open(song.path())?);
let source = Decoder::new(file)?;
self.sink.set_volume(1.0);
self.sink.append(source.convert_samples::<f32>());
self.is_playing = true;
self.is_paused = false;
self.current_file = Some(song.file_name().to_string_lossy().to_string());
self.start_time = Some(Instant::now());
println!("{}: Playing {}", "Now playing".green().bold(), self.current_file.as_ref().unwrap().blue());
Ok(())
} else {
Err(format!("{}: Invalid song index", "Error".red()).into())
}
} else {
Err("No songs available".into())
}
}
This method:
- Stops any currently playing track.
- Loads and decodes the selected song.
- Starts playback.
6. Implementing Playback Controls
The player handles commands like play, pause, resume, stop, and volume adjustment:
match command {
InputCommands::Play => { /* Calls play() */ }
InputCommands::Pause => {
if self.is_playing {
self.sink.pause();
self.is_paused = true;
println!("{}: Playback paused", "Info".yellow());
}
}
InputCommands::Resume => {
if self.is_paused {
self.sink.play();
self.is_paused = false;
self.is_playing = true;
println!("{}: Playback resumed", "Info".green());
}
}
InputCommands::Stop => {
if self.is_playing {
self.sink.stop();
self.is_playing = false;
println!("{}: Playback stopped", "Info".red());
}
}
InputCommands::Volume(vol) => {
if (0.0..=1.0).contains(&vol) {
self.sink.set_volume(vol);
println!("{}: Volume set to {:.1}", "Success".green(), vol);
} else {
println!("{}: Volume must be 0.0 to 1.0", "Error".red());
}
}
_ => println!("{}: Invalid command", "Error".red()),
}
These functions control playback using rodio::Sink
methods like .play()
, .pause()
, .stop()
, and .set_volume()
.
7. Displaying Available Songs
Users can list all available songs:
pub fn list(&self) {
if let Some(sound_map) = &self.available_songs {
println!("\n{}", "Available Songs:".green().bold());
println!("{}", "-------------------------------".green());
println!("{:<6} {:<}", "Index".to_string().bold(), "Filename".to_string().bold());
for (index, entry) in sound_map {
let filename = entry.file_name().to_string_lossy();
if let Some(current) = &self.current_file {
if filename == *current {
println!("{:<6} {:<} {}", index.to_string().green(), filename.green(), "▶".green());
} else {
println!("{:<6} {:<}", index, filename);
}
} else {
println!("{:<6} {:<}", index, filename);
}
}
println!();
}
}
Running the Application
- Clone the repository:
git clone https://github.com/Parado-xy/rust-cli-music-player
cd rust-cli-music-player
- Run the application with a music directory:
cargo run -- --dir /path/to/music
- Use the available commands:
play <number> # Play a song
pause # Pause playback
resume # Resume playback
stop # Stop playback
volume <0.0-1.0> # Adjust volume
list # Show available songs
status # Show playback status
exit # Quit the player
Conclusion
This Rust CLI Music Player is a simple but powerful terminal-based music player. It utilizes rodio
for audio playback, clap
for argument parsing, and colored
for improved UI. Future improvements could include playlist support and file format filtering.
Top comments (3)
This is a really cool project, wow!
Thanks 🙏
This looks cool, a small feedback would be to replace
--how-to
command with--help
or-h
(short format), as most CLI users will intuitively use the help command. This could be a major UX improvement.