Showing Csound k-Values in Matplotlib Animation

The goal of this notebook is to show how Csound control signals can be seen in real-time in the Python Matplotlib using the Animation module. This can be quite instructive for teaching Csound. Written by Joachim Heintz, August 2019.

Choosing the matplotlib backend

Not every matplotlib backend is capable to show animations. At the time of writing this notebook, the option %matplolib inline can only diplay static images. Use %matplotlib instead:


In [1]:
%matplotlib qt5

The backend Qt5Agg is — as well as some others — capable to show animations. (If necessary, you should be able to choose an appropriate backend by editing your matplotlib.rc file.)

Basic animation in matplotlib

For a basic animation using the FuncAnimation we only need two elements:

  • a matplotlib figure
  • an animation function

The figure can be created in many ways in matplotlib. I choose here the subplots() function in pyplot. It returns a figure and an axes object. The figure object is needed as input for the FuncAnimation. The axes object is modified by some settings, and the method plot returns a Line2D object which will then be modified during the animation.
The animation function is updated in the call to FuncAnimation every interval (default=200) milliseconds. The variable i in this function is a frame counter, starting from zero.


In [2]:
from matplotlib import pyplot as plt
from matplotlib import animation

fig, ax = plt.subplots()
ax.set(xlim=(0,5), ylim=(0,1))
line, = ax.plot([], [], lw=2)

def animate(i, x=[], y=[]):
    x.append(i/10)
    y.append(i/50)
    line.set_data(x, y)

anim = animation.FuncAnimation(fig, animate, interval=100)

You should see a line which starts at (0,0) and moves in five seconds to (5,1).

Displaying a Csound control signal

If we want to reproduce this very basic example by using a Csound control signal rather than the y-signal generated in the animate function, we have to do this:

  • create the signal in Csound and send it via chnset
  • receive the signal in the animation function

The crucial point here is to run the csound instance in a way that it does not block the execution of the animation. This can be easily done in the way which is shown by François Pinot in the threading notebook.

Note: close the precedent graphics canvas window before lauching the next example.


In [3]:
import ctcsound as csound
from matplotlib import pyplot as plt
from matplotlib import animation

orc = '''
instr 1
 kVal linseg 0, p3, 1
 chnset kVal, "val"
endin
'''
sco = "i1 0 5\n" #try 0.2 as start instead

cs = csound.Csound()
cs.setOption('-odac')
cs.compileOrc(orc)
cs.readScore(sco)
cs.start()

pt = csound.CsoundPerformanceThread(cs.csound())
pt.play()

fig, ax = plt.subplots()
ax.set(xlim=(0,5), ylim=(0,1))
line, = ax.plot([], [], lw=2)

def animate(i, x=[], y=[]):
    x.append(i/10)
    y.append(cs.controlChannel('val')[0])
    line.set_data(x, y)

anim = animation.FuncAnimation(fig, animate, interval=100)

You should see more or less the same here: a line starting from (0,0) to (5,1).
Well, more or less ... --- Depending on the time the backend needs to create the canvas, your line will be shifted a bit . A simple way to deal with it is to start the first instrument a bit later. In my case. 0.2 instead of 0 is a good option.
Remember to execute these commands before you run the example again:


In [4]:
pt.stop()
pt.join()
cs.reset()

Approaching the comfort zone

The next version applies some more consistency to the variable settings. You can set any frame rate in milliseconds in the tmint variable. And the x-axis will shift if the time has reached 4/5 of its size. So you can watch how the line moves as long as your instrument duration allows ...


In [5]:
import ctcsound as csound
from matplotlib import pyplot as plt
from matplotlib import animation

orc = '''
ksmps = 128
seed 0
instr 1
 kVal randomi 0, 1, 1, 3
 chnset kVal, "val"
endin
'''
sco = "i1 0.2 99999\n"

#plot and animation settings
xlim=(0,5)
ylim=(0,1)
tmint = 100 #time interval in ms
cschn = 'val' #csound channel name

cs = csound.Csound()
cs.setOption('-odac')
cs.compileOrc(orc)
cs.readScore(sco)
cs.start()

pt = csound.CsoundPerformanceThread(cs.csound())
pt.play()

fig, ax = plt.subplots()
ax.set(xlim=xlim, ylim=ylim)
line, = ax.plot([], [], lw=2)
fps = 1000/tmint
xrange = xlim[1] - xlim[0]
xshow = 4/5
xclear = 1-xshow

