Extending Csound

Adding Unit Generators

If the existing Csound unit generators do not suit your needs, it is relatively easy to extend Csound by writing new unit generators in C or C++. The translator, loader, and run-time monitor will treat your module just like any other, provided you follow some conventions.

Historically, this has been done with builtin unit generators, that is, with code that is statically linked with the rest of the Csound executable.

Today, the preferred method is to create plugin unit generators. These are dynamic link libraries (DLLs) on Windows, and loadable modules (shared libraries that are dlopened) on Linux. Csound searches for and loads these plugins at run time on the path defined in OPCODEDIR. You can also load plugin opcodes from the command line using the --opcode-lib flag.

The advantage of this method, of course, is that plugins created by any developer at any time can be used with already existing versions of Csound.

Creating a Builtin Unit Generator

You need a structure defining the inputs, outputs and workspace, plus some initialization code and some perf-time code. Let's put an example of these in two new files, newgen.h and newgen.c. The examples given are for Csound 5. For earlier versions, all opcode functions omit the first parameter (CSOUND *csound).

/* newgen.h  -  define a structure */

/* Declares Csound structures and functions. */
#include "csoundCore.h"

typedef struct
{
  OPDS h;                                         /* required header */
  MYFLT *result, *istrt, *incr, *itime, *icontin; /* addr outarg, inargs */
  MYFLT curval, vincr;                            /* private dataspace */
  long countdown;                                 /* ditto */
} RMP;


/* newgen.c -  init and perf code */
/* Declares Csound structures and functions. */
#include "csoundCore.h"
/* Declares RMP structure. */
#include "newgen.h"

int rampset (CSOUND *csound, RMP * p)     /* at note initialization: */
{
  if (*p->icontin == FL(0.0))
    p->curval = *p->istrt;                /* optionally get new start value */
  p->vincr = *p->incr / csound->esr;      /* set s-rate increment per sec. */
  p->countdown = *p->itime * csound->esr; /* counter for itime seconds */
  return OK;
}

int ramp (CSOUND *csound, RMP * p)        /* during note performance: */
{
  MYFLT *rsltp = p->result;               /* init an output array pointer */
  int nn = csound->ksmps;                 /* array size from orchestra */
  do
    {
      *rsltp++ = p->curval;               /* copy current value to output */
      if (--p->countdown > 0)             /* for the first itime seconds, */
        p->curval += p->vincr;            /* ramp the value */
    }
  while (--nn);
  return OK;
}

Now we add this module to the translator table in entry1.c, under the opcode name rampt:

#include "newgen.h"

int rampset(CSOUND *, RMP *), ramp(CSOUND *, RMP *);

/*   opname    dsblksiz  thread    outypes   intypes   iopadr    kopadr    aopadr  */

{ "rampt",  S(RMP),  5,        "a",      "iiio",     (SUBR) rampset, (SUBR) NULL, (SUBR) ramp  },

Finally you must relink Csound with the new module. Add the name of the C file to the libCsoundSources list in the SConstruct file:

libCsoundSources = Split('''
Engine/auxfd.c
...
OOps/newgen.c
...
Top/utility.c
''')

Run scons just as you would for any other Csound build, and the new module will be built into your Csound.

The above actions have added a new generator to the Csound language. It is an audio-rate linear ramp function which modifies an input value at a user-defined slope for some period. A ramp can optionally continue from the previous note's last value. The Csound manual entry would look like:

ar rampt istart, islope, itime [, icontin]

istart -- beginning value of an audio-rate linear ramp. Optionally overridden by a continue flag.

islope -- slope of ramp, expressed as the y-interval change per second.

itime -- ramp time in seconds, after which the value is held for the remainder of the note.

icontin (optional) -- continue flag. If zero, ramping will proceed from input istart . If non-zero, ramping will proceed from the last value of the previous note. The default value is zero.

