<snip> 22254.5454 (recurring) and not 22250.0Hz?<snip>
This would cause the pitch to be 0.02% too low I think, but that's OK IMHO.
As explained before, there are two major but rather independent things happening: a) the samples are assembled/mixed into buffer. This happens at 22250 hz or 370 times every 60hz vbl. There's not much to do about this on a Mac as that's how the hardware works. And b) there are times when you need to change the samples currently being used. That is re-evaluated every 20ms/50hz for a MOD. A) is like someone pressing keys on a keyboard. That person needs to press the right keys for the right tone. And b) is someone telling person (a) to change keys. That happens at its own speed/frequency. If person B "runs" at the wrong speed, then the song will run faster. But the pitch will not change.
Yes, there's essentially 4 levels to all sample playback sequencers:
- Sample buffer to hardware copying (which might be DMA, but it's the beginning of the VBL routine in this case). This is the most hard, real-time code, 22kHz.
- Static waveform generation per voice and mixing (if the hardware audio doesn't support multiple, sampled channels like it does on an Amiga or Archimedes, but not an early Mac). This is
stereo. Here, we control a static amp and frequency per voice. These are usually decoupled from (1), but run at the same frequency. As long as the buffer audio generation is ahead of (1) it'll playback OK.
- Synthesiser, which selects waveforms and ramps volumes according to the envelopes and effects. This is
music and runs at a lower frequency, usually fast enough not to hear envelope changes. This performs channel to voice allocation (in MOD files it's 1:1).
- Sequencer which is your (b) which selects the notes per voice; their timings; programme to channel allocation. This is also
music in this code.
Really, just try it. If you think my code/comment in that lines doesn't make sense, then remove it, run the code again and see what happens. In this case the MOD will replay 20% too fast but _not_ at a 20% higher pitch. That's the way it was before I changed that. Just take the code, play around with it and see what happens.
I've run it and believe it I'm just trying to make sense of it. It's not you, it's me.
Going back to the 5/6 playback rate. OK, I finally get it.
On the Atari ST, it only generates 4ms worth of audio on each pass (but for all 4 parts). It doesn't need to generate a frame's worth of audio on each pass, because sample playback isn't hard locked to video frames. So, on every pass it needs to generate the 4ms of buffer audio (layer (2) above), and on every 5th pass (20ms) it needs to handle the sequencer and synth side of things (layers (3) and (4)).
On the Mac, it generates 16.7ms word of audio on each pass. That's already close to the 20ms sequencer and synth update rate, so 5/6 times it also runs that code (layers (3) and (4)) as well as layer (2), and on the 6th pass it just uses the same waveform generation (layer (2)) as before. Hence the overall rate is 50Hz, or 20ms (or close enough given that the frame rate is actually 60.15Hz not 60Hz).
This means the MOD playback sequencer and synth layers aren't quite right, but most people wouldn't notice. Short drum sample or staccato note triggered every 80ms (approx 12Hz) would sound evenly-spaced on an ST, but on this Mac code would get triggered for 67ms the first time, then 83ms the second, third, fourth, fifth times. That might just be audibly detectable, though my example is rather contrived. It won't make any difference with respect to the voices, because the unevenness would be synced across all of them.
Proper synchronisation means making sure
music is called every 22250/50=445 (89x5) generated samples. Yet we need to generate 370 samples per frame as an unrolled loop. The "easiest" way, IMHO is to split the unrolled loop into a separate subroutine (Samp1Gen) and then be able to JSR into the unrolled loop up to two times.
Code:
SampSplit: ;count needs to start at 445*(Samp1GenEnd-Samp1Gen)
;currently 16w=32b, so 7120
sub.w #LEN*(Samp1GenEnd-Samp1Gen),count ;<0 means two parts, >=0 means 1 part.
bpl.s SampGenAll
;Want to JSR to Samp1Gen-count
move.w Samp1GenEnd-Samp1Gen,%d7
sub.w count,%d7 ;correct offset.
jsr Samp1Gen-.(%pc,%d7) ;correct offset into Samp1Gen
bsr music
;Now we want to do -count samples, so it's Samp1GenEnd+count
move.w Samp1GenEnd,%d7
add.w count,%d7
jsr Samp1Gen-.(%pc,%d7) ;correct offset into Samp1Gen
add.w #445*(Samp1GenEnd-Samp1Gen),count ;
bra.s SampSplitDone
SampGenAll:
bsr Samp1Gen
SampSplitDone:
This means deleting lines 307 to 312 ("/* Mac runs in vbl at 60Hz. So skip" .. to "bra.s nomus") and the "bsr music" in line 316; copying the .rept LEN .endr to a subroutine:
Code:
Samp1Gen:
add.w %a4,%d1
;.. a single iteration
move.b %d7,(%a6)+
Samp1GenEnd:
.rept LEN-1
add.w %a4,%d1
;.. a single iteration as before
move.b %d7,(%a6)+
rts
And finally replacing the old .rept LEN code with the SampSplit code above and defining count as:
count: DC.W 445*(Samp1GenEnd-Samp1Gen) .
Again, as you say you don't plan to modify your MOD code, which is fine - it's just interesting to do some analysis on the code and in this case (fool that I am, I haven't tested it yet), look at correcting a minor timing inconsistency.
Thanks for publishing it all!
-cheers from Julz