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.
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:
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.
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:
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:
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:
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:
Involving both LFOs can lead to a very rich chorus combination, with 8 'voices' available.
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:
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:
A ramp LFO can be established, and turned into a triangle wave thusly:
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.
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].
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.
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:
When reading the delay back, simply add the two components:
This is rarely required to actually do, but is handy if the need arises.
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:
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:
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:
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.
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).
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.
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.