One point to make before getting too deep into this is that effects are usually updated differently on row-ticks than they are on the ticks inbetween. Many of them (like volslide and vibrato) are not updated at all on row-ticks, and others (ex. set tempo, set volume) do something immediately, and then nothing on the mid-ticks.
Because of this, we'll actually make 2 tables of function pointers, one with the functions to update each effect on row-ticks, and one for inbetween ticks. To keep from having to make 2 functions for each one since most of them are either row-tick only or non-row only anyway, we'll make NULL a valid pointer in the table, and check for it before actually branching to the function. This way, we only write functions we need, and don't have to do an if(thisIsARowTick) in every one. Elegant as well as efficient (yay!).
Next is how to deal with things being done in the wrong order, and not having access to any local variables. For the ordering, we need to make some variables to specify what to do and what not to do. For example, we need a flag to specify wether to actually trigger the note or not, for things like note delay or tone porta (pitch slide to note, so keep playing the current note and only move toward the new one each tick). Then for things like that fine slide up (which is an immediate single tick's worth of pitch slide), we'll store a slide amount that gets initialized to 0, and then apply that after starting the note. This way, we can set it to the fine slide amount before the note is played, and still have the effect after. Sort of special-casing it, but not too ugly at least.
However, to keep things simple, we'll ignore these troublesome effects for now and deal with them next time.
The one problem with all these variables is that we have to pass them around to the effect functions, which is even worse when they're all in a function pointer table so they have to have matching parameters.
To make this as easy as using all the locals in the first place, we'll just shove everything that effects ever need access to into a structure and pass the address of that around. Then the effect functions only need a single argument, and if we work with the struct in place of the seperate local vars in our main updating, there's not even any data copying going on. Very good.
We'll call the structure MOD_UPDATE_VARS, because it's only used as local vars during updating. It looks like this right now:
typedef struct _MOD_UPDATE_VARS
{
MOD_CHANNEL *modChn; // Pointer to the current channel (sndMod.channel[curChn])
SOUND_CHANNEL *sndChn; // Corresponding mixer channel (sndChannel[curChn])
u8 note; // These 4 are just the local vars from MODProcessRow
u8 sample;
u8 effect;
u8 param;
// If TRUE, play the note after effect processing is done (if there is one).
// If FALSE, never play note either way. Needed by things like note delay to
// prevent note from being played immediately. Initially TRUE on row-ticks,
// FALSE on middle ticks, but can be set TRUE during middle ticks for note
// delay to finally play the note
BOOL playNote;
// If TRUE, set the mixer channel volume to the MOD channel volume after effect
// processing. If FALSE, never change the current mix volume. Initially FALSE on
// row and mid ticks. Set TRUE if new sample specified, or volume effects used
BOOL setMixChnVol;
// Like volume, recalculates mix increment after effect processing. Initally FALSE on
// row and mid ticks. Set TRUE by note getting played, or by pitch-modifying effects
BOOL setMixChnFreq;
} MOD_UPDATE_VARS;
However, beause I hate having booleans all over taking up 8 bits each when they really only need 1, we'll combine those 3 flags into one variable. We'll name it updateFlags, and make a nice enum to remember which is which:
typedef enum _MOD_UPDATE_FLAGS
{
MOD_UPD_FLG_PLAY_NOTE = BIT00,
MOD_UPD_FLG_SET_VOL = BIT01,
MOD_UPD_FLG_SET_FREQ = BIT02,
} MOD_UPDATE_FLAGS;
typedef struct _MOD_UPDATE_VARS
{
MOD_CHANNEL *modChn; // Pointer to the current channel (sndMod.channel[curChn])
SOUND_CHANNEL *sndChn; // Corresponding mixer channel (sndChannel[curChn])
u8 note; // These 4 are just the local vars from MODProcessRow
u8 sample;
u8 effect;
u8 param;
u8 updateFlags; // MOD_UPDATE_FLAGS
} MOD_UPDATE_VARS;
Now that the variable accessing problem is sorted out, we can create the function pointer table for the effects and get the MOD updating set up to call them, and to work with the update flags.
The function pointers will each take a pointer to a MOD_UPDATE_VARS, and return nothing. Since we're doing 2 tables, one for row-ticks and one for mid-ticks, we'll make another enum for selecting the table. Result:
typedef void (*MOD_EFFECT_FUNC_PTR)(MOD_UPDATE_VARS *vars);
typedef enum _MOD_EFFECT_TABLE
{
MOD_EFFECT_TABLE_ROW,
MOD_EFFECT_TABLE_MID,
MOD_EFFECT_TABLE_NUM,
} MOD_EFFECT_TABLE;
static const MOD_EFFECT_FUNC_PTR modEffectTable[MOD_EFFECT_TABLE_NUM][16] =
{
{ // MOD_EFFECT_TABLE_ROW
NULL, // 0x0: Arpeggio
NULL, // 0x1: Porta up
NULL, // 0x2: Porta down
NULL, // 0x3: Tone porta
NULL, // 0x4: Vibrato
NULL, // 0x5: Volslide+Tone porta
NULL, // 0x6: Volslide+Vibrato
NULL, // 0x7: Tremolo
NULL, // 0x8: Set panning
NULL, // 0x9: Sample offset
NULL, // 0xA: Volume slide
NULL, // 0xB: Jump to order
NULL, // 0xC: Set volume
NULL, // 0xD: Break to row
NULL, // 0xE: Special (more on this later)
NULL // 0xF: Speed/Tempo
},
{ // MOD_EFFECT_TABLE_MID
NULL, // 0x0: Arpeggio
... same as above
}
};
Now we can just plug in our effect functions as we write them and magically they work!
...as soon as we add the code to call them in MODProcessRow.
One tricky thing is that as you can see in the table, all 16 possible effects are used, so we have no extra value to use for no-effect like we do with notes and samples. That is, unless we wanted to use more than 4 bits to store it, which we don't because we'll be compressing patterns later on, and it's worth a little trouble to save that one extra bit.
The way MOD handles it, which we will use too, is to say that effect 0 (arpeggio) with parameter 0 means no-effect. Most effects take a 0 parameter to mean that they should use the last parameter used with that effect, but that's really just to make things more convenient when writing music, as you can just copy/paste the parameter every time to get the same result. Since arpeggio is not supposed to do that, all is well.
Anyway, all we have to do is check if both are 0, which is annoying but no big deal. To make it one microscopic bit faster, we could read effect as a u16, since param comes immediately after it. That way we could check both at once. Totally unimportant optimization, but I like optimizing annoying things to at least have some fun putting them in. I won't do it here, but if you do, be careful that effect is 16-bit aligned, or you'll accidentally get effect and whatever byte comes before it, due to the funky behavior of ARM processors with unaligned reads.
We'll also add two more variables to the MOD_CHANNEL structure to store the effect and param for updating mid-ticks:
typedef struct _MOD_CHANNEL
{
u32 frequency; // Current frequency of note being played, in Hz
u8 sample; // Last sample used on this channel
u8 vol; // Current volume
u8 effect; // Current effect running (set to 0 on row tick if no effect/parameter)
u8 param; // Current parameter (set to 0 row tick if no effect/parameter)
} MOD_CHANNEL;
There are still quite a few more variables left to add once we get started on coding the individual effects, but we'll get to those later.
So now all we need to do is modify MODProcessRow to call the effect function from the table, and handle update flags. Before that though, here's something that will make things easier later on. We'll need to initialize our MOD_UPDATE_VARS struct before we use it, so rather than going through and setting everything one at a time in code, we'll make a pre-set MOD_UPDATE_VARS struct in ROM to copy in. Actually 2 structs, one for row-ticks and one for mid-ticks:
static const MOD_UPDATE_VARS modDefaultVars[MOD_EFFECT_TABLE_NUM] =
{
{ // MOD_EFFECT_TABLE_ROW
NULL, // modChn
NULL, // sndChn
MOD_NO_NOTE, // note
MOD_NO_SAMPLE, // sample
0, // effect
0, // param
// Play note if there is one, but still do nothing if there isn't.
// Don't set volume or frequency unless something specifically needs it.
MOD_UPD_FLG_PLAY_NOTE // updateFlags
},
{ // MOD_EFFECT_TABLE_MID
NULL, // modChn
NULL, // sndChn
MOD_NO_NOTE, // note
MOD_NO_SAMPLE, // sample
0, // effect
0, // param
// Don't do anything unless something specifically needs it
0 // updateFlags
}
};
Don't worry too much about these for now. updateFlags is the only one that really matters, since the others are overwritten anyway. This is so that every time I add a variable later on, I can just tell the initial values on row and mid ticks, and you'll know they go right into the table.
Here is the new MODProcessRow to handle updateFlags. Gray is unchanged code (except for accessing vars struct instead of plain locals, which makes no difference), green is changed, but still the basic idea from before, and yellow is completely new code.
static void MODProcessRow()
{
s32 curChannel;
for(curChannel = 0; curChannel < SND_MAX_CHANNELS; curChannel++)
{
// Quick initialization, with values for row-tick
MOD_UPDATE_VARS vars = modDefaultVars[MOD_EFFECT_TABLE_ROW];
vars.modChn = &sndMod.channel[curChannel];
vars.sndChn = &sndChannel[curChannel];
// Read in the pattern data, advancing rowPtr to the next channel in the process
vars.note = *sndMod.rowPtr++;
vars.sample = *sndMod.rowPtr++;
vars.effect = *sndMod.rowPtr++;
vars.param = *sndMod.rowPtr++;
// Set these for the mid-ticks
vars.modChn->effect = vars.effect;
vars.modChn->param = vars.param;
// Set sample and channel volume BEFORE effect processing, because
// some effects read the sample from the MOD channel rather than vars,
// and some need to override the default volume
if(vars.sample != MOD_NO_SAMPLE) // Never set local to memory anymore (explained below)
{
// Set sample memory
vars.modChn->sample = vars.sample;
vars.modChn->vol = sndMod.sample[vars.sample].vol;
// Don't set mixer channel volume until after effect processing
//vars.sndChn->vol = vars.modChn->vol;
vars.updateFlags |= MOD_UPD_FLG_SET_VOL;
}
// Effect 0 is arpeggio, but is also used as no-effect if the param is 0 too.
// This is where all effects do their work, the rest is just to support this one call.
if( (vars.effect != 0 || vars.param != 0) &&
(modEffectTable[MOD_EFFECT_TABLE_ROW][vars.effect] != NULL) )
(*modEffectTable[MOD_EFFECT_TABLE_ROW][vars.effect])(&vars);
// MOD_UPD_FLG_PLAY_NOTE is set by default, so play the note if there
// is one, and if MOD_UPD_FLG_PLAY_NOTE hasn't been specifically unset.
if( (vars.note != MOD_NO_NOTE) &&
(vars.updateFlags & MOD_UPD_FLG_PLAY_NOTE) )
MODPlayNote(&vars);
// Set the mixer volume like the block above that handles new samples used to do
if(vars.updateFlags & MOD_UPD_FLG_SET_VOL)
vars.sndChn->vol = vars.modChn->vol;
// Like MODPlayNote used to do
if(vars.updateFlags & MOD_UPD_FLG_SET_FREQ)
vars.sndChn->inc = vars.modChn->frequency * sndVars.rcpMixFreq >> 16;
}
} // MODProcessRow
This still accomplishes the same thing as the original version as long as none of those effects do anything, aside from that one little modification I made on the sample memory.
If you remember, I had it set the local 'sample' to the MOD channel's sample memory if there was no sample specified in the pattern. While this does work, I thought about it for a while and came to the conclusion that it's unnecessary. Anytime something wants to use the sample memory, it should do it explicitly. The local should also remain as being exactly what was loaded from the pattern data, to match the other locals.
The placement of the call to the effect functions here is very important. It's not too difficult to see here that the sample volume needs to be set before the call, and note playing needs to be done after, but what if we were writing an S3M or XM player? Not only do you have a lot more in general to think about, but you also have things like global volume running around causing trouble.
One problem that can come up in XM is if you mix volume envelopes and note delay. We already set the new sample and channel volume up above in the sample block, only skipped setting them on the mixer channel. This is fine for MOD because nothing else will need to change the mixer channel settings during the ticks until the note triggers, but volume envelopes still do need updating every tick, and do change the mixer channel volume. Since we overwrote the sample memory, we don't even know what volume envlope was being used anymore, not to mention the old note volume is gone too.
For any format other than MOD, I would recommend special-casing note delay to completely prevent processing of the channel until later. You'll need to save the note/sample/effect/param locals loaded from the pattern though, or maybe remember the location in the pattern that you loaded them from.
Moving on, next up in the code is MODPlayNote. For now there's only a couple of little differences, so here it is with the lengthy comments stripped out of the unchanged parts:
static void MODPlayNote(MOD_UPDATE_VARS *vars)
{
const SAMPLE_HEADER *sample;
if(vars->modChn->sample == MOD_NO_SAMPLE)
{
return;
}
// This used to be local sample, but uses the sample memory now
// because of that change earlier. Also, vars has pointers to
// modChn and sndChn set up, so no need for the old locals
sample = &sndMod.sample[vars->modChn->sample];
vars->modChn->frequency = noteFreqTable[sample->finetune*60 + vars->note];
// Set up the mixer channel
vars->sndChn->data = sample->smpData;
vars->sndChn->pos = 0;
// Let update flags take care of setting the inc, just to recycle code
// because it may also need to be set by effects without playing a note
//vars->sndChn->inc = vars->modChn->frequency * sndVars.rcpMixFreq >> 16;
vars->updateFlags |= MOD_UPD_FLG_SET_FREQ;
vars->sndChn->length = (sample->loopLength != 0 ?
sample->loopStart + sample->loopLength :
sample->length) << 13;
vars->sndChn->loopLength = sample->loopLength << 13;
} // MODPlayNote
Still accomplishes the same thing as before.
That does it for the row-ticks, so now we need to fill in that 'else' in MODUpdate, which says that we'll update effects there later. Well now we will, and it happens to look quite a lot like a stripped down MODProcessRow. Colored to show similarities to said function:
static void MODUpdateEffects()
{
s32 curChannel;
for(curChannel = 0; curChannel < SND_MAX_CHANNELS; curChannel++)
{
// Bail if there's no effect to update
if( sndMod.channel[curChannel].effect != 0 ||
sndMod.channel[curChannel].param != 0 )
{
// Initialize with mid-tick values now
MOD_UPDATE_VARS vars = modDefaultVars[MOD_EFFECT_TABLE_MID];
vars.modChn = &sndMod.channel[curChannel];
vars.sndChn = &sndChannel[curChannel];
// Already made sure there was an effect, so just check the function.
// Notice that we're using the mid-tick table now.
if(modEffectTable[MOD_EFFECT_TABLE_MID][vars.modChn->effect] != NULL)
(*modEffectTable[MOD_EFFECT_TABLE_MID][vars.modChn->effect])(&vars);
if( (vars.note != MOD_NO_NOTE) &&
(vars.updateFlags & MOD_UPD_FLG_PLAY_NOTE) )
MODPlayNote(&vars);
if(vars.updateFlags & MOD_UPD_FLG_SET_VOL)
vars.sndChn->vol = vars.modChn->vol;
if(vars.updateFlags & MOD_UPD_FLG_SET_FREQ)
vars.sndChn->inc = vars.modChn->frequency * sndVars.rcpMixFreq >> 16;
}
}
} // MODUpdateEffects
As you can see here, those last 3 statements are exact replicas of what was in MODProcessRow, and therefore a waste of space. Chop, chop, put it in a function.
static void MODHandleUpdateFlags(MOD_UPDATE_VARS *vars)
{
if( (vars->note != MOD_NO_NOTE) &&
(vars->updateFlags & MOD_UPD_FLG_PLAY_NOTE) )
MODPlayNote(vars);
if(vars->updateFlags & MOD_UPD_FLG_SET_VOL)
vars->sndChn->vol = vars->modChn->vol;
if(vars->updateFlags & MOD_UPD_FLG_SET_FREQ)
vars->sndChn->inc = vars->modChn->frequency * sndVars.rcpMixFreq >> 16;
} // MODHandleUpdateFlags
Doesn't that make you feel all warm and fuzzy inside?
Guess what? That concludes the preparations. Now all that remains is writing a bunch of little functions, and cluttering our wonderful player with those few dreaded special-cases. Horror! But it will sound good.
Most of that will be taken care of next time, but in order to show the beauty of what we've set up, and to make the song sound much more like it's supposed to, we'll do one quick and easy effect first.