In the preceding recipes, there was a single thread running; this is the default way to use Python, due to the GIL (Global Interpreter Lock). Then, the user has the possibility to interact with the Csound instance during the performance loop. This is illustrated in the following diagram:
To use Csound in a more flexible way, one can use multithreading. Because of the GIL limitations, it is better to yield the multithread machinery through C libraries. When a C function is called from Python using ctypes, the GIL is released during the function call.
Csound has an helper class called CsoundPerformanceThread. When there is a running Csound instance, one can start a new thread by creating a new object of type CsoundPerformanceThread with a reference to the Csound instance as argument. Then, the main Python thread will run allowing the user to interract with it, while the performance thread will run concurrently in the C world, outside of the GIL. The user can send messages to the performance thread, each message being sent with a call to a C function through ctypes, releasing the GIL during the function call. Those messages can be: play(), pause(), togglePause(), stop(), record(), stopRecord(), scoreEvent(), inputMessage(), setScoreOffsetSeconds(), join(), or flushMessageQueue().
When a very long score is used, it is thus easy to implement a REPL (read-eval-print loop) system around Csound. This is illustrated in the following diagram:
So let's start a Csound instance from Python, with a four hours long score:
In [1]:
import ctcsound
cs = ctcsound.Csound()
csd = '''
<CsoundSynthesizer>
<CsOptions>
-d -o dac -m0
</CsOptions>
<CsInstruments>
sr = 48000
ksmps = 100
nchnls = 2
0dbfs = 1
instr 1
idur = p3
iamp = p4
icps = cpspch(p5)
irise = p6
idec = p7
ipan = p8
kenv linen iamp, irise, idur, idec
kenv = kenv*kenv
asig poscil kenv, icps
a1, a2 pan2 asig, ipan
outs a1, a2
endin
</CsInstruments>
<CsScore>
f 0 14400 ; a 4 hours session should be enough
</CsScore>
</CsoundSynthesizer>
'''
cs.compileCsdText(csd)
cs.start()
Out[1]:
Then, let's start a new thread, passing the opaque pointer of the Csound instance as argument:
In [2]:
pt = ctcsound.CsoundPerformanceThread(cs.csound())
pt.play()
Now, we can send messages to the performance thread:
In [3]:
pt.scoreEvent(False, 'i', (1, 0, 1, 0.5, 8.06, 0.05, 0.3, 0.5))
pt.scoreEvent(False, 'i', (1, 0.5, 1, 0.5, 9.06, 0.05, 0.3, 0.5))
When we're done, we stop the performance thread and reset the csound instance:
In [4]:
pt.stop()
pt.join()
cs.reset()
Note that we can still access the csound instance with other methods, like controlChannel() or setControlChannel():
In [5]:
csd = '''
<CsoundSynthesizer>
<CsOptions>
-odac
</CsOptions>
<CsInstruments>
sr = 44100
ksmps = 64
nchnls = 2
0dbfs = 1
seed 0
instr 1
iPch random 60, 72
chnset iPch, "pch"
kPch init iPch
kNewPch chnget "new_pitch"
if kNewPch > 0 then
kPch = kNewPch
endif
aTone poscil .2, mtof(kPch)
out aTone, aTone
endin
</CsInstruments>
<CsScore>
i 1 0 600
</CsScore>
</CsoundSynthesizer>
'''
cs.compileCsdText(csd)
cs.start()
pt = ctcsound.CsoundPerformanceThread(cs.csound())
pt.play()
We can ask for the values in the Csound instance ...
In [6]:
print(cs.controlChannel('pch'))
... or we can set our own values to the Csound instance:
In [7]:
cs.setControlChannel('new_pitch',73)
At the end, stop and reset as usual:
In [8]:
pt.stop()
pt.join()
cs.reset()