Home Products Free DSP Programs Support Forums Knowledge Base Distributors   About   Contact  

Coding Examples

This section attempts to cover issues commonly encountered in programming the FV-1, hopefully explaining the process through examples. Often, this is more effective than an overly technical and precise document. I hope it is helpful.


Varying delays

It is convenient to use a POT input to vary a delay, for instance a predelay for a reverb. Unfortunately, the resolution of the POT inputs (512 conditions from end to end) provides fine resolution, but this is not always a good thing. The user will often hear a severe pitch bend while rotating the control, but this can actually be made to largely go away by intentionally coarsening the POT resolution:

rdax pot0,1
and %01111100_00000000_00000000
sof 0.0625,0
wrax addr_ptr,0

This code will reduce the POT resolution to only 5 bits, only allowing 32 settings of delay, but this is usually adequate when the maximum delay is only 100mS (3mS increments). the SOF instruction reduces the span of the control (after quantization to 5 bits), and in this case to 0.0625 of the 32768 location space, or 2048 locations, or 62.5mS at Fs=32768Hz.

If the 'jump' in delay from the user rotating the control is less than the period of a cycle of a given frequency of interest, then it will be heard as a pitch bend; if the jump is several cycles, then it will be heard as a slight 'tick' only.

back to coding examples top

Chorus

Chorus is really simple in the FV-1, but the details are important, and difficult to remember, so here it is.

We usually want to sweep a delay sinesoidally, and the sine LFOs produce a +/- output, unlike the ramp LFO that produce a positive output only. Therefore, when we do chorus operations, we specify the peak sweep in samples, either when we establish the LFO, or if we modify its _RANGE value later. A typical chorus program would include a delay that already has an input signal written to it. What the chorus program lines do is simply access the memory through one of the LFOs. Because at any given sample instant, the address we want the signal from may be actually between two real samples in the delay RAM, we write two chorus instruction lines that together sum a bit of the two adjacent delay RAM samples. There are many variants of the CHO instruction so to begin, let's establish a sine LFO to do the sweeping:

skp run,1
wlds sin0,20,100

WLDS means 'write, load, sine', and is followed by which sine LFO (in this case sin0), then the rate at which it will run, and the peak number of samples it will sweep through. It is a bipolar oscillator, so the reference of 100 actually means +/-100 samples, or 200 samples peak to peak. The rate value of 20 gives an LFO frequency of about 0.8 Hz.

Now we'll read from the delay that our signal has been written to, let's call the delay CDEL:

cho rda,sin0,sin|reg|compc,cdel+100
cho rda,sin0,sin,cdel+101

The interpolated result is in the accumulator, but the cryptic lines require explanation. CHO is the basic instruction, but when the first argument is RDA, it must be followed by a few specifications, separated by commas. The first argument is which LFO is to be used, the second is a combination of arguments, and the last is the delay name plus the mid position around which the pointer will sweep. The list of arguments separated by | (if there are more than one), direct many processes within the chorus interpolation engine. First of all, we can specify whether the SIN or COS output is to be used, which allows a single LFO to drive two different chorus processes in 'quadrature' (one 90 degrees out of phase with the other). The first reference to an LFO in the pair of CHO operations must also include 'REG', which directs the engine to freeze it's internal wave generator (so that it will be fixed for both operations). The next possible options include COMPC and COMPA. Using COMPA will invert the waveform, so that tow chorus processes can use the same LFO, but their sweeping will be opposite. COMPC is needed to perform interpolation, effectively complimenting the fractional portion of the address for that instruction line. If COMPA is not used, as in the code above, then the COMPC will need to be used in the first line only, but if COMPA is used, then COMPC should be in the second line:

cho rda,sin0,sin|reg|compa,cdel+100
cho rda,sin0,sin|compc,cdel+101

Notice that the first line specifies a midpoint in the CDEL memory space, and the second line indicates a position that is 1 memory location greater.

For a wild set of chorus outputs, you can set up a single sine LFO and get 4 outputs, that we'll call c1 through c4:

