My last article finished with reading the pattern tables from the mode files. In this post I want to finish parsing the entire file so we can move onto playing it. First we need to work out where the pattern data is and how long it is.
Checking the format
The pattern data is located after the pattern table and before the sample data ( so far we have only read the sample information but not the actual sound data ) but there are a couple of complications;
- The pattern table might be followed by a format tag which describes how many samples and channels the file contains. If it is a known tag we should just skip it. The oldest versions of the MOD file do not have a format tag. If we dont recognize a tag it is either because the file does not have one and is in the original format or it is a new one that we dont recognize.
- The number of patterns is not described anywhere but the size of the pattern data is worked out as by deducting the size of everything else from the files size. What remains must be pattern data. Because each pattern has 64 notes for each channel and each note (I will get to them) is 4 bytes, the size of one patterns is;
pattern_size = 64 * 4 * num_channels
We can use the the pattern_size to check our assumptions about the mod file; Taking the modulo pattern_size from pattern_data_size should always be zero.
Given the complexities around the format tag I have decided to change the function
identify_num_samples
to get_format
which will attempt to identify the tag and derive the number of channels and samples from it. The new structure and function are below;struct FormatDescription{ num_channels : u32, num_samples : u32, has_tag : bool // Is the format description based on a tag } fn get_format(file_data: &Vec<u8> ) -> FormatDescription { let format_tag = String::from_utf8_lossy(&file_data[1080..1084]); match format_tag.as_ref() { "M.K." | "FLT4" | "M!K!" | "4CHN" => FormatDescription{ num_channels : 4, num_samples : 31, has_tag : true }, _ => FormatDescription{ num_channels : 4, num_samples : 15, has_tag : false } } }
The only new bit of syntax in the above code is the use of
|
to list all the matches that will get the same result.
With the new format description we can rewrite the sample reading loop to use it and skip the tag if we think it is present.
let format = get_format(&file_data); for _sample_num in 0..format.num_samples { //... read the sample info //.. // Skip the tag if one has been identified if format.has_tag { let format_tag = String::from_utf8_lossy(&file_data[offset..(offset + 4)]); offset += 4; }
Before working out the space left for the patterns we need to check how much space is used by the sample data.
let mut total_sample_size = 0; for sample in &mut samples { total_sample_size = total_sample_size + sample.size; }
Now we finally have the information required to work out the pattern size and to check our assumptions about the mod file we are reading.
let total_pattern_size = file_data.len() as u32 - (offset as u32) - total_sample_size; let single_pattern_size = format.num_channels * 4 * 64; let num_patterns = total_pattern_size / single_pattern_size; if total_pattern_size % single_pattern_size != 0 { panic!( "Unrecognized file format. Pattern space does not match expected size") }
The
total_pattern_size
is all the that remains after we deduct what has already been read and what we know will be taken up by the samples.
The
single_pattern_size
is the size of one pattern which depends on the number of channels.
Dividing the two will give us the number of patterns in the file. We finally do a sanity check by taking the modulo and checking that is is zero ( which tells us that there are no bytes that are unaccounted for )
Notes
The basic pattern building block is a note which specifies which sample to play, its period and any effects. The data is packed into 4 bytes in the following manner
byte0 byte1 byte2 byte3
76543210 76543210 76543210 76543210
SSSSPPPP pppppppp ssssEEEE AAAAAAAA
SSSSssss = sample number
PPPPpppppppp = period
EEEE = effect
AAAAAAAA = effect argument
We get the sample number by combining the high bits from byte 0 and low bits from byte 2.The very first mod could only have 15 samples so only the low bits in byte 2 mattered.
The period is a reference to how many clock ticks between sending each byte of data to the digital-to-analog converter. Because the format originated on the Amiga this is with reference to its clock tick frequency. We will convert this into sample rate but for now we will just store the period. The low bits of byte 0 are bite 11-8 and the rest is stored in byte1.
The effect and its argument can be be extracted with similar bit manipulation. Below the Note structure and its constructor. The only new bit of Rust coding here is the structure initializer. When the variable name and structure field names match we can use abbreviate initializer syntax
Struct{ field, field12...}
instead of needing to write Strcut{ field = field, field2 = field2 }
.struct Note{ sample_number: u8, period: u32, effect: u8, effect_argument: i8, } impl Note{ fn new( note_data : &[u8]) -> Note { let sample_number = (note_data[2] & 0xf0) >> 4; let period = ((note_data[0] & 0x0f) as u32) * 256 + (note_data[1] as u32); let effect = note_data[ 2] & 0x0f; let effect_argument = note_data[3] as i8; Note{ sample_number, period, effect, effect_argument } } }
Converting numbers to enums
The above works but it would be better to store the effect as an enum rather than a number. So I created the following
enum
for all the effects.enum Effect { Arpeggio = 0, SlideUp = 1, SlideDown = 2, //....all other effects }
I thought I could simply cast the
u8
into an Effect
but I would get the compile error non-primitive cast: `u8` as `Effect`
Given Rust's focus on safety this restriction seems sensible enough as a simple type cast could potentially cause the number to cast into an undefined value. But it is also very annoying and I was hoping to find some built-in functionality that would check the value against different variants on the enum and return
Err
on failure but I have not found any.
Thinking about it a bit more I realized I am not really making full use of the Rust enums. Instead of converting the effect number into an enum value I should combine it with the effect argument. This is exactly the sort of thing the Rust enums are designed for. So I changed my effect enum to;
enum Effect{ ..// VolumeSlide{ volume_change_per_tick : i8 }, // 12 PositionJump{ pattern_table_pos : u8 }, // 13 SetVolume{ volume : u8 }, // 14 SetSpeed{ speed : u8 }, // 15 }
and I set up an
Impl
block for the Effect
to handle the conversion from effect_number
and effect_argument
into an Effect
impl Effect{ fn new( effect_number : u8, effect_argument : i8 ) -> Effect { match effect_number { 0 => match effect_argument { 0 => Effect::None, _ => panic!( format!( "unhandled arpeggio effect: {}", effect_number ) ) }, // ... 14 => Effect::SetVolume{ volume : effect_argument as u8 }, 15 => Effect::SetSpeed{ speed : effect_argument as u8 }, _ => panic!( format!( "unhandled effect number: {}", effect_number ) ) } } }
This feels better as now the
Effect
captures the effect type and the data associated with it. Because the conversion happens through the match
syntax I have to deal with effects that I can't handle yet. For now I am panicking because I want to quickly identify mods that the code cant handle but I can imagine changing it to either ignore or report the unhandled effect.
My only complaint about this solution is that the link between the Effect and its numerical representation is only captured in the
new
function. It would be nicer if it could somehow be part of the enum definition.
The interesting new bit of code is the use of a nested
match
statement. In the mod file the arpeggio effect is only an effect if it has a non-zero argument. If it is zero it is basically a no-op.Reading the patterns
Patterns are 64 lines long and store the note data for every channel. This gives us the following structure for storing the pattern data and the Pattern constructor for constructing an empty pattern without any note data;
pub struct Pattern { channels: Vec<Vec<Note>> // outer vector is the lines (64). Inner vector holds the notes for the line } impl Pattern{ fn new( ) -> Pattern { let mut channels : Vec<Vec<Note>> = Vec::new(); for _channel in 0..64 { channels.push( Vec::new() ); } Pattern{ channels } } }
The data is parsed a note and line at a time and used to set the pattern and finally we read the sample data.
for sample_number in 0..samples.len() { let length = samples[sample_number].size; for _idx in 0..length { samples[sample_number].samples.push(file_data[offset] as i8); offset += 1; } }
It is a bit ugly because we convert each
u8
into a i8
in a loop. I imagined there would be standard slice conversion functions but I can't find any. I am sure there are unsafe ways of doing it but that sort of defeats the point of using Rust.
In my next article I will look into audio and threads with Rust in preparation for playing out the mod music.I have uploaded all the code to https://github.com/janiorca/articles/tree/master/mod_player-2
No comments:
Post a Comment