I finished my last post with having a fully parsed mod file in memory and ready to be played. Before I start working on the code that handles the mod playing I need to get some important building blocks in place.
- Audio. Rust does not come with audio capabilities so I need to add crate that provides audio streaming capability
- Threads. In order to stream the music while letting other processing take place the player needs to run on its own thread. The main thread should still be able to control the player thread which means I need to be able to create threads and pass data between them.
By the end of this post I want to get the code into a state where the main thread lets the user pick instruments from the mod file and play them back on the audio thread.
Crates
Rust uses crates to add new functionality to programs. To access the functionality provided by a crate we need to add it as a dependency to the
.toml
file and add extern crate
to the .rs
file that uses functionality from the crate.
So far I haven't really looked at
.toml
files. It is just something automatically created by cargo when I create a new project. To add a dependency I need to create a new section called [dependencies]
which has a list of dependency names and the version numbers.
Dependencies often have their own dependencies. Rust will automatically include any sub-dependencies when it builds the project.
In my case I decided to use the cpal crate for audio streaming. To add it to my project I added the following lines to the
.toml
file[dependencies]
cpal = "0.8.2"
Next time I build the project, cargo will download and compile the cpal crate and any crates that it depends on. Rust will also keep track of which version(s) it has downloaded so it will only download the dependencies again if the version is explicitly changed.
cpal and closures
I decided to use cpal because it provides the right level of access to the audio hardware, has some good documentation and seems to be widely used and is still supported.
The cpal documentation gives a pretty good overview of how to setup streaming audio. Using their example as a starting point I created the following code to stream silence.
fn setup_stream( ) { let device = cpal::default_output_device().expect("Failed to get default output device"); let format = device.default_output_format().expect("Failed to get default output format"); let event_loop = cpal::EventLoop::new(); let stream_id = event_loop.build_output_stream(&device, &format).unwrap(); event_loop.play_stream(stream_id.clone()); event_loop.run(move |_, data| { match data { cpal::StreamData::Output { buffer: cpal::UnknownTypeOutputBuffer::F32(mut buffer) } => { for sample in buffer.chunks_mut(format.channels as usize) { sample[0] = 0.0; sample[1] = 0.0; } } }
The first half creates a device, picks a format and creates and starts an output stream. The interesting part is the call to
event_loop.run
, which transfer control of the current thread to the cpal streaming code that will run indefinitely. The closure passed in as an argument is called whenever it needs a new piece of audio to play back.
Closures in Rust are like anonymous functions that can take ownership of their immediate environment.
Closures are defined using the syntax
|arg| code
where arg
is zero or more arguments and code
is the code that is executed when the closure is called. Optionally, the closure definition can be prefixed by a move
that transfers ownership of any used variables to the closure.
In our case the closure takes two arguments
_
and _data
. We define a two argument closure because that is what cpal expects to call. We don't care about the first argument, a stream id so the code uses _
which is a special variable name prefix in Rust to indicate that the variable is not used ( and thus shouldn't generate and unused variable warning).
The second argument is the data buffer that should receive the sound samples. The
match
syntax is used the extract an output buffer of type f32
. For now, I will only deal with f32
sound buffers to keep the code simple. The inner loop copies the sound into the buffer. (The code assumes we are dealing with a stereo buffer. If that is not the case the code will panic)
The above code works but will block forever because the call to
event_loop.run
never returns.threads and message channels
Creating a thread in Rust is simple. The call
thread::spawn( closure )
will create a new thread and start its execution at the given closure.
I can modify the earlier code to wrap the
event_looop.run
inside a thread closure to let it run in its own thread. Both closures are move
closures so the thread will take ownership of any variables that are used in the event loop.thread::spawn( move || { event_loop.run(move |_, data| { //... } }
Now I have two threads; the main thread and the audio thread created specifically to run the audio event loop.
I want to able to control what the audio stream is playing from the main thread so they need some way of exchanging data. Rust's
std::sync
library has support for message passing between threads. The messages are passed along a channel with a transmitter and receiver.
A channel is created with
let (tx,rx) = mpsc::channel();
where
tx
is the transmitter and rx
is the receiver. Because I want the main thread to send messages to the audio thread I will return tx
as a return value from the setup_stream
function.
Before I can declare the return type of
setup_stream
I need to define the type of messages that will travel on the channel.enum PlayerCommand{ PlayInstrument{ index : u8 } }
Now I can change the call signature of
setup_stream
to indicate that it returns a PlayerCommand channel sender;fn setup_stream( ) -> mpsc::Sender<PlayerCommand> {
The player thread needs to handle commands when it receives them. So for each iteration of audio streaming I want to check if there is a new message and process it. The channel Receiver has a non-blocking
try_recv()
method that will return a Result
that is Ok
if a new message was available. The first bit of code in the event_loop
closure is now;let message = rx.try_recv(); if message.is_ok() { match message.unwrap() { PlayerCommand::PlayInstrument{ index } => { instrument_number = index; instrument_pos = 0; } }; }
The code above is pretty straightforward. It checks if a new message is available and then uses
match
to handle it. instrument_number
and instrument_pos
are used to track current playing instrument and the playing position of the instument. Both of the variables are defined in the scope setup_stream
but because both surrounding closures are marked as move
the thread owns them.
The main thread can now store the sender and use it to send messages
let tx = setup_stream(); loop{ // loop for picking instrument // ... tx.send(PlayerCommand::PlayInstrument{ index : instrument } ); }
Sharing data between threads
I can now send commands to the player thread but it also needs to have access to the mod data.
If I try to pass a
&Song
to the thread I get complaints from the compiler about it needing a static lifetime. The problem is that the compiler does not know how long the player thread runs for and it could potentially run longer than the original Song
object exists for. This would leave the player thread with an invalid reference.
I could fix the issue by doing what the compiler suggests and give
&Song
argument a static lifetime mofifier but this is not what I really want. I potentially do want to change the song for another song at some later point in the program and free up the resoures taken by the first song. I need smart pointers.
Rust comes with a range of different smart pointers that are designed for different use cases. The one I need here is
Arc<>
or Atomic Reference Counted pointers. It is safe to use between threads because it is atomic and thus cant be left in an undefined state even if two thread try to access it at the exactly same time. Because it is reference counted it does not need to worry about setting life times, once the last reference to Ar<>
goes away,it is automatically destroyed.
Because
Arc<>
is reference counted it uses the method clone
to create a new instances of itself. Calling clone
does not make a copy of the underlying data but merely increments the reference counter by one and hands back a new reference to the same underlying data.
A new instance of
Arc<>
is created using sync::Arc::new( data )
so the lines for creating the song and setting up audio thread become.let song = sync::Arc::new( read_mod_file("AXELF.MOD") ); let tx = setup_stream(song.clone());
I also need to make the matching change to
setup_stream
so it accepts a Arc
fn setup_stream( song : sync::Arc<Song> ) -> mpsc::Sender<PlayerCommand> {
Playing an instrument
I can now rewrite the streaming code to play out the selected instrument from the mod file.
// handle messages like before... match data { cpal::StreamData::Output { buffer: cpal::UnknownTypeOutputBuffer::F32(mut buffer) } => { for sample in buffer.chunks_mut(format.channels as usize) { let mut sound_sample = 0.0; if instrument_number > 0 { let the_sample : &Sample = ¤t_song.samples[ instrument_number as usize]; sound_sample = the_sample.samples[ instrument_pos as usize ] as f32/ 256.0; instrument_pos += 1; if instrument_pos >= the_sample.size { instrument_number = 0; } } sample[0] = sound_sample; sample[1] = sound_sample; } } _ => (), }
This is all pretty standard Rust code. Initially the
sound_sample
is set to zero but if the instrument_number
is non-zero we play play keep playing its sample data until we reach the end and switch back to instrument zero to stop playing.
The code now allows us to playback individual sounds samples from the mod file. This gives us the first audible confirmation that the code is doing the right thing.
In the next post I want to play back the note data so I can start hearing the actual mod music.
All the code for the current article is at https://github.com/janiorca/articles/tree/master/mod_player-3
Stafaband combines two powerful methods for downloading new music: peer-to-peer (P2P) torrent sharing, and cloud-based downloads.
ReplyDeleteHarrah's Kansas City Casino & Hotel | JSH Hub
ReplyDeleteHotel 청주 출장샵 details. This luxurious hotel is set in the heart 용인 출장안마 of 전주 출장안마 downtown Kansas City with a 4-star accommodation in 경상북도 출장샵 Harrah's Kansas City, MO. This luxury hotel is 고양 출장마사지 set in the heart of downtown