These examples are based Steven Yi's Python "How to use the Csound API" files on https://github.com/csound/csoundAPI_examples
All of the examples assume having the ctcsound module imported which is the module containing the Python interface to the Csound API. This must be executed before the examples are run.
This adaptation of Steven's examples as a notebook using ctcsound has been written by Mitch Kaufman.
In [1]:
import ctcsound
This example is a barebones example for creating an instance of Csound, compiling a pre-existing CSD, calling "perform" to run Csound to completion, then reset. The first thing we do is import the ctcsound module, which is the module containing the Python interface to the Csound API.
All of the examples below must have the ctcsound module imported.
In [2]:
c = ctcsound.Csound()
ret = c.compileCsd("test1.csd")
if ret == ctcsound.CSOUND_SUCCESS:
c.start()
c.perform()
c.reset()
In this example, we move from using an external CSD file to embedding our Csound ORC and SCO code within our Python project. Besides allowing encapsulating the code within the same file, using the compileOrc() and compileSco() API calls is useful when the SCO or ORC are generated, or perhaps coming from another source, such as from a database or network.
In [3]:
# Defining our Csound ORC code within a multiline String
orc = """
sr=44100
ksmps=32
nchnls=2
0dbfs=1
instr 1
aout vco2 0.5, 440
outs aout, aout
endin"""
# Defining our Csound SCO code
sco = "i1 0 1"
#c = ctcsound.Csound()
c.setOption("-odac") # Using SetOption() to configure Csound
# Note: use only one commandline flag at a time
c.compileOrc(orc) # Compile the Csound Orchestra string
c.readScore(sco) # Compile the Csound SCO String
c.start() # When compiling from strings, this call is necessary before doing any performing
c.perform() # Run Csound to completion
c.reset()
In this example, we use a while loop to perform Csound one audio block at a time. This technique is important to know as it will allow us to do further processing safely at block boundaries. We will explore the technique further in later examples.
Note that c.performKsmps() and c.performBuffer() return False while the score is not finished. Here, a dot is displayed for each pass in the loop:
In [4]:
orc = """
sr=44100
ksmps=32
nchnls=2
0dbfs=1
instr 1
aout vco2 0.5, 440
outs aout, aout
endin"""
# Our Score for our project
sco = "i1 0 1"
#c = ctcsound.Csound() # create an instance of Csound
c.setOption("-odac") # Set option for Csound
c.compileOrc(orc) # Compile Orchestra from String
c.readScore(sco) # Read in Score from String
c.start() # When compiling from strings, this call is necessary before doing any performing
# The following is our main performance loop. We will perform one block of sound at a time
# and continue to do so while it returns 0, which signifies to keep processing. We will
# explore this loop technique in further examples.
while not c.performKsmps():
print('.', end='')
print()
c.reset()
In this example, we use a CsoundPerformanceThread to run Csound in a native thread. Using a native thread is important to get the best runtime performance for the audio engine. It is especially important for languages such as Python that do not have true native threads and that use a Global Interpreter Lock. CsoundPerformanceThread has some convenient methods for handling events, but does not have features for doing regular processing at block boundaries. In general, use CsoundPerformanceThread when the only kinds of communication you are doing with Csound are through events, and not using channels.
In [5]:
# Our Orchestra for our project
orc = """
sr=44100
ksmps=32
nchnls=2
0dbfs=1
instr 1
aout vco2 0.5, 440
outs aout, aout
endin"""
# Our Score for our project
sco = "i1 0 1"
#c = ctcsound.Csound() # create an instance of Csound
c.setOption("-odac") # Set option for Csound
c.compileOrc(orc) # Compile Orchestra from String
c.readScore(sco) # Read in Score from String
c.start() # When compiling from strings, this call is necessary before doing any performing
t = ctcsound.CsoundPerformanceThread(c.csound()) # Create a new CsoundPerformanceThread, passing in the Csound object
t.play() # starts the thread, which is now running separately from the main thread. This
# call is asynchronous and will immediately return back here to continue code
# execution.
t.join() # Join will wait for the other thread to complete. If we did not call join(),
# after t.play() returns we would immediate move to the next line, c.stop().
# That would stop Csound without really giving it time to run.
c.reset()
In [6]:
orc = """
sr=44100
ksmps=32
nchnls=2
0dbfs=1
instr 1
ipch = cps2pch(p5, 12)
kenv linsegr 0, .05, 1, .05, .7, .4, 0
aout vco2 p4 * kenv, ipch
aout moogladder aout, 2000, 0.25
outs aout, aout
endin"""
In [7]:
sco = "i1 0 1 0.5 8.00"
#c = ctcsound.Csound() # create an instance of Csound
c.setOption("-odac") # Set option for Csound
c.compileOrc(orc) # Compile Orchestra from String
c.readScore(sco) # Read in Score from pre-written String
c.start() # When compiling from strings, this call is necessary before doing any performing
# The following is our main performanceu loop. We will perform one block of sound at a time
# and continue to do so while it returns 0, which signifies to keep processing.
while (c.performKsmps() == 0):
pass
c.reset()
Knowing that we pass strings into Csound to pass note events, we can also generate the string. In the second example, sco2 starts as an empty string. Using a for-loop, we append to sco2 note strings using a string formatting string that has its replacement values replaced. The replace values are calculated using the i value, and the result is an ascending note line.
In [8]:
sco2 = ""
for i in range(13):
sco2 += "i1 %g .25 0.5 8.%02g\n"%(i * .25,i)
print(sco2)
#c = ctcsound.Csound() # create an instance of Csound
c.setOption("-odac") # Set option for Csound
c.compileOrc(orc) # Compile Orchestra from String
c.readScore(sco2) # Read in Score from pre-written String
c.start() # When compiling from strings, this call is necessary before doing any performing
# The following is our main performanceu loop. We will perform one block of sound at a time
# and continue to do so while it returns 0, which signifies to keep processing.
while (c.performKsmps() == 0):
pass
c.reset()
In the final example, we are going to generate a list of lists. This example generates a score using a random function and then converts to String. The top-level list represents our score as a whole, and each sub-list within it represents the data for a single note. The main list is then processed in two ways: first, it processes each sub-list and joins the values together into a single note string; second, it joins each individual note string into a single, large score string, separated by newlines. The end result is a sequence of 13 notes with random pitches.
The final example represents a common pattern of development. For systems that employ some event-based model of music, it is common to use some kind of data structure to represent events. This may use some kind of common data structure like a list, or it may be represented by using a class and instances of that class.
In [9]:
from random import randint
vals = [] #initialize a list to hold lists of values
for i in range(13): #populate that list
vals.append([1, i * .25, .25, 0.5, "8.%02g"%(randint(0,15))])
# convert list of lists into a list of strings
vals = ["i" + " ".join(map(str,a)) for a in vals]
# now convert that list of strings into a single string
sco3 = "\n".join(vals)
print('Here is the list of lists that was converted into a list of strings:')
print()
print(vals)
print()
print('Here is the list of score events that was generated into a single string:')
print()
print(sco3)
#c = ctcsound.Csound() # create an instance of Csound
c.setOption("-odac") # Set option for Csound
c.compileOrc(orc) # Compile Orchestra from String
c.readScore(sco3) # Read in Score from pre-written String
c.start() # When compiling from strings, this call is necessary before doing any performing
# The following is our main performanceu loop. We will perform one block of sound at a time
# and continue to do so while it returns 0, which signifies to keep processing.
while (c.performKsmps() == 0):
pass
c.reset()
This example continues on from Example 5, rewriting the example using a Class called Note. The note example has its str method implemented to generate a well-formatted Csound SCO note. This example also shows how a list of notes could be used multiple times.The first loop through we use the notes as-is, and during the second time we generate the notes again with the same properties except we alter the fifth p-field up 4 semitones.
Note: Altering a Notes values like this is alright for this example, but it is a destructive edit. Real world code might make copies of Notes or alter the score generation to maintain the original values.
In [10]:
from random import randint
def midi2pch(num):
"Convert MIDI Note Numbers to Csound PCH format"
return "%d.%02g" % (3 + (num / 12), num % 12)
class Note(object):
def __init__(self, *args):
self.pfields = list(args)
def __str__(self):
retVal = "i"
for i in range(len(self.pfields)):
if(i == 4):
retVal += " " + midi2pch(self.pfields[i])
else:
retVal += " " + str(self.pfields[i])
return retVal
# Our Orchestra for our project
orc = """
sr=44100
ksmps=32
nchnls=2
0dbfs=1
instr 1
ipch = cps2pch(p5, 12)
kenv linsegr 0, .05, 1, .05, .7, .4, 0
aout vco2 p4 * kenv, ipch
aout moogladder aout, 2000, 0.25
outs aout, aout
endin"""
#c = ctcsound.Csound() # create an instance of Csound
c.setOption("-odac") # Set option for Csound
c.compileOrc(orc) # Compile Orchestra from String
notes = [] #initialize a list to hold lists of values
for i in range(13): #populate that list
notes.append( Note(1, i * .25, .25, 0.5, randint(60,75)) )
# now convert list of Note objects to string
sco = ""
for n in notes:
sco += "%s\n"%n # this implicitly calls the __str__ method on the Note Class
# generate notes again transposed a Major 3rd up
for n in notes:
n.pfields[4] += 4
n.pfields[1] += .125
sco += "%s\n"%n
print('Here is the list of score events that was generated:')
print()
print(sco)
c.readScore(sco) # Read in Score generated from notes
c.start() # When compiling from strings, this call is necessary before doing any performing
# The following is our main performance loop. We will perform one block of sound at a time
# and continue to do so while it returns 0, which signifies to keep processing.
while (c.performKsmps() == 0):
pass
c.reset()
This example introduces using Csound's Channel System to communicate continuous control data (k-rate) from a host program to Csound. The first thing to note is the RandomLine class. It takes in a base value and a range in which to vary randomly. The reset functions calculates a new random target value (self.end), a random duration in which to run (self.dur, expressed as # of audio blocks to last in duration), and calculates the increment value to apply to the current value per audio-block. When the target is met, the Randomline will reset itself to a new target value and duration.
In this example, we use two RandomLine objects, one for amplitude and another for frequency. We start a Csound instrument instance that reads from two channels using the chnget opcode. In turn, we update the values to the channel from the host program. In this case, because we want to keep our values generating in sync with the audio engine, we use a while-loop instead of a CsoundPerformanceThread. To update the channel, we call the SetChannel method on the Csound object, passing a channel name and value.
Note: The getValue method on the RandomLine not only gets us the current value, but also advances the internal state by the increment and by decrementing the duration.
In [11]:
from random import randint, random
class RandomLine(object):
def __init__(self, base, range):
self.curVal = 0.0
self.reset()
self.base = base
self.range = range
def reset(self):
self.dur = randint(256,512)
self.end = random()
self.increment = (self.end - self.curVal) / self.dur
def getValue(self):
self.dur -= 1
if(self.dur < 0):
self.reset()
retVal = self.curVal
self.curVal += self.increment
return self.base + (self.range * retVal)
# Our Orchestra for our project
orc = """
sr=44100
ksmps=32
nchnls=2
0dbfs=1
instr 1
kamp chnget "amp"
kfreq chnget "freq"
printk 0.5, kamp
printk 0.5, kfreq
aout vco2 kamp, kfreq
aout moogladder aout, 2000, 0.25
outs aout, aout
endin"""
#c = ctcsound.Csound() # create an instance of Csound
c.setOption("-odac") # Set option for Csound
c.setOption("-m7") # Set option for Csound
c.compileOrc(orc) # Compile Orchestra from String
sco = "i1 0 60\n"
c.readScore(sco) # Read in Score generated from notes
c.start() # When compiling from strings, this call is necessary before doing any performing
# The following is our main performance loop. We will perform one block of sound at a time
# and continue to do so while it returns 0, which signifies to keep processing.
amp = RandomLine(.6, .2) # create RandomLine for use with Amplitude
freq = RandomLine(400, 80) # create RandomLine for use with Frequency
c.setControlChannel("amp", amp.getValue()) # Initialize channel value before running Csound
c.setControlChannel("freq", freq.getValue()) # Initialize channel value before running Csound
print('Initial amp value is: ' + str(amp.getValue()))
print('Initial freq value is: ' + str(freq.getValue()))
while (c.performKsmps() == 0):
c.setControlChannel("amp", amp.getValue()) # update channel value
c.setControlChannel("freq", freq.getValue()) # update channel value
c.reset()
This example builds on Example 7 by replacing the calls to SetChannel with using GetChannelPtr. In the Csound API, using SetChannel and GetChannel is great for quick work, but ultimately it is slower than pre-fetching the actual channel pointer. This is because Set/GetChannel operates by doing a lookup of the Channel Pointer, then setting or getting the value. This happens on each call. The alternative is to use channelPtr, which fetches the Channel Pointer and lets you directly set and get the value on the pointer. When a pointer is returned by a function of the API, ctcsound encapsulates this pointer in an ndarray (numpy array). Once the pointer is connected to the array, values can be written directly into the array through the channel.
In [12]:
from random import randint, random
class RandomLine(object):
def __init__(self, base, range):
self.curVal = 0.0
self.reset()
self.base = base
self.range = range
def reset(self):
self.dur = randint(256,512)
self.end = random()
self.slope = (self.end - self.curVal) / self.dur
def getValue(self):
self.dur -= 1
if(self.dur < 0):
self.reset()
retVal = self.curVal
self.curVal += self.slope
return self.base + (self.range * retVal)
# Our Orchestra for our project
orc = """
sr=44100
ksmps=32
nchnls=2
0dbfs=1
instr 1
kamp chnget "amp"
kfreq chnget "freq"
printk 0.5, kamp
printk 0.5, kfreq
aout vco2 kamp, kfreq
aout moogladder aout, 2000, 0.25
outs aout, aout
endin"""
#c = ctcsound.Csound() # create an instance of Csound
c.setOption("-odac") # Set option for Csound
c.setOption("-m7") # Set option for Csound
c.compileOrc(orc) # Compile Orchestra from String
sco = "i1 0 60\n"
c.readScore(sco) # Read in Score generated from notes
c.start() # When compiling from strings, this call is necessary before doing any performing
# The following calls return a tuple. The first value of the tuple is a numpy array
# encapsulating the Channel Pointer retrieved from Csound and the second
# value is an error message, if an error happened (here it is discarded with _).
ampChannel, _ = c.channelPtr("amp",
ctcsound.CSOUND_CONTROL_CHANNEL | ctcsound.CSOUND_INPUT_CHANNEL)
freqChannel, _ = c.channelPtr("freq",
ctcsound.CSOUND_CONTROL_CHANNEL | ctcsound.CSOUND_INPUT_CHANNEL)
amp = RandomLine(.4, .2)
freq = RandomLine(400, 80)
ampChannel[0] = amp.getValue() # note we are now setting values in the ndarrays
freqChannel[0] = freq.getValue()
print('Initial amp value is: ' + str(amp.getValue()))
print('Initial freq value is: ' + str(freq.getValue()))
while (c.performKsmps() == 0):
ampChannel[0] = amp.getValue()
freqChannel[0] = freq.getValue()
c.reset()
This example continues on from Example 9 and just refactors the creation and setup of numpy arrays into a createChannel() function. This example illustrates some natural progression that might occur in your own API-based projects, and how you might simplify your own code.
In [13]:
from random import randint, random
class RandomLine(object):
def __init__(self, base, range):
self.curVal = 0.0
self.reset()
self.base = base
self.range = range
def reset(self):
self.dur = randint(256,512)
self.end = random()
self.slope = (self.end - self.curVal) / self.dur
def getValue(self):
self.dur -= 1
if(self.dur < 0):
self.reset()
retVal = self.curVal
self.curVal += self.slope
return self.base + (self.range * retVal)
# The following call return a tuple. The first value of the tuple is a numpy array
# encapsulating the Channel Pointer retrieved from Csound and the second
# value is an error message, if an error happened (here it is discarded with _).
def createChannel(channelName):
chn, _ = c.channelPtr(channelName,
ctcsound.CSOUND_CONTROL_CHANNEL | ctcsound.CSOUND_INPUT_CHANNEL)
return chn
###############################
# Our Orchestra for our project
orc = """
sr=44100
ksmps=32
nchnls=2
0dbfs=1
instr 1
kamp chnget "amp"
kfreq chnget "freq"
printk 0.5, kamp
printk 0.5, kfreq
aout vco2 kamp, kfreq
aout moogladder aout, 2000, 0.25
outs aout, aout
endin"""
#c = ctcsound.Csound() # create an instance of Csound
c.setOption("-odac") # Set option for Csound
c.setOption("-m7") # Set option for Csound
c.compileOrc(orc) # Compile Orchestra from String
sco = "i1 0 60\n"
c.readScore(sco) # Read in Score generated from notes
c.start() # When compiling from strings, this call is necessary before doing any performing
ampChannel = createChannel("amp") # uses utility method to create a channel and get numpy array to write to
freqChannel = createChannel("freq")
amp = RandomLine(.4, .2)
freq = RandomLine(400, 80)
ampChannel[0] = amp.getValue()
freqChannel[0] = freq.getValue()
while (c.performKsmps() == 0):
ampChannel[0] = amp.getValue()
freqChannel[0] = freq.getValue()
c.reset()
This example continues on from Example 10 and introduces a ChannelUpdater object. The ChannelUpdater will create and store a numpy array that is wrapping a Csound Channel. Additionally, it will store and call an object that has a getValue() method to update values in the channel when update() is called.
This example continues the illustration of a progression of a project. Note that the process has changed a little bit where we now create a number of ChannelUpdater objects and store them in a list. The list is then iterated through for updating the channel with the latest values. In a real-world project, this kind of scenario occurs when there are n-number of items to update channels and one wants to have a flexible number that may even change dynamically at runtime.
In [14]:
from random import randint, random
class RandomLine(object):
def __init__(self, base, range):
self.curVal = 0.0
self.reset()
self.base = base
self.range = range
def reset(self):
self.dur = randint(256,512)
self.end = random()
self.slope = (self.end - self.curVal) / self.dur
def getValue(self):
self.dur -= 1
if(self.dur < 0):
self.reset()
retVal = self.curVal
self.curVal += self.slope
return self.base + (self.range * retVal)
def createChannel(channelName):
chn, _ = c.channelPtr(channelName,
ctcsound.CSOUND_CONTROL_CHANNEL | ctcsound.CSOUND_INPUT_CHANNEL)
return chn
class ChannelUpdater(object):
def __init__(self, channelName, updater):
self.updater = updater
self.channel = createChannel(channelName)
def update(self):
self.channel[0] = self.updater.getValue()
###############################
# Our Orchestra for our project
orc = """
sr=44100
ksmps=32
nchnls=2
0dbfs=1
instr 1
kamp chnget "amp"
kfreq chnget "freq"
kres chnget "resonance"
printk 0.5, kamp
printk 0.5, kfreq
printk 0.5, kres
aout vco2 kamp, kfreq
aout moogladder aout, 2000, kres
outs aout, aout
endin"""
#c = ctcsound.Csound() # create an instance of Csound
c.setOption("-odac") # Set option for Csound
c.setOption("-m7") # Set option for Csound
c.compileOrc(orc) # Compile Orchestra from String
sco = "i1 0 60\n"
c.readScore(sco) # Read in Score generated from notes
c.start() # When compiling from strings, this call is necessary before doing any performing
# Create a set of ChannelUpdaters
channels = [ChannelUpdater("amp", RandomLine(.4, .2)),
ChannelUpdater("freq", RandomLine(400, 80)),
ChannelUpdater("resonance", RandomLine(0.4, .3))]
# Initialize all Channel Values
for chn in channels:
chn.update()
while (c.performKsmps() == 0):
for chn in channels: # update all channel values
chn.update()
c.reset()
This example demonstrates a minimal Graphical User Interface application. The setup of Csound and starting of the CsoundPerformanceThread is done in the global scripting space. Afterwards, a Tkinter GUI is created that has one button. The button's callback (the command action) routes to a function that just sends an event to Csound.
For this example, since there is no need to synchronize continous channel data changes with Csound, it is more efficient to use the CsoundPerformanceThread, as it is a native thread. We use the CsoundPerformanceThread's inputMessage() function to ensure that the message is processed in a thread-safe manner.
In [15]:
from tkinter import *
from random import randint, random
###############################
# Our Orchestra for our project
orc = """
sr=44100
ksmps=32
nchnls=2
0dbfs=1
instr 1
kenv linsegr 0, .05, 1, .05, .9, .8, 0
aout vco2 p4 * kenv, p5
aout moogladder aout, 2000, p6
outs aout, aout
endin"""
#c = ctcsound.Csound() # create an instance of Csound
c.setOption("-odac") # Set option for Csound
c.setOption("-m7") # Set option for Csound
c.compileOrc(orc) # Compile Orchestra from String
c.start() # When compiling from strings, this call is necessary before doing any performing
perfThread = ctcsound.CsoundPerformanceThread(c.csound())
perfThread.play()
class Application(Frame):
def __init__(self,master=None):
master.title("Csound API GUI Example")
self.items = []
self.notes = []
Frame.__init__(self,master)
self.pack()
self.createUI()
self.master.protocol("WM_DELETE_WINDOW", self.quit)
def createUI(self):
self.size = 600
self.canvas = Canvas(self,height=self.size,width=self.size,bg="darkgray")
self.canvas.pack()
# create button and setup the playNote() callback
self.button = Button(self.canvas, text='Play Note', command=self.playNote)
self.button.pack()
def playNote(self):
perfThread.inputMessage("i1 0 2 .5 400 .25")
def quit(self):
self.master.destroy()
perfThread.stop()
perfThread.join()
app = Application(Tk())
app.mainloop()
c.reset()
This example demonstrates a slightly more advanced GUI example. It uses a slider to allow setting the value of the frequency that the notes initiated by the button will play at.
Note: the actual use of update() here is not thread-safe. In real-world usage, we would need to drive Csound from a loop calling PerformKsmps to ensure thread-safety. For this example, the updating generally works as there are few things demanding computation.
In [16]:
from tkinter import *
from random import randint, random
###############################
# Our Orchestra for our project
orc = """
sr=44100
ksmps=32
nchnls=2
0dbfs=1
gkpch chnexport "freq", 1
instr 1
kpch port gkpch, 0.01, i(gkpch)
printk .5, gkpch
kenv linsegr 0, .05, 1, .05, .9, .8, 0
aout vco2 p4 * kenv, kpch
aout moogladder aout, 2000, .25
outs aout, aout
endin"""
#c = ctcsound.Csound() # create an instance of Csound
c.setOption("-odac") # Set option for Csound
c.setOption("-m7") # Set option for Csound
c.compileOrc(orc) # Compile Orchestra from String
c.start() # When compiling from strings, this call is necessary before doing any performing
perfThread = ctcsound.CsoundPerformanceThread(c.csound())
perfThread.play()
def createChannel(channelName):
chn, _ = c.channelPtr(channelName,
ctcsound.CSOUND_CONTROL_CHANNEL | ctcsound.CSOUND_INPUT_CHANNEL)
return chn
class SliderWrapper(object):
def __init__(self, csound, channelName, slider):
self.slider = slider
self.channel = createChannel(channelName)
def update(self):
self.channel[0] = self.slider.get()
class Application(Frame):
def __init__(self,master=None):
master.title("Csound API GUI Example")
self.items = []
self.notes = []
Frame.__init__(self,master)
self.pack()
self.createUI()
self.master.protocol("WM_DELETE_WINDOW", self.quit)
def createUI(self):
self.size = 600
self.canvas = Canvas(self,height=self.size,width=self.size)
self.canvas.pack()
self.button = Button(self.canvas, text='Play Note', command=self.playNote)
self.button.pack()
self.freqSlider = Scale(self.canvas,from_=80.0, to=600.0,command=self.setFreq,label="Freq")
self.freqSlider.pack()
self.freqUpdater = SliderWrapper(c, "freq", self.freqSlider)
def playNote(self):
perfThread.inputMessage("i1 0 2 .3")
def setFreq(self, val):
print(val)
self.freqUpdater.update()
def quit(self):
self.master.destroy()
perfThread.stop()
perfThread.join()
app = Application(Tk())
app.mainloop()
c.stop
del c