cho rda,sin0,sin|reg|compc,cdel+800
cho rda,sin0,sin,cdel+801
wrax c1,0
cho rda,sin0,sin|reg|compa,cdel+400
cho rda,sin0,sin|compa,cdel+401
wrax c2,0
cho rda,sin0,cos|reg|compc,cdel+1100
cho rda,sin0,cos,cdel+1101
wrax c3,0
cho rda,sin0,cos|reg|compa,cdel+1400
cho rda,sin0,cos|compc,cdel+1401
wrax c4,0

Involving both LFOs can lead to a very rich chorus combination, with 8 'voices' available.

back to coding examples top

Servo technique for sweeping through delay memory

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. This can be automatically performed with the LFOs however, if they can be controlled to 'point' to the proper position in memory. Often, the delay-varying signal is a POT input or perhaps a sine LFO, or even a generated triangle wave.

Let's say the value we want to access is in a register we'll call MPOS, and we'll use the RMP0 LFO to 'servo' to that location. We'll assume the accumulator is cleared going into the routine:

skp run,1 ;only establish the LFO on the first sample pass
wldr rmp0,0,4096 ;set rmp0 to its widest range
cho rdal,rmp0 ;load in the current RMP0 pointer
rdax mpos,-1 ;subtract the desired position
wrax rmp0_rate,0 ;write the position error to the ramp rate register

Then we can use the normal CHO instructions to cause the RMP0 LFO to do the access, complete with interpolation, in a a very few instructions:

cho rda,rmp1,reg|compc,del
cho rda,rmp1,0,del+1

back to coding examples top

Generating triangle waves

A ramp LFO can be established, and turned into a triangle wave thusly:

skp run,1 ;establish LFO on first sample cycle only
wldr rmp0,10,4096   ;set values for rate and range
cho rdal,rmp0 ;read LFO saw wave, accumulator value will range from 0 to 0.5
sof 1,-0.25 ;offset by -0.25, giving a saw wave that ranges from -0.25 to +0.25
absa     ;make negative values positive, making the result a triangle.

The first two instructions set the LFO RAMP0 with a rate of 20 and a range of 4096. When used in a pitch transposer, this means it will address through 4096 delay memory locations, but when read with the CHO instruction, it will appear in the accumulator as a 0 to 0.5 ramp (sawtooth). The output of this triangle generator will be 0 to +0.25.

back to coding examples top

Cross fading between two signals

When you need to crossfade between two signals, say, between an input and an effect output, the following code can be used. Let's say we have register values INR and OUTR, and we want to cross fade between them using POT0. We will assume the previous operation cleared the accumulator, as in [wrax  xyz,0].

rdax outr,1
rdax inr,-1
mulx pot0
rdax inr,1

The result will be in the accumulator. If the value of POT0 is zero, then the accumulator will be zero at the end of the MULX operation, and only INR will be in the accumulator at the end of the code block. If POT0 is effectively 1.0, then the output will be:

OUTR - INR + INR = OUTR

This can be used to create adjustable shelving filters, shown elsewhere.

back to coding examples top

Overcoming delay RAM limitations

Although the internal processing and the register resolution in the FV-1 is 24 bits, the delay RAM uses a floating point technique which, although having an extreme dynamic range, has somewhat limited resolution. Any objection the sonic quality of the floating point technique can be overcome by establishing two delays, let's call one DLS and the other DMS. Let's say we write to the delay memory from an accumulator value, and we do the following:

wra  dms,1 ;write ACC to the MS delay, and pass the accumulator value on.
rda dms,-1  ;read the memory value back, and subtract it from the original signal (in ACC)
wra dls,0 ;write any trivial errors that remain to the LS delay

When reading the delay back, simply add the two components:

rda dms#,1 ;read end of MS delay, the normal floating point signal
rda dls#,1 ;read and add LS delay, any floating point errors.

This is rarely required to actually do, but is handy if the need arises.

back to coding examples top

POT skip routines

A single program may be written as several programs, selectable by one of the POT values. This allows the number of FV-1 programs to be expanded. Since a simple reverb can be written with as few as 30 instructions, potentially 4 different reverb algorithms can be put into a single program, selected by a POT value. To do this, we use the skip instruction. For example, consider the following:

