I've been doing some more digging and came across the following:
Although a memory pointer can be loaded (ADDR_PTR), and used to address memory, if it is to be clear of audible artifacts, an interpolation must be performed at each sample, as the delay is varied. This normally would mean tearing the address pointer value into an integer and a fractional part, and using the fractional value to interpolate between adjacent samples.
I went through all the Spin-provided code to find an example of this, and I found something in the ROM reverb + flanger.
Code: Select all
;--------------------------------------------------------
; now formulate delay pointer,
; and pass through to fract calc:
;--------------------------------------------------------
or fladel_138 < 8 ;fladel^ + 138 ;get midpoint address pointer
rdax tri, 0.03125 ;add triangle wave modulation, scaled to fit delay range
wrax addr_ptr, 1.0 ;establish address for lower interpolation sample
;--------------------------------------------------------
; address pointer set, now develop fraction of tri:
;-------------------------------------------------------
and 0x0000ff ;mask off integer portion of address, leaving a fractional value in the lowest acc byte
sof -2.0, 0 ;these operations shift the resulting fractional value to the range 0.0 to 0.999...
sof -2.0, 0 ;only -2.0 is exact, but it changes sign of shifted value
sof -2.0, 0
sof -2.0, 0
sof -2.0, 0
sof -2.0, 0
sof -2.0, 0
sof -2.0, 0
sof -2.0, 0
sof -2.0, 0
sof -2.0, 0
sof -2.0, 0
sof -2.0, 0
sof -2.0, 0
sof 1.9999 ,0 ;15 shifts, last one is positive, and 1.9999 is nearly 2.0...
wrax k2, 1.0 ;write result as coefficient for second sample read
sof -1.0, 0.999 ;K1 is 1-K2 (or very nearly)
wrax k1, 0.0 ;write result as coefficient for first sample read
;-------------------------------------------------------
;read from first pointer:
;-------------------------------------------------------
rmpa 1.0 ;, 0 ;read memory for first sample read
mulx k1 ;multiply by K1
wrax temp, 0 ;and store in temp, while clearing the acc
;-------------------------------------------------------
;get second pointer:
;-------------------------------------------------------
or fladel_139 < 8 ;fladel^ + 139 ;form second pointer
rdax tri, 0.03125 ;add triangle waveform again
wrax addr_ptr, 0 ;establish address for upper interpolation sample
rmpa 1.0 ;, 0 ;read second interpolation sample
mulx k2 ;multiply by K2
rdax temp, 1.0 ;add temp (first value*K1)
wrax fladout, 1.0 ;write the result to the flanger delay output
mulx mix ;multiply by the mix value
rda fladel, 1.0 ;add the input to the flanger delay
wrax flaout, 0.0 ;write result to flaout and clear acc
That's a lot of stuff! But here's what's happening in a nutshell:
Staring with a clear accumulator, we OR in an address in the middle of the delay memory. Next, we take the value of the "tri" register which holds our modulation position, scaled by a small coefficient to match it to the delay range. Then we stash that in ADDR_PTR.
The next section of the code takes the value in ADDR_PTR and masks off / discards the integer portion. We're left with just the frational part in the ACC, and then we SOF it a bunch of times to get it back up to the range of 0-0.999. This value becomes "k2," which is the crossfade coefficient for the fractional part of the delay. We then do (1 - k2) to get k1 - essentially we need to have the total amplitude of both delay value add up to one, so this gets it for us.
Still with me? Good.
Next, read from the ADDR_PTR and multiply it by K1, then store it away. Simple.
Now we start over again, and we OR in the second pointer value, add the triangle wave value and use that as the new ADDR_PTR value. Read from the delay at that point and multiply by K2. Add that to the first read and you're all done.
The long and short of it is, you need to read from two adjacent samples and then interpolate / crossfade. That's a LOT of instructions, and it still ends up making the delay access a bit slower than a straight wra / rda.
Here's what I ended up doing:
Code: Select all
cho rdal,rmp0 ;servo ramp0 to correct position using value in modpos
rdax modpos,-1 ;copied from knowledge base
sof -2.0, 0
sof -2.0, 0
wrax rmp0_rate,0
cho rda,rmp0,reg|compc,del ;read from delay
cho rda,rmp0,0,del+1
All I'm doing is making the ramp move faster! Simple! The code is a servo - meaning it uses feedback to move the pointer between the current position and the desired position. I've just added 6dB of gain to the servo, so that it changes faster. The biggest problem with using the servo technique is that rapid changes take too long to change the delay time. If you use a triangle wave to modulate a delay line (making a flanger for example) then fast rates result in a lower modulation depth. The servo can't change as fast as the LFO so you end up with a slew rate limiting issue. If you speed up the ramp rate, then the delay ends up at the right spot much faster. If you SOF the modpos / position register a few more times you get super-rapid access to any delay location but it squares up the response a lot so it's not suitable for sine or triangle waves any more.
One of these days I'll take apart the interpolation / crossfade code some more and make it work for me but the results weren't as promising as I expected them to be.