Ok, back to what we'll actually use.
First, we'll make another little helper function to slide the period and clip it to some minumum and maximum values. I grabbed the lowest note at the lowest finetune (C-0, finetune -8) and highest note at highest finetune (B-4, finetune 7) from the notePeriodTable, but they're pretty arbitrary.
#define MOD_PERIOD_MIN 53 // Highest pitch
#define MOD_PERIOD_MAX 1814 // Lowest pitch
static s32 MODPitchSlide(s32 period, s32 slide)
{
period += slide;
if (period > MOD_PERIOD_MAX)
period = MOD_PERIOD_MAX;
else if (period < MOD_PERIOD_MIN)
period = MOD_PERIOD_MIN;
return period;
}
This can be recycled for all our pitch sliding needs. I made it use signed variables so it won't ever wrap around, since the lower limit check isn't 0 this time. I'd kind of rather keep it unsigned because we'll always be using it with unsigneds, but oh well.
Porta up and down (0x1, 0x2)
Now to put that helper function to use. We'll need a new variable in MOD_CHANNEL to remember the speed incase of 0 parameters. These two share the same memory, so for example if you slide UP at 10 periods per tick, and then slide DOWN with a 0 parameter, it will go down at 10 too. To us, that means only one variable to store:
typedef struct _MOD_CHANNEL
{
u32 period; // Current period of note being played
...
s8 volslideSpeed; // Current volslide speed
u8 portaSpeed; // Current pitch slide up/down speed
} MOD_CHANNEL;
Nothing to it. Now for the row-tick function to check the parameter and set the variable if needed:
static void MODFXPortaRow(MOD_UPDATE_VARS *vars)
{
if(vars->param != 0)
{
vars->modChn->portaSpeed = vars->param;
}
// Else use the speed like it is
}
Boy, that was easy. Notice that I didn't even specify wether that's porta up or porta down. Since both share the same speed memory, there wouldn't be any difference if we made another function.
However, the mid-ticks are different, because it needs to add or subtract the speed depending on the effect. We could make a single function and check the effect number, but I'll make two:
static void MODFXPortaUpMid(MOD_UPDATE_VARS *vars)
{
vars->modChn->period =
MODPitchSlide(vars->modChn->period, -vars->modChn->portaSpeed); // Negative=higher pitch
vars->updateFlags |= MOD_UPD_FLG_SET_FREQ;
}
static void MODFXPortaDownMid(MOD_UPDATE_VARS *vars)
{
vars->modChn->period =
MODPitchSlide(vars->modChn->period, vars->modChn->portaSpeed); // Positive=lower pitch
vars->updateFlags |= MOD_UPD_FLG_SET_FREQ;
}
Then we'll put all these in the table and move on with our lives (remember, the row function can be used for both):
static const MOD_EFFECT_FUNC_PTR modEffectTable[MOD_EFFECT_TABLE_NUM][16] =
{
{ // MOD_EFFECT_TABLE_ROW
...
MODFXPortaRow, // 0x1: Pitch slide up
MODFXPortaRow, // 0x2: Pitch slide down
...
},
{ // MOD_EFFECT_TABLE_MID
...
MODFXPortaUpMid, // 0x1: Pitch slide up
MODFXPortaDownMid, // 0x2: Pitch slide down
...
}
};
Tone porta (0x3)
This one is a little trickier. Our goal is to slide up or down at param periods per tick, until we get to the note that was played along with the effect. The note becomes sort of a second parameter, and doesn't get played like a normal note. You can also just put a bunch of tone porta effects after a note to keep sliding toward it, like so:
Note Smp FX/Param
C-1 01 ---
C-2 01 310
--- -- 300
--- -- 300
--- -- 300
That would play the note C-1, and then slide toward C-2 at 10 periods per tick for 4 rows.
The point is, it needs to remember what note it was sliding toward, which means we actually need 2 new variables in MOD_CHANNEL for this one.
typedef struct _MOD_CHANNEL
{
u32 period; // Current period of note being played
...
s8 volslideSpeed; // Current volslide speed
u8 portaSpeed; // Current pitch slide up/down speed
u8 tonePortaNote; // Current note to slide toward
u8 tonePortaSpeed; // Speed to slide toward it
} MOD_CHANNEL;
For the row-tick, we just need to store the note/speed if they're there, and stop the note from playing (else there wouldn't be any sliding to do!)
static void MODFXTonePortaRow(MOD_UPDATE_VARS *vars)
{
if(vars->note != MOD_NO_NOTE)
vars->modChn->tonePortaNote = vars->note;
// Else use the note like it is
if(vars->param != 0)
vars->modChn->tonePortaSpeed = vars->param;
// Else use the speed like it is
vars->modChn->updateFlags &= ~MOD_UPD_FLG_PLAY_NOTE;
}
For mid-ticks, all we have to do is check wether the current period is less than or greater than the period of the target note, and slide up or down accordingly. Of course, we want to stop once we get there so we need to check if we went too far:
static void MODFXTonePortaMid(MOD_UPDATE_VARS *vars)
{
// Get the note frequency same as we do in MODPlayNote
u16 targetPeriod = notePeriodTable[
vars->modChn->finetune*60 + vars->modChn->tonePortaNote];
if(vars->modChn->period < targetPeriod) // Need to slide up
{
vars->modChn->period = MODPitchSlide(
vars->modChn->period, vars->modChn->tonePortaSpeed); // Positive=lower pitch
if(vars->modChn->period > targetPeriod) // Don't go too far
vars->modChn->period = targetPeriod;
}
else if (vars->modChn->period > targetPeriod) // Need to slide down
{
vars->modChn->period = MODPitchSlide(
vars->modChn->period, -vars->modChn->tonePortaSpeed); // Negative=higher pitch
if(vars->modChn->period < targetPeriod) // Don't go too far
vars->modChn->period = targetPeriod;
}
// Else we're already at the target period
}
Tone porta+volslide (0x5)
This is a combo effect, which behaves exactly like a regular volslide, and a tone porta with param 0 at the same time. The parameter goes with the volslide part, so the tone porta speed has to have been set by a previous effect. We'll do this the easy way and reuse the tone porta and volslide row-tick functions, and trick tone porta into thinking it got a 0 parameter. Mid-ticks just need to call both updates:
static void MODFXTonePortaVolslideRow(MOD_UPDATE_VARS *vars)
{
// Param goes with the volslide part, tone porta just continues
// So handle volslide like normal
MODFXVolslideRow(vars);
// Now trick the tone porta into thinking there was a
// 0 'continue' param. This update vars param won't be
// used again, so changing it won't hurt anything
vars->param = 0;
MODFXTonePortaRow(vars);
}
static void MODFXTonePortaVolslideMid(MOD_UPDATE_VARS *vars)
{
MODFXVolslideMid(vars);
MODFXTonePortaMid(vars);
}
Nothing to it.
Jump to order (0xB), and break to row (0xD)
These effects are pretty similar, so I'll do them both at once to save space. 0xB is useful to create looping songs. Just put it at the end of the song to jump back to whatever pattern you want to start repeating from. The actual jump doesn't take place until the next row is about to play, so any notes on the same row as this effect will still be played. 0xD is similar, except it just breaks to the next order, but starts playing param rows into it.
To make these happen, we'll add some simple variables to the MOD struct. The first one is breakOrder which stores the next order to play. Normally it will be curOrder+1, but 0xB can change it. The next one is breakRow, normally 0.
typedef struct _MOD
{
...
u8 nextOrder; // Normally curOrder+1, unless an effect changes it
u8 breakRow; // Starting row when order changes (normally 0)
...
} MOD;
Then in MODUpdate, instead of just incrementing the current order and setting the row to 0, we set the order to nextOrder, and row to breakRow. Then set nextOrder to curOrder+1, and breakRow to 0, so it ends up acting just like it did before. The difference is that the effects can change those variables to make it do what THEY want.
I'll also take this opportunity to clean things up a bit. Where originally in MODUpdate we had this ugly block of code with a bug in it:
if(++sndMod.curRow >= MOD_PATTERN_ROWS)
{
sndMod.curRow = 0;
if(++sndMod.curOrder >= sndMod.orderCount)
{
s32 i;
for(i = 0; i < SND_MAX_CHANNELS; i++)
sndChannel[i].data = NULL;
sndMod.state = MOD_STATE_STOP;
return;
}
else
{
sndMod.rowPtr = sndMod.pattern[sndMod.order[sndMod.curOrder]];
}
}
We shall now have this pretty block of code, without the bug:
if(sndMod.curRow++ >= MOD_PATTERN_ROWS)
{
if(MODSeek(sndMod.nextOrder, sndMod.breakRow) == FALSE)
return;
sndMod.curRow++;
}
This uses the new function MODSeek to advance to the next order, instead of doing it directly. MODSeek sets the position you tell it to, sets nextOrder to curOrder+1 and breakRow to 0, and sets the rowPtr. If the order you pass in is past the end of the song, it stops the song and returns FALSE, which we check for here and bail out if so.
The next line, sndMod.curRow++, is the fix for the bug I mentioned. Without that, it skips the last row of the first pattern.
Imagine if our patterns were only 3 rows long. When first starting the song, you set curRow to 0, and then call MODUpdate. It increments curRow to 1 and checks if it's >= 3 (pattern length). It's not, so play 1st row of data.
Next tick, inc curRow to 2, check >= 3, it isn't so play 2nd row of data.
Next tick, inc curRow to 3, check >= 3, it is, so break to next order.
Looking back on that, only 2 rows were actually played, not 3. Furthermore, on the break, it would reset curRow to 0 and then play the first row of the new pattern. The reason it's sneaky is because the break happens after curRow's increment, while on the initial pattern example above, we would come in from the start and increment curRow to 1 before the first row is played.
Anyway, down at the end of MODSeek, we'll set nextOrder and breakRow to their normal values:
static BOOL MODSeek(u32 order, u32 row)
{
...
sndMod.nextOrder = sndMod.curOrder + 1;
sndMod.breakRow = 0;
sndMod.rowPtr = sndMod.pattern[sndMod.order[sndMod.curOrder]] +
sndMod.curRow*4*SND_MAX_CHANNELS; // 4 bytes/channel/row
return TRUE; // TRUE = continue playing
} // MODSeek
And in MODUpdate, seek to these positions when advancing to the next order:
void MODUpdate()
{
if(++sndMod.tick >= sndMod.speed)
{18664365703
sndMod.tick = 0;
if(sndMod.curRow++ >= MOD_PATTERN_ROWS)
{
if(MODSeek(sndMod.nextOrder, sndMod.breakRow) == FALSE)
return; // FALSE = song ended
sndMod.curRow++;
}
...
So now all that's left is the effect functions themselves:
static void MODFXJumpToOrder(MOD_UPDATE_VARS *vars)
{
sndMod.nextOrder = vars->param;
sndMod.curRow = MOD_PATTERN_ROWS; // Break next update
}
static void MODFXBreakToRow(MOD_UPDATE_VARS *vars)
{
sndMod.breakRow = vars->param;
sndMod.curRow = MOD_PATTERN_ROWS; // Break next update
}
Into the function table (row-tick only) and that's that.
Set panning (0x8 and 0xE8)
Our mixer is mono-only, so we have no panning variables. If we did though, MOD_CHANNEL would have a u8 called pan, and it would be set to the parameter of effect 0x8 (0 being left, 255 being right). Effect 0xE8 does the same thing, but not as good. It has a 4-bit parameter, so 0 is left, and 15 is right. Just multiply by 16 to get it up to the same range as 0x8.
Then to calculate the actual left and right volume based on a 255 position pan:
leftVol = vol * (255-pan) >> 8;
rightVol = vol * pan >> 8;
Technically those should be divided by 255 instead of shifted 8 (div 256), but it's such a small difference it's not worth the trouble.
Fine pitch slides (0xE1, 0xE2)
These are one of the irritating special case-needing effects I mentioned in day 6 of the series. They act like a regular pitch slide, except only take effect on row-ticks, and do nothing on mid-ticks. The problem is that if you play a note along with a fine slide, the slide still needs to change the new note's frequency. We have our code set up to call the effect functions before the note sets its new frequency, so in comes the special case to fix it.
We'll add a variable to MOD_UPDATE_VARS (initialized to 0 in the modDefaultVars structs), and set it to the fine slide amount in the effect, and apply it in MODHandleUpdateFlags after the note gets played.
typedef struct _MOD_UPDATE_VARS
{
...
s8 fineSlide; // Slide amount applied after note is played
} MOD_UPDATE_VARS;
void MODFXSpecialRow(MOD_UPDATE_VARS *vars)
{
switch(vars->modChn->param >> 4)
{
...
case 0x1: // Fine slide up
vars->fineSlide = -(vars->modChn->param & 0xf); // Negative=higher pitch
break;
case 0x2: // Fine slide down
vars->fineSlide = vars->modChn->param & 0xf; // Positive=lower pitch
break;
...
}
}
static void MODHandleUpdateFlags(MOD_UPDATE_VARS *vars)
{
...
if( (vars.note != MOD_NO_NOTE) &&
(vars.updateFlags & MOD_UPD_FLG_PLAY_NOTE) )
{
MODPlayNote(&vars);
}
// Apply the fine slide if needed (this will set MOD_UPD_FLG_SET_FREQ too)
if(vars.fineSlide != 0)
{
MODPitchSlide(vars.modChn, vars.fineSlide);
vars->updateFlags |= MOD_UPD_FLG_SET_FREQ;
}
...
}
It's not THAT bad, just a little distracting from the main work being done.
Glissando (0xE3)
This is supposed to make pitch slides only slide in full notes so it sounds like you're sliding your finger along a keyboard, rather than a smooth change in pitch. I've never actually implemented it before because MODPlug tracker is the only player I've ever seen actually do it, and it only does it on tone porta effects (not 0x1 and 0x2), so I'm not quite sure if that's the correct behavior, and there's so little support for it elsewhere that very few songs will probably ever use it. Maybe I'll add it to the player sometime, but for now I won't bother.
Vibrato (0x4) and tremolo (0x7)
Vibrato sort of wiggles the frequency up and down in a sine wave pattern, and tremolo does the same except on the volume. The parameter is read as 0xSD where S is the speed and D is the depth. We use a table to find the actual amount to modify the frequency. It looks like this:
static const s8 vibratoSineTab[32] =
{
0, 24, 49, 74, 97,120,141,161,
180,197,212,224,235,244,250,253,
255,253,250,244,235,224,212,197,
180,161,141,120, 97, 74, 49, 24
};
The first half of a sine wave. For the second half, we just subtract these values inetad of adding them.
The goal is to have a final range of + or - 32 MOD periods at maximum depth. Actually a little less, because the max depth is 15, not 16, and the table only goes to 255 instead of 256, but close enough.
We'll actually go for a final range of +-64, because we'll be using the same code for both vibrato and tremolo, and tremolo needs the higher range. We'll just divide by 2 when using it for vibrato.
So, to get +-64, when you multiply the 4-bit depth by the 8-bit table, you get 12 bits, so shift down by 6 to get to 6 bits left (0-64). Since we just subtract this value for the second half of the wave, we have our desired range.
The speed parameter is how many entries in the table to step over on each tick. For example, at a speed of 8, it would go through the table like 0,180,255,180,0,-180,-255,-180, then repeat. For this, we'll keep a vibratoTick variable and add the speed to it each tick, and use that as the index into the table (negating it if it's >32, and wrapping to 0 when it hits 64).
To complicate things, there are 3 OTHER wave types you can use instead of the sine. They all still have the final range of +-64 at max depth, and all repeat after 64 ticks. The types are: Sine, Square, Ramp down, and Random.
We just did sine. For square wave, all we have to do is move the pitch up or down by depth*4 (getting that +-64 range). Up on the first 32 ticks, down on the second 32.
For ramp down, we want to slide +64 at tick 0, and go smoothly down to -64 by tick 64. Looks something like this:
\ |\ |\ |\
\ | \ | \ | \
\| \| \| \
We'll calculate it mathematically instead of using a table, because it's so easy: subtract the tick from 32, and multiply by 2. Without the *2, at tick 0, you get 32-0=32; at 32, you get 32-32=0; at 64, 32-64=-32. Then multiply by depth to get +-512 and shift down 3 bits to get +-64.
Random can be done just about any way you want, because it's random :)
Also, the speed really has no meaning here because there's a new random number every tick either way. The easiest (but not the best sounding) way to do it is to make a table of 64 random numbers and step through it like normal. We'll make them s8's, so -128 to +127. Then multiply by the 4-bit depth for up to +-2048, then shift down 5 bits to +-64.
Normally, the vibrato tick gets reset to 0 when a new note is played, but there's a flag you can set so the tick just continues from where it was. Out of laziness, we'll call it noRetrig, because the default is that you DO reset the tick, so when noRetrig is false and a new note is played, we retrigger. Then we don't have to initialize it anywhere, because the whole MOD struct is memset'd to 0 when loading :)
So, to make the code shareable between vibrato and tremolo, we'll make a struct for all the related variables, and declare two of them:
typedef struct _MOD_VIBRATO_PARAMS
{
s8 slide; // Ranges +-64. Vibrato needs to shift down 1 bit
u8 speed : 4; // Added to tick each update
u8 depth : 4; // Multiplied by table value, and shifted down
u8 tick : 6; // Position in table. Full cycle is 64 ticks
u8 waveform : 2; // Type of vibration. See MOD_WAVEFORM in Sound.c
u8 noRetrig : 1; // If FALSE, reset tick to 0 when a new note is played
u8 pad : 7; // Unused, align to 4 bytes
} MOD_VIBRATO_PARAMS;
typedef struct _MOD_CHANNEL
{
...
MOD_VIBRATO_PARAMS vibrato; // Vibrates frequency
MOD_VIBRATO_PARAMS tremolo; // Vibrates volume
} MOD_CHANNEL;
Then we'll make some 'helpers', which are really more of the whole effect. They basically do exactly what the long explanation up top says.
static void MODInitVibrato(MOD_UPDATE_VARS *vars, MOD_VIBRATO_PARAMS *vibrato)
{
if(vars->param & 0xf != 0)
vibrato->depth = vars->param & 0xf;
if(vars->param >> 4 != 0)
vibrato->speed = vars->param >> 4;
if(vibrato->noRetrig == FALSE && vars->note != MOD_NO_NOTE)
vibrato->tick = 0;
}
static void MODUpdateVibrato(MOD_VIBRATO_PARAMS *vibrato)
{
// Increment the tick. All wave types use a cycle of 0-63 on
// the tick. Since it's a 6-bit bitfield, it wraps automatically.
vibrato->tick += vibrato->speed;
//vibrato->tick &= 63; no need for this
switch(vibrato->waveform)
{
case MOD_WAVEFORM_SINE:
vibrato->slide = vibratoSineTab[vibrato->tick]*vibrato->depth >> 6;
if(vibrato->tick >= 32)
vibrato->slide = -vibrato->slide;
break;
case MOD_WAVEFORM_RAMP:
vibrato->slide = (32 - vibrato->tick)*vibrato->depth >> 3;
break;
case MOD_WAVEFORM_SQUARE:
vibrato->slide = vibrato->depth << 2;
if(vibrato->tick >= 32)
vibrato->slide = -vibrato->slide;
break;
case MOD_WAVEFORM_RANDOM:
vibrato->slide = vibratoRandomTab[vibrato->tick]*vibrato->depth >> 5;
break;
}
} // MODUpdateVibrato
Then the effects themselves just call those functions, passing in either &vars->modChn->vibrato or &vars->modChn->tremolo, and then set the MOD_UPD_FLG_SET_FREQ or VOL, respectively. So simple it's not with the space.
Then we need to apply the slide amounts without modifying the actual period/volume memory. To do that, we'll go to where those are set to the mixer channels, MODHandleUpdateFlags, and create some temporary versions that we can modify:
static void MODHandleUpdateFlags(MOD_UPDATE_VARS *vars)
{
if(vars->updateFlags & MOD_UPD_FLG_SET_VOL)
{
// Temporary volume to apply tremolo if needed
u32 vol = vars->modChn->vol;
if(vars->modChn->tremolo.slide != 0)
vol = MODVolumeSlide(vol, vars->modChn->tremolo.slide);
vars->sndChn->vol = vol;
}
if( (vars->note != MOD_NO_NOTE) &&
(vars->updateFlags & MOD_UPD_FLG_PLAY_NOTE) )
{
MODPlayNote(vars);
}
if(vars->fineSlide != 0)
{
// This has to happen after the note is played
vars->modChn->period =
MODPitchSlide(vars->modChn->period, vars->fineSlide);
vars->updateFlags |= MOD_UPD_FLG_SET_FREQ;
}
if(vars->updateFlags & MOD_UPD_FLG_SET_FREQ)
{
u32 period = vars->modChn->period;
// Shift down 1 bit here because the slide range needs to be
// +-32 MOD periods, but the slide variable is +-64 because
// tremolo reuses the same code and needs the larger range
if(vars->modChn->vibrato.slide != 0)
period = MODPitchSlide(period, vars->modChn->vibrato.slide >> 1);
// mixFreqPeroid is already shifted up 12 bits for fixed-point
vars->sndChn->inc = div(sndVars.mixFreqPeriod, period);
}
} // MODHandleUpdateFlags
Vibrato waveform (0xE4) and tremolo waveform (0xE7)
These set the waveform types used in the last effects. If the parameter is less than 4, it sets the waveform and sets the noRetrig flag to FALSE (so it DOES retrigger), if it's 4 to 7, set the waveform to param&3 and noRetrig to TRUE (so it continues with the previous tick). 8-15 are technically invalid, but we'll just AND everything off so they work (easier than checking for them).
void MODFXSpecialRow(MOD_UPDATE_VARS *vars)
{
u32 param = vars->modChn->param & 0xf;
switch(vars->modChn->param >> 4)
{
...
case 0x4: // Vibrato waveform
vars->modChn->vibrato.waveform = param & 3;
vars->modChn->vibrato.noRetrig = (param & 8) ? TRUE : FALSE;
break;
...
case 0x7: // Tremolo waveform
vars->modChn->tremolo.waveform = param & 3;
vars->modChn->tremolo.noRetrig = (param & 8) ? TRUE : FALSE;
break;
...
}
}
Fine volslide (0xEA, 0xEB)
Slide the volume up or down once on the row-tick. Yawn.
void MODFXSpecialRow(MOD_UPDATE_VARS *vars)
{
u32 param = vars->modChn->param & 0xf;
switch(vars->modChn->param >> 4)
{
...
case 0xA: // Fine volume slide up
vars->modChn->vol = MODVolumeSlide(vars->modChn->vol, param);
vars->updateFlags |= MOD_UPD_FLG_SET_VOL;
break;
case 0xB: // Fine volume slide down
vars->modChn->vol = MODVolumeSlide(vars->modChn->vol, -param);
vars->updateFlags |= MOD_UPD_FLG_SET_VOL;
break;
...
}
}
Retrigger note (0xE9), note cut (0xEC), and note delay (0xED)
These are all fairly similar and very small. They all need a tick variable, but can never be active all at once, so we'll make a union for it (and shove the arpeggio tick in there too, while we're at it). Feel free to just make a single variable and use it for all of them if you like, I just think it's a little clearer this way.
One thing to note here is that retrig and delay just set MOD_UPD_FLG_PLAY_NOTE to trigger it. They're not supposed to do anything unless there was really a note specified on this row, but MODHandleUpdateFlags checks for that when handling the flag, so no need to bother checking here.
typedef struct _MOD_CHANNEL
{
...
union
{
u8 retrigTick; // MOD ticks until note should retrigger
u8 noteCutTick; // MOD ticks until note should cut
u8 noteDelayTick; // MOD ticks until note should play
u8 arpeggioTick; // Cycles 0-2 for original note and arpeggio notes
}
...
} MOD_CHANNEL;
void MODFXSpecialRow(MOD_UPDATE_VARS *vars)
{
u32 param = vars->modChn->param & 0xf;
switch(vars->modChn->param >> 4)
{
...
case 0x9: // Retrigger note
vars->modChn->retrigTick = param;
break;
case 0xC: // Note cut
vars->modChn->noteCutTick = param;
break;
case 0xD: // Note delay
// For this one, don't touch the mixer channel volume until the note
// is actually played. Otherwise it would sound bad if there was a
// quiet note and the volume jumped up before the delayed note started
vars->modChn->noteDelayTick = param;
vars->modChn->updateFlags &= ~(MOD_UPD_FLG_PLAY_NOTE | MOD_UPD_FLG_SET_VOL);
break;
...
}
}
void MODFXSpecialMid(MOD_UPDATE_VARS *vars)
{
u32 param = vars->modChn->param & 0xf;
switch(vars->modChn->param >> 4)
{
...
case 0x9: // Retrigger note
if(--vars->modChn->retrigTick == 0)
{
vars->note = vars->modChn->note; // MODHandleUpdateFlags will
vars->updateFlags |= MOD_UPD_FLG_PLAY_NOTE; // take care of playing this
}
break;
case 0xC: // Note cut
if(--vars->modChn->noteCutTick == 0)
{
vars->modChn->vol = 0; // Why actually stop it when
vars->updateFlags |= MOD_UPD_FLG_SET_VOL; // it's easier to mute it?
vars->modChn->effect = vars->modChn->param = 0; // Did the deed, so stop the effect
}
break;
case 0xD: // Note delay
if(--vars->modChn->noteDelayTick == 0)
{
// Like retrig, but set the volume and only trigger once
vars->note = vars->modChn->note;
vars->updateFlags |= MOD_UPD_FLG_PLAY_NOTE | MOD_UPD_FLG_SET_VOL;
vars->modChn->effect = vars->modChn->param = 0;
}
break;
...
}
}
Pattern loop (0xE6)
This loops a section a specified number of times (only within the current pattern). First give it a 0 parameter to set the loop point, then use it again with the number of times to jump back to that point. An example:
Row
10 C-2 01 E60
11 G-2 01 ---
12 C-3 01 E64
That will set the loop point at row 10, and then play those three notes 5 times in a row (once through, then loop 4 times). Each channel needs to track its loop position and loops left individually, so you can get multiple pattern loops going on at once (confusing as it would be). So, they go into MOD_CHANNEL:
typedef struct _MOD_CHANNEL
{
...
u8 patLoopPos; // Set to current row when an E60 effect is used
u8 patLoopCount; // Number of times left to loop
...
} MOD_CHANNEL;
void MODFXSpecialRow(MOD_UPDATE_VARS *vars)
{
u32 param = vars->modChn->param & 0xf;
switch(vars->modChn->param >> 4)
{
...
case 0x6: // Pattern loop
if(param == 0)
vars->modChn->patLoopPos = sndMod.curRow;
else
{
if(vars->modChn->patLoopCount == 0) // First time we hit it.
vars->modChn->patLoopCount = param+1; // +1 because we decrement
// before checking
if(--vars->modChn->patLoopCount != 0)
{
// Loop back to the stored row in this order next tick
sndMod.breakRow = vars->modChn->patLoopPos;
sndMod.nextOrder = sndMod.curOrder; // Don't advance the order
sndMod.curRow = MOD_PATTERN_ROWS; // This triggers the break
vars->modChn->patLoopCount--;
}
}
break;
...
}
}
Pattern delay (0xEE)
This causes a delay of the time it would take to play param rows. All notes on the row with the effect are played, just the next row doesn't come until it's finished. Very easy, add a countdown variable in the MOD struct, set it in the effect, and in MODUpdate when the tick overflows and it's time to play a new row, check the cuontdown before doing anything.
typedef struct _MOD
{
...
u8 patDelay; // Rows left to wait (normally 0)
} MOD;
void MODFXSpecialRow(MOD_UPDATE_VARS *vars)
{
u32 param = vars->modChn->param & 0xf;
switch(vars->modChn->param >> 4)
{
...
case 0xE: // Pattern delay
sndMod.patDelay = param;
break;
...
}
}
void MODUpdate()
{
if(++sndMod.tick >= sndMod.speed)
{
sndMod.tick = 0;
if(sndMod.patDelay == 0)
{
if(sndMod.curRow++ >= MOD_PATTERN_ROWS)
{
if(MODSeek(sndMod.nextOrder, sndMod.breakRow) == FALSE)
return; // FALSE = song ended
sndMod.curRow++;
}
MODProcessRow();
}
else
{
sndMod.patDelay--;
}
}
else
{
MODUpdateEffects();
}
} // MODUpdate
Undefined #2 (0xEF)
Finally, the last effect! It doesn't do anything :-P
3. Closing
Well there you have it, everything you need to know, in full detail, to make a MOD player. I didn't really intend for it to get so long, but that's the way of such things. Hopefully it will at least serve as a useful reference for the specifics of each effect, because no one else seems to document them quite completely (and now I know why).
Nonetheless I had a great time writing this series, taking something I'm familiar with and trying to keep the code as compact and hack-free as possible.
However, just because it's finished doesn't mean the series has to end. There's still plenty of stuff to do if I ever decide to do it. Like, going down to the wonderful, soul-tormenting world of hardcore mixer optimization, adding nice things like pattern compression, cool things like filtering and reverb (also called burning CPU cycles), figuring out that cursed glissando effect, exploring other music formats (S3M/XM/IT), and porting the player to the Nintendo DS!
That last one is kind of troublesome, because the DS uses all hardware channels, meaning no buffering. Instead, we have to change the channel settings on the fly in a timer interrupt, which is generally pretty fishy business. I think I'll give it a try though, it should be fun challenge getting it to act right.
Until next time, happy coding!