I wrote a command-line YouTube-backend music player in Rust, called surge (SOURCE).
The reddit post is here.
The key components are:
- YouTube API to find songs
- youtube-dl to download them
- libmpv to play them
The motivation behind surge is twofold - I’m trying to minimize my use of the mouse, and I wanted readline features in a music player, e.g. ctrl-r
to reverse search an album I had searched before.
What I wanted to achieve
As described in the linked Reddit post, my application was designed like this:
loop {
// receive user events - play song, queue song, search song
line => cmd.execute(line),
}
cmd
is a monolithic struct which contains the libmpv
audio player and the actual session information - latest search results, now playing, etc. cmd
knows how to understand and execute commands.
My desire was to implement a continuous-playback feature. Naively, this would be a thread which behaved like a human constantly queueing related songs.
This seemed easy at first because I had the building blocks:
- I had the
related
command to load related songs - I had the
queue
command to queue songs from the loaded list
To put it very simply, I needed a thread which could send ["related", "queue"]
to cmd
in a loop.
However, I had no idea where to put/spawn this robot
thread within the readline loop.
Enter the mpsc
The mpsc is:
Multi-producer, single-consumer FIFO queue communication primitives. This module provides message-based communication over channels
The key suggestion I received from Reddit was to separate my application logic into the following:
- A command thread to receive commands
- A readline thread to take human input and send commands to the command thread
- A robot thread to send the automated continuous playback commands to the command thread
let (tx, rx) = channel();
let tx_ = tx.clone();
let mut threads = Vec::with_capacity(3);
threads.push(command_thread(..., &rx, ...))
threads.push(readline_thread(..., &tx, ...))
threads.push(robot_thread(..., &tx_, ...))
for t in threads {
if let Ok(t) = t {
t.join().expect("Couldn't join thread");
}
}
Note that the Sender is clonable to send it to other threads.
AtomicBool to control the robot thread
Another design choice arose when I had to find a way for the human readline thread to control the robot thread.
At first I was ready to use the mpsc
again - a new set of senders and receivers. However, since this is a hobby project for learning, I wanted to try a different solution.
Enter the AtomicBool
.
N.B. I didn’t magically know about the AtomicBool - I started with a Mutex<bool>
and clippy told me to use an AtomicBool instead. I can’t overstate how helpful clippy is to learn idiomatic Rust.
An AtomicBool is a thread-safe boolean:
//readline thread toggling the ROBOT_SHOULD_RUN AtomicBool
ROBOT_SHOULD_RUN.store(
!ROBOT_SHOULD_RUN.load(Ordering::SeqCst),
Ordering::SeqCst,
);
//robot thread checking if it should run
if !ROBOT_SHOULD_RUN.load(Ordering::SeqCst) {
thread::sleep(time::Duration::from_secs(1));
}
SeqCst?
I don’t know either. Somebody at work told me “just use SeqCst - sequential consistency - and once you’re comfortable with that, I’ll explain the other options to you”.
The Ordering
doc explains the various consistency options. This is kinda scary stuff for me to read so for now I haven’t dug too deep.
Conclusion
Ask for help. The Rust subreddit community is helpful, and when you’re stuck bashing your head against something, it’s worth it to take a step back and ask for the opinions of others.