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.
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!
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!).
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 | |
| 0 | Pitch = OriginalPitch | |
| 1 | Pitch = OriginalPitch + 'x' semitones | |
| 2 | Pitch = OriginalPitch + 'y' semitones | |
| 3 | Pitch = OriginalPitch | |
| 4 | Pitch = OriginalPitch + 'x' semitones | |
| 5 | Pitch = OriginalPitch + 'y' semitones | |
| 6 | Pitch = 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.
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.
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.
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.
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.
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.
Our lame mixer doesn't support panning.
/*************************************************************************************
* 8xx - Set Panning
*************************************************************************************/
MOD_8xx:
pop {pc}
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.
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.
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).
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.
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!
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.
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.
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.
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.
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.
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.
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)
This effect does the same thing as vibrato control, but it affects 7xy instead. Skip!
This is a mini version of 8xx, we don't support panning so we'll skip it.
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.
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.
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} @
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
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 @ ...
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 Playback | Contents | Next: Profiling and Optimization |