First of all, we aren't going to build an 100% accurate MOD player here. That would use up much more memory. We will simply make a MOD player that works. A few modules (with stupid effect abuse) will probably not play right.
One thing we'll need is a module to play! Add a "data" directory to the project folder, modify the makefile to include MODs in "data" into the project.
at top: DATA := data ------------------------------ at bottom: %.MOD.o : %.MOD #-------------------- @echo $(notdir $<) @$(bin2o)
Here's a cool mod if you don't have one: SAC09.MOD
We'll make a new function called MOD_Play where the user can pass the module data.
// main.c
#include <gba.h>
#include <stdio.h>
#include "mod.h"
#include "SAC09_MOD.h"
int main( void )
{
// initialize interrupt handler
irqInit();
MOD_Setup();
MOD_Play( SAC09_MOD );
consoleInit( 0, 4, 0, NULL, 0, 15 );
BG_COLORS[0] = RGB8( 0, 0, 255 );
BG_COLORS[241] = RGB8( 255, 255, 255 );
SetMode( MODE_0 | BG0_ON );
iprintf( "\n Another MOD Player..." );
while(1) {
VBlankIntrWait();
}
return 0;
}
Now what exactly should MOD_Play do? First thing to do is examine the module 'signature' or verification word. It's at offset 1080 in the file. The string here determines how many channels the module has.
| String | Number of Channels |
| M.K. | 4 channels |
| FLT4 | 4 channels |
| 4CHN | 4 channels |
| 6CHN | 6 channels |
| 8CHN | 8 channels |
| FLT8 | 8 channels |
| xCHN | x(1-9) channels |
| 10CH | 10 channels |
| xxCH | xx(1-??) channels |
Sometimes there is no signature, this is for really old modules which have 4 channels and only 15 samples instead of 31. We don't care about these. We also don't care about anything greater than 8 channels. What we'll do is just test for "M.K.", then "FLTx", then "xCHN", and error if the channel count is greater than 8. I think there's also "FLxx"...
/*******************************************************
* MOD_Play( source )
*
* Start playback of module. MOD data MUST be aligned by
* 4 bytes.
*******************************************************/
.thumb_func
MOD_Play:
push {r4,lr}
ldr r4,=MOD_Vars @ save module address
str r0, [r4, #MV_Address]
ldr r1,=1080 @ read signature
ldr r1, [r0, r1]
ldr r2,='M'|('.'<<8)|('K'<<16)|('.'<<24)
cmp r1, r2 @ test for "M.K."
bne 1f
mov r3, #4 @ match = 4 channels
b found_channel_count
1:
ldr r2,=('F'|('L'<<8)|('T'<<16))<<8
lsl r3, r1, #8 @ test for "FLTx"
cmp r3, r2
bne 1f
lsr r3, r1, #24 @ get last character
single_digit_test:
sub r3, #'0' @ subtract '0' offset
cmp r3, #0
ble errornous_signature @ bad channel count
cmp r3, #8
bgt errornous_signature @ bad channel count
b found_channel_count
1:
ldr r2,=('C'|('H'<<8)|('N'<<16))
lsr r3, r1, #8 @ test for "xCHN"
cmp r3, r2
bne 1f
lsl r3, r1, #24 @ get first character
lsr r3, #24
b single_digit_test @ use the FLTx test
1:
found_channel_count: @ r3 = channel count
strb r3, [r4, #MV_ChCount] @ store channel count
@ ...............
errornous_signature:
mov r0, #0 @ disable playback
strb r0, [r4, #MV_Active]
pop {r4}
pop {r0}
bx r0
Notice the MOD_Vars reference, this is the structure that will hold all of our playback variables.
/********************************************************** * MOD Variables Structure **********************************************************/ .struct 0 MV_Address: .space 4 @ address of module MV_PattAddress: .space 4 @ address of beginning of current pattern MV_ChCount: .space 1 @ number of channels in module MV_Tick: .space 1 @ tick counter MV_Row: .space 1 @ row counter MV_Pos: .space 1 @ position in order list MV_Frac: .space 2 @ tick fraction 0.15 fixed point, MSB = update flag MV_Rate: .space 2 @ tick rate MV_Speed: .space 1 @ ticks per row MV_Active: .space 1 @ playing or not MV_Pattern: .space 1 @ current pattern # .align 2 MV_Period: .space 2 @ variable for channel processing MV_Volume: .space 1 @ variable for channel processing MV_Size: @ size of variables structure
Maybe more to come later, but this is the basics we need.
We must figure out a way to time the module playback according to our 256hz interval (16KHz / 64 samples). The tick rate of a MOD depends on the current 'BPM' value. The BPM is reset to 125 at startup, and can be changed by a certain MOD pattern effect. To convert a BPM value into Hz we divide by 2.5 (Hz = BPM / 2.5). That's 50hz on startup. To handle our 256hz interval, we will add another variable to split up each tick into a 15bit fractional part. We'll calculate the 'ticks per frame' with "(bpm/2.5)/256" or "bpm/640" or "(bpm*32768)/640" for our fixed point format.
/*******************************************************************
* MOD_SetRate( bpm )
*
* Changes the tick rate according to a BPM value.
*******************************************************************/
.thumb_func
MOD_SetRate:
push {lr}
lsl r0, #15 @ r0 = bpm * 32768
mov r1, #640/4 @ r1 = 640
lsl r1, #2
swi 0x06 @ r0 = bpm / 640 in 0.15 fixed point
ldr r1,=MOD_Vars @ store rate
strh r0, [r1, #MV_Rate]
pop {pc}
The other thing that controls the timing is the 'Speed' of the module. This is the amount of ticks there are per row. It can be changed by a certain pattern effect. It's initialized to 6 at startup.
Anyway, then we initialize the rest of the stuff:
...
mov r0, #6 @ set speed = 6
strb r0, [r4, #MV_Speed]
mov r0, #125 @ set bpm = 125
bl MOD_SetRate
mov r0, #0x1
lsl r0, #15 @ set MSB of fraction (update flag)
strh r0, [r4, #MV_Frac]
mov r0, #0 @ restart position
bl MOD_SetPosition
mov r0, #1 @ activate playback
strb r0, [r4, #MV_Active]
pop {r0}
bx {r0}
errornous_signature:
mov r0, #0 @ disable playback
strb r0, [r4, #MV_Active]
pop {r0}
bx r0
...
MOD_SetPosition changes the current 'position' of the playback and loads a pattern index from the order list, it also resets the tick and row variables.
/******************************************************************** * MOD_SetPosition( position ) * * Sets the position in the order list. ********************************************************************/ .thumb_func MOD_SetPosition: ldr r1,=MOD_Vars @ read module address ldr r2, [r1, #MV_Address] ldr r3,=950 @ 950 = offset of ORDER COUNT ldrb r3, [r2, r3] @ read order count cmp r0, r3 @ compare with position blt 1f ldr r3,=951 @ invalid order, useldrb r0, [r2, r3] @ cmp r0, #128 @ (951) contains the restart position beq 1f @ unless it is 128 mov r0, #0 1: strb r0, [r1, #MV_Pos] @ save position mov r3, #952/4 @ r3 = order list offset lsl r3, #2 add r3, r0 @ add index ldrb r0, [r2, r3] @ copy pattern# strb r0, [r1, #MV_Pattern] @ ldrb r3, [r1, #MV_ChCount] @ calculate pattern address mul r3, r0 @ base + 1084 + (patt * (ch*4*64) lsl r3, #2+6 @ add r3, r2 @ ldr r0,=1084 @ add r3, r0 @ str r3, [r1, #MV_PattAddress] @ mov r0, #0 @ reset tick and row strb r0, [r1, #MV_Tick] strb r0, [r1, #MV_Row] bx lr
As you can see (if you actually read the code), the order list is located at address 952 (0x3B8) in the module, it's 128 entries wide and contains a bunch of indexes to patterns to be played.
Let's take a break and actually look at the MOD format.
Address Size Description 0 20 Title of module (just a regular string) 22 30*31 Sample headers (explained later) 950 1 Length of song 951 1 Restart position 952 128 Order List 1080 4 Signature 1084 ... Pattern data ??? ... Sample data
Pretty simple! The title doesn't concern us, so we'll skip over that. The 'length' of the song selects the size of the order list. If the module plays past the length then it should either stop or start again from the 'Restart position'.
The restart position isn't always valid, in most MODs it is "128" which means there is no restart position (use 0, or stop the module). If it's not 128, then once the MOD plays to the length then you should start from the position stored in there.
The Order List is just an array that contains pattern indexes to be played in order.
We also see the signature that we examined earlier.
Now we can write the very basics of our module player. We'll read and advance through the pattern data and progress through the order list. This will be our function that needs to be called every 'frame' (256hz).
/**********************************************************************
* MOD_Frame()
*
* MOD player routine
**********************************************************************/
.thumb_func
MOD_Frame:
push {r4,r5,lr}
ldr r4,=MOD_Vars
ldrb r0, [r4, #MV_Active] @ cancel function if MOD isn't active
cmp r0, #0 @
beq mf_cancel @
ldrh r5, [r4, #MV_Frac] @ shift out MSB of fraction
lsl r5, #17 @ and process tick if it's set
bcc dont_update @
bl MOD_Tick @
dont_update:
lsr r5, #17 @
ldrh r1, [r4, #MV_Rate] @ add rate to fraction
add r5, r1 @
strh r5, [r4, #MV_Frac] @
mf_cancel:
pop {r4,r5,pc} @ return to main routine
/**********************************************************************
* MOD_Tick()
*
* Process a MOD tick
**********************************************************************/
.thumb_func
MOD_Tick:
push {r4-r7,lr}
ldr r4,=MOD_Vars
ldrb r0, [r4, #MV_Tick] @ parse pattern data on tick0
cmp r0, #0 @
bne dont_advance_pattern @
ldr r0, [r4, #MV_PattAddress]
ldrb r1, [r4, #MV_ChCount]
ldrb r2, [r4, #MV_Row]
mul r2, r1 @ get current address
lsl r2, #2 @ (patt + (row * ch * 4))
add r0, r2 @
ldr r5,=MOD_Channels
@-----------------------------------------------------------------------------------
read_pattern:
@-----------------------------------------------------------------------------------
@*******************************************************************
@
@ 32-bit Pattern Entry (each cell is 4 bits)
@ ___ ___ ___ ___ _______ ___ ___
@ | x | y | s | e | pp | S | P |
@ | | | | '-----|---'---- Ppp - amiga period (0=disabled)
@ | | '---|-----------'-------- Ss - sample index (1..31, 0=disabled)
@ | | '-------------------- e - effect index
@ '---'---------------------------- xy - effect parameters
@
@*******************************************************************
mov r7, #0 @ r7 = flags
ldmia r0!, {r3} @ read pattern entry
lsl r2, r3, #(6*4) @ parse sample#
lsr r2, #(7*4) @
lsl r2, #(1*4) @
lsl r6, r3, #(2*4) @
lsr r6, #(7*4) @
add r2, r6 @
beq 1f @ skip if sample is zero (null)
add r7, #CHFLAG_SAMP @ set sample flag
ldrb r6, [r5, #MODCH_DSAMPLE] @ store sample and set 'NEWSAMP' flag
cmp r2, r6 @ if sample numbers differ
beq 1f @
add r7, #CHFLAG_NEWSAMP @
strb r2, [r5, #MODCH_DSAMPLE] @
1:
lsl r2, r3, #(7*4) @ mask amiga period
lsr r2, #(5*4) @
lsl r6, r3, #(4*4) @
lsr r6, #(6*4) @
add r2, r6 @
beq 1f @ skip if zero
add r7, #CHFLAG_NOTE @ set flag and save new period
strh r2, [r5, #MODCH_DPERIOD] @
1:
lsl r2, r3, #(3*4) @ mask and save effect number
lsr r2, #(7*4) @
strb r2, [r5, #MODCH_DEFFECT] @
lsr r2, r3, #(6*4) @ mask and save effect parameters
strb r2, [r5, #MODCH_DPARAMS] @
strb r7, [r5, #MODCH_FLAGS] @ store flags
add r5, #MODCH_SIZE @ increment channel pointer
sub r1, #1 @ decrement and loop
bne read_pattern @ ...
dont_advance_pattern:
@-----------------------------------------------------------------------------------------
ldr r5,=MOD_Channels @
ldrb r6, [r4, #MV_ChCount] @
@
@---------------------------------------------------------------------------
update_channels:
@---------------------------------------------------------------------------
bl MOD_UpdateChannel @ update MOD channels
add r5, #MODCH_SIZE @
sub r6, #1 @
bne update_channels @
@---------------------------------------------------------------------------
ldrb r0, [r4, #MV_Tick] @ increment tick
ldrb r1, [r4, #MV_Row] @ increment row
add r0, #1 @
ldrb r2, [r4, #MV_Speed] @ exit if tick < speed
cmp r0, r2 @
blt mt_exit @
mov r0, #0
add r1, #1 @
cmp r1, #64 @ exit if row < 64
blt mt_exit @
ldrb r0, [r4, #MV_Pos] @ increment position
add r0, #1 @
bl MOD_SetPosition @ (resets row/tick)
pop {r4-r7,pc} @ return (thumb)
mt_exit:
strb r0, [r4, #MV_Tick]
strb r1, [r4, #MV_Row]
pop {r4-r7,pc} @ return (thumb)
What MOD_Frame does is handles when a tick should be processed. Basically it just checks the top bit of FRAC to see if it's set (and processes a tick if so), and then it clears that bit and adds the tick rate to the number. If the number overflows 15 bits then the top bit (16th bit) gets set for us for the next frame. It's cool how it works out like that.
The MOD_Tick function can be divided up into three sections: the 'reading' part, the 'update' part, and the 'progression' part. In the first part we are just reading pattern entries (4 bytes each), parsing the entries out of the packed format, and copying it into our channel data (see below). The second part calls the "Update Channel" function on each of the channels in the module. The last part does the tick/row/position advancement through the module. Later on, the last part may get a little more complicated since there are some pattern commands that affect the module progression.
Let's have a look at our channel structure.
/********************************************************** * MOD channel structure **********************************************************/ .struct 0 MODCH_DPERIOD: .space 2 @ amiga period from pattern MODCH_PERIOD: .space 2 @ current amiga period MODCH_DSAMPLE: .space 1 @ sample number MODCH_DEFFECT: .space 1 @ effect number MODCH_DPARAMS: .space 1 @ effect parameters MODCH_VOLUME: .space 1 @ volume MODCH_MEM_3: .space 1 @ mem:glissando MODCH_MEM_4: .space 1 @ mem:vibrato MODCH_MEM_7: .space 1 @ mem:tremolo MODCH_VAR: .space 1 @ variable (for effects) MODCH_FLAGS: .space 1 @ channel flags MODCH_FINETUNE: .space 1 @ finetuning (found in sample header) .align MODCH_SIZE: @ size of [16 bytes] .equ CHFLAG_SAMP, 1 @ sample flag .equ CHFLAG_NEWSAMP, 2 @ new sample (sample != previous) .equ CHFLAG_NOTE, 4 @ note flag
I think this is everything we need. The first few entries are direct pattern data.
We store 2 period values in the channel structure, one is the 'target' period, the other is the 'current' period. This is necessary for 'pitch slides'.
Each channel has a volume level that ranges from 0->64.
Some of the pattern effects can omit the parameter and expect to use a parameter stored in memory (there's three effects that do that). So we need 1 byte for each of them.
MODCH_VAR will be our general-purpose variable for controlling the behavior of certain effects. For example, it will be our sine position during the vibrato effect.
The last thing in the structure is a few flags to tell us when there's new pattern data. We see some enumerations below it.
We are ready to create MOD_UpdateChannel now. After this part, we will hear basic playback of the MOD!
/*********************************************************************************
* MOD_UpdateChannel( channel )
*
* Updates a MOD channel
* Expects r4 = Vars
* Expects r5 = channel
*********************************************************************************/
.equ UCFLAG_START, 1
.thumb_func
MOD_UpdateChannel:
push {r6-r7, lr}
mov r7, #0 @ r7 will be our processing flags
ldrb r0, [r4, #MV_Tick] @ branch if tick isn't zero
cmp r0, #0 @
bne nonzero_tick @
@ process tick0 things:
ldrb r6, [r5, #MODCH_FLAGS] @ test sample flag
lsr r6, #1 @ ...
bcc sample_test
ldrb r0, [r5, #MODCH_DSAMPLE]
bl GetSamplePointer
ldrb r1, [r0, #SAMPLE_VOLUME] @ copy volume and finetune
strb r1, [r5, #MODCH_VOLUME] @
ldrb r1, [r0, #SAMPLE_FINETUNE] @
strb r1, [r5, #MODCH_FINETUNE] @
sample_test:
lsr r6, #1 @ set START flag on new sample
bcc newsample_test @
mov r0, #UCFLAG_START @
orr r7, r0 @
newsample_test: @
lsr r6, #1 @ test note flag
bcc no_note @
ldrh r0, [r5, #MODCH_DPERIOD] @ copy period value
ldrb r1, [r5, #MODCH_FINETUNE] @ get finetune scaler
lsl r1, #1 @
ldr r2,=Finetune_LUT @
ldrsh r1, [r2, r1] @
mul r1, r0 @ apply finetune to period
asr r1, #19 @
add r0, r1 @
strh r0, [r5, #MODCH_DPERIOD] @ save period value
strh r0, [r5, #MODCH_PERIOD] @ set internal period
mov r0, #UCFLAG_START @ and set start flag
orr r7, r0 @
@ (this will change later (for glissando))
no_note:
mov r0, #0 @ clear flags
strb r0, [r5, #MODCH_FLAGS] @
nonzero_tick:
ldrh r0, [r5, #MODCH_PERIOD]
strh r0, [r4, #MV_Period]
ldrb r0, [r5, #MODCH_VOLUME]
strb r0, [r4, #MV_Volume]
@ --update effects--
@ --update sound--
ldr r0,=MOD_Channels @ determine channel number
sub r0, r5, r0 @ (offset - base) / 16
lsr r0, #4 @
ldr r1,=Voice_Map @ get voice number (mapped)
ldrb r0, [r1, r0] @ get voice struct address
mov r1, #VOICE_SIZE @
mul r0, r1 @
ldr r6,=MOD_VoicesLeft @
add r6, r0 @ r6 = voice
lsr r7, #1 @ test START bit
bcc start_flag_cleared
ldrb r0, [r5, #MODCH_DSAMPLE]
cmp r0, #0 @ skip if sample is invalid
beq start_flag_cleared @
bl GetSamplePointer @ get sample
ReadAmigaWord r1, r0, #SAMPLE_REPEATLEN, r2 @ if repeat length <= 1 then loop is disabled
cmp r1, #1 @
blt sample_doesnt_loop @
@ loop enabled:
ReadAmigaWord r2, r0, #SAMPLE_REPEAT, r3 @ length = repeat start + repeat length
add r2, r1 @
add r2, r2 @ *2 for real value
str r2, [r6, #VOICE_REMAIN]
@ add r1, r1 @ *2 for real value
strh r1, [r6, #VOICE_LOOP] @ save loop param
b sample_does_loop
sample_doesnt_loop: @ loop disabled:
ReadAmigaWord r2, r0, #SAMPLE_LENGTH, r1 @ copy sample length and clear loop
add r2, r2 @
strh r2, [r6, #VOICE_REMAIN] @
mov r2, #0 @
strh r2, [r6, #VOICE_LOOP] @
sample_does_loop:
ldrb r0, [r5, #MODCH_DSAMPLE] @ copy data pointer to SOURCE
bl GetSampleDataPointer @
str r0, [r6, #VOICE_SOURCE] @
start_flag_cleared:
ldrh r1, [r4, #MV_Period] @ read period value
cmp r1, #0
beq invalid_period
ldr r0,=55420 @ get 55420 / period
swi 0x06 @
@ result must be within 10 bits
ldrb r1, [r4, #MV_Volume] @ create word of vvvvvvrr rrrrrrrr
cmp r1, #64 @ clamp volume to 0..63
blt 1f
mov r1, #63
1: lsl r1, #10 @ v = volume
orr r1, r0 @ r = rate
b valid_period
invalid_period:
mov r1, #0
valid_period:
strh r1, [r6, #VOICE_C1] @ write to C1
pop {r6-r7, pc} @ return
Okay that's the last huge piece of code I'll paste :). Let me explain what's going on.
First thing the function does is check if the tick is zero. MOD usually has different behavior for stuff depending on if the tick is zero or not. Here, we check for tick 0, and if it is, then we process stuff like new notes. Alot of the pattern effects work differently according to the tick number.
Now, in the first bit inside the "tick 0" section, we test the 'sample' flag to see if the current pattern entry contains a sample number. If there is a sample value present, then we copy the finetuning and "default volume" out of the sample structure, and store it in our internal structure. Here is the sample structure (first sample is at offset (20) in the file (after the title)).
Offset Size Desc 0 22 Name 22 2 Length 24 1 Finetune 25 1 Default Volume 26 2 Repeat Start 28 2 Repeat Length
One thing to note about the double-byte values here, they're in big endian format. To get the value on a GBA, you do byte1*0x100 + byte2. Also, to get the real value, you must multiply it by two.
The Default Volume entry is the volume level that will be copied into the channel if the sample's index is present. It can be overridden by a pattern effect.
The Finetune is a "signed nibble", so values 0..15 = 0,1,2,3,4,5,6,7,-8,-7,-6,-5,-4,-3,-2,-1. This entry brings up a certain problem, we have to apply the finetune to the amiga period. Some people suggest to unpack the patterns into memory and convert the amiga periods to notes. We are a bit short on memory so we'll have to do something else. What we'll do is keep a small table with 16 entries containing multiplier values for the 16 finetune settings. Finetune -8 drops the pitch 1 semitone, and finetune 7 raises the pitch by 7/8 of a semitone. We see in the second part of the 'tick 0' section that we multiply the amiga period by a value in our look-up table. Here is the table:
/********************************************************* * Finetune scaler lookup table * * Input: 4-bit finetune value, Output: signed 16bit scaler * Value = round((2 ^ (-input * (1/96)) - 1) * 32768 * 16) * * To modify period: * period = period + ((period * table[finetune]) >> 19) **********************************************************/ Finetune_LUT: @ __0__|___1__|___2__|___3___|____4__|___5___|___6___|___7___| .hword 0, -3771, -7516, -11233, -14924, -18589, -22227, -25839 .hword 31176, 27180, 23212, 19273, 15363, 11480, 7626, 3799 @ -8 -7 -6 -5 -4 -3 -2 -1
Doing it this way brings up another similar problem, we'll bump into it when coding the effects.
Now we're at the part that is executed every tick. The first thing we do is copy a couple of our variables into a temporary position, this is so we can modify them without changing the original value (and causing some error). We won't do that until we start coding the effects (like vibrato will add an offset to the pitch without modifying the actual pitch).
Finally, we update the 'voice' for our channel. To spread out the channels, I made another table to map the channels to their voices:
/********************************************************** * Voice Map * * Maps channels to voices **********************************************************/ Voice_Map: .byte 0, 4, 5, 1, 2, 6, 7, 3 @ left, right, right, left, left, right, right, left
Next thing to do is copy our information from the channel to the voice structure. One interesting part is the one with that divide. What's going on here is calculating a 'rate' value from an amiga period. The formula to calculate a HZ value from an amiga period is: 7093789.2 / (period*2). For computing our rate, it would be: (7093789.2 / (period*2)) / 16384 * 256, which simplified+rounded is: (55420 / period).
We probably want to keep the divides in our code to a minimum. There's no hardware for division in the GBA, so dividing is somewhat expensive.
Test the program! You should hear a corrupted sounding version of the song you linked. Note that with SAC09.MOD there will be a big pause in the beginning because we did not implement the "pattern break" effect yet.
| Previous: Simple Sound Mixer | Contents | Next: Many MOD Effects |