def animate(i, x=[], y=[]):
    x.append(i/fps)
    y.append(cs.controlChannel(cschn)[0])
    line.set_data(x, y)
    if i > fps*xrange*xshow:
        ax.set_xlim(i/fps-xrange*xshow,i/fps+xrange*xclear)

anim = animation.FuncAnimation(fig, animate, interval=tmint)

In [6]:
pt.stop()
pt.join()
cs.reset()

Latency and further optimizations

The goal of the approach here is not to have live video for a musical performance, but to use the nice features of matplotlib for showing how a control signal is moving. But it seems that even for simple sounding examples it works, as the example below suggests.
There are a number of optimizations which I have not used. If necessary, they should improve the performance:

  • On the matplotlib side, an init function can be used. Depending on the kind of animation, the option blit=True can save some speed (in this case, the init and the animate function must return the line, variable then).
  • On the ctcsound side, using the method channelPtr rather than the raw controlChannel should be more efficient.

In [7]:
import ctcsound as csound
from matplotlib import pyplot as plt
from matplotlib import animation

orc = '''
ksmps = 128
nchnls = 2
0dbfs = 1
seed 0
instr 1
 kMidiPitch randomi 57, 62, 1, 3
 kVibr = poscil:k(randomi:k(0,1,.2,3),randomi:k(3,8,1))
 kDb randomi -20, 0, 1/3, 3
 kPan randomi 0, 1, 1, 3
 chnset kMidiPitch, "pitch"
 chnset kDb, "vol"
 chnset kPan, "pan"
 aSnd vco2 ampdb(kDb), mtof(kMidiPitch+kVibr)
 aL, aR pan2 aSnd, kPan
 out aL, aR 
endin
'''
sco = "i1 0.2 99999\n"

xlim_pv=(0,5)
xlim_pan=(0,1)
ylim_pch=(57,62)
ylim_vol=(-20,0)
ylim_pan=(0,0.2)
tmint = 100
chn_pch = 'pitch'
chn_vol = 'vol'
chn_pan = 'pan'

cs = csound.Csound()
cs.setOption('-odac')
cs.compileOrc(orc)
cs.readScore(sco)
cs.start()

pt = csound.CsoundPerformanceThread(cs.csound())
pt.play()

fig, ax = plt.subplots(3, tight_layout=True, gridspec_kw={'height_ratios': [3, 3, 1]})
ax[0].set(xlim=xlim_pv, ylim=ylim_pch, title='Pitch', xticks=())
ax[1].set(xlim=xlim_pv, ylim=ylim_vol, title='Volume (dB)', xticks=())
ax[2].set(xlim=xlim_pan, ylim=ylim_pan, title='Pan', xticks=[0,0.5,1], xticklabels=['L','M','R'], yticks=())
ax[0].spines['top'].set_visible(False)
ax[1].spines['top'].set_visible(False)
ax[2].spines['top'].set_visible(False)
ax[0].spines['right'].set_visible(False)
ax[1].spines['right'].set_visible(False)
ax[2].spines['right'].set_visible(False)
ax[2].spines['left'].set_visible(False)
pchline, = ax[0].plot([], [], lw=2, c='r')
volline, = ax[1].plot([], [], lw=2, c='b')
panpnt, = ax[2].plot(0.5, 0.1, 'go', lw=4)
fps = 1000/tmint
xrange = xlim_pv[1] - xlim_pv[0]
xshow = 4/5
xclear = 1-xshow

def animate(i, x_pv=[], y_pch=[], y_vol=[]):
    x_pv.append(i/fps)
    y_pch.append(cs.controlChannel(chn_pch)[0])
    pchline.set_data(x_pv, y_pch)
    y_vol.append(cs.controlChannel(chn_vol)[0])
    volline.set_data(x_pv, y_vol)
    if i > fps*xrange*xshow:
        ax[0].set_xlim(i/fps-xrange*xshow,i/fps+xrange*xclear)
        ax[1].set_xlim(i/fps-xrange*xshow,i/fps+xrange*xclear)
    x_pan = cs.controlChannel(chn_pan)[0]
    panpnt.set_data(x_pan,0.1)

anim = animation.FuncAnimation(fig, animate, interval=tmint)

In [8]:
pt.stop()
pt.join()
cs.reset()