Basic MOD Playback

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.

StringNumber of Channels
M.K.4 channels
FLT44 channels
4CHN4 channels
6CHN6 channels
8CHN8 channels
FLT88 channels
xCHNx(1-9) channels
10CH10 channels
xxCHxx(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.

MOD Timing

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, use 
	ldrb	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 MixerContentsNext: Many MOD Effects