CsoundManual- Previous section - Contents - Index - Next

CSCORE

Cscore is a standalone program for generating and manipulating numeric score files. It comprises a number of function subprograms, called into operation by a user-written main program. The function programs augment the C language library functions; they can optionally read standard numeric score files, can massage and expand the data in various ways, then write the data out as a new score file to be read by a Csound orchestra.

The user-written main program is also in C. It is not essential to know the C language well to write a main program, since the function calls have a simple syntax, and are powerful enough to do most of the complicated work. Additional power can come from C later as the need arises.


CsoundManual- Top of this section - Previous - Contents - Index - Next

Events, Lists, and Operations

An event in Cscore is equivalent to one statement of a standard numeric score. It is either created or read in from an existing score file. An event is comprised of an opcode and an array of pfield values stored somewhere in memory. Storage is organized by the following structure:

     struct event {
          char op;              /* opcode */
          char tnum;
          short pcnt;
          float p[PMAX+1];      /* pfields */
     };

Any function subprogram that creates, reads, or copies an event function will return a pointer to the storage structure holding the event data. The event pointer can be used to access any component of the structure, in the form of e->op or e->p[n]. Each newly stored event will give rise to a new pointer, and a sequence of new events will generate a sequence of distinct pointers that must themselves be stored. Groups of event pointers are stored in a list, which has its own structure:

     struct evlist {
          int nslots;            /* size of this list      */
          struct event *e[1];    /* list of event pointers */
     };

Any function that creates or modifies a list will return a pointer to the new list. The list pointer can be used to access any of its component event pointers, in the form of a->e[n]. Event pointers and list pointers are thus primary tools for manipulating the data of a score file.

Pointers and lists of pointers can be copied and reordered without modifying the data values they refer to. This means that notes and phrases can be copied and manipulated from a high level of control. Alternatively, the data within an event or group of events can be modified without changing the event or list pointers. Cscore provides a library of programming methods or function subprograms by which scores can be created and manipulated in this way.

In the following summary of Cscore function calls, some simple naming conventions are used:

     the symbols e, f are pointers to events (notes);
     the symbols a, b are pointers to lists (arrays) of such events;
     the letters ev at the end of a function name signify operation on an event;
     the letter l at the start of a function name signifies operation on a list.

     calling syntax         description
     e = createv(n);          create a blank event with n pfields
     e = defev("...");        defines an event as per the character string ...
     e = copyev(f);           make a new copy of event f
     e = getev();             read the next event in the score input file
     putev(e);                write event e to the score output file
     putstr("...");           write the character string ... to score output

     a = lcreat(n);           create an empty event list with n slots
     a = lappev(a,e);         append event e to list a
     n = lcount(a);           count the events now in list a
     a = lcopy(b);            copy the list b (but not the events)
     a = lcopyev(b);          copy the events of b, making a new list
     a = lget();              read events from score input (to next s or e)
     lput(a);                 write the events of list a to score output
     a = lsepf(b);            separate the f statements from list b into list a
     a = lcat(a,b);           concatenate (append) the list b onto the list a
     lsort(a);                sort the list a into chronological order by p[2]
     a = lxins(b,"...");      extract notes of instruments ... (no new events)
     a = lxtimev(b,from,to);  extract notes of time-span, creating new events

     relev(e);                release the space of event e
     lrel(a);                      release the space of list a (but not events)
     lrelev(a);               release the events of list a, and the list space
 


CsoundManual- Top of this section - Previous - Contents - Index - Next

Writing a Main program.

The general format for a main program is:

     #include  <csound/cscore.h>
     main()
     {
          /*  VARIABLE DECLARATIONS     */

          /*  PROGRAM BODY              */
     }

The include statement will define the event and list structures for the program. The following C program will read from a standard numeric score, up to (but not including) the first s or e statement, then write that data (unaltered) as output.

     #include  <csound/cscore.h>
     main()
     {
          struct evlist *a;   /* a is allowed to point to an event list */

          a = lget();         /* read events in, return the list pointer */
          lput(a);            /* write these events out (unchanged) */
          putstr("e");        /* write the string e to output */
     }

After execution of lget(), the variable a points to a list of event addresses, each of which points to a stored event. We have used that same pointer to enable another list function (lput) to access and write out all of the events that were read. If we now define another symbol e to be an event pointer, then the statement

          e = a->e[4]; 

