Many MOD Effects

Finally, some fun. This is the part of the program that will take up a large chunk of our binary.

There are 30 MOD effects total that can do different things during playback.

First thing to do here is create a function to jump to the different effect routines.

/*********************************************************************
 * MOD_ProcessEffect( channel )
 *
 * Processes an effect for a channel.
 * r4 = vars
 * r5 = channel
 *********************************************************************/
.thumb_func
MOD_ProcessEffect:

	ldrb	r0, [r5, #MODCH_DPARAMS]	@ read params, tick, effect#
	ldrb	r1, [r4, #MV_Tick]		@
	ldrb	r2, [r5, #MODCH_DEFFECT]	@
	add	r2, r2				@ 
	
	cmp	r1, #0				@ zero flag = tick 0
	add	pc, r2				@ jump into table
	nop					@
MOD_Effect_Jump_Table:
	b	MOD_0xy		@ Arpeggio
	b	MOD_1xx		@ Pitch Slide Up
	b	MOD_2xx		@ Pitch Slide Down
	b	MOD_3xx		@ Glissando
	b	MOD_4xy		@ Vibrato
	b	MOD_5xy		@ Volume Slide + Glissando
	b	MOD_6xy		@ Volume Slide + Vibrato
	b	MOD_7xy		@ Tremolo
	b	MOD_8xx		@ Set Panning
	b	MOD_9xx		@ Sample Offset
	b	MOD_Axy		@ Volume Slide
	b	MOD_Bxx		@ Position Jump
	b	MOD_Cxx		@ Set Volume
	b	MOD_Dxx		@ Pattern Break
	b	MOD_Exy		@ Extended Effects
	b	MOD_Fxx		@ Set Tempo

Now, I figure that the parameter of the effect is going to be accessed by most of the functions, so we'll load it here in the beginning instead of in each effect routine!

Another thing is the tick number, alot (all) of the effects depend on the tick number being zero or non-zero. We'll do the compare with zero here. Then all we have to do in the effect routines is a conditional branch.

One more thing is the actual jump table, instead of using absolute memory addresses, we'll make a table of unconditional branches which can range +-2048 in our program, which is plenty. To enter the table, we add "effect number * 2" to the program counter. We need that 'nop' spacer because the CPU pipeline causes the PC value to be a little bit ahead.

Time for our first effect, we'll start with the easiest one.

Fxx - Set Tempo

This effect can change either the Speed or BPM of the playback. The minimum BPM value is 32, maximum 255. If the parameter of this effect is under 32, then this effect changes the Speed, which ranges from 1->31, otherwise it sets the BPM. We ignore this effect if the parameter is zero.

/*************************************************************************************
 * Fxx - Set Tempo
 *************************************************************************************/
.thumb_func
MOD_Fxx:
	bne	1f				@ on tick 0:	
	cmp	r0, #32				@ set SPEED if param < 32
	bge	2f				@
	cmp	r0, #0				@ exit if param == 0
	beq	1f				@
	strb	r0, [r4, #MV_Speed]		@
	bx	lr				@
2:	bl	MOD_SetRate			@ otherwise set new BPM
1:	pop	{pc}				@

Now SAC09.MOD will play with the correct timing!

Cxx - Set Volume

Actually, this is the easiest effect. On tick zero you copy the parameter into the channel volume, clamping it if it exceeds 64. We also need to set the volume in our 'copy' of the volume.

/*************************************************************************************
 * Cxx - Set Volume
 *************************************************************************************/
.thumb_func
MOD_Cxx:
	bne	1f				@ on tick 0:
	cmp	r0, #64				@ clamp volume to 0->64
	blt	2f				@ and set channel volume
	mov	r0, #64				@
2:	strb	r0, [r5, #MODCH_VOLUME]		@
	strb	r0, [r4, #MV_Volume]		@
1:	pop	{pc}				@

SAC09.MOD should sound much better with this effect implemented. It's a heavily used effect in MODs ('set volume' gets its own pattern column in the more advanced module formats!).

0xy - Arpeggio

This effect rapidly changes the pitch of a sound to 3 different notes. Each tick, it either resets the note, adds x semitones, or adds y semitones. It's done in order:

Tick#Effect
0Pitch = OriginalPitch
1Pitch = OriginalPitch + 'x' semitones
2Pitch = OriginalPitch + 'y' semitones
3Pitch = OriginalPitch
4Pitch = OriginalPitch + 'x' semitones
5Pitch = OriginalPitch + 'y' semitones
6Pitch = OriginalPitch
...Etc...

If both parameters are zero, then this effect does nothing, it's used as the 'empty' (000) effect column then.

/*************************************************************************************
 * 0xy - Arpeggio
 *************************************************************************************/
.thumb_func
MOD_0xy:
	beq	1f				@ on nonzero ticks:
	
	mov	r2, r0				@ preserve parameter
	beq	1f				@ exit if params == 0

	mov	r0, r1				@ get tick MOD 3
	mov	r1, #3				@
	swi	0x06				@ swi6 returns x MOD y in r1
	
	adr	r0, Arpeggio_Table-2	
	
	cmp	r1, #1				@ compare with 1
	blt	1f				@ less than: == 0 (exit function)
	bgt	2f				@ greater than: == 2 (add y)
						@ otherwise: == 1 (add x)
	lsr	r2, #4				@ read table[x]
	lsl	r2, #1				@
	beq	1f				@ exit if x == 0
	ldrh	r2, [r0, r2]			@
	b	_arp_calcperiod

2:	lsl	r2, #32-4			@ read table[y]
	lsr	r2, #32-4-1			@
	beq	1f				@ exit if y == 0
	ldrh	r2, [r0, r2]			@
_arp_calcperiod:
	
	ldrh	r0, [r4, #MV_Period]		@ multiply period by scaler
	mul	r0, r2				@
	lsr	r0, #16				@
	strh	r0, [r4, #MV_Period]		@
	
1:	pop	{pc}

nop	@ <- spacer
/********************************************************
 * Arpeggio Table
 * 
 * Contains multipliers to raise pitch 1->15 semitones.
 * Calculator: (2y(-x / 12) * 65536) + 0.5=;
 ********************************************************/
Arpeggio_Table:
	.hword       61858,58386,55109,52016,49097,46341,43740	@ 1->7
	.hword 41285,38968,36781,34716,32768,30929,29193,27554	@ 8->15

As you can see I made another multiplication table for sliding up a certain amount of semitones.

I was a bit lazy and used the Divide SWI again... but it shouldn't be too hard for the function to divide a 5bit value by 3.

With this effect implemented, SAC09.MOD is now sounding even better! Alot of chiptunes use arpeggio heavily.

1xx - Pitch Slide Up

This effect slides the amiga period up a certain amount on non-zero ticks.

/*************************************************************************************
 * 1xx - Pitch Slide Up
 *************************************************************************************/
.thumb_func
MOD_1xx:
	beq	1f				@ on nonzero ticks:
	ldrh	r1, [r5, #MODCH_PERIOD]		@ subtract param from period
	sub	r1, r0				@
	cmp	r1, #MIN_PERIOD			@ clip to minimum period
	bge	2f				@
	mov	r1, #MIN_PERIOD			@
2:	strh	r1, [r5, #MODCH_PERIOD]		@ save period
	strh	r1, [r4, #MV_Period]		@ set period copy
1:	pop	{pc}

To slide the pitch up, we must slide the amiga period down. We'll clip the amiga period to the lowest value our mixer can handle. With our 2.8 fixed point Rate, our lowest period (highest pitch) allowed is 55.

2xx - Pitch Slide Down

Same as 1xx, except it slides the pitch down. We can share a bit of code here (this must be directly below 1xx code).

/*************************************************************************************
 * 2xx - Pitch Slide Up
 *************************************************************************************/
.thumb_func
MOD_2xx:					@ note sharing code with above function
	beq	1b				@ on nonzero ticks:
	ldrh	r1, [r5, #MODCH_PERIOD]		@ add param to period
	add	r1, r0				@
	ldr	r2,=MAX_PERIOD			@ clip to maximum period
	cmp	r1, r2				@
	blt	2b				@
	mov	r1, r2				@
	b	2b				@

The maximum period we'll allow here is 1712, or one octave lower than the lowest Protracker note.

3xx - Glissando

Here is the first effect that is a bit more difficult to implement. What this effect does is slide the period up or down towards another period. The target period is the value in the last pattern entry. If a glissando effect is present, then we must not copy the pattern entry into our period value. We add this modification to our new-note code.


	ldrb	r1, [r5, #MODCH_EFFECT]		@ don't copy period on effect 3xx (glissando) 
	cmp	r1, #3				@
	beq	no_note				@
	cmp	r1, #5				@ or 5xy (glissando+volslide)
	beq	no_note				@

	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:

Now we can write the routine.

/*************************************************************************************
 * 3xx - Glissando
 *************************************************************************************/
.thumb_func
MOD_3xx:
	bne	doGlissando			@ on tick0:
	cmp	r0, #0				@   copy nonzero params into memory
	beq	1f				@
	strb	r0, [r5, #MODCH_MEM_3]		@
1:	pop	{pc}				@

doGlissando:					@ on nonzero ticks:
	ldrh	r0, [r5, #MODCH_PERIOD]		@ current period
	ldrh	r1, [r5, #MODCH_DPERIOD]	@ target period
	ldrb	r2, [r5, #MODCH_MEM_3]		@ read 'speed'
	cmp	r0, r1				@ determine slide direction
	bgt	gliss_slide_down		@
gliss_slide_up:
	add	r0, r2				@ period += speed
	cmp	r0, r1				@ clip to the target period
	blt	gliss_finished			@
	mov	r0, r1				@
	b	gliss_finished			@
gliss_slide_down:
	sub	r0, r2				@ period -= speed
	cmp	r0, r1				@ clip to the target period
	bgt	gliss_finished			@
	mov	r0, r1				@
gliss_finished:
	strh	r0, [r5, #MODCH_PERIOD]
	strh	r0, [r5, #MV_Period]
	pop	{pc}

On tick0, glissando doesn't do anything except record the parameter into memory. On other ticks, we add or subtract the effect parameter to/from the current period to slide towards our target period. We also have to keep this function 'reusable', since it can be used again by effect 5xy.

4xy - Vibrato

What this one does is adds a sine*depth value to the pitch to create a 'vibrato' sound. First thing we need here is a sine table. Tremolo can use this table too. Specification is: 180 degrees in 32 entries in 0.8 fixed point. Here's the one in protracker:

/*************************************************************************************
 * Sine Table
 *************************************************************************************/
PT_SineTable:
	.byte   0,  24,  49,  74,  97, 120, 141, 161, 180, 197, 212, 224, 235, 244, 250, 253,
	.byte 255, 253, 250, 244, 235, 224, 212, 197, 180, 161, 141, 120,  97,  74,  49,  24

On non-zero ticks, first thing we do is add 'x' to our MODCH_VAR value, we also wrap it to 0->63 (AND 63). Then we read a value from the sine table (table[VAR&31]), multiply the value by 'y' (y is depth), divide by 128, and then add it to our period 'copy'.

On zero ticks, we just need to copy the effect parameter into memory (if it's non-zero). But we also need to calculate the pitch again since we are not saving it in the channel memory.

/*************************************************************************************
 * 4xy - Vibrato
 *************************************************************************************/
.thumb_func
MOD_4xy:
	bne	doVibrato			@ on tick0
	cmp	r0, #0				@   copy nonzero params into memory
	beq	doVibratoWithoutAdvancement	@
	strb	r0, [r5, #MODCH_MEM_4]		@
	b	doVibratoWithoutAdvancement	@
	
doVibrato:					@ on nonzero ticks:
	ldrb	r0, [r5, #MODCH_MEM_4]		@ read parameters
	ldrb	r1, [r5, #MODCH_VAR]		@ add x to var
	lsr	r2, r0, #4			@
	add	r1, r2				@
	lsl	r1, #32-6			@ var = var & 63
	lsr	r1, #32-6			@
	strb	r1, [r5, #MODCH_VAR]		@

doVibratoWithoutAdvancement:
	ldrb	r0, [r5, #MODCH_MEM_4]		@ we need to repeat this...
	ldrb	r1, [r5, #MODCH_VAR]		@

	lsl	r0, #32-4			@ mask y
	lsr	r0, #32-4			@
	
	ldr	r2,=PT_SineTable		@ read sine table [var & 31]
	lsl	r3, r1, #32-5			@
	lsr	r3,     #32-5			@
	ldrb	r2, [r2, r3]			@
	
	mul	r2, r0				@ get sine * depth / 128
	lsr	r2, #7				@
	
	ldrh	r0, [r4, #MV_Period]		@ add or subtract value to/from period
	cmp	r1, #32				@ depending on sine position 'half'
	blt	1f				@
	sub	r0, r2				@
	b	vib_finished			@
1:	add	r0, r2				@
	b	vib_finished			@
	
vib_finished:
	strh	r0, [r4, #MV_Period]		@ save period
	pop	{pc}				@

There should be subtle difference to the sound of SAC09.MOD now.

7xy - Tremolo

This function is basically the same as vibrato, except instead of affecting the pitch, we affect the volume. Also, we divide the scaled sine value by 64 instead of 128.

Now, the key to small code is to reuse code whenever possible. Alot of the vibrato function can be reused for tremolo. Here's what I did.

/*************************************************************************************
 * 4xy - Vibrato
 *************************************************************************************/
.thumb_func
MOD_4xy:
	bne	doVibrato			@ on tick0
	cmp	r0, #0				@   copy nonzero params into memory
	beq	doVibratoWithoutAdvancement	@
	strb	r0, [r5, #MODCH_MEM_4]		@
	b	doVibratoWithoutAdvancement	@
	
doVibrato:					@ on nonzero ticks:
	ldrb	r0, [r5, #MODCH_MEM_4]		@ read parameters
	bl	AddSpeedToSinePosition

doVibratoWithoutAdvancement:
	ldrb	r0, [r5, #MODCH_MEM_4]		@ read parameters
	bl	CalculateSineValue
	lsr	r2, #7				@ divide by 128
	
	ldrh	r0, [r4, #MV_Period]		@ add or subtract value to/from period
	cmp	r1, #32				@ depending on sine position 'half'
	blt	1f				@
	sub	r0, r2				@
	b	vib_finished			@
1:	add	r0, r2				@
	b	vib_finished			@
	
vib_finished:
	strh	r0, [r4, #MV_Period]		@ save period
	pop	{pc}				@
	

/*************************************************************************************
 * Sine Table
 *************************************************************************************/
PT_SineTable:
	.byte   0,  24,  49,  74,  97, 120, 141, 161, 180, 197, 212, 224, 235, 244, 250, 253
	.byte 255, 253, 250, 244, 235, 224, 212, 197, 180, 161, 141, 120,  97,  74,  49,  24

.thumb_func
AddSpeedToSinePosition:				@ r0 = param
	ldrb	r1, [r5, #MODCH_VAR]		@ add x to var
	lsr	r2, r0, #4			@
	add	r1, r2				@
	lsl	r1, #32-6			@ var = var & 63
	lsr	r1, #32-6			@
	strb	r1, [r5, #MODCH_VAR]		@
	bx	lr

.thumb_func
CalculateSineValue:				@ r0 = params
	ldrb	r1, [r5, #MODCH_VAR]
	
	lsl	r0, #32-4			@ mask y
	lsr	r0, #32-4			@
	
	ldr	r2,=PT_SineTable		@ read sine table [var & 31]
	lsl	r3, r1, #32-5			@
	lsr	r3,     #32-5			@
	ldrb	r2, [r2, r3]			@
	
	mul	r2, r0				@ get sine * depth
	bx	lr				@ r1=position, r2=value

/*************************************************************************************
 * 7xy - Tremolo
 *************************************************************************************
.thumb_func
MOD_7xy:
	bne	doTremolo			@ on tick0:
	cmp	r0, #0				@   record nonzero params
	beq	doTremoloWithoutAdvancement	@
	strb	r0, [r5, #MODCH_MEM_7]		@
	b	doTremoloWithoutAdvancement	@

doTremolo:					@ on nonzero ticks:
	ldrb	r0, [r5, #MODCH_MEM_7]		@ update sine position
	bl	AddSpeedToSinePosition		@

doTremoloWithoutAdvancement:
	ldrb	r0, [r5, #MODCH_MEM_4]		@ get sine value
	bl	CalculateSineValue		@
	lsr	r2, #6				@ divide by 64
	
	ldrb	r0, [r4, #MV_Volume]		@ add/sub sine to volume
	cmp	r1, #32				@
	blt	1f				@
	sub	r0, r2				@
	bpl	trem_finished			@ clip values under 0
	mov	r0, #0				@
	b	trem_finished			@
1:	
	add	r0, r2				@ (add)
	cmp	r0, #64				@ clip values over 64
	ble	trem_finished			@
	mov	r0, #64				@

trem_finished:
	strb	r0, [r4, #MV_Volume]		@ save volume
	pop	{pc}

Two sub-routines were made from the vibrato code so they could be shared with tremolo.

8xx - Set Panning

Our lame mixer doesn't support panning.

/*************************************************************************************
 * 8xx - Set Panning
 *************************************************************************************/
MOD_8xx:
	pop	{pc}

Axy - Volume Slide

This effect is used to slide the volume up or down. On nonzero ticks, if the 'x' parameter is nonzero then this effect slides the volume up by 'x' units (and stops at 64). If the 'y' param is nonzero then this param slides the volume down by 'y' units (and stops at 0). If both are nonzero, then you should slide up by 'x' and not slide down by 'y'.

/*************************************************************************************
 * Axy - Volume Slide
 *************************************************************************************/
MOD_Axy_Backdoor:
	push	{lr}
	b	1f
 
.thumb_func
MOD_Axy:
	beq	vs_exit				@ on tick0
1:	ldrb	r2, [r5, #MODCH_VOLUME]		@ read volume
	lsr	r1, r0, #4			@ test slide direction
	beq	vs_slide_down			@
	
vs_slide_up:
	add	r2, r1				@ add x to volume
	cmp	r2, #64				@ clip values above 64
	blt	vs_store			@
	mov	r2, #64				@
	b	vs_store			@

vs_slide_down:
	lsl	r0, #32-4			@ mask y
	lsr	r0, #32-4			@
	sub	r2, r0				@ subtract y from volume
	bpl	vs_store			@ clip values under 0
	mov	r2, #0				@

vs_store:
	strb	r2, [r5, #MODCH_VOLUME]		@ store volume
	strb	r2, [r4, #MV_Volume]		@

vs_exit:
	pop	{pc}

Notice the 'backdoor' there, we'll use this for effects 5xy and 6xy.

5xy - Volume Slide + Glissando, 6xy - Volume Slide + Vibrato

This function performs the glissando or vibrato effect (using its parameter stored in memory) and also does a volume slide with the current params.

/*************************************************************************************
 * 5xy - Volume Slide + Glissando
 *************************************************************************************/
.thumb_func
MOD_5xy:
	beq	1f				@ on nonzero ticks
	bl	MOD_Axy_Backdoor		@ do volume slide
	b	doGlissando			@ do glissando
1:	pop	{pc}

/*************************************************************************************
 * 6xy - Volume Slide + Glissando
 *************************************************************************************/
.thumb_func
MOD_6xy:
	beq	1b				@ on nonzero ticks
	bl	MOD_Axy_Backdoor		@ do volume slide
	b	doVibrato	

These two effects are probably a bit more difficult to implement if we didn't prepare ahead.

9xx - Sample Offset

I was really dreading this one, but it's not too hard to implement. This effect modifies the starting offset of the sample for a certain note.

C-5 01 000 <- play sample normally
... .. ...
C-5 01 902 <- play sample starting at offset 512

What we need to do is add an "x*256" offset to our sample calculation method. We'll add another byte variable to our structure (MV_Offset), and clear it before effect processing. The voice loading code needs to be modified now.

	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	r1, r1					@ *2 for real value
	add	r2, r2					@ *2 for real value
	
	b	sample_does_loop
sample_doesnt_loop:					@ loop disabled:
	ReadAmigaWord r2, r0, #SAMPLE_LENGTH, r1	@ copy sample length and clear loop
	mov	r1, #0					@ 0 loop
	add	r2, r2					@

sample_does_loop:

	strh	r1, [r6, #VOICE_LOOP]			@ set loop param
	ldrb	r3, [r4, #MV_Offset]			@ get sample offset * 256
	lsl	r3, #8					@
	cmp	r3, r2					@ zero it if it exceeds sample length
	blt	1f					@
	mov	r3, #0					@
1:	
	sub	r2, r3					@ subtract sample offset
	str	r2, [r6, #VOICE_REMAIN]			@ set remaining
	
	ldrb	r0, [r5, #MODCH_DSAMPLE]		@ copy data pointer to SOURCE
	bl	GetSampleDataPointer			@
	add	r0, r3					@ add sample offset
	str	r0, [r6, #VOICE_SOURCE]			@

Then a simple function to copy the parameter...

/*************************************************************************************
 * 9xx - Sample Offset
 *************************************************************************************/
.thumb_func
MOD_9xx:
	bne	1f
	strb	r0, [r4, #MV_Offset]
1:	pop	{pc}

Yay. This effect is pretty common, but not so common in chiptunes (guess why).

Bxx - Position Jump

This is one of those effects that changes the flow of the module. When this effect is processed, then instead of advancing to the next row when this one is done, we 'jump' to another position and resume playing from there. This effect is commonly used to create an song that loops forever. We need 2 more bytes in our VARS structure, MV_Jump, and MV_JumpRow. The second one won't be used until later (pattern break). Now, before incrementing the row position, we will check if MV_Jump is nonzero, if it isn't then we will jump to a new position.

	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

	ldrb	r2, [r4, #MV_Jump]		@ test for position jump
	cmp	r2, #0				@
	beq	1f				@
	sub	r0, r2, #1			@  do position jump
	ldrb	r1, [r4, #MV_JumpRow]		@
	bl	MOD_SetPosition			@
	pop	{r4-r7, pc}			@ return (thumb)
1:
	
	add	r1, #1				@
	cmp	r1, #64				@ exit if row < 64
	blt	mt_exit				@
	
	ldrb	r0, [r4, #MV_Pos]		@ increment position
	add	r0, #1				@
	mov	r1, #0
	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)

I also modified the MOD_SetPosition routine to accept a row value as the second parameter. We can code the effect now.

/*************************************************************************************
 * Bxx - Position Jump
 *************************************************************************************/
.thumb_func
MOD_Bxx:
	bne	1f				@ on tick 0:
	add	r0, #1				@
	strb	r0, [r4, #MV_Jump]		@
1:	pop	{pc}

There's a small bug here, "BFF" is supposed to jump to entry 255, but it will wrap to zero in our 8-bit variable. I don't expect any MOD to reach 256 orders though.

Dxx - Pattern Break

Ah, I've been waiting for this one :). With this one I won't have to hear that huge annoying pause in the beginning of SAC09.MOD anymore.

/*************************************************************************************
 * Dxx - Pattern Break
 *************************************************************************************/
.thumb_func
MOD_Dxx:
	bne	1f				@ on tick 0:
	ldrb	r1, [r4, #MV_Jump]		@ set jump = position + 1
	cmp	r1, #0				@ ONLY if its not set already
	bne	2f				@
	ldrb	r1, [r4, #MV_Pos]		@
	add	r1, #1+1			@
	strb	r1, [r4, #MV_Jump]		@
2:						@

	lsr	r1, r0, #4			@ convert value into hex
	mov	r2, #10				@
	mul	r1, r2				@
	lsl	r0, #32-4			@
	lsr	r0, #32-4			@
	add	r0, r1				@
	strb	r0, [r4, #MV_JumpRow]		@ set JumpRow

1:	pop	{pc}				@ return

Basically what this does is set the position jump variable to the current position + 1, and then sets the row number. We have to check if position jump was set already, because Bxx and Dxx can be used together to jump to a certain order with a row offset. It can also be used for silly things like a backward scrolling pattern. Now, some genius decided to store the value as BCD (0x23 = 23), so we need to convert this to a proper value (x*10 + y), and then store it in the JumpRow variable.

With this effect implemented, SAC09.MOD should be correctly rendering!

Exy - Extended Effects

Well, here is the last effect, which brings about another 15 effects... *scream*. We need another little jump table routine for this effect.

/*************************************************************************************
 * Exy - Extended Effects
 *************************************************************************************/
.thumb_func
MOD_Exy:
	lsr	r3, r0, #4			@ mask X
	lsl	r3, #1				@ and *2 for jump offset
	lsl	r0, #32-4			@ mask Y
	lsr	r0, #32-4			@
	
	cmp	r1, #0				@ zero flag = tick0

	add	pc, r3				@ add offset to pc
MOD_Unused:
	pop	{pc}				@

MOD_Exy_Jump_Table:
	b	MOD_Unused	@ E0 - Set Filter
	b	MOD_E1y		@ E1 - Fine Pitch Slide Up
	b	MOD_E2y		@ E2 - Fine Pitch Slide Down
	b	MOD_Unused	@ E3 - Glissando Control
	b	MOD_Unused	@ E4 - Vibrato Control
	b	MOD_Unused	@ E5 - Set Finetune
	b	MOD_E6y		@ E6 - Pattern Loop
	b	MOD_Unused	@ E7 - Tremolo Control
	b	MOD_Unused	@ E8 - Set Panning
	b	MOD_E9y		@ E9 - Retrigger Note
	b	MOD_EAy		@ EA - Fine Volume Slide Up
	b	MOD_EBy		@ EB - Fine Volume Slide Down
	b	MOD_ECy		@ EC - Note Cut
	b	MOD_EDy		@ ED - Note Delay
	b	MOD_EEy		@ EE - Pattern Delay
	b	MOD_Unused	@ EF - Unused Effect

Param x contains the index of the new effect. Six of the effects we won't bother with, because they are too much trouble and they are so rarely used that it will be a waste of effort.

E0y - Set Filter

This is a super old effect that was used to control the hardware filtering thingy on the amiga (or something). Don't bother with this effect.

E1y - Fine Pitch Slide Up, E2y - Fine Pitch Slide Down

These effects can reuse the code in 1xx/2xx, but instead of updating on nonzero ticks, these effects are only updated on tick 0.

/*************************************************************************************
 * E1y - Fine Pitch Slide Up
 *************************************************************************************/
.thumb_func
MOD_E1y:
	bne	1f
	b	doPitchUp
1:	pop	{pc}

/*************************************************************************************
 * E2y - Fine Pitch Slide Down
 *************************************************************************************/
.thumb_func
MOD_E2y:
	bne	1f
	b	doPitchDown
1:	pop	{pc}

doPitchUp/doPitchDown are new labels that I inserted right after the tick check in the 1xx/2xx code.

E3y - Glissando Control

What this effect does is changes the behavior of 3xx. It toggles whether to have 3xx do a smooth slide, or slide in semitones. I have never figured out a decent way to implement this. We won't bother with this effect.

E4y - Vibrato Control

This function changes the behavior of vibrato. It can change the waveform that is used (sine/ramp/square/random), and a few other things. This effect is so rarely used though, so we'll skip it.

E5y - Set Finetune

This effect changes the finetune for a certain instrument. I've only seen one MOD that uses this effect, and (as expected) it was really stupid. It used this effect to create fake vibrato by rapidly changing the finetune. This effect will be too much work to implement with our current setup, let's skip it.

E6y - Pattern Loop

Ok, here's another important effect. This effect can be used to loop a certain part of a pattern 'y' times.

We need three more bytes in our variables struct, MV_PLoopCount, MV_PLoopRow, and MV_PLoopJump. Actually, these variables are supposed to be in the channel structure, but that's just stupid. Some people (who enjoy breaking players) like to nest their pattern loop commands in different channels. That's something that we just won't support.

/*************************************************************************************
 * E6y - Pattern Loop
 *************************************************************************************/
.thumb_func
MOD_E6y:
	bne	1f				@ on tick 0:
	cmp	r0, #0				@   if param == 0
	bge	2f				@
	ldrb	r0, [r4, #MV_Row]		@   copy row#
	strb	r0, [r4, #MV_PLoopRow]		@
	pop	{pc}				@

2:	ldrb	r1, [r4, #MV_PLoopCount]	@   else
	cmp	r1, #0				@   if count == 0
	bne	2f				@
	add	r0, #1				@     count = param +1
	strb	r0, [r4, #MV_PLoopCount]	@
	mov	r0, #1				@
	strb	r0, [r4, #MV_PLoopJump]		@     enable jump
	pop	{pc}
2:						@   else
	sub	r1, #1				@     count --
	strb	r1, [r4, #MV_PLoopCount]	@
	strb	r1, [r4, #MV_PLoopJump]		@     jump if count != 0
1:	pop	{pc}				@

What this does (on tick 0 only) first is check if the param is zero. Param 0 indicates that this is the beginning of the pattern loop, so we mark down the row number (for jumping to later).

If it's not zero, then we check our counter value, if our count is zero, then we set it to "param+1" and enable the row jump. If it's not zero then we decrement it and enable the jump only if it's still not zero. This will effectively loop the section of the pattern 'y' times.

We need a small modification in our advancement routine (to change the row number).

...

	ldrb	r2, [r4, #MV_PLoopJump]		@ test for pattern loop
	cmp	r2, #0				@
	beq	1f				@
	mov	r2, #0				@ disable jump and set new row
	strb	r0, [r4, #MV_PLoopJump]		@
	ldrb	r1, [r4, #MV_PLoopRow]		@
	b	mt_exit				@
1:

	ldrb	r2, [r4, #MV_Jump]		@ test for position jump
	cmp	r2, #0				@
	beq	1f				@
	sub	r0, r2, #1			@  do position jump
	ldrb	r1, [r4, #MV_JumpRow]		@
	bl	MOD_SetPosition			@
	pop	{r4-r7, pc}			@ return (thumb)
1:
	
	add	r1, #1				@
	cmp	r1, #64				@ exit if row < 64
	blt	mt_exit				@
	
	ldrb	r0, [r4, #MV_Pos]		@ increment position
	add	r0, #1				@
	mov	r1, #0
	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)

E7y - Tremolo Control

This effect does the same thing as vibrato control, but it affects 7xy instead. Skip!

E8y - Set Panning

This is a mini version of 8xx, we don't support panning so we'll skip it.

E9y - Retrigger Note

A very important effect. This effect 'retriggers' the sample on certain ticks.

/*************************************************************************************
 * E9y - Retrigger Note
 *************************************************************************************/
.thumb_func
MOD_E9y:
	bne	1f				@ on tick0:
	swi	0x07				@ divide tick / param (r1/r0)
	cmp	r1, #0				@ if tick MOD param == 0
	bne	1f				@
	mov	r0, #UCFLAG_START		@
	orr	r7, r0				@ set start flag
1:	pop	{pc}

Using the divide function probably isn't very smart. But again, we're only dividing tiny values so it should finish quickly. We set the start flag if if ((tick MOD param) == 0). Remeber that r7 holds our update flags.

EAy - Fine Volume Slide Up, EBy - Fine Volume Slide Down

These two effects, like the pitch slides, can reuse the volume slide code.

/*************************************************************************************
 * EAy - Fine Volume Slide Up
 *************************************************************************************/
.thumb_func
MOD_EAy:
	bne	1f				@ on tick 0
	lsl	r0, #4				@ do "Ay0"
	b	MOD_Axy_Fine			@
1:	pop	{pc}				@

/*************************************************************************************
 * EBy - Fine Volume Slide Up
 *************************************************************************************/
.thumb_func
MOD_EBy:
	bne	1f				@ on tick 0
	b	MOD_Axy_Fine			@ do "A0y"
1:	pop	{pc}

Hacked up stuff like this is probably one of the advantages we have over C code. MOD_Axy_Fine is a new label that appears after the tick check in MOD_Axy.

ECy - Note Cut

This is a fairly used effect that 'cuts' the volume of a note when the tick count reaches a certain number.

/*************************************************************************************
 * ECy - Note Cut
 *************************************************************************************/
.thumb_func
MOD_ECy:
	cmp	r1, r0				@ if tick == param
	bne	1f				@
	mov	r0, #0				@ cut volume
	strb	r0, [r5, #MODCH_VOLUME]		@
	strb	r0, [r4, #MV_Volume]		@
1:	pop	{pc}				@

EDy - Note Delay

This effect is somewhat difficult to implement in more advanced module formats. It's not so bad in the MOD format though.

What it does is delays the start of a note until a certain tick.

C-5 01 000 <- start note on tick 0
... .. ...
C-5 01 SD3 <- start note on tick 3

What we'll do is add a 'delayed' flag to our update flags. We set the 'delayed' flag if the tick count is less than the param.

/*************************************************************************************
 * EDy - Note Delay
 *************************************************************************************/
.thumb_func
MOD_EDy:
	cmp	r1, r0				@ if tick < param
	bge	1f				@
	mov 	r0, #UCFLAG_DELAYED		@ delay note
	orr	r7, r0				@
1:	pop	{pc}

If UCFLAG_DELAYED is set, then we will skip the 'update voice' section of our code.

@---------------------------------------------------------------
@ update sound
@---------------------------------------------------------------
	lsr	r7, #1
	bcs	voice_delayed
	
	ldr	r0,=MOD_Channels		@ determine channel number
	sub	r0, r5, r0			@ (offset - base) / 16
	...

In case you're wondering I switched the value of UCFLAG_START so that we can check the delayed flag first.

.equ	UCFLAG_START, 2
.equ	UCFLAG_DELAYED, 1

EEy - Pattern Delay

Ah, the last effect! This effect delays the row progression for the duration of 'y' rows. We'll add one more byte to our variables struct. It should look like this now:

/**********************************************************
 * 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 #
MV_PattDelay:	.space 1	@ pattern delay variable
MV_Period:	.space 2	@ variable for channel processing
MV_Volume:	.space 1	@ variable for channel processing
MV_Offset:	.space 1	@ sample offset
MV_Jump:	.space 1	@ position jump
MV_JumpRow:	.space 1	@   row offset
MV_PLoopCount:	.space 1	@ pattern loop counter
MV_PLoopRow:	.space 1	@ pattern loop target row
MV_PLoopJump:	.space 1	@ pattern loop jump enable
.align 4
MV_Size:			@ size of variables structure

Now a small function to set the delay amount.

/*************************************************************************************
 * EEy - Pattern Delay
 *************************************************************************************/
.thumb_func
MOD_EEy:
	bne	1f				@ on tick 0:
	ldrb	r1, [r4, #MV_PattDelay]		@
	cmp	r1, #0				@ if pattdelay == 0 then
	bne	1f				@
	add	r0, #1				@ set counter
	strb	r0, [r4, #MV_PattDelay]		@
1:	pop	{pc}

And in the progression part...

	...
	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

	ldrb	r2, [r4, #MV_PattDelay]		@ test for pattern delay
	cmp	r2, #0				@
	beq	1f				@
	sub	r2, #1				@ decrement pattern delay counter
	strb	r2, [r4, #MV_PattDelay]		@
	bne	mt_exit				@ stop progression if not zero
1:	
	...	

One more thing is needed, we don't want to re-process pattern data when the row is delayed.

	...
	ldrb	r0, [r4, #MV_Tick]		@ parse pattern data on tick0
	cmp	r0, #0				@ 
	bne	dont_advance_pattern		@ 

	ldrb	r0, [r4, #MV_PattDelay]		@ dont read pattern on delay rows
	cmp	r0, #0				@
	bne	dont_advance_pattern		@
	...

That's it

The MOD player is now fully functional (well, except for those parts we skipped). Test a bunch of songs to make sure everything is working. Now it's time to polish the product!

Oops 1 bug, "strh r1, [r5, #MV_Period]" in glissando, should be r4.

Oops another bug, I used 'blt' instead of 'ble'.

	bl	GetSamplePointer			@ get sample
	ReadAmigaWord r1, r0, #SAMPLE_REPEATLEN, r2	@ if repeat length <= 1 then loop is disabled
	cmp	r1, #1					@
	ble	sample_doesnt_loop			@

A few more things, we want to reset some of the variables when we set the pattern, and we also want to reset the channel VAR on new notes.

	mov	r0, #0				@ reset variable
	strb	r0, [r1, #MV_Tick]
	strb	r0, [r1, #MV_Jump]
	strb	r0, [r1, #MV_JumpRow]
	strb	r0, [r1, #MV_PattDelay]
	strb	r0, [r1, #MV_PLoopCount]
	strb	r0, [r1, #MV_PLoopRow]
	strb	r0, [r1, #MV_PLoopJump]

	pop	{r0}				@ set row#
	strb	r0, [r1, #MV_Row]		@
	
	bx	lr

And..

	lsr	r6, #1				@ test note flag
	bcc	no_note				@

	mov	r0, #0				@
	strb	r0, [r5, #MODCH_VAR]		@ reset variable
	
Previous: Basic MOD PlaybackContentsNext: Profiling and Optimization