First Steps#

The Csound API is a set of C functions and C++ classes that expose to hosts programs the functionalities of Csound.

libcsound is a python module wrapping the access to the Csound API using two Python classes: Csound and PerformanceThread. libcsound uses ctypes to access C shared libraries.

NB: csound itself needs to be installed in order to use libcsound

[1]:
import libcsound
print(libcsound.VERSION)
7000

Csound Instance#

Creating a Csound instance spawns a csound process.

[2]:
cs = libcsound.Csound()

This object can return useful information about the underlying process

[3]:
from pprint import pprint

csoundversion = cs.version()
majorver = csoundversion // 1000
minorver = (csoundversion % 1000) // 100

print(f"Version id: {csoundversion}, version tuple: {majorver}.{minorver}.{csoundversion%10}")

# On csound 6 there is an api version, this does not exist in csound 7
if csoundversion < 7000:
    print(f"API version: {cs.APIVersion()}")

# List of available opcodes, print only the beginning of the list
pprint(cs.getOpcodes()[:20])

# List OSC opcodes
print("\n--- OSC opcodes ---\n")
pprint(set(opc.name for opc in cs.getOpcodes() if opc.name.startswith("OSC")))
Version id: 7000, version tuple: 7.0.0
[OpcodeDef(name='ATSadd', outtypes='a', intypes='kkSiiopo', flags=0),
 OpcodeDef(name='ATSadd', outtypes='a', intypes='kkiiiopo', flags=0),
 OpcodeDef(name='ATSaddnz', outtypes='a', intypes='kSiop', flags=0),
 OpcodeDef(name='ATSaddnz', outtypes='a', intypes='kiiop', flags=0),
 OpcodeDef(name='ATSbufread', outtypes='', intypes='kkSiop', flags=0),
 OpcodeDef(name='ATSbufread', outtypes='', intypes='kkiiop', flags=0),
 OpcodeDef(name='ATScross', outtypes='a', intypes='kkSikkiopoo', flags=0),
 OpcodeDef(name='ATScross', outtypes='a', intypes='kkiikkiopoo', flags=0),
 OpcodeDef(name='ATSinfo', outtypes='i', intypes='Si', flags=0),
 OpcodeDef(name='ATSinfo', outtypes='i', intypes='ii', flags=0),
 OpcodeDef(name='ATSinterpread', outtypes='k', intypes='k', flags=0),
 OpcodeDef(name='ATSpartialtap', outtypes='kk', intypes='i', flags=0),
 OpcodeDef(name='ATSread', outtypes='kk', intypes='kSi', flags=0),
 OpcodeDef(name='ATSread', outtypes='kk', intypes='kii', flags=0),
 OpcodeDef(name='ATSreadnz', outtypes='k', intypes='kSi', flags=0),
 OpcodeDef(name='ATSreadnz', outtypes='k', intypes='kii', flags=0),
 OpcodeDef(name='ATSsinnoi', outtypes='a', intypes='kkkkSiop', flags=0),
 OpcodeDef(name='ATSsinnoi', outtypes='a', intypes='kkkkiiop', flags=0),
 OpcodeDef(name='K35_hpf', outtypes='a', intypes='axxOPo', flags=0),
 OpcodeDef(name='K35_lpf', outtypes='a', intypes='axxOPo', flags=0)]

--- OSC opcodes ---

{'OSCbundle',
 'OSCcount',
 'OSCinit',
 'OSCinitM',
 'OSClisten',
 'OSCraw',
 'OSCsend',
 'OSCsend_lo'}
[4]:
del cs
                   overall amps:      0.0
           overall samples out of range:        0
0 errors in performance
Elapsed time at end of performance: real: 1.653s, CPU: 0.140s

Performing an orchestra#

csound code can be compiled in the form of an external .csd file, the text inside the .csd file or just csound orchestra.

In this case we will be performing in real-time, so we need to set the output to dac.

[2]:
cs = libcsound.Csound()
cs.setOption('-odac')
[2]:
0

Now we need to compile some actual code. Notice that settings like ksmps (number of samples per computation cycle) and 0dbfs (amplitude scaling) need to be set at the first compilation action.

[3]:
cs.compileOrc(r'''
sr = 44100
ksmps = 64
0dbfs = 1

instr 1
  ; set default values
  ;             p4   p5  p6    p7
  pset 0, 0, 0, 0.1, 60, 0.01, 0.2
  iamp = p4
  ipitch = p5
  iattack = p6
  irelease = p7
  a0 = vco2(iamp, mtof(ipitch)) * linsegr:a(0, iattack, 1, irelease, 0)
  outch 1, a0
endin
''')
PSET: isno=??, pmax=7
[3]:
0

Schedule some events

[4]:
score = [
    (1, 0, 10, 0.1, 60),
    (1, 0.5, 10, 0.05, 62)
]

for event in score:
    cs.scoreEvent('i', event)

# Since we are in realtime mode, we need to set the end time of the performance
# so that the perform loop ever returns
cs.setEndMarker(11)

To actually perform the score we need to call .perform(), which executes all the events scheduled until now

[5]:
cs.perform()
--Csound version 7.0 (double samples) Jan  3 2025
[commit: 7e3505c5d43b0d433091bc5e9ca973162cc35ce4]
libsndfile-1.2.2
graphics suppressed, ascii substituted
sr = 44100.0, kr = 689.062, ksmps = 64
0dBFS level = 1.0, A4 tuning = 440.0
orch now loaded
audio buffered in 256 sample-frame blocks
ALSA output: total buffer size: 1024, period size: 256
writing 256 sample blks of 64-bit floats to dac
SECTION 1:
new alloc for instr 1:
           T  0.501 TT  0.501 M:  0.11661