will set it to the contents of the 4th slot in the evlist structure. The contents is a pointer to an event, which is itself comprised of an array of parameter field values. Thus the term e->p[5] will mean the value of parameter field 5 of the 4th event in the evlist denoted by a. The program below will multiply the value of that pfield by 2 before writing it out.

     #include  <csound/cscore.h>
     main()
     {
          struct event  *e;   /* a pointer to an event          */
          struct evlist *a;

          a = lget();         /* read a score as a list of events    */
          e = a->e[4];        /* point to event 4 in event list \fIa\fR */
          e->p[5] *= 2;       /* find pfield 5, multiply its value by 2 */
          lput(a);            /* write out the list of events         */
          putstr("e");        /* add a "score end" statement          */
}

Now consider the following score, in which p[5] contains frequency in cps.

     f 1 0 257 10 1
     f 2 0 257 7 0 300 1 212 .8
     i 1 1 3 0 440 10000
     i 1 4 3 0 256 10000
     i 1 7 3 0 880 10000
     e

If this score were given to the preceding main program, the resulting output would look like this:

     f 1 0 257 10 1
     f 2 0 257 7 0 300 1 212 .8
     i 1 1 3 0 440 10000
     i 1 4 3 0 512 10000      ; p[5] has become 512 instead of 256.
     i 1 7 3 0 880 10000
     e

Note that the 4th event is in fact the second note of the score. So far we have not distinguished between notes and function table setup in a numeric score. Both can be classed as events. Also note that our 4th event has been stored in e[4] of the structure. For compatibility with Csound pfield notation, we will ignore p[0] and e[0] of the event and list structures, storing p1 in p[1], event 1 in e[1], etc. The Cscore functions all adopt this convention.

As an extension to the above, we could decide to use a and e to examine each of the events in the list. Note that e has not preserved the numeral 4, but the contents of that slot. To inspect p5 of the previous listed event we need only redefine e with the assignment

          e = a->e[3]; 

More generally, if we declare a new variable f to be a pointer to a pointer to an event, the statement

          f = &a->e[4]; 

will set f to the address of the fourth event in the event list a, and *f will signify the contents of the slot, namely the event pointer itself. The expression

          (*f)->p[5],

like e->p[5], signifies the fifth pfield of the selected event. However, we can advance to the next slot in the evlist by advancing the pointer f. In C this is denoted by f++.

In the following program we will use the same input score. This time we will separate the ftable statements from the note statements. We will next write the three note-events stored in the list a, then create a second score section consisting of the original pitch set and a transposed version of itself. This will bring about an octave doubling.

By pointing the variable f to the first note-event and incrementing f inside a while block which iterates n times (the number of events in the list), one statement can be made to act upon the same pfield of each successive event.

     #include  <csound/cscore.h>
     main()
     {
          struct event *e,**f;     /* declarations. see pp.8-9 in the */
          struct evlist *a,*b;     /* C language programming manual */
          int n;

          a = lget();              /* read score into event list "a" */
          b = lsepf(a);            /* separate f statements */
          lput(b);                 /* write f statements out to score */
          lrelev(b);               /* and release the spaces used     */
          e = defev("t 0 120");    /* define event for tempo statement */
          putev(e);                /* write tempo statement to score */
          lput(a);                 /* write the notes */
          putstr("s");             /* section end */
          putev(e);                /* write tempo statement again */
          b = lcopyev(a);          /* make a copy of the notes in "a" */
          n = lcount(b);           /* and count the number copied */
          f = &a->e[1];
          while (n--)              /* iterate the following line n times: */
              (*f++)->p[5] *= .5;  /* transpose pitch down one octave */
          a = lcat(b,a);           /* now add these notes to original pitches */
          lput(a);
          putstr("e");
     }

The output of this program is:

     f 1 0 257 10 1
     f 2 0 257 7 0 300 1 212 .8
     t 0 120
     i 1 1 3 0 440 10000
     i 1 4 3 0 256 10000
     i 1 7 3 0 880 10000
     s
     t 0 120
     i 1 1 3 0 440 10000
     i 1 4 3 0 256 10000
     i 1 7 3 0 880 10000
     i 1 1 3 0 220 10000
     i 1 4 3 0 128 10000
     i 1 7 3 0 440 10000
     e

