Forem

Ojalla
Ojalla

Posted on

I created a CLI-Music Player in Rust!

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:

  1. clap – For command-line argument parsing.
  2. colored – For colored terminal output.
  3. rodio – For audio playback.
  4. ctrlc – To handle Ctrl+C for graceful exit.

To install them, run:

cargo add clap colored rodio ctrlc
Enter fullscreen mode Exit fullscreen mode

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),
        )
}
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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>,
}
Enter fullscreen mode Exit fullscreen mode

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(())
}
Enter fullscreen mode Exit fullscreen mode

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())
    }
}
Enter fullscreen mode Exit fullscreen mode

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()),
}
Enter fullscreen mode Exit fullscreen mode

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!();
    }
}
Enter fullscreen mode Exit fullscreen mode

Running the Application

  1. Clone the repository:
git clone https://github.com/Parado-xy/rust-cli-music-player
cd rust-cli-music-player
Enter fullscreen mode Exit fullscreen mode
  1. Run the application with a music directory:
cargo run -- --dir /path/to/music
Enter fullscreen mode Exit fullscreen mode
  1. 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
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
silviaodwyer profile image
Silvia O'Dwyer

This is a really cool project, wow!

Collapse
 
paradoxy profile image
Ojalla

Thanks 🙏

Collapse
 
igadii profile image
Idris Gadi

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.