new alloc for instr 1:
           T 11.000 TT 11.000 M:  0.17023
End of Performance
[5]:
1

Scheduling events#

If some kind of interaction between the running csound process and python is needed, it is possible to use .performKsmps(), which processes one cycle of samples at a time (determined by ksmps) and yields control to the user.

In the example below we use this to schedule events by calculating time from python. This is just to show that any kind of process from python can be used to interact with a running csound instance

[50]:
import random
import time

# We need this in order to be able to use keyboard interrupt. Otherwise csound
# itself sets a signal handler and python looses control
libcsound.csoundInitialize(signalHandler=False, atExitHandler=False)

cs = libcsound.Csound()

# Realtime processing
cs.setOption('-odac')

# Disable printing whenever a new event is scheduled
cs.setOption('-m128')

# The orchestra
cs.compileOrc(r'''
sr = 44100
nchnls = 2
ksmps = 64
0dbfs = 1

instr 1
  iamp = p4
  ipitch = p5
  iattack = p6
  irelease = p7
  a0 = oscili(iamp, mtof(ipitch)) + oscili(iamp, mtof(ipitch+0.12))
  outall a0 * linsegr:a(0, iattack, 1, iattack*2, 0.2, irelease, 0)
endin
''')

print("\nPress I-I to interrupt\n")

# Schedule a note every 1/8 of a second. The pitch is chosen at random
# from a given scale, in this case a c-major scale
# A set of pitch classes are selected at random from the given
# scale and if the random pitch is part of that class certain
# parameters are modified

t0 = tchord = time.time()
scale = [0, 2, 4, 5, 7, 9, 11]
pitches = [60 + 12 * octave + step for octave in [0, 1, 2] for step in scale]
chords = [(2, 4, 7), (4, 5), (2, 7, 11), (0, 5, 9), (9, 11), (0, 2), (0, 4, 7)]
chord = random.choice(chords)

def getparams(pitch, chord):
    if pitch % 12 in chord:
        dur = random.uniform(1.5, 2.5)
        amp = random.uniform(0.04, 0.2)
        att = random.uniform(0.01, 0.1)
        rel = random.uniform(0.4, 0.8)
    else:
        dur = random.uniform(0.1, 0.2)
        amp = random.uniform(0.04, 0.08)
        att = 0.01
        rel = 0.3
    return dur, amp, att, rel

while cs.performKsmps() == libcsound.CSOUND_SUCCESS:
    t = time.time()
    if t - tchord > 8:
        chord = random.choice(chords)
        tchord = t
    if t - t0 > 1/8.:
        pitch = random.choice(pitches)
        dur, amp, att, rel = getparams(pitch, chord)
        cs.scoreEvent('i', (1, 0, dur, amp, random.choice(pitches), att, rel))
        t0 = t


Press I-I to interrupt

--Csound version 7.0 (double samples) Jan  3 2025
[commit: 7e3505c5d43b0d433091bc5e9ca973162cc35ce4]
libsndfile-1.2.2
graphics suppressed, ascii substituted
sr = 44100.0, kr = 689.062, ksmps = 64
0dBFS level = 1.0, A4 tuning = 440.0
orch now loaded
audio buffered in 256 sample-frame blocks
ALSA output: total buffer size: 1024, period size: 256
writing 512 sample blks of 64-bit floats to dac
SECTION 1:

---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
Cell In[50], line 60
     57         rel = 0.3
     58     return dur, amp, att, rel
---> 60 while cs.performKsmps() == libcsound.CSOUND_SUCCESS:
     61     t = time.time()
     62     if t - tchord > 8:

File ~/dev/python/libcsound/libcsound/api7.py:1082, in Csound.performKsmps(self)
   1080 if not self._started:
   1081     self.start()
-> 1082 return bool(libcsound.csoundPerformKsmps(self.cs))

KeyboardInterrupt:

The same can be achieved with csound counting time

[52]:
sr = cs.sr()
t0 = cs.currentTimeSamples() / sr

while cs.performKsmps() == libcsound.CSOUND_SUCCESS:
    t = cs.currentTimeSamples() / sr   #  <------------------ use csound time, the result should be very similar
    if t - tchord > 8:
        chord = random.choice(chords)
        tchord = t
    if t - t0 > 1/8.:
        pitch = random.choice(pitches)
        dur, amp, att, rel = getparams(pitch, chord)
        cs.scoreEvent('i', (1, 0, dur, amp, random.choice(pitches), att, rel))
        t0 = t

---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
Cell In[52], line 4
      1 sr = cs.sr()
      2 t0 = cs.currentTimeSamples() / sr
----> 4 while cs.performKsmps() == libcsound.CSOUND_SUCCESS:
      5     t = cs.currentTimeSamples() / sr   #  <------------------ use csound time, the result should be very similar
      6     if t - tchord > 16:

File ~/dev/python/libcsound/libcsound/api7.py:1082, in Csound.performKsmps(self)
   1080 if not self._started:
   1081     self.start()
-> 1082 return bool(libcsound.csoundPerformKsmps(self.cs))

KeyboardInterrupt:
[ ]: