A Beginner's Guide to Programming Digital Audio Effects in the kX Project environment
most recent online version
Digital audio effects are a small branch of the science of digital signal processing
(DSP), which comprises many different types of applications such as image processing,
communications, medical instrumentation, military instrumentation, deep sea
and space exploration etc. All these deal with processing different signals
(a signal being a stream of continuous data) and so does audio processing.
The digital signal processor
When a signal (in our case analog audio signal) enters the input of the soundcard
it goes into an ADC (analog to digital converter), which transforms the voltage
of the sound signal in regular intervals of time (in our case it does this 48000
times per second - 48KHz) to a number, thus making it digital (discrete). So,
48000 numbers form 1 second of audio signal or vice-versa. Then every number
of this continuous array of values passes through the digital signal processor
where it gets transformed by the effects. After that it goes into the DAC (digital
to analog converter) where it gets analog again and goes to the sound card outputs.
In some cases the signal might be extracted before it gets transformed by the
DAC, and sent to the digital output of the sound card in digital form.
Let's start by opening kX Editor. Rigt-click the kX icon in the taskbar and
open kX Editor.
NOTE: In kX editor you can use both the decimal and hexadecimal (aka machine)
number systems. It is advisable to use decimal, because this is the natural
“human” number system and decimal numbers are automaticly transformed
Unprocessed, intermediate and processed data of the audio signal stream is stored in physical registers. There are several types of registers:
1. Input and output registers. Incomming data unprocessed by the current effect is stored in the input register, so subsequently it can get processed. Already processed data is stored in the output register, so it can be rooted to physical outputs or other effects. Each microcode can have several input and output registers depending on how many channels we want it to have. For example, if it's going to be stereo we would need two of each type. If it's only mono we will need just one of each type. The use of such registers is not necessary for effects which don't need both inputs or outputs – for instance peak meters or wave generating effects. But at least one type is needed, otherwise the system would be meaningless and useless.
2.Static and temp registers. They are used for storing intermediate data during instruction execution. The value of a static register is preserved until it is overwritten (next sample cycle), or the microcode is reinitialized (reloading or resetting the plugin). Temp registers are used for the present sample cycle only, so their last value can not be used in the next cycle – it will be zero. The idea behind the temp register is that it can be shared by all loaded effects, but such sharing is not supported by kX at the present time, so it is recommended that static be used most of the time. Despite that we'll use temp registers in the examples for learning purposes only.
Static and temp registers don't have to be initialized with a value, but if an initial value is needed you can assign such to a static register:
NOTE: when you use certain numbers (constants) directly in the microcode which are not present in hardware (in a special read-only memory), they are automatically transferred to static registers when the code is compiled.
3.Control register. This is a read-only register and has to be initialized with a value. When the code is compiled, a fader is automatically created for that register, so it can be user controlled. It can have values between 0 and 1 (although you can assign values greater than that, when you move the corresponding slider the value is automaticaly transformed between 0 and 1).
The assigned value is the default value, so each time the code is reinitialized the control register will have this value.
4. Constants. There are certain constant values, which are defined in hardware in a read-only memory on the chip and can be used directly in the code (not with a static register) for the purpose of saving some resources. Such are 0, 0.125, 0.5, 0.75, -1, 1, 2, 3, 4, 8, 10, 20, 100, etc. If not hardware-defined constants are used, they will be automatically transformed to static registers, as mentioned before.
5. Accum register. This register gives us access to the dsp accumulator. When an instruction is executed, its result is automatically stored in it overwriting the previous value and then copied to the result register of that particular instruction. You can access the accumulator with the accum keyword and it can be used only as an A operand. It is 67 bit wide and has 4 guard bits. It can be used when we want an unsaturated or unwrapped intermediate result, for instance:
6.CCR (Condition Code Register). This register is used in the skip instruction. Its value is set after each instruction, based on its result. You can acces it with the ccr keyword.
7.TRAM Access Data Registers. These registers are for delay lines. There are write registers (which write samples in the delay line) and read registers (which read (extract) samples from the delay line). You can change the address of these registers within the declared delay line, thus changing the lenght of the delay line and the lenght of the actual delay. In Dane this is done by putting an & sign before the name of the corresponding register.
All instructions have the following syntax:
instruction result R ,operand A, operand X, operand Y
Operands are registers.
1.MACS and MACSN (multiply-accumulate with saturation on overflow) That means that you multiply two numbers, then add them to a third number and store the value in the result register. These instructions operate with fractional numbers only (they perform fractional multiplication)! If the result exeeds -1 or 1 it is truncated to -1 or 1. This means that you can't use whole (integer) numbers with these two instructions except “fractional” 1.
MACS R = A + X*Y
This is a simple volume control program.
You can try it in kX Editor. Click on "Save Dane Source" (on the right of the window) not "Export to C++". Save it, then right click on the DSP window and select "Register Plugin". Open the file and it should now be with the other effects - you know where they are. Or you can find the file in windows explorer and double-click on it – it will automatically get registered.
2.MACW and MACWN (MAC with wraparound on overflow) Same as MACS and MACSN, but when the value exeeds -1 or 1 it wraps around. This is presumably to minimize noise when saturation occurs with MACS and MACSN. If you have 0.5 + 0.7, the result instead of 1 on saturation, will be -0.8 with warparound.
3.MACINTS (saturation) and MACINTW (wraparound). Same as MACS and MACW, but they perform integer multiplication. That means that you can multiply a fractional value with an integer value as well as integer with an integer. These two instructions always assume that the Y operand is an integer.
MACINTS R = A + X*Y
NOTE: I won't write the info part (name, copyright etc.) anymore. You can do that yourselves.
If we want to control the amount of gain:
4. ACC3. This instruction just sums three numbers. It saturates on overflow. The values of the operands can be all fractional (we treat the result as fractional) or all integer (we treat the result as integer).
ACC3 R = A +X +Y
Mix of three mono sources plus a volume control.
5. MACMV (MAC plus parallel move of operand A to operand R). The result of X*Y is added to the previous value of the accumulator and the result is again moved into the accumulator. At the same time the value of A is copied to R. This is very useful for filters, since it simultaneously accomplishes the MAC and the data shift required for filtering.
R = A, accum += X * Y; or (accum = accum + X*Y)
See the EQ Lowpass disection at the end of the document.
6.ANDXOR used for generating standard logical instructions. I haven't had any experience with this instruction and there doesn't seem to be much use of it. If you want more details take a look at the As10k1 manual. If anyone wants to add to this section, please contact me (info in the end of the document).
7. TSTNEG, LIMIT, LIMITN give the possibility of using something close to "if... then..." statements in the microcode.
TSTNEG R = (A >= Y) ? X : ~X
If A>=Y, the result will be X, else (if A<Y) X is complemented (becomes
LIMIT R = (A >= Y) ? X : Y
If A>=Y the result will be X, else (if A<Y) the result will be Y.
LIMITN R = (A < Y) ? X : Y
If A<Y the result will be X, else (if A>Y) the result will be Y
A simple hard clipping fuzz. It cuts the wave over 0.05 and under -0.05, thus producing harmonics.
8. LOG and EXP. LOG converts linear data into sign-exponent-mantissa (scientiffic/logarithmic) and EXP does the oposite. They have many uses as one E-mu/Creative Technology Center official states: "for data compression, dB conversion, waveshaping and log domain arithmetic approximating division and roots".
LOG R, Lin_data, Max_exponent, Sign_register
Lin_data: Data to be converted. It would be interpreted as fractional format.
0x0 - normal
Since the EXP instruction has exactly the opposite behaviour than LOG, we should only discuss the LOG one. The opposite behaviour means that if you calculate the EXP of the result of a LOG instruction, you get the operand of the LOG instruction again (if the same resolutions are used):
Plots of LOG for the full input range. We can see the difference between Max_exp=0x1F (maximum allowed value) and Max_exp=0x1 (minimum allowed value). We can also see, comparing the two graphs, how the sign_register parameter acts.
Plots of the various max_exponent values.
Why are these instructions called LOG and EXP?
NOTE: We allways remember trigonometry, but not always remeber logarithms. Take a look at your old math notes if you don't remember what a neperian logarithm is. -;)
LOG y, x, res, 0x1 approximates to y = log[base](x) + 1 = ln(x) / ln(base) + 1 with:
Once again, as the EXP instruction has the opposite behaviour than LOG, it can be approximated to the mathematical exp instruction.
EXP y, x, res, 0x1 approximates to y = base^(x - 1) = exp( (x - 1) * ln(base) ) with:
Absolute value with the LOG instruction
There is a loss of two (maybe tree) bits with these expressions. Although the loss of two or tree bits is really insignificant (we still have 32-3 = 29 bits), it is better to use the TSTNEG instruction to do the normal absolute value operation.
Log domain arithmetic
We can use the LOG and EXP instructions to perform operations like divisions and/or roots. But don't forget these two important things:
1.Since LOG and EXP are not exactly the log and exp functions, the next is
only an approximation, and may not be enough in many cases.
These algorithms are based on the formulas:
log (a ^ b) = b * log (a)
Square root approximation:
As we can see in the graph, this algorihtm gives a very good aproximation to the square root:
Cubic root approximation:
Wors aproximation to the cubic root than to the square root, but still very good.
Division approximation with an unsigned result:
Based on the following formula:
a / b = exp ( log(a / b) ) = exp ( log(a) - log(b) )
-We can't store a fractional number greater than 1.0, so the operand b
must be greater than operand a in order to get a result smaller
Division approximation with a signed result:
9. INTERP - this instruction performs linear interpolation between two points. The main use of this operation is for simple single instruction low-pass filter for reverberation and other not very demanding filtering tasks. It is also useful for wet/dry signal mixing or pan control, a simple high-pass filter, envelopes, waveshaping and many other things.
INTERP R = (1 - X) * A + X * Y
This is very useful for one instruction one-pole low-pass filter, which has the following formula:
out = coef * in + (1 - coef) * out
The coefficient can be calculated with the following formula:
We can combine the low-pass filter with the soft clipping log instruction.
We get a simple soft clipping fuzz/overdrive effect.
10. The SKIP instruction provides flow control in the dsp code. It skips a given number of instructions under certain conditions, making use of the already mentioned CCR.
SKIP R, CCR, TEST_VALUE, number of instructions to skip
TEST_VALUE is a register containing a value which indicates under which conditions
The CCR is set after each instruction based upon the result of the instruction (R operand). Thus, to make a skip into our program we need two instructions. The first can be any instruction that sets the value to be tested. The second is the skip instruction which tests the value, and sets the amount of skipped instructions. So, simply said, the result of the instruction before the skip instruction is tested by the skip instruction, and based on it a given number of instructions below the skip one are omited.
These are the most common cases and they are used for almost all situations:
They are a necessary part of effects like delay, echo, reverb, chorus and many
We declare delay data registers like that:
We can have as many read and write registers as the dsp has and they should not overlap. We can also have several read registers following one write register, so we have one point at which data is entered into the delay line and many points at which it is read/extracted. This is the best way to think of delay lines and their registers – as points. The further one read point is away from the write point, the more delayed the data at it will be.
Some simple effects "disection" and explanation
Let's start simple.
It is as simple as that. For more input channels the concept is the same - we mix all left channels to the output left channel and all right channels to the output right channel.
3. EQ Lowpass, Bandpass, Highpass, Notch
The example is a biquadratic (aka biquad) lowpass, but all four are basically the same, olny the coefficients are different. Although better methods exist, we'll use this one because of it's simplicity.
NOTE: The values of the registers containing the filter coefficients are C++ controlled. You can't control them with faders created by Dane only by copying the code. For our example we'll assume that they are static and not user controllable.
These filters are IIR (Infinite Impulse Response) and their difference equation is:
y[n] = b0*x[n] + b1*x[n-1] + b2*x[n-2]
Because the effect is stereo there are actually two filters, so we can get rid of the second (right channel) part, because it is the same thing. Of course we're doing this for learning purposes, if we want the effect to remain stereo we have to use one filter for each channel – left and right.
NOTE:. Remember that the values of static registers are preserved for the next
sample cycle and can be reused until they are overwritten. Thus they form a
delay line from which we get the delayed samples for the above formula.
That's all for now. I hope this guide helps you start programming dsp effects. Ater you learn the basics, you might want to start studying more complex kX effects and search for dsp code on the web, which you can port to the kX environment.
Feel free to ask anything related to digital audio.
Have fun with DSP.
Copyright © kX Project, 2001-2015.
All rights reserved .