The file newgen.h includes a one-line list of output and input parameters. These are the ports through which the new generator will communicate with the other generators in an instrument. Communication is by address, not value, and this is a list of pointers to values of type MYFLT (which is double if the macro USE_DOUBLE is defined, and float otherwise). There are no restrictions on names, but the input-output argument types are further defined by character strings in entry1.c (inargs, outargs). Inarg types are commonly x, a, k, and i, in the normal Csound manual conventions; also available are o (optional, defaulting to 0), p (optional, defaulting to 1). Outarg types include a, k, i and s (asig or ksig). It is important that all listed argument names be assigned a corresponding argument type in entry1.c. Also, i-type args are valid only at initialization time, and other-type args are available only at perf time. Subsequent lines in the RMP structure declare the work space needed to keep the code re-entrant. These enable the module to be used multiple times in multiple instrument copies while preserving all data.

The file newgen.c contains two subroutines, each called with a pointer to the Csound instance and a pointer to the uniquely allocated RMP structure and its data. The subroutines can be of three types: note initialization, k-rate signal generation, a-rate signal generation. A module normally requires two of these: initialization, and either k-rate or a-rate subroutines which become inserted in various threaded lists of runnable tasks when an instrument is activated. The thread-types appear in entry1.c in two forms: isub, ksub and asub names; and a threading index which is the sum of isub=1, ksub=2, asub=4. The code itself may reference (but should only read) public members of the CSOUND structure defined in csoundCore.h, the most useful of which are:

        OPARMS  *oparms
        MYFLT   esr                 user-defined sampling rate
        MYFLT   ekr                 user-defined control rate
        int     ksmps               user-defined ksmps
        int     nchnls              user-defined nchnls
        int     oparms->odebug      command-line -v flag
        int     oparms->msglevel    command-line -m level
        MYFLT   tpidsr              2 * PI / esr

Function tables

To access stored function tables, special help is available. The newly defined structure should include a pointer

FUNC        *ftp;

initialized by the statement

ftp = csound->FTFind(csound, p->ifuncno);

where MYFLT *ifuncno is an i-type input argument containing the ftable number. The stored table is then at ftp->ftable, and other data such as length, phase masks, cps-to-incr converters, are also accessed from this pointer. See the FUNC structure in csoundCore.h, the csoundFTFind() code in fgens.c, and the code for oscset() and koscil() in OOps/ugens2.c.

Additional Space

Sometimes the space requirement of a module is too large to be part of a structure (upper limit 65279 bytes, due to the unsigned short dsblksiz parameter and reserved codes >= 0xFF00), or it is dependent on an i-arg value which is not known until initialization. Additional space can be dynamically allocated and properly managed by including the line

AUXCH      auxch;

in the defined structure (*p), then using the following style of code in the init module:

csound->AuxAlloc(csound, npoints * sizeof(MYFLT), &p->auxch);

The address of this auxiliary space is kept in a chain of such spaces belonging to this instrument, and is automatically managed while the instrument is being duplicated or garbage-collected during performance. The assignment

void *auxp = p->auxch.auxp;

will find the allocated space for init-time and perf-time use. See the LINSEG structure in ugens1.h and the code for lsgset() and klnseg() in OOps/ugens1.c.

File Sharing

When accessing an external file often, or doing it from multiple places, it is often efficient to read the entire file into memory. This is accomplished by including the line

MEMFIL    *mfp;

in the defined structure (*p), then using the following style of code in the init module:

p->mfp = csound->ldmemfile(csound, filname);

where char *filname is a string name of the file requested. The data read will be found between

(char *)  p->mfp->beginp; and (char *) p->mfp->endp;

Loaded files do not belong to a particular instrument, but are automatically shared for multiple access. See the ADSYN structure in ugens3.h and the code for adset() and adsyn() in OOps/ugens3.c.

String arguments

To permit a string input argument (MYFLT *ifilnam, say) in our defined structure (*p), assign it the argtype S in entry1.c, and include the following code in the init module:

strcpy(filename, (char*) p->ifilnam);

See the code for adset() in OOps/ugens3.c, lprdset() in OOps/ugens5.c, and pvset() in OOps/ugens8.c.

Adding a Plugin Unit Generator

The procedure for creating a plugin unit generator is very similar to the procedure for creating a builtin. The actual unit generator code would normally be identical. The differences are as follows.

Again supposing that your unit generator is named newgen, perform the following steps:

  1. Write your newgen.c and newgen.h file as you would for a builtin unit generator. Put these files in the csound5/Opcodes directory.

  2. #include "csdl.h" in your unit generator sources, instead of csoundCore.h.

  3. Add your OENTRY records and unit generator registration functions at the bottom of your C file. Example (but you can have as many unit generators in one plugin as you like):

    #define S sizeof
    
    static OENTRY localops[] = {
            { "rampt",  S(RMP),  5, "a", "iiio",  (SUBR) rampset, (SUBR) NULL, (SUBR)ramp  },
    };
    
    /*
     * The following macro from csdl.h defines
     * the "csound_opcode_init()" opcode registration
     * function for the localops table.
     */
    LINKAGE
  4. Add your plugin as a new target in the plugin opcodes section of the SConstruct build file:

    pluginEnvironment.SharedLibrary('newgen',
        Split('''Opcodes/newgen.c
        Opcodes/another_file_used_by_newgen.c
        Opcodes/yet_another_file_used_by_newgen.c'''))
  5. Run the Csound 5 build in the regular way.

OENTRY Reference

The OENTRY structure (see H/csoundCore.h, Engine/entry1.c, and Engine/rdorch.c) contains the following public fields:

opname, dsblksiz, thread, outypes, intypes, iopadr, kopadr, aopadr
dsblksiz

There are two types of opcodes, polymorphic and non-polymorphic. For non-polymorphic opcodes, the dsblksiz flag specifies the size of the opcode structure in bytes, and arguments are always passed to the opcode at the same rate. Polymorphic opcodes can accept arguments at different rates, and those arguments are actually dispatched to other opcodes as determined by the dsblksiz flag and the following naming convention (note: the following list is not complete, see Engine/entry1.c for all possible special dsblksiz codes):

0xffff

The type of the first output argument determines which unit generator function is actually called: XXX -> XXX.a, XXX.i, or XXX.k.

0xfffe

The types of the first two input arguments determine which unit generator function is actually called: XXX -> XXX.aa, XXX.ak, XXX.ka, or XXX.kk, as in the oscil unit generator.

0xfffd

Refers to one input argument of type a or k, as in the peak unit generator.

thread

Specifies the rate(s) at which the unit generator's functions are called, as follows:

Table 22. Rate at which ugens are called according to thread parameter

0 i-rate or k-rate (B out only)
1 i-rate
2 k-rate
3 i-rate and k-rate
4 a-rate
5 i-rate and a-rate
7 i-rate and (k-rate or a-rate)


outypes

Lists the return values of the unit generator functions, if any. The types allowed are (note: the following list is not complete, see Engine/entry1.c for all possible output types):

Table 23. List of out types for ugens

i i-rate scalar
k k-rate scalar
a a-rate vector
x k-rate scalar or a-rate vector
f f-rate streaming pvoc fsig type
m multiple a-rate output arguments


intypes

Lists the arguments the unit generator functions take, if any. The types allowed are (note: the following list is not complete, see Engine/entry1.c for all possible input types):

Table 24. List of in types ofr ugens

i i-rate scalar
k k-rate scalar
a a-rate vector
x a-rate scalar or a-rate vector
f f-rate streaming pvoc fsig type
S String
B  
l  
m Begins an indefinite list of i-rate arguments (any count)
M Begins an indefinite list of arguments (any rate, any count)
N Begins an indefinite list of (optional a-, k-, i-, or S-rate)-rate arguments (any odd count)
n Begins an indefinite list of i-rate arguments (any odd count)
O Optional k-rate, defaulting to 0
o Optional i-rate, defaulting to 0
p Optional i-rate, defaulting to 1
q Optional i-rate, defaulting to 10
V Optional k-rate, defaulting to 0.5
v Optional i-rate, defaulting to 0.5
j Optional i-rate, defaulting to -1
h Optional i-rate, defaulting to 127
y Begins an indefinite list of a-rate arguments (any count)
z Begins an indefinite list of k-rate arguments (any count)
Z Begins an indefinite list of alternating k-rate and a-rate arguments (kaka...) (any count)


iopadr

The address of the unit generator function (of type int (*SUBR)(CSOUND *, void *)) that is called at i-time, or NULL for no function.

kopadr

The address of the unit generator function (of type int (*SUBR)(CSOUND *, void *)) that is called at k-rate, or NULL for no function.

aopadr

The address of the unit generator function (of type int (*SUBR)(CSOUND *, void *)) that is called at a-rate, or NULL for no function.