rdax  pot0,1  
and %01100000_00000000_00000000   ;mask off only 2 bits, leaving only 4 possibilities
skp zro,reverb1 ;if zero, the skip over other code to reverb1
sof 1,-0.25 ;subtract 1/4
skp zro,reverb2 ;if zero, skip over other code to reverb2
sof 1,-0.25 ;subtract 1/4
skp zro,reverb3 ;if zero, skip over other code to reverb3
clr   ;clear the accumulator, there's 1/4 left in it!

We mask off the LS bits of the POT value, so that only 4 possible conditions can result. We want to skip on the ZRO (accumulator is zero) condition, so that when we actually skip to the target routine, the accumulator is cleared. This is why we put a clr at the end of the routine, because we are effectively entering the 'reverb4' routine at the end of the code block.

Remember, no skip can exceed 64 instructions! If you decide to actually do 4 reverbs in one program, you might consider establishing a label at the end of the code, let's call it PEND, and terminating each reverb with:

skp run,pend ;skip to end of code

skipping on the 'RUN' condition is absolute, except for the very first sample cycle. Alternatively, if the last line left the accumulator zeroed, then we can skip on zero instead, as in:

wrax adcr,0
skp zro,pend

If the 64 instruction limit for skips is a problem, you can always establish a label within the code that skips again to the final location.

back to coding examples top

Use of the LDAX instruction

LDAX is a pseudo-op, actually constructed from the powerful RDFX instruction. When used, the RDFX instruction will be coded, with a coefficient argument of 1.0. This causes the referenced register to be loaded into the accumulator, destroying any previous accumulator contents. Most of the operations in the FV-1 are MAC operations, where results are accumulated on each instruction execution. Skip operations are often based on the accumulator being positive or negative, which means that a CLR (clearing the accumulator) may be required before any of the accumulate instructions may be used. If the first instruction of a skipped-to routine is a read from a register (but not delay memory), the LDAX instruction can be used, freeing up an instruction cycle (no CLR required).

back to coding examples top

Adjustable shelving filters

Often a pot of an internally generated variable will be needed to control the depth of a filter. In the case of simple filters, the handy RDFX, WRLX and WRHX commands can be used. We'll assume the signal to be filtered is in the accumulator pror to our code block, and that the result will be in the accumulator at the end of the block. We will have to establish a temporary register we'll call TEMP and a filter register we'll call FIL, and a controlling value in a register we'll call FCON. This will be a low pass filter.

wrax   temp,-1 ;save filter input in temp, and pass on in negated form
rdfx fil,0.1 ;do an RDFX operation on FIL, at about 0.1*Fs/2pi, about 520Hz at Fs=32768
wrhx fil,-1 ;causes a high pass filter at this frequency, with an infinite shelf
mulx fcon ;scale HPF filter output by FCON, (which ranges 0 to 1.0)
rdax temp,1   ;add back the signal at the filter input

It's only 5 lines of code, and it does a great job, but there are some details to consider. First of all, we do any shelving by subtracting from the input signal the output of a filter. Usually, the subtraction is done in a WRLX operation (for low pass filters) by setting the WRLX argument to a negative value, with -1 meaning a full subtraction... That is, when the filter (internal to the shelving operation) is passing signal, the subtraction will cause a deep rejection at the overall filter output. In this case however, where we need a control signal to control the amount of shelving, it's more convenient to think of the control as a positive number.

The code example can be torn apart easiest by looking at the 2nd and 3rd lines, where we have produced a high pass filter with infinite rejection (falls at 6db per octave). Notice however, that we saved the accumulator in the first line into TEMP, but multiplied the saved value by -1 back into the accumulator. Therefore, the signal going into our high pass filter is phase inverted. This allows us to do the MULX operation with a positive coefficient (FCON). The final sum is of the saved value performs an effective subtraction, and the pass band frequencies are in phase with the input. At an FCON value of zero, no overall filtering is performed. At an effective FCON value of 1, the filter will fall at 6dB per octave. At an FCON value of 0.5, the filter will shelve out at -6dB.

back to coding examples top

Spin Semiconductor