Delay RAM access methods?

Algorithm development and general DSP issues

Moderator: frank

Post Reply
pharaohamps
Posts: 34
Joined: Thu Nov 09, 2006 6:58 am
Contact:

Delay RAM access methods?

Post by pharaohamps »

I'm working on a program that uses a square wave to access different memory locations. If I just update the ADDR_PTR, I get a nasty discontinuity each time the square wave flips the pointer address. I kind of expect that.

The alternative is to use the servo method:

Code: Select all

cho rdal,rmp0      ;servo ramp0 to correct position using value in modpos 
rdax modpos,-1      ;copied from knowledge base 
wrax rmp0_rate,0 
cho rda,rmp0,reg|compc,del   ;read from delay 
cho rda,rmp0,0,del+1 
That works much better - no more nasties. However, I don't get the sharp transition I'm looking for, as the ramp LFO takes some time to correct to the new position. It's a trapezoidal response rather than truly square. Is there a way I can just access two adjacent samples and interpolate them together? I really want the delay time to move between my two extremes with a clean step.
Digital Larry
Posts: 338
Joined: Mon Nov 12, 2012 1:12 pm
Contact:

Post by Digital Larry »

Using a single ADDR_PTR and jumping around
This servo technique uses the difference between where you are and where you're going to set the ramp rate, so it goes fast when you're far away and slows down the closer you get, going to zero when you land on your destination.

You might be able to process the delta between your current position and target somehow so it stays faster longer, but I don't know if you can totally prevent the pitch bend in the transitions this way. I don't think I've used the servo technique, but I have used a lot of "smoothing" low pass filters on the delay time value, which always give the pitch bend.

Interpolating between adjacent samples is also known as low-pass filtering, so maybe you could insert an LPF, but if you suddenly jump from a low value to a large value, there's just about no getting around introducing a click/thump of some kind even if you filter it.

Using two ADDR_PTRs and flipping between them
Sounds like you might want to cross-fade at the transition point, similar to the way the standard pitch shift algorithms cross fade between two offset buffers to prevent glitches at those points. Then the challenge is to not be pitch shifting at those points, but just pointing to different parts of the memory. However, the built-in cross fade is designed for pitch shifting, so it spends about half of the time fading back and forth, rather than suddenly doing it right near the transition point, and there's no way to control this.

You could generate a triangle LFO centered around zero, and then give it a bunch of gain so that the tops and bottoms clip off and the transitions are pushed closer to the edges. SOF that to a 0 to 1.0 range and use it to control a crossfade block. You'd need to synchronize some other stuff with that LFO so that the delay time for each of the two inputs was changed only when the volume was fully down.

Sorry I don't have a specific solution in mind as this sort of stuff stretches my brain past the breaking point at times.
pharaohamps
Posts: 34
Joined: Thu Nov 09, 2006 6:58 am
Contact:

Post by pharaohamps »

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.
Digital Larry
Posts: 338
Joined: Mon Nov 12, 2012 1:12 pm
Contact:

Post by Digital Larry »

I think the crossfade/adjacent sample interpolation code is mostly for modulation effects where the delay length is changing slowly and you wind up wanting to change delay position less frequently than once per sample period. If you skip ahead by one sample every sample period, you're reading the signal back twice as fast and this is how you get an octave up pitch shift. For chorusing and flanging, most of the time you are looking for something way more subtle than that.

So for example, you want to fade smoothly from one sample to the next one (right next door in the RAM) over a period of ten sample increments. You use this linear interpolation method which is built into the FV-1's hardware.

My take on what you are looking for is the ability to make large jumps instantaneously from one area of the delay RAM to another, which is likely to introduce a click because you jumped way over a section of the audio signal. Slowing that transition down (playing back some intermediate samples rather than suddenly jumping) in order to prevent the click introduces a pitch shift.

But don't let me discourage you! I may have misconstrued your goal.
pharaohamps
Posts: 34
Joined: Thu Nov 09, 2006 6:58 am
Contact:

Post by pharaohamps »

Really any time you are moving the delay pointer in real time you've got to do SOMETHING to keep the artifacts down. If you modulate the "servo delay" in SpinCAD, you get sort of grainy sounding artifacts as the pointer moves. If you instead use a straightforward chorus setup such as:

Code: Select all

mem chorus 3000
rdax adcl, 1.0
wra chorus, 0
cho rda, sin0, sin|reg|compc, chorus + 200
cho rda, sin0, sin, chorus+201
wrax chorout, 0.5
rdax adcl, 0.5
wrax dacl, 0
... Then the chip handles moving the pointer about the desired point (in this case chorus+200) and then the second line pulls a second sample and adds it to the first. This works really well, no audible artifacts.

Any time you move the pointer, you're going to have to deal with some kind of noise or glitch in the results. I've been thinking about the app note servo method, and it seems like a really smart way to handle this. Servo just means we're using feedback to get to the right spot. You can certainly break up the address of the desired sample yourself, then do the math to get the crossfade coefficients, etc. but it's WAY simpler to just use the hardware to manage that for you. CHO RDA and you're done more or less. Why use the ramp LFO? Well, the hardware only does the indirect read and crossfading for you if you use an LFO to access the delay position. It's exactly the same as the chorus example above.

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
CHO RDA means read the delay address that the LFO is pointing to.
RMP0 is the LFO we want to use to get the pointer address.
REG means we freeze the LFO so that we can grab its value without it updating in the background.
COMPC takes the "complement" of the fractional part of the address. This does all the hard work from the flanger example in one line of code. The result of this line is the fractional part of the address multiplied by the correct crossfade coefficient.
DEL is the delay we want to use.

The next line is just a simple delay read - go to the position indicated by RMP0, no options so just a plain read, then read from the DEL delay plus one sample. The fractional part is in the accumulator already, so pulling this next sample automagically crossfades. And we're done.

I agree with you that the only time you need to do this is if you have a constantly moving pointer. If you're just using a pot to vary the delay time you can use the method in SpinCAD - mask off the pot for a lower resolution to force the pointer to integer values. That was Keith Barr's favorite method and I can see why, it's cheap in terms of resources and doesn't use up any LFOs.

The ONLY issue with using the ramp to servo the delay pointer is that it's slow. It's so easy to do, and so cheap (just two lines!) that it's almost insane not to use either a chorus or a ramp servo to move the delay pointer. If you increase the servo gain then it's faster. I wonder if it would be possible to get finer control of the servo gain? Maybe use an EXP function to massage the modulation position value so that large value differences give the servo amp more gain and small moves get less.. I'll have to work on that!
Digital Larry
Posts: 338
Joined: Mon Nov 12, 2012 1:12 pm
Contact:

Post by Digital Larry »

Very few of the blocks in SpinCAD are "original" work, although a few are I guess. I agree with you that the sanctioned method for high resolution chorus/flanging is a better approach - it's been in my backlog for about two years now!
pharaohamps
Posts: 34
Joined: Thu Nov 09, 2006 6:58 am
Contact:

Post by pharaohamps »

I wouldn't get hung up about "original." It's not like you're copy-pasting stuff and it magically works. I've looked at some of your Java code and it makes my brain hurt.
Post Reply