Introduction
Traditionally, the practice has been to supply Csound with two inputs: an orchestra and a score. This is how we have been taught to use Csound in Barry Vercoe's A Beginning Tutorial[1]. In fact, this is exactly how many users and non-users view Csound: a system based on the paradigm of an orchestra driven by a fixed-pfield numerical score. However, for more than twenty years now, the capability to accept MIDI input, and indeed, line events, has been there, which dilutes, if not obliterates, the idea of an orchestra-score monolith that is believed to be the basis of Csound. Later, the addition of widget controls in the final releases of Csound 4 also added another alternative means of using Csound. With Csound 5 and the firm establishment of the API, the standard numeric score became effectively only one of many ways of controlling the system. In fact, even the adjective "numeric" was by then questionable, as strings could be used in pfields instead of numbers.
In Csound 6, the system has matured not only to allow various means of controls, but also to require only that an orchestra is present in order to make sound. In other words, it is not necessary to have a score to run Csound. In fact, with the --daemon mode, it is possible to start Csound without any input at all, and supply orchestras and control inputs as needed (via the API, frontends, or as UDP data). Since it is possible to recompile orchestra code on-the-fly, new styles of programming using only the orchestra language are possible.
In this article, I would like to explore a style of programming Csound which employs solely the orchestra to control how instruments are run, without the use of the numerical score. It not only allows a uniform and possibly more elegant means of programming, but also novel forms of using Csound that were not possible with the traditional use of the standard numeric score.
A note about the examples: all original code used here is
orchestra-only and uses the Csound 6 syntax (this of course excludes the first
example taken from Vercoe's tutorial). It can be run by placing it in
the orchestra section of a CSD file, by compiling it through an API
call, by selecting it in CsoundQT and using the command evaluate
selection, by using the online compiler,
or by sending it via UDP to a Csound server (--port=N
option).
(The following references examples you can download for this article from the following link: scoreless_csound.zip)
I. Simple event scheduling
Traditional scores
Let us begin by considering Vercoe's tutorial example[1], which has the following orchestra,
sr = 44100 ; audio sampling rate is 44.1 kHz kr = 4400 ; control rate is 4410 Hz ksmps = 10 ; number of samples in a control period (sr/kr) nchnls = 1 ; number of channels of audio output instr 1 kctrl line 0, p3, 10000 ; amplitude envelope asig oscil kctrl, cpspch(p5), 1 ; audio oscillator out asig ; send signal to channel 1 endin
which is driven by the following score:
f1 0 256 10 1 ; a sine wave function table ; a pentatonic scale i1 0 .5 0 8.01 i1 .5 . . 8.03 i1 1.0 . . 8.06 i1 1.5 . . 8.08 i1 2.0 . . 8.10 eWe can translate this into a scoreless code by employing the
schedule
opcode. Each one of the i-statements in the score, below,
is mapped into an instance of schedule
in global space
(ie. outside the instruments, also known as instr 0
).
schedule 1, 0, .5, 0, 8.01 schedule 1, .5, .5, 0, 8.03 schedule 1, 1, .5, 0, 8.06 schedule 1, 1.5, .5, 0, 8.08 schedule 1, 2, .5, 0, 8.10 instr 1 kctrl linen 10000,0.01,p3,0.1 asig oscili kctrl, cpspch(p5) out asig endin
Note that we can get rid of the sr
, kr
, ksmps
, nchnls
lines because
they are using the default values. We also do not need to create a
sinewave table, because Csound 6 has a default table (-1), which
is used by default by the oscil
opcode. Replacing the line
for a linen
also removes the clicks heard in the original
example, as the sounds finish more gracefully; we should also use oscili
instead of oscil
for a cleaner sound.
Event generation
The above example does not really demonstrate any particular advantage
over the traditional score. However, we can do better. Consider this:
the schedule
lines above have quite a bit of repetition in
their parameters. Surely we can do something to avoid all that
longhand coding. This is what we do:
ipch init 8.01 icnt init 0 start: if icnt > 5 igoto end schedule 1, icnt/2, .5, 0, ipch ipch = (icnt == 1 ? ipch+0.03 : ipch+0.02) icnt += 1 igoto start end: instr 1 ...
With a few lines of orchestra code, we generated the same sequence of events programmatically. This is something that would not be possible to do in the score (after all, the score is not a programming language, even though it has some preprocessing support). Loops are possible in the score, but not very sophisticated ones. Even a simple one like in the example above would not be possible because of the use of branching/conditionals.
The benefit of this is that we can change the generative patterns by
doing simple modifications to the code. For instance, we can create
sequences of the same pattern by placing another loop around it. It is
also very easy to control the duration and start time of events, by
making them depend on a given variable (idur
).
idur init 0.1 ipch init 8.01 icnt init 0 icnt2 init 0 until icnt2 > 4 do start: if icnt > 5 igoto end schedule 1, (icnt2*5 + icnt)*idur,idur, 0, ipch ipch = (icnt == 1 ? ipch+0.03 : ipch+0.02) icnt += 1 igoto start end: icnt = 0 icnt2 += 1 ipch = 8.01 + icnt2*0.01 od instr 1 ...
In fact, we can think of a number of different ways we can exploit such programmatic capabilities of scoreless Csound. The examples above give us just a glimpse of what is possible.
II. Extending the range
Performance time
We are not limited to using global-space code. By going beyond it to using instruments themselves to run other instruments, we can start moving away from a fixed score. After all, although the examples above were generative, they created finite sets of events. What if we want to generate a pattern to be repeated indefinitely?
In that case, we will use an instrument that runs indefinitely, and
take advantage of the built-in perform loop. An active instrument will
be able to schedule events according to a trigger. For this we use
the metro
opcode to generate the trigger at a given rate, and event
to issue the events. Note that because we are working
at performance time, we do not need to write an explicit loop, the
code will be running iteratively at the k-rate. We schedule this
instrument to run indefinitely.
schedule 2,0,-1,8.01 instr 2 idur = 0.1 kcnt init 0 kpch init p4 if metro(1/idur) == 1 then event "i",1,0,idur,0,kpch if kcnt > 5 then kpch = p4 kcnt = 0 else kpch = (kcnt == 1 ? kpch+0.03 : kpch+0.02) kcnt += 1 endif endif endin instr 1 ...
Interestingly, with this code, if we want to create interlocking patterns, all we need is to schedule other instance(s) of the instrument, starting slightly after the first.
schedule 2.0,0,-1,8.01 schedule 2.1,0.05,-1,8.07
We have to use different fractional p1 values for schedule
to
make sure two separate indefinitely-running instances would be issued.
Otherwise, the second schedule would just replace the instance
launched by the first, due to the use of negative p3.
Data sources
All previous examples used a simple algorithm to set the pitch of each event: if we are in the second step we jump by three semitones, otherwise we jump by two. We implemented it using conditionals, but we could have ignored the algorithm and stored the parameter data somewhere and source it from there. A simple way of doing this is using a table, and placing the required parameter values there. The table then can be read with the desired index. The example below demonstrates this approach.
schedule 2.1,0,-1,1 it ftgen 1,0,6,-2,8.01,8.03,8.06,8.08,8.10 instr 2 idur = 0.1 kcnt init 0 if metro(1/idur) == 1 then event "i",1,0,idur,0,table:k(kcnt,1)*p4 kcnt = (kcnt == 4 ? 0 : kcnt+1) endif endin instr 1 ...
In this particular case, it generates a more compact code. Tables are particularly useful if we want to draw a given shape or use a certain function to create a pattern, as some GENs (Generator Routines) can do it very well, with a few parameters.
We can also use arrays to store data. Below is shown a version that replaces the table using a global array.
schedule 2.1,0,-1,0 gkpch[] fillarray 8.01,8.03,8.06,8.08,8.10 instr 2 idur = 0.1 kcnt init 0 if metro(1/idur) == 1 then event "i",1,0,idur,0, gkpch[kcnt]+p4 kcnt = (kcnt == 4 ? 0 : kcnt+1) endif endin instr 1 ...
Here we can also depart from the original Vercoe example and add another parameter, amplitude. To make it a slightly more interesting pattern, we will use an array containing 4 values, which will go in and out-of-phase with the 5-pitch pattern. Using a modulus operation, we make sure the index is inside the correct range for each array.
schedule 2.1,0,-1,0 gkpch[] fillarray 8.01,8.03,8.06,8.08,8.10 gkamp[] fillarray 0.5,0.05,0.6,0.9 instr 2 idur = 0.1 kcnt init 0 i1 lenarray gkpch i2 lenarray gkamp if metro(1/idur) == 1 then event "i",1,0,idur, gkamp[kcnt%i2], gkpch[kcnt%i1]+p4 kcnt = (kcnt == i1*i2-1 ? 0 : kcnt+1) endif endin instr 1 kctrl linen p4*0dbfs,0.01,p3,0.1 asig oscili kctrl, cpspch(p5) out asig endin
III. Recursion
From head to tail
Another way of looking at event generation is to think that an
instrument can schedule itself to run again at some point. In this
case, we have the classic example of recursion used to repeat an
action, instead of a loop. In this case, we can actually get rid of
the metro
trigger and the conditional. In fact, we can get
rid of instrument 2 altogether and do everything from instr 1. We
only need a single schedule from global space to prime the
process and then the rest is done by recursion, as shown in the following example.
(This technique is also known as temporal recursion[2].)
schedule 1,0,0.5,0,0 gipch[] fillarray 8.01,8.03,8.06,8.08,8.10 giamp[] fillarray 0.5,0.05,0.6,0.9 instr 1 icnt = p5 i1 lenarray gipch i2 lenarray giamp kctrl linen giamp[icnt%i2]*0dbfs,0.01,p3,0.1 asig oscil kctrl, cpspch(gipch[icnt%i1]+p4) out asig icnt = (icnt == i1*i2-1 ? 0 : icnt+1) schedule 1,p3,p3,p4,icnt endin
Recursion is a nice programming device that can be used to very good
effect, for both sequential instances (as in this example) and also
for parallel/chordal textures. The only thing to watch out for, is to make
sure we do not enter an eternal recursive path, and lock Csound out.
This happens when the next event scheduled falls in the same k-cycle
as the calling instrument and there are no conditions set to finish
up the recursion. So we should be careful when scheduling sequential
events such as the above to make sure the next start event is scheduled
for a time that is ksmps/sr
ahead. It is possible to add in a
conditional check for that in the code.
Randomness
A great advantage of scheduling events from the orchestra is that we can use all the generating facilities that the huge collection of Csound opcodes provides. For instance, we can use oscillators to read table patterns and produce periodic parameter changes; we can use signal measurement, eg. of amplitude, pitch, centroid, to control how instruments are run; and so on. The possibilities are very wide.
One particular example of this is worth exploring here: the use of random number generators. Completing the set of examples above, we can introduce random choices of parameters to the recursion process. We can keep the structure of the previous example, but instead of counting up, we calculate a random index into the arrays for each event. In order to give the performance a slight swing, we can use another random generator to add small deviations to the event start times and durations:
schedule 1,0,0.5,0,0 gipch[] fillarray 8.01,8.03,8.06,8.08,8.10 giamp[] fillarray 0.1,0.02,0.2,0.3 instr 1 indx = p5 i1 lenarray gipch i2 lenarray giamp kctrl linen giamp[indx%i2]*0dbfs,0.01,p3,0.1 asig oscil kctrl, cpspch(gipch[indx%i1]+p4) out asig idur = gauss(0.05)+0.25 schedule 1,idur,idur*2,p4,int(linrand:i(100)) endin
Again, to generate another pattern to interlock with our first one, we
can just add one or more "primers". Each one will generate a stream
of recursive events, and we can use p4 to transpose it (an octave in
this case).
Also, using seed
, we can guarantee a new random sequence
every time we run the code, as shown in the code snippit below.
seed 0 schedule 1,0,0.5,0,0 schedule 1,0.075,0.5,1,0
Depending on the frontend used, it will be possible to modify the recursive instruments and recompile them on-the-fly. This will then produce a change in the patterns that are generated. It is also possible to recompile other "primers" to add new streams and more complexity to the process. The process can be halted by recompiling a blank instrument 1. This type of interactive programming is something that goes hand-in-hand with the scoreless style discussed in this article.
IV. Conclusion
This article discussed a programming style that is well supported in Csound 6, but that has so far been not very widely used. Not only does it allow us to harness the power and expressivity of the Csound (orchestra) language, but it also induces approaches such as interactive programming, which can be used to modify running code on-the-fly. In a way, we have only scratched the surface in terms of possibilities enabled by this. However, the methods presented here should provide a good starting point for experimentation.
The scoreless style has already been used by some composers, most notably by Iain McCurdy in his classic series of Haiku[3] works. It is hoped that this article will encourage others to explore the possibilities provided by the ideas presented here.
References
[1] Barry Vercoe, "A Beginning Tutorial." [Online] Available: http://www.csounds.com/tootsother/vercoetut/Vercoe.html. [Accessed October 14, 2014].
[2] Andrew Sorenson, "The Many Faces of Temporal Recursion." [Online] Available: http://extempore.moso.com.au/temporal_recursion.html. [Accessed October 16, 2014].
[3] Ian McCurdy, "Csound Haiku." [Online] Available: http://iainmccurdy.org/csoundhaiku.html. [Accessed October 15, 2014].