Csound API. Using generated code with other languages
The cool thing about Csound is that it's not only a text to audio converter. It's also a C-library! Also it has bindings to many languages! Python, Java, Clojure, Lua, Clojure, Csharp, C++, racket, VB! Also it works on Android, iOS, and RaspPi.
We can create audio engine with Haskell and then we can wrap it in the UI written in some another language! Let's look at how it can be done.
Interaction with generated code.
We can interact with Csound with two main methods.
Channels for updating values.
Named instruments for triggering instruments.
With channel we can update specific value inside Csound code. We can create a global channel and then send write or read values with another program.
We can make a channel and the we can read/write values. We can pass four types of values:
Constant doubles (
Control rate signals. Signals to control the audio (
Audio rate signals. Signals that encode the audio output. (
Let's create a pair of channels to control the volume and the frequency of audio signal:
module Main where import Csound.Base volume = text "volume" frequency = text "frequency" instr = do vol <- chnGetCtrl volume cps <- chnGetCtrl frequency return (vol * osc cps) main = writeCsd "osc.csd" instr
So we have a file
main.csd that encodes our audio engine.
Let's create Python program to control our audio.
We are going to use Csound API and we need to install the python bindings.
On Debian/Ubuntu we can install it with
> sudo apt-get python-csound
On Windows it's installed with Csound installer. You can download it
from the official Csound site. On OSX we can install it with
The source code with examples can be found at the github directory.
How to use channels with Python
There is a cool GitHub project Csound API examples that shows how to ue the Csound with various languages. We can quikly check how to use the audio engine that we have generated with our language of choice. We are going to illustrate the Csound API workflow with Python. The python examples is based on the information from this repo.
import csnd6 class Controll: def __init__(self, volume, frequency): engine = csnd6.Csound() engine.SetOption("-odac") engine.Compile("osc.csd") thread = csnd6.CsoundPerformanceThread(engine) thread.Play() self.engine = engine self.thread = thread self.set_volume(volume) self.set_frequency(frequency) def set_volume(self, volume): self.engine.SetChannel("volume", volume) def set_frequency(self, frequency): self.engine.SetChannel("frequency", frequency) def close(self): self.thread.Stop() self.thread.Join()
We create an object that can start a Csound engine and update volume and frequency. In the initialization step we create an audio engine? load file "osc.csd" to it and start csound in the separate thread:
engine = csnd6.Csound() engine.SetOption("-odac") engine.Compile("osc.csd") thread = csnd6.CsoundPerformanceThread(engine) thread.Play()
Then we save the state for the object:
self.engine = engine self.thread = thread
and set the initial values for frequency and volume:
These functions update values for csound channels. So with channels we can propagate changes from python to csound:
def set_volume(self, volume): self.engine.SetChannel("volume", volume)
The last method
close stops the engine:
def close(self): self.thread.Stop() self.thread.Join()
What's interesting with thism code is that we can control our engine within
the python interpreter. It's very simple skeleton for creation of Live coding with python and haskell combo!
Let's try some commands. Navigate to the directory with our python file
$ python > from oscil import Controll > c1 = Controll(0.5, 220) > c1.set_frequency(440) > c1.set_volume(0.3) > c1.set_volume(0.1) > c1.close()
We can instantiate several Csound audio engines!
> c1 = Controll(0.5, 220) > c2 = Controll(0.3, 330) > c3 = Controll(0.6, 110) > c3.set_frequency(150) > > for c in [c1, c2, c3]: > c.close()
With channels we can update a continuous signal. With Csound API we can also trigger the instruments with notes or messages.
We can send messages to instruments. To send the message we need to know the numeric identifier of the instrument. When we use Csound directly we know what numbers do we assign to the instruments. But Haskell wrapper hides this process from us.
Csound also provides named instruments. We can assign not only unique numeric value to the instrument, but also a name as a string. There is no need to use the named instruments in the haskell wrapper since we can use plain haskell values to construct instruments and framework will take care about allocation of integer identifiers. But named insturments can help us when we want to trigger instrument with program that is written in another language through Csound API.
There is a function:
trigByName :: (Arg a, Sigs b) => String -> (a -> SE b) -> SE b trigByName name instrument = aout
It takes an instrument name and instrument definition and creates an instrument with the given name. We can not use this instrument with Haskell. There are no way to trigger it. But we can trigger it with Csound API.
All basic Csound API functions can be found in the module
Csound.Control.Instr (see the API section).
Let's write a simple program:
module Main where import Csound.Base instr :: (D, D) -> SE Sig instr (amp, cps) = return $ (sig amp) * fades 0.01 0.1 * osc (sig cps) main = writeCsd "message.csd" $ trigByName "osc" instr
If we run this code with
runhaskell it will produce the
message.csd file that
contains the definition of our audio engine.
We create an instrument that has name
osc. It takes in amplitude and frequency and produces mono output.
The Csound API the csound thread object has a method
That takes in a string with Csound note-triggering expression.
If you know the Csound the syntax of
i-score statment should be straightforward to you.
But don't skip the next section. It explains not only the Csound syntax but also
how it's related to Haskell code.
Csound i-score statment
The Csound musicians trigger insturmnets with
i-score statements. It can look like this:
i "osc" 0 10 0.5 220
i is special syntax for
i-statement. Then goes the list of arguments that are separated with spaces.
The first argument is the instrument identifier. It's an integer number or string (note the mandatory double quotes).
Then we can see two parameters that are hidden from the haskell user. It's delay to trigger the note
and note duration. Both are in seconds. In the example we have a note with no delay that lasts for 10 seconds.
Then we can see the arguments that our haskell-instrument takes in. They are amplitude value and frequency value.
InputMessage code for our python code looks like this:
def play(self, delay, duration, volume, frequency): self.thread.InputMessage("i \"%s\" %f %f %f %f" % ("osc", delay, duration, volume, frequency))
We use python string-formating syntax to substitute
f's with floats and
s's with strings.
Note the escaped double quotes in the python code!
Now we are ready to look at the python code:
import csnd6 class Audio: def __init__(self): engine = csnd6.Csound() engine.SetOption("-odac") engine.Compile("message.csd") thread = csnd6.CsoundPerformanceThread(engine) thread.Play() self.engine = engine self.thread = thread def play(self, delay, duration, volume, frequency): self.thread.InputMessage("i \"%s\" %f %f %f %f" % ("osc", delay, duration, volume, frequency)) def close(self): self.thread.Stop() self.thread.Join()
The initialization and termination of audio engine are the same as in the previous example.
The new funtion is
play. The syntax is already explained. We take in dleay to trigger the note,
note's duration and pair of our Haskell parameters (amplitude and frequency).
Let's try out our engine in the python interpreter:
$ python > from message import Audio > c = Audio() > c.play(1, 3, 0.5, 220) > c.play(0, 2, 0.3, 330) > c.close()
Triggering instruments as procedures
Sometimes we don't want to produce the sound as the response to messages. Sometimes we want to update some parameters. You can imagine a drone sound going on or arpeggiator and we want to update a note or LFO rate with message. To do it we can use the function:
trigByName_ :: (Arg a) => String -> (a -> SE ()) -> SE () trigByName_ name instrument = aout
Note the underscore at the end. It creates a named procedure.
The procedure can be called with Csound API just in the same way as
an ordinary instrument. It's useful to know the
It turns the instrument off. By default all Csound instrument last
for some time. With
turnoff we can simulate instant reaction procedure.
It does some work (robably updates the global parameters) and then
it turns itself off. The pattern of usage looks like this:
procedure args = do doSomeStuff turnoff main = trigByName_ "update_param" procedure
Creation of MIDI-controlled instruments
If we want to create a VST plugin we want to be able to control
our csound insturment in the MIDI-like manner.
We want to send note on and note off messages. This functionality
can be simulated with
trigByName_ function and global variables.
There are predefined library function that already implement this
trigByNameMidi :: (Arg a, Sigs b) => String -> ((D, D, a) -> SE b) -> SE b trigByNameMidi name instrument = ...
The instrument takes in two mandatory arguments: pitch and amplitude midi-keys. It produces an audio signal as output. We can use it with Csound API just as in previous examples. We have special format for Csound arguments to simulate note-on/off behavior:
i "givenName" delay duration 1 pitchKey volumeKey auxParams -- note on i "givenName" delay duration 0 pitchKey volumeKey auxParams -- note off
Alongside with delay and duration we have another hidden argument. It's the fourth argument.
1 for note on and
0 for note off. Which note to turn off is determined by pitch key.
There is a procedure version of the function:
trigByNameMidi_ :: (Arg a, Sigs b) => String -> ((D, D, a) -> SE ()) -> SE () trigByNameMidi_ name instrument = ...
Monophonic MIDI-controlled instruments
The monophonic instruments need special treatment:
trigNamedMono :: D -> D -> String -> SE (Sig, Sig) trigNamedMono portamentoTime releaseTime name = ...
The function is located at the module
Csound.Control.Midi (see section Mono-midi synth).
The argument list for Csound is the same as for normal midi instruments.
i "givenName" 1 delay duration pitchKey volumeKey -- note on i "givenName" 0 delay duration pitchKey volumeKey -- note off
There are predefined midi-like named functions for patches (see section Csound API at the module
patchByNameMidi :: (SigSpace a, Sigs a) => String -> Patch D a -> SE a patchByNameMidi name patch = ... monoPatchByNameMidi :: (SigSpace a, Sigs a) => String -> Patch Sig a -> SE a monoPatchByNameMidi name patch = ... monoSharpPatchByNameMidi :: (SigSpace a, Sigs a) => String -> Patch Sig a -> SE a monoSharpPatchByNameMidi name patch = ...
If you are interested in non-trivial application that uses Csound API
you can look at the python synthesizer called tiny-synth.
It uses functions for named midi-controlled patches. It features 100+ patches from the standard
Example: Audio player
Let's create a command line audio player. We are going to create 3 instruments. One for playing wavs and aiffs, another one for playing mp3s and the last one to stop player.
-- the file Player.hs module Main where import Csound.Base declick :: Sig2 -> Sig2 declick = mul (fades 0.01 0.1) playWav :: Str -> SE Sig2 playWav file = return $ declick $ diskin2 file 1 playMp3 :: Str -> SE Sig2 playMp3 file = return $ declick $ mp3in file stop :: Unit -> SE () stop _ = do turnoffByName "wav" 0 0.1 turnoffByName "mp3" 0 0.1 turnoff main = writeCsd "player.csd" $ do wavs <- trigByName "wav" playWav mp3s <- trigByName "mp3" playMp3 trigByName_ "stop" stop return $ wavs + mp3s
Let's take this file apart. The first thing we create is declicking envelope so that playback starts and fades without clicks:
declick :: Sig2 -> Sig2 declick = mul (fades 0.01 0.1)
Next we define an instruemnt to play wavs and aiffs:
playWav :: Str -> SE Sig2 playWav file = return $ declick $ diskin2 file 1
We define an instrument to play mp3s:
playMp3 :: Str -> SE Sig2 playMp3 file = return $ declick $ mp3in file
We define an instrument to turn off any notes for all instruments that play wavs and mp3s.
stop :: Unit -> SE () stop _ = do turnoffByName "wav" 0 0.1 turnoffByName "mp3" 0 0.1 turnoff
It uses the new function
turnoffByName. The function
is defined to turnoff named instruments. The first argument is the name of the instrument.
The next is the code for turning off. Zero means turnoff all instances. The last argument is for release time (in seconds).
At the main function we assign names to instruments and direct the output to speakers.
main = writeCsd "player.csd" $ do wavs <- trigByName "wav" playWav mp3s <- trigByName "mp3" playMp3 trigByName_ "stop" stop return $ wavs + mp3s
So we can create a file with audio engine and give it a name
player.csd with command:
> runhaskell Player.hs
Let's look at the python code:
import csnd6, os.path, time def is_mp3(filename): filename, file_extension = os.path.splitext(filename) return file_extension == '.mp3' class Player: def __init__(self): engine = csnd6.Csound() engine.SetOption("-odac") engine.Compile("player.csd") thread = csnd6.CsoundPerformanceThread(engine) thread.Play() self.engine = engine self.thread = thread def play_file_by_ext(self, ext, file): self.thread.InputMessage("i \"%s\" 0 -1 \"%s\"" % (ext, file)) def stop(self): self.thread.InputMessage("i \"stop\" 0 0.01") time.sleep(0.02) def play(self, file): self.stop() if is_mp3(file): self.play_file_by_ext("mp3", file) else: self.play_file_by_ext("wav", file) def close(self): self.thread.Stop() self.thread.Join()
The initialization and termination are the same as in previous examples. In the body of the instrument we use a trick to play note forever. To play note forever in the Csound we have to invoke it with negative duration. Look at the code for triggering the notes:
def play_file_by_ext(self, ext, file): self.thread.InputMessage("i \"%s\" 0 -1 \"%s\"" % (ext, file))
Notice the duration of the note is set to
-1. It's going to held the note forever.
play function we stop all previous instances and then start new note. We determine the file type by extension:
def play(self, file): self.stop() if is_mp3(file): self.play_file_by_ext("mp3", file) else: self.play_file_by_ext("wav", file)
Let's try it out in the terminal:
$ python > from player import Player > p = Player() > p.play("muzzy.wav") > p.stop() > p.play("song.mp3") > p.close()