Adding Opcodes to Csound

Hans Mikelson

hans@csounds.com

Introduction

In the last issue I described how to compile Csound. One of the main reasons you would wish to recompile Csound is so that you can add your own opcodes to Csound. In this article I describe how to add opcodes to Csound. The first step is to determine if an opcode is needed. The second step is to prototype the opcode in Csound. Third is to translate the Csound code into C code. Fourth is to consider storage requirements. Fifth is to test and benchmark the opcode. Sixth is to write the manual entry and provide a working example. I will describe how I created the opcode biquad.

Why add another opcode?

The first step in adding an opcode to Csound is to determine if an opcode is needed. There are already so many opcodes that it literally takes years to learn how to use them all. The opcode should be generally useful in a variety of situations. The opcode may be a well known or famous algorithm. The opcode should offer some advantage to programming directly in Csound. If the algorithm requires kr=sr a significant speed advantage can be obtained by coding the algorithm in C. Coding an opcode can make otherwise long and clumsy code easy to work with and understand. If the opcode fits several of the above criteria you should consider submitting it for inclusion in the canonical sources.

I wanted to add the biquad opcode to Csound because I was experimenting with different types of filters and wanted to be able to sweep them at k-rate which was not possible with the current general purpose filters. The other reason was that many DSP applications make use of biquadratic filters and combinations of them. Higher order filters may be subject to numerical accuracy problems and so they are not used as often.

Designing the opcode

The next step is to design the opcode. I like to prototype the opcode in Csound if that is possible so that I can get a feeling for how the opcode is used. As you are looking at the number of control parameters think about if some of the parameters can be eliminated, combined or made optional. What is the appropriate order for the parameters? For many generating opcodes the first parameters are amplitude and frequency followed by the control parameters. The optional parameters must go at the end. Decide which parameters should be varied at i-rate, k-rate, or a-rate. Try to choose a name that is short, descriptive and not easily confused with some other function. To avoid breaking existing orchestras do not start the name of the opcode with any of the following letters or letter pairs: i, k, a, gi, gk, ga.

The formula for the biquadratic filter is:

Following is the Csound prototype for biquad:

sr=44100

kr=44100

ksmps=1

nchnls=2



       instr 1



axn    =     gainsig

ayn    =     (kb0*axn + kb1*axnm1 + kb2*axnm2 - ka1*aynm1 - ka2*aynm2)/ka0;

axnm2  =     axnm1;

axnm1  =     axn;

aynm2  =     aynm1;

aynm1  =     ayn;



       endin

This instrument implements a typical digital filter. The values of the coefficients determine the behavior of the filter. The values for aynm2, aynm1, axnm2 and axnm1 are updated at the end. Note that this requires sr=kr to function properly.

Csound to C

Now that the prototype is completed it is time to convert the Csound code to C. The first step is to create a header file for the opcodes you are adding. I called my header file biquad.h. Any variables whose values need to be retained between k-cycles must have variables allocated in the structure for the opcode.

/* Structure for biquadratic filter */

typedef struct {

    OPDS h;

    float	*out, *in, *b0, *b1, *b2, *a0, *a1, *a2;

    float	xnm1, xnm2, ynm1, ynm2;

} BIQUAD;

OPDS h appears in every opcode structure and contains useful information about the behavior of the opcode. The variables *out and *in are pointers to the audio output stream and to the audio input stream. The variables *b0, *b1, *b2, *a0, *a1 and *a2 are pointers to the coefficients of the opcode which point to kb0, kb1, kb2, ka0, ka1 and ka2 respectively. The variables xnm1, xnm2, ynm1 and ynm2 are used for storing the delayed input and output signals between k-cycles.

The next task in creating an opcode is to create the functions for the opcode itself. Usually there will be a function for i-time initialization and others for either k-rate output or a-rate output. I created these functions in the file biquad.c. The function biquadset performs any initializations which are required at i-time. In this case I initialize the delayed input and output signals to zero. The parameter passed to this function is a pointer to the structure defined as BIQUAD.

void biquadset(BIQUAD *p)