Next we extend the above program by using the while statement to look at p[5] and p[6]. In the original score p[6] denotes amplitude. To create a diminuendo in the added lower octave, which is independent from the original set of notes, a variable called dim will be used.

     #include <csound/cscore.h>
     main()
     {
          struct event *e,**f;
          struct evlist *a,*b;
          int n, dim;               /* declare new variable as integer */

          a = lget();
          b = lsepf(a);
          lput(b);
          lrelev(b);
          e = defev("t 0 120");
          putev(e);
          lput(a);
          putstr("s");
          putev(e);                /* write out another tempo statement */
          b = lcopyev(a);
          n = lcount(b);
          dim = 0;                 /* initialize dim to 0 */
          f = &a->e[1];
          while (n--){
               (*f)->p[6] -= dim;  /* subtract current value of dim */
               (*f++)->p[5] *= .5; /* transpose, move f to next event */
               dim += 2000;        /* increase dim for each note */
          }
          a = lcat(b,a);
          lput(a);
          putstr("e");
     } 

The increment of f in the above programs has depended on certain precedence rules of C. Although this keeps the code tight, the practice can be dangerous for beginners. Incrementing may alternately be written as a separate statement to make it more clear.

          while (n--){
               (*f)->p[6] -= dim;
               (*f)->p[5] *= .5;
               dim += 2000;
               f++;
          }

Using the same input score again, the output from this program

is:

     f 1 0 257 10 1
     f 2 0 257 7 0 300 1 212 .8
     t 0 120
     i 1 1 3 0 440 10000
     i 1 4 3 0 256 10000
     i 1 7 3 0 880 10000
     s
     t 0 120
     i 1 1 3 0 440 10000           ; Three original notes at
     i 1 4 3 0 256 10000           ; beats 1,4 and 7 with no dim.
     i 1 7 3 0 880 10000
     i 1 1 3 0 220 10000           ; three notes transposed down one octave
     i 1 4 3 0 128 8000            ; also at beats 1,4 and 7 with dim.
     i 1 7 3 0 440 6000
     e 

In the following program the same three-note sequence will be repeated at various time intervals. The starting time of each group is determined by the values of the array cue. This time the dim will occur for each group of notes rather than each note.

Note the position of the statement which increments the variable dim outside the inner while block.

      #include  <csound/cscore.h>

    int cue[3]={0,10,17};              /* declare array of 3 integers */

     main()
     {
          struct event *e, **f;
          struct evlist *a, *b;
          int n, dim, cuecount, holdn;  /* declare new variables */

          a = lget();
          b = lsepf(a);
          lput(b);
          lrelev(b);
          e = defev("t 0 120");
          putev(e);
          n = lcount(a);
          holdn = n;                    /* hold the value of "n" to reset below */
          cuecount = 0;                 /* initilize cuecount to "0" */
          dim = 0;
          while (cuecount <= 2) {       /* count 3 iterations of inner "while" */
               f = &a->e[1];            /* reset pointer to first event of list "a" */
               n = holdn;               /* reset value of "n" to original note count */
               while (n--) {
               (*f)->p[6] -= dim;
               (*f)->p[2] += cue[cuecount];       /* add values of cue */
               f++;
               }
               printf("; diagnostic:  cue = %d\n", cue[cuecount]);
               cuecount++;
               dim += 2000;
               lput(a);
          }
          putstr("e");
     }

Here the inner while block looks at the events of list a (the notes) and the outer while block looks at each repetition of the events of list a (the pitch group repetitions). This program also demonstrates a useful trouble-shooting device with the printf function. The semi-colon is first in the character string to produce a comment statement in the resulting score file. In this case the value of cue is being printed in the output to insure that the program is taking the proper array member at the proper time. When output data is wrong or error messages are encountered, the printf function can help to pinpoint the problem.

Using the identical input file, the C program above will generate:

     f 1 0 257 10 1
     f 2 0 257 7 0 300 1 212 .8
     t 0 120

     ; diagnostic:  cue = 0
     i 1 1 3 0 440 10000
     i 1 4 3 0 256 10000
     i 1 7 3 0 880 10000

     ; diagnostic:  cue = 10
     i 1 11 3 0 440 8000
     i 1 14 3 0 256 8000
     i 1 17 3 0 880 8000

     ; diagnostic:  cue = 17
     i 1 28 3 0 440 4000
     i 1 31 3 0 256 4000
     i 1 34 3 0 880 4000
     e

Further development of these scores will lead the composer to techniques of score manipulation which are similar to serial techniques of composition. Pitch sets may be altered with regard to any of the parameter fields. The programing allows transpositions, time warping, pitch retrograding and dynamic controls, to name a few.


CsoundManual - Top of this section - Previous - Contents - Index - Next

Compiling a Cscore program

A Cscore program example.c is compiled and linked with its library modules by the command:

     $ cc  -o myprog  example.c  -lcscore

The resulting executable file myprog is run by typing:

     $ myprog                            (no input, output printed on the screen)
     $ myprog < scorin                (input score named \fIscorin\fR, output on screen)
     $ myprog < scorin  > scorout  (input as above, output into a file)


CsoundManual - Top of this section - Previous - Contents - Index - Next section

Adding your own Cmodules to Csound

If the existing Csound generators do not suit your needs, you can write your own modules in C and add them to the run-time system. When you invoke Csound on an orchestra and score file, the orchestra is first read by a table-driven translator 'otran' and the instrument blocks converted to coded templates ready for loading into memory by 'oload' on request by the score reader. To use your own C-modules within a standard orchestra you need only add an entry in otran's table and relink Csound with your own code.

The translator, loader, and run-time monitor will treat your module just like any other provided you follow some conventions. 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:

     typedef struct {                   /*  newgen.h  -  define a structure */
          OPDS      h;                  /* required header */
          float          *result, *istrt, *incr, *itime, *icontin; /* addr outarg, inargs   */
          float          curval, vincr;           /* private dataspace */
          long      countdown;                    /* ditto       */
     }  RMP;

     #include "cs.h"                    /*  newgen.c  -  init and perf code */
     #include "newgen.h"

     void rampset(p)                    /* at note initialization:     */
       register  RMP  *p;
     {
          if  (*p->icontin == 0.)
               p->curval = *p->istrt;   /* optionally get new start value */
          p->vincr = *p->incr / esr;    /* set s-rate increment per sec.   */
          p->countdown = *p->itime * esr; /* counter for itime seconds     */
     }

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

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

     #include "newgen.h"
     void rampset(), ramp();

     /*   opcode    dspace    thread    outarg    inargs      isub       ksub     asub    */
     { "rampt",     S(RMP),   5,   "a", "iiio",     rampset,  NULL,     ramp  },

Finally we relink Csound to include the new module. Under Unix this means changing the Makefile in three places:
1. Add the name newgen.o to the variable OBJS.
2. Add the name newgen.h as a dependency for entry.o
3. Create a new dependency, newgen.o: newgen.h
Now run 'make csound'. If your host is a Macintosh, simply add newgen.h and newgen.c to one of the Csound segments and invoke the C compiler.

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 floats. There are no restrictions on names, but the input-output argument types are further defined by character strings in entry.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 entry.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 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 theseinitialization, and either k-rate or a-rate subroutineswhich become inserted in various threaded lists of runnable tasks when an instrument is activated. The thread-types appear in entry.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 global variables defined in cs.h and oload.c, the most useful of which are:

       float     esr       user-defined sampling rate
       float     ekr       user-defined control rate
       float     ensmps    user-defined ksmps
       int       ksmps     user-defined ksmps
       int       nchnls    user-defined nchnls
       int       odebug    command-line -v flag
       int       initonly  command-line -I flag
       int       msglevel  command-line -m level
       float     pi, twopi obvious constants
       float     tpidsr    twopi / esr
       float     sstrcod   special code for string arguments

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 = ftpfind(p->ifuncno);

where float *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 cs.h, the ftfind() code in fgens.c, and the code for oscset() and koscil() in ugens2.c.

Additional space

Sometimes the space requirement of a module is too large to be part of a structure (upper limit 65535 bytes), 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:

               if (p->auxch.auxp == NULL)
                    auxalloc(npoints * sizeof(float), &p->auxch);

The address of this auxilliary 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

               char *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 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:

               if (p->mfp == NULL)
                    p->mfp = ldmemfile(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 ugens3.c.

String arguments

To permit a quoted string input argument (float *ifilnam, say) in our defined structure (*p), assign it the argtype S in entry.c, include another member char *strarg in the structure, insert a line

                    TSTRARG( "rampt",  RMP)  \

in the file oload.h, and include the following code in the init module:

               if (*p->ifilnam == sstrcod)
                    strcpy(filename, unquote(p->strarg));

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