Let's deal with the roms first. The Constant Event rom smoothly progresses through the events while the frames advance. The delays are all rather random, so it looks like they read right. The events are over 400, so I don't think this is the problem.
The Song Event rom sticks at event 0 while HUGE delays count down (from like 210000). The frame advances.
Now, let's talk about computer music. The tracker format started simply - you had one track per audio channel, and the score was processed once per vblank int (Euro... 50Hz; for NTSC, they simply skipped one int per 6 vblanks). The vblank tick was a fixed beat rate. When they eventually wanted higher/variable beat rates, they switched to using one of the CIA timers (on the Amiga), or repurposed the 13.8Hz PIT timer on the PC. The score was still processed once per tick, but now the tick rate could vary, though 125 bpm tended to be the standard default value for the tempo.
For these old players, the instruments simply ran constantly in the background, looping if they were a looped instrument, while the score would change the volume or frequency or instrument asynchronously at the tempo. Let's look at a simple example: the SCD PCM chip. The gate array has one timer that can be used by programs for timing. You would set the tempo like this
Code: Select all
| void pcm_set_timer(uint16_t bpm);
| TIMER = 32552 / (bpm * 2) - 1
| should be 32552 * 5 / (bpm * 2) - 1, but that's too big for TIMER, so
| we leave out the * 5 and compensate in the int handler
.global pcm_set_timer
pcm_set_timer:
move.l #32552,d0
move.l 4(sp),d1
cmpi.l #64,d1
bhs.b 0f
moveq #64,d1 /* lowest tempo TIMER can handle */
0:
add.w d1,d1
divu.w d1,d0
subq.w #1,d0
ble.b 1f /* safety check */
move.w d0,TIMER.w
1:
rts
Notice how our timer can't handle too large a value, so I left out the *5 you'd normally use in calculating the tempo. To compensate, I multiply the event delays by 5. So the tempo is 5X as fast, but the delays are 5X as long, so it evens out. You may or may not need to work around issues like this on various platforms.
The first thing the timer int callback does is process the score
This is the regular MOD callback entry standard for all mod players. Then it processes any channel handling that may be needed after the score processes events. We start by checking if the the tempo or timescale changed. Normally you'd just have tempo, but due to limits in the ram for the PCM in the SCD, I added a timescale for when instruments are resampled to fit in ram.
Code: Select all
if ((tempo != gMod->BeatsPerMinute) || (scale != gMod->TimeScale))
{
tempo = gMod->BeatsPerMinute;
scale = gMod->TimeScale;
uint16_t bpm = 0;
if (scale & 0x20)
bpm += (tempo << 1);
if (scale & 0x10)
bpm += tempo;
if (scale & 0x08)
bpm += (tempo >> 1);
if (scale & 0x04)
bpm += (tempo >> 2);
if (scale & 0x02)
bpm += (tempo >> 3);
if (scale & 0x01)
bpm += (tempo >> 4);
pcm_set_timer(bpm);
}
Now handle the pcm channels
Code: Select all
/* update channels */
for (i = 0; i < gMod->NumberOfChannels; i++)
{
pcm_set_ctrl(0xC0 + i); // turn on pcm chip and select channel
if (voices[i].pending & PENDING_STOP)
{
pcm_set_off(i);
pcm_set_env(0);
voices[i].pending &= ~PENDING_STOP;
}
if (voices[i].pending & PENDING_START)
{
pcm_set_off(i);
pcm_set_start(voices[i].start, voices[i].offset);
pcm_set_loop((voices[i].start << 8) + voices[i].loop);
voices[i].pending |= PENDING_VOL|PENDING_PER;
}
if (voices[i].pending & PENDING_VOL)
{
pcm_set_env(voices[i].vol);
pcm_set_pan(voices[i].pan);
voices[i].pending &= ~PENDING_VOL;
}
if (voices[i].pending & PENDING_PER)
{
pcm_set_period(voices[i].period);
voices[i].pending &= ~PENDING_PER;
}
if (voices[i].pending & PENDING_START)
{
pcm_set_on(i);
voices[i].pending &= ~PENDING_START;
}
}
If the score turned off a channel, we stop it. If the score changed the volume, we set the channel volume. If the score changed the period or started a channel, we set the channel period. If the score started a channel, we now start it (the start, loop, volume, and period have been set at this point).
MUS was designed to play almost identically to MOD so that the standard methods of playback could be adapted. Only the score handling changed. The tempo was fixed at 140Hz (or 70Hz for one game). You process the score once per tempo tick, subtracting one from any delay until it's 0, then process any events. All the while, the sound channels run in the background.
To make timing on the 32X easier, I use the sound playback for timing. I set the number of samples per buffer to be exactly one tick worth of time. It doesn't need to be TOO exact - close enough is fine... you'll never hear the difference between say 375 samples, and 375.47138 samples (which you can't do). So in my 32X mod player, I set the tempo like this
Code: Select all
static void SetBeat(struct ModTag* Mod_p, uint8_t bpm)
{
uint16_t nsmp = (SAMPLE_RATE * 5)/(bpm * 2);
Mod_p->NumSamples = (nsmp > MAX_NUM_SAMPLES) ? MAX_NUM_SAMPLES : nsmp;
}
I check against the maximum number of samples that fit in a buffer in case the song uses a ridiculously low tempo, but otherwise, the number of samples sets the callback rate for processing the score. This would be done exactly the same for MUS playing where it can also change the tempo. Note that we always mix this many samples per callback. When the PWM DMA int occurs, we know we've played one tick worth of audio, so we start the next dma, we process the score (subtracting 1 from the delay until it's 0) processing any events, and mix NumSamples worth of samples into the next buffer knowing that NumSamples will give us one tick worth of delay at the current tempo.
We start the next dma, THEN process the score, THEN mix the next buffer - that way we have the most time possible to process the score and do the mixing.
What you should NEVER do is have a fixed buffer length that you try to fill by mixing 'delay' numbers of samples until the buffer is full. Samples never directly correspond to delay ticks in any case. At any reasonable sample rate for the 32X, you have HUNDREDS of samples per delay tick, not ONE sample. So any 32X MUS player (or derivative) should look just like my MOD player for 32X, but with the Handle_MUS code I posted rather than the PlayMOD code.