{

    /* The biquadratic filter is initialized to zero.    */

    p->xnm1 = p->xnm2 = p->ynm1 = p->ynm2 = 0.0f;

} /* end biquadset(p) */

The next function defined is called biquad. This function generates a-rate output of the filtered signal. A pointer to the biquad structure is passed as an argument to this function. This function consists of a number of variable definitions and initializations followed by a do loop. The function biquad will be called each k-period. In order to produce a-rate output a loop is executed ksmps times writing to and incrementing *out each time. The variable n is initialized to ksmps and then used as a counter in the while loop so it is executed ksmps times. The variables out and in are assigned to p->in and p->out to speed access to these values. Local copies of input parameters a0, a1, a2, b0, b1 and b2 are also made to improve performance. The a-rate actions are performed inside the do-while loop. First xn is read from *in which is then incremented to point to the next input sample. Next yn is computed using the filter equation. Next the values of the delayed signal variables is updated. Finally the new value of yn is written to the output list which is then incremented.

void biquad(BIQUAD *p)

{

    long n;

    float *out, *in;

    float xn, yn;

    float a0 = *p->a0, a1 = *p->a1, a2 = *p->a2;

    float b0 = *p->b0, b1 = *p->b1, b2 = *p->b2;

    n    = ksmps;

    in   = p->in;

    out  = p->out;

    do {

      xn = *in++

      yn = (b0*xn + b1*(p->xnm1) + b2* (p->xnm2) - a1*(p->ynm1) -

	    a2*(p->ynm2))/a0;

      p->xnm2 = p->xnm1;

      p->xnm1 = xn;

      p->ynm2 = p->ynm1;

      p->ynm1 = yn;

      *out++ = yn;

    } while (--n);

}

The next step is to modify the file entry.c to make it aware of the new opcode and describe the opcodes input parameters and output configuration. This several additions to the entry.c file. The first thing to do is to include your header file. At the beginning of entry.c you will see many include statements so simply add yours to the end of them in this case I used:

#include biquad.h

The next step is to add an entry for each of the functions created.

void    biquadset(void*), biquad(void*);

The next task is to add your opcode to the list of opcodes.

{ "biquad", S(BIQUAD),   5,     "a",    "akkkkkk",biquadset, NULL, biquad   },

There are eight fields in this list entry. The first field is the name of your opcode in quotes. The field is S followed by the name of your structure in parentheses. The next field is a number which determines when the subroutines are called. In particular 5 means to call routines at i-time and a-rate. Other numbers are explained in comments in the entry.c file. The next field indicates the type and number of outputs. In this case an "a" indicates audio rate output. If two values were being generated then "aa" would be used. Two k-rate output values would be indicated by "kk" etc. The next field indicates the input parameters "akkkkkk". This represents first the audio input signal then the six k-rate coefficients. The next three entries are the names of the functions to be called at i-time, k-rate and a-rate. Since this opcode does not use any k-rate function this value is NULL. It should be possible to recompile Csound at this point after which you will be able to utilize your new opcode in your own orchestras. This opcode would be called from an orchestra as follows:

aout biquad asig, kb0, kb1, kb2, ka0, ka1, ka2

Testing and benchmarking

The next step is to test your opcode and see if it works. Be sure to test under the extreme and typical settings of the input parameters. Bench mark the opcode and see how fast it runs. After benchmarking look at the code and see if there is anything you could change to make it more efficient.

Write the manual

The final step is creating the manual entry. The manual should describe what the opcode's intended purpose is and any limitations. The manual should describe each of the input parameters and give their expected ranges. The manual should provide a working example of the opcode in action. Don't forget to include your name so everyone can thank you for writing such a great opcode.

Conclusion

I conclude by reviewing the steps in creating an opcode. Design the algorithm behind your opcode. If possible implement the opcode in Csound so you can test it and get used to using it. Create the header and c files for you opcode and add the data structures and functions required by your opcode. Modify entry.c to add the new opcode description to Csound. By now you should be able to start creating you own opcodes for Csound and adding to the most powerful sound processing and synthesis tool on the planet.