Sunday, 10 March 2019

Mod player in Rust - part 5. Rust modules

For this post I am going to change my the style slightly. Instead of going through all the new code I am going to focus on the bits that were more interesting or challenging.
The mod player is now able to play most of the files that I throw at if although there are still some files that sound wrong or are missing some effects. Below a clip of the player playing an old school 90s demo tune.

Rust module basics

Getting my head around Rust's module system was a bit trickier than I thought. I am so used to the C-style model where each file is a translation unit that is compiled into its own .obj file which are then linked together that anything else seemed just wrong.
My initial mistake was to think that the key word mod was used to import other modules into a Rust file. This appeared to work as long as I was using it to 'import' modules into the main.rs file. As soon as I started using it in other .rs. files I got loads of unexpected errors.
My key to understanding the module system was to realize that modules form a tree which is rooted in the main.rs file. The key word mod is used to declare other sub-modules that are used in the current module. I split my project into three modules;
  • main the root module for the starting point and orchestrating all the calls into the library
  • mod_player for the code that parses and streams the audio mod file. This module has one sub-module of it's own;
    • textout that can be used to print info about the module and the players current stat
When the file main.rs has the following statement;
mod mod_player
it tells the compiler that the root module has a sub module mod_player. The compiler will locate this by looking for a file called mod_player.rs in the root folder or by looking for a file mod.rs in the folder /mod_player.
Because text_out is a sub-module of mod_player I need to declare it inside the file mod_player.rswith the line.
pub mod textout
In this case I have added the key word pub because I want anyone using mod_player to also be able to access textout. Without the pub keyword the textout module would be a private module only visible to mod_player.
I can now use public functions and structures from mod_player and textout by specifying the path to the items I want to use. To load an audio mod file using the functionality in mod_player I use,
let song = mod_player::read_mod_file("song_name.MOD");
and to print out some info about it using the textout sub-module I can write
mod_player::textout::print_song_info( &song );
Both functions read_mod_file and print_song_info have to be declared public with a pub modifier to visible. It is not enough fro the containing modules to be public.

Extending impl in sub-modules

For my library I wanted all the printing related functionality to be in the texout module while still preserving the clear relationship between data and the methods. Ideally, I could just extend the existing impl blocks.
Fortunately Rust lets me do exactly that by declaring a new impl block and adding the new methods. The only slight complications is in how to identify the structure that the impl block refers to. There are three equivalent ways of declaring the extension;
  • declare the full path from the crate root using the crate path specifier
impl crate::mod_player::Sample{
  • declare the the relative path using super path specifier
impl super::Sample{
  • use use to bring the structure into the current module
use super::Sample;      // at top of the file
...
impl Sample{
The three versions do the same thing; bring the Sample from the parent module into the scope of the current module.
I ended up using the last method as it brings the Sample structure into scope everywhere within the sub-module and means I don't need to use path specifiers elsewhere. The nice thing about the using use is that one statement can bring multiple items into the module. The line
use super::{Note,Song,Effect};
brings NoteSong and Effect into the current module.

Exporting to WAV

One of the challenges with debugging an audio player is that debugging consist of listening to the playback and trying to spot anything that doesn't sound right and then trying to figure out where exactly the anomaly took place. Writing the sound out into a wav file makes it easier to work out the exact play time.
The hound library provides exactly the functionality I needed for writing out WAV files. I am still impressed by how easy it is to add crates to a project. Once I decided I wanted the hound crate it took comfortably less than half an hour to get WAV export working ( and most of that was spent on modifying my code ).
Because exporting to WAV is not a part of mod_playing functionality I put the functionality into the root module in main.rs

Searching tuple arrays

I changed the way notes were printed from periods into actual notes. That meant having a function that takes a period value and converts it into a string. My first instinct was to use the match syntax but this becomes very ugly and unnecessarily verbose.
fn note_string( period : u32 ) -> &str{
    return match val{
        113 => "B-5", 
        120 => "A-5", 
        ...
    }
}
With 60 notes this becomes a very ugly function. There is also the nagging feeling that the performance of this code will be pretty suboptimal. ( Granted, performance is not really an issue in this instance but the code is representative of situations where the performance would matter so I think its worth investigating)
What I really want is a map that is statically initialized. There doesn't seem to be a Rust native way of initializing a map. There is the lazy_static crate which would let me create the a static map but I want to learn to use vanilla Rust efficiently before starting to use too many other crates.
While Rust doesn't let me declare static maps I can declare static arrays of tuples and do a binary search on it. I can declare a static array of all of the (period, note) pairs as;
static NOTE_FREQUENCY_STRINGS : [ (u32, &str ); 60 ]= [
( 57,  "B-6" ), ( 60,  "A#6" ), ( 64,  "A-6" ),( 67,  "G#6" ), ( 71,  "G-6" ), ( 76,  "F#6" ), ( 80,  "F-6" ), ( 85 , "E-6" ), ( 90,  "D#6" ), ( 95 , "D-6" ), ( 101, "C#6" ), ( 107, "C-6"), 
...
I can use the slice function binary_search_by to search through the tuple array.
    fn note_string( &self ) -> &str{
        let idx = NOTE_FREQUENCY_STRINGS.binary_search_by( | val | val.0.cmp( &self.period) );
        if idx.is_ok() {
            return  NOTE_FREQUENCY_STRINGS[ idx.unwrap() ].1;
        } else { "..." }
    }
Because I am searching through tuples I need to provide the search function with my own comparator function. That is what |val|val.0.cmp( &self.period) declares. For each tuples the comparator function is called, it takes the first part and compares it to the period value. I am using the standard cmp function that returns an Ordering enum that the comparator expects.
I am finding that it is really worth spending time understanding slice and all of its methods.

Casting bug

This was the first time in a long time that I had to deal with a casting bug. One of the effects in a mod file is tone portamento which will change the currently playing tone into the target tone at the specified speed. Handling the effect consists of changing the period counter which controls time between subsequent samples. Increasing or decreasing the period moves the tone lower or higher.
The original code which worked most of the time is below. The two checks are used to constrain the period to a range that is supported by the mod format.
fn change_note( current_period : u32, change : i32 ) -> u32 {
    let mut result : u32 = ( current_period as i32 + change ) as u32;
    if result > 856 {  result = 856;  }
    if result < 113 { result = 113; }
    result
}
The current_period must always a positive number but change can be negative because the period can increase or decrease. The current_period is cast to i32 to make it compatible with change and the result is cast to u32 because that is the type of current_period and also the return type. This worked perfectly for all the mod files I initially tested with but then I came across one that had weird popping sounds.
The code breaks down when change is negative and larger than current_period. So if period is 120 and change is -200 the value of ( current_period as i32 + change ) is -80. When -80 is cast to u32 the result is a very large positive number.
The simple fix was to change the code to;
fn change_note( current_period : u32, change : i32 ) -> u32 {
    let mut result = current_period as i32 + change;
    if result > 856 {  result = 856;  }
    if result < 113 { result = 113; }
    result as u32
}
This lets Rust decide the type of result and only does the casting at the very end. This is not really a Rust specific bug but highlights the fact that by giving you more control, the compiler also give you more ways to shoot yourself in the foot.

Next Steps

All the code so far has been uploaded into https://github.com/janiorca/articles/tree/master/mod_player-5
The next step is to improve the effects coverage as there are still some files that sound wrong ( or won't play at all ). The second step is to turn this into a crate that can be used by other projects.

Sunday, 17 February 2019

Mod player in Rust - part 4. Finally some music

In this post we will finally have some music. Below a quick preview of the player in action. 

In this post I will spend some time discussing how the Amiga sound hardware worked. The original mod file format and how it is played back is intimately linked with the Amiga hardware.
Tracking the play position

The speed at which the mod is played is based on counting vertical blank interrupts ( the interrupt triggered at each hardware screen refresh https://en.wikipedia.org/wiki/Vertical_blank_interrupt ) On PAL systems the VBI frequency is 50hz and on the NTSC systems it is 60hz. The song's playing speed is defined as the number of vertical blanks between two lines in a pattern. ( This means that songs developed on a PAL system will play slightly too fast on an NTSC system. )
Mod files also have an option to specify the playback speed in beats per minute (BPM) but this was not used often because many of the mod songs were intended to be used with demos. Demos often ran all of their code on the vertical blank interrupt but for many values of BPM the player could not have run on the same interrupt, making it much harder to manage the processor time. ( There also seems to be a lot variability between players and mods over how exactly the BPM should be handled ) For now I am going to concentrate on mods that used VBI based speed.
When playing a mod with a VBI based speed on an Amiga, keeping track of the speed was just a matter of checking how many times the interrupt had been called since playing the last line. This is not an option for my Rust player as it does not run on an interrupt. Instead I can use the device sample rate to track theoretical vblanks using the following formula
samplesPerVblank = \frac {deviceSampleRate}{vblankFrequency}
If the playback device has a sample rate of 48000 and I am simulating the playback on a PAL device, the samplesPerVblank is 48000/50 = 960. That means that after every 960 samples it is time to increment the vertical blank counter by one, and if it matches the playback speed it is time to play a new line.
Once we have played all the lines from the current pattern it is time to move to the next pattern.
The PlayerState keeps track of all the above information.
struct PlayerState{
    song_pattern_position: u32,             // where in the pattern table are we currently
    current_line: u32,                      // current position in the pattern
    song_speed: u32,                        // in vblanks
    current_vblank : u32,                   // how many vblanks since last play line
    current_vblank_sample : u32,            // how many device samples have we played for the current 'vblank'
    samples_per_vblank: u32,                // how many device samples per 'vblank'
}

Playing samples

The mod files use frequency shifting to player different notes of the same sound. If the original sound effect was sampled at 10kh, playing it back at double the original speed increases its frequency to 20kh and plays it one octave higher.
On the original Amiga hardware the DMA (Direct Memory Access) is responsible for transferring audio data from memory and sending it to the DAC ( Digital to Analog Converter ). The DMA speed is controlled by setting the number of system clock ticks between samples. On a PAL system each clock tick is 281.937 nanoseconds, thus there are 3,546,895 clock ticks per second ( on a NTSC system there a tick is 279.365 nanoseconds and there are 3,579,545 clock ticks per second).
For hardware design reasons the smallest interval between samples is 123 system clock ticks on a PAL system so this works out as 34.678 micro seconds per sample or about 28.8 khz. (On NTSC systems the minimum clock counts between samples is 124)
The note data in mod files specify the number of system clocks ticks each sample byte is played for before fetching a new one. This is the period in the note data. When the note states a period of 428 it means that means that each sample should be held for 120 microseconds (428 clocks * 0.281397 microsecs per clock = 120 microsecs) yielding a sample rate of about 8kHz. To play the same sample one octave higher a period of 214 would be used.
We can emulate this by behaviour by resampling the sounds at a speed that matches intended playback speed. We just need to map device sample duration into Amiga clock ticks; If the playback device has a sample rate of 48,000 we know that each device sample corresponds to 73.89 clock ticks (3,546,895/48,000 = 73.89). So each time we calculate a new sample we advance the playback position by;
\frac {clockTicksPerSecond}{period*deviceSampleRate} = \frac{clockTicksPerDeviceSample}{period}
To keep track of clockTicksPerDeviceSampleI add an additional variable to the PlayerState;
    clock_ticks_per_device_sample : f32,    // how many amiga hardware clock ticks per device sample
Some of the mod effects are created by varying the channel's period value while the note is playing. This is why the channel stores the period value rather than the value for clockTicksPerDeviceSample/period
In addition to the period. Each channel need to track what it's sample number, position within the sample and playback volume;
struct ChannelInfo {
    sample_num: u8,         // which sample is playing 
    sample_pos: f32,         
    period : f32,           
    volume: f32, 
}

Putting it together

I am going to quickly run through the main blocks of the playing code. This is all pretty straight forward Rust code.
First, I replace the instrument playing logic from previous version of the code with a call to next_sample which returns a tuple that has the left and right samples;
        cpal::StreamData::Output { buffer: cpal::UnknownTypeOutputBuffer::F32(mut buffer) } => {
            for sample in buffer.chunks_mut(format.channels as usize) {
                let ( left, right ) = next_sample(&song, &mut player_state);
                sample[0] = left;
                sample[1] = right;
            }
        }
The above uses destructuring to assign the members of the returned tuple into left and right. Sadly, Rust doesn't seem to allow for destructuring directly into the destination array.
The code for next_sample tracks how many samples it has played for the current vblank and whether enough vblanks have passed to play a new line. The code keeps track of vblanks and lines separately because many of the effects require updates that occur on the vblank.
fn next_sample(song: &Song, player_state: &mut PlayerState) -> (f32, f32) {
    let mut left = 0.0;
    let mut right = 0.0;

    // Have we reached a new vblank
    if player_state.current_vblank_sample >= player_state.samples_per_vblank {
        player_state.current_vblank_sample = 0;

        // Is it time to play a new note line
        if player_state.current_vblank >= player_state.song_speed {
            player_state.current_vblank = 0;
            play_line( song, player_state );
        }
        player_state.current_vblank += 1;
    }
    player_state.current_vblank_sample += 1;
    // .. handle sample play back
The next interesting bit is in play_line which updates the current line and pattern position and plays a note for each channel;
fn play_line(song: &Song, player_state: &mut PlayerState ) {
    // use the pattern table to work out which pattern we are in    
    let pattern_idx = song.pattern_table[player_state.song_pattern_position as usize];
    let pattern = &song.patterns[ pattern_idx as usize];

    pattern.print_line(player_state.current_line as usize);
    let line = &pattern.lines[ player_state.current_line as usize ];
    for channel_number in 0..line.len(){
        play_note(&line[ channel_number as usize ], player_state, channel_number, song);
    }
    player_state.current_line += 1;
    if player_state.current_line >= 64 {
        // end of the pattern
        player_state.song_pattern_position += 1;
        player_state.current_line = 0;
        println!("");
    }
}
The play_note function sets up the new sample ( if one is set ) and handles effects. For now, this code will only handle a minimal set of effects to keep the code size and complexity down. The function needs access to the PlayerState structure instead of just the channel data because some of the effects, like setting the speed, are global.
fn play_note(note: &Note, player_state: &mut PlayerState, channel_num: usize, song: &Song) {
    if note.sample_number > 0 {
        // sample number 0, means that the sample keeps playing. The sample indices starts at one, so subtract 1 to get to 0 based index
        player_state.channels[channel_num].volume = song.samples[(note.sample_number - 1) as usize].volume as f32 / 64.0;    // Get volume from sample
        player_state.channels[channel_num].sample_num = note.sample_number;
        player_state.channels[channel_num].sample_pos = 0.0;
        if note.period != 0 {
            player_state.channels[channel_num].period = note.period as f32;
        }
    }

    match note.effect {
        Effect::SetSpeed{ speed } => {
            player_state.song_speed = speed as u32;
        }
        Effect::SetVolume{ volume } => {
            player_state.channels[channel_num].volume = volume as f32;
        }
        Effect::None => {}
        _ => {
            println!("Unhandled effect" );
        }
    }
}
Finally, the second half of the next_sample performs the sample retrieval and mixing into device channels
for channel_number in 0..player_state.channels.len() {
    let channel_info: &mut ChannelInfo = &mut player_state.channels[channel_number];
    if channel_info.sample_num != 0 {
        let current_sample: &Sample = &song.samples[(channel_info.sample_num - 1) as usize];
        let mut channel_value: f32 = current_sample.samples[(channel_info.sample_pos as u32) as usize] as f32;             
        // max channel vol (64), sample range [ -128,127] scaled to [-1,1] 
        channel_value *= channel_info.volume / (128.0*64);

        // update position and check if we have reached the end of the sample
        channel_info.sample_pos +=  player_state.clock_ticks_per_device_sample / channel_info.period;
        if channel_info.sample_pos >= current_sample.size as f32 {
            channel_info.sample_num = 0;
        }
        let channel_selector = ( channel_number as u8 ) & 0x0003; 
        if channel_selector == 0 || channel_number as u32 == 0 || channel_number == 3 {
            left += channel_value;
        } else {
            right += channel_value;
        }
    }
}
I need to control the range of the sound output from the mixer so it has a range that works with the playback device. As cpal uses the range [-1,1] for devices supporting floating point channels I have decided to use the same range for mixer output.
Mod files only support stereo. The mod channels 0 and 3 should be played on the left channel and 1 and 2 on the right. If there are more channels, the same allocation scheme is used ( 4 and 7 are let, 5 and 6 are right)

Success

The code we have developed so far will play simple mod files that have minimal effects. The mod file I have included ( axelf.mod ) is an example of such a mod file. Most mods make heavy use of effects and cannot be played with this code.
In my next post I will look into supporting more effects so I can support a larger set of mod files. The code is also growing to be quite large so it is time to look at a Rust modules as a better of organizing code.
All the code for the current article is at
https://github.com/janiorca/articles/tree/master/mod_player-4

Mod player in Rust - part 5. Rust modules

For this post I am going to change my the style slightly. Instead of going through all the new code I am going to focus on the bits that w...