Let's install everything. First thing we need is a csound compiler. When it's installed properly we can type in the terminal:
> csound
It will print the long message. Ubunut/Dbian users can install the Csound with apt-get
:
> sudo apt-get install csound csound-gui
Next thing is a working Haskell environment with ghc
and cabal-install
It can be installed with Haskell Platform.
If it works to install the csound-expression
we can type in the terminal:
> cabal install csound-expression
Let's have a break and take a cup of tea. The library contains a lot of modules to install.
Let's start the ghci
and load the main module Csound.Base
. It exports
all modules:
> ghci
Prelude> :m +Csound.Base
Prelude Csound.Base>
We can play a sine wave with 440 Hz:
> dac $ osc 440
Pressing Ctrl+C stops the sound. The expression osc 440
makes the sine wave and
the function dac
makes a file tmp.csd
in the current directory invokes the csound
on it and sends the output to speakers.
Let's modify the signal. It has a constant amplitude envelope but we can change it
with linseg
function.
> dac $ linseg [0, 1, 1, 3, 0] * osc 440
The function linseg
creates a signal of linear segments
linseg [a, durA, b, durB, c, durC, ...]
It starts from the a
value then in durA
seconds it goes to the value b
,
then in durB
seconds it goes to the value c
etc. It stays constant at the
final value. So the expression linseg [0, 1, 1, 3, 0]
produces an linear envelope
that starts from 0
then reaches 1
(maximum for the sound signal) in one second
and finally goes to zero in 3 seconds.
Let's add a 5% vibrato to the sound:
> dac $ linseg [0, 1, 1, 3, 0] * osc (440 * (1 + 0.05 * osc 5))
We can factor out the envelope and apply it to the amplitude and the vibrato:
> let env = linseg [0, 1, 1, 3, 0]
> dac $ env * osc (440 * (1 + 0.05 * env * osc 5))
We can apply an envelope to any parameter. Let's make a slide, and change the waveform to sawtooth:
> dac $ env * saw (220 + 220 * env)
We can play a tone with three odd harmonics:
> dac $ env * oscBy (sines [1, 0, 0.5, 0, 0.25]) 440
We can play an mp3 file:
> dac $ ar2 $ mp3in $ text "/home/anton/listen/The Kinks-Waterloo Sunset.mp3"
We used the functions:
ar2 :: (Sig, Sig) -> (Sig, Sig)
text :: String -> Str
mp3in :: Tuple a => Str -> a
The function mp3in
takes a Csound-string with the filename and produces a sound of the mp3 file.
The function ar2
forces the output to be a pair of signals. The function text
converts the Haskell strings to Csound strings.
There is even better way to read sound files. What if we have a short drum loop and we want to mix it with harmony. We need only 10 minutes of it. We can do it like this:
> dac $ takeSnd (10 * 60) $ loopSnd "drum.wav" + (mul 0.5 $ loopSnd "harmony.mp3")
Let's review the functions:
loopSnd :: String -> (Sig, Sig)
takeSnd :: Sigs a => Double -> a -> a
mul :: SigSpace a => Sig -> a -> a
The function loopSnd
repeats endlessly a given file (note that it's important
to write the files with extensions. Wav-files and mp3s a treated differently
the decision is based on the file extension).
The function mul
scales the tuples of signals with a scalar signal.
The takeSnd
truncates the sound to the given amount of seconds.
Let's take a look at the types of the functions:
osc :: Sig -> Sig
saw :: Sig -> Sig
oscBy :: Tab -> Sig -> Sig
linseg :: [D] -> Sig
text :: String -> Str
mp3in :: Tuple a -> Str -> a
These functions almost all primitive types of the library. Let's look at the types:
-
Sig
: a numeric signal (a stream of numbers) -
D
: a number -
Str
: a string -
Tab
: an array of numbers (See the moduleCsound.Tab
for constructors of the arrays) -
Spec
: a spectrum of the signal -
Tuple
: type class of tuples of csound values -
Arg
: type class of scalar csound values (they are not signals or spectrums) -
Sigs
: type class of tuples of signals. -
Unit
: the csound tuple of zero length. It's constructed with the functionunit
Functions that produce a random values or procedures are wrapped
in the special type SE
(or side effect). Let's look at the white noise
signature:
noise :: Sig -> Sig -> SE Sig
It takes an amplitude and the beta of low pass filter and returns a signal that
is wrapped in the SE
. The type SE
is a Functor
, Applicative
and Monad
so we can use the standard functions to process it:
> dac $ fmap (env * ) $ noise 1 0
We can make a sound in real time with midi keyboard. Midi instrument has the type:
instr :: Sigs a => Msg -> SE a
The type Msg
signifies the midi-messages, the type class Sigs
contains all tuples of signals. Let's make a simple midi instrument:
> let instr msg = return $ 0.5 * sig (ampmidi msg 1) * fades 0.01 0.5 * osc (sig $ cpsmidi msg)
The fades
function adds fade in and fade out phases to the signal with
the specified given length. With the functions ampmidi
and cpsmidi
we can read the amplitude and frequency from the midi message.
The function sig
converts numbers to signals.
Now we can play it with the function midi
:
> dac $ midi instr
If we don't have a hardware midi-device we can use a virtual midi-keyboard.
To do it we need to use vdac
in place of dac
:
> vdac $ midi instr
The function midi
takes a mid-instrument and starts to listen
on all channels for the events. If we want to specify the concrete channel
we should use the function midin
:
midin :: Sigs a => Int -> (Msg -> SE a) -> SE a
What if we want to play a pure tone on the first channel and the sawtooth on the second one? We can just add the resulting signals. First let's take a waveform as a parameter for the instrument:
> let instr f msg = return $ 0.5 * sig (ampmidi msg 1) * fades 0.01 0.5 * f (sig $ cpsmidi msg)
Now let's set up everything and add some reverb:
> vdac $ fmap (\asig -> 0.5 * nreverb asig 1 0.2) $ (midin 1 $ instr osc) + (midin 2 $ instr saw)
The example shows that applying the effect is as simple as applying a function.
But notice the use of fmap
. We are applying the function to the signal
that is wrapped in the SE. That's why we should use fmap
.
We can use a shortcut in defining the midi-instrument. There is a class that converts expressions to midi-instruments:
class MidiInstr a where
type MidiInstrOut a :: *
onMsg :: a -> Msg -> SE (MidiInstrOut a)
The only one method onMsg
takes something and converts it to midi-instruments.
We can define an instrument from the wave form:
> vdac $ midi $ onMsg osc
Here the method takes a function that converts a frequency to signal. The method gets the frequency from the midi message, converts it to a constant signal, applies the given waveform to it and multiplies everything on the amount of the amplitude taken from the midi message.
The type class MidiInstr
contains many useful instances. You can convert
anything that expects an amplitude and frequency and produces the tuple of signals:
(D, D) -> Sig
(D, D) -> SE Sig
(D, D) -> (Sig, Sig)
(D, D) -> SE (Sig, Sig)
...
We can trigger an instrument with the event streams. Event stream contains some primitive scalar csound-values (they are not signals).
The most simple event stream is created with the function metroE
:
metroE :: Sig -> Evt Unit
It creates a stream of repeating events. the first argument is the frequency of the repetition (in Hz). Let's create a stream of notes:
> let notes = fmap (const 440) $ metroE 2
Now we can trigger the instrument on the stream with the function sched
:
sched :: (Arg a, Sigs b) => (a -> SE b) -> Evt (D, a) -> b
It takes an instrument, the event stream and produces the mixed signal.
An instrument is a function it takes a tuple of primitive Csound values
(type class Arg
) and produces a tuple of signals (type class Sigs
).
An event stream contains a list of pairs. It's duration of the note and
the parameter for the instrument.
Let's create a simple instrument:
> let instr x = return $ 0.5 * osc (sig x)
And trigger the notes:
> dac $ sched instr $ withDur 0.25 notes
<interactive>:63:34:
Couldn't match type `Integer' with `D'
Expected type: Evt D
Actual type: Evt Integer
In the second argument of `withDur', namely `notes'
In the second argument of `($)', namely `withDur 0.25 notes'
In the second argument of `($)', namely
`sched instr $ withDur 0.25 notes'
We've got an error message about using Integer
in place of D
.
Let's look at the type of the notes
:
> :t notes
notes :: Evt Integer
The ghci
converts numeric literals to integers, but we need a csound integer.
Let's give the ghci
a hint:
> let notes = fmap (const (440::D)) $ metroE 2
> :t notes
notes :: Evt D
Now we can invoke the instrument and hear the result:
> dac $ sched instr $ withDur 0.25 notes
The function withDur
appends a constant value for the duration of the note
to all events on the stream. Let's make out events more interesting.
We can play a list of events in the loop and make it faster:
> let notes = cycleE [440::D, 330, 220] $ metroE 4
Or we can play them at random and skip some elements with the inverse of the given frequency:
> let notes = randSkip 0.75 $ oneOf [440::D, 330, 220] $ metroE 4
Event streams are monoids. We can merge two event streams together with
the function mappend
. Let's merge two streams. One plays a note 440 every 3
beat and another plays a 330 every 7 beat.
> let notes = let m = metroE 4 in (mappend (every 0 [2] $ repeatE (440 :: D) m) (every 0 [7] $ repeatE 330 m))
We used the functions:
repeatE :: a -> Evt b -> Evt a -- repeats the same event
every :: Int -> [Int] -> Evt a -> Evt a -- skips events from the stream
-- in beat patterns
every firstSkip beatPattern evt
Beat pattern is a sequence of integers. Every integer n
in the sequence means play one beat and skip (n-1)
beats.
There are many functions defined for events. We can find them
in the module Csound.Control.Evt
.
We can play a list of notes.
> let notes = CsdEventList 4 [(0, 1, 440::D), (1, 1, 220), (2, 2, 330)]
The type CsdEventList
contains the total duration of the scores
and the list of triplets: (startTime, duration, instrumentArguments)
.
Now we can trigger the instrument:
> dac $ mix $ sco instr notes
The functions:
sco :: (Arg a, Sigs b, CsdSco f) => (a -> SE b) -> f a -> f (Mix b)
mix :: (Sigs a, CsdSco f) => f (Mix a) -> a
The function sco
takes an instrument and a list of notes and
converts it to the list of sounds. The function mix
converts
the list of sounds to the single sound. But what is the type class CsdSco
?
It's a generic type of the values that can be converted to the CsdEventList
We can find out the complete definition in the module Csound.Control.Instr
.
The library csound-expression
is meant to be open to any score-generation libraries.
To use our favorite library we should make in instance for the type class CsdSco
.
There is an instance for the type Score
from the library temporal-music-notation
.
It's in the separate package temporal-csound
. We can install it with cabal-install:
> cabal install temporal-csound
Now we can load the csound with module Csound
:
> ghci
Prelude> :m +Csound
It reexports the module Csound.Base
and brings the definition of the instance
for CsdSco
in the scope. There are seven main functions to remember:
-- Constructs a score with the single note (it lasts for one second)
temp :: a -> Score a
-- Constructs a score that contains nothing and lasts for some time.
rest :: Double -> Score a
-- Stretches the score in the time domain with the given coefficient.
-- It gets faster or slower.
str :: Double -> Score a -> Score a
-- Delays all events with the given amount of time.
del :: Double -> Score a -> Score a
-- A sequential composition of scores. It's short for melody.
-- It plays the scores one after the other.
mel :: [Score a] -> Score a
-- A parallel composition. It's short for harmony.
-- It plays all scores at the same time.
har :: [Score a] -> Score a
-- Repeats the score several times.
loop :: Int -> Score a -> Score a
Let's make a simple tune:
Prelude Csound> let ns = fmap temp [220, 330, 440::D]
Prelude Csound> let notes = str 0.25 $ mel [loop 2 (mel ns), har ns]
Prelude Csound> dac $ mix $ sco instr notes
We can hear the distortion in the final chord. A sound signal is a function that can not exceed the value of 1. It's clipped before it's send to the speakers. We can scale the sound to remove the distortion:
> dac $ 0.5 * (mix $ sco instr notes)
What if we don't want or tune to fade out? We can apply an effect to it. First let's define a fader instrument. It takes a signal and multiplies it with an linear envelope:
> let fader x = return $ linseg [1, idur * 0.5, 1, idur * 0.5, 0] * x
The constant idur
is the hack that let's us query the total duration of the note.
If you know the Csound it's equivalent to the argument p3
of the instrument.
So the fader let's the signal to be unchanged for the first half of the note
and then reduces it to zero in the second one.
Now we can use the eff
function:
eff :: (Sigs a, Sigs b, CsdSco f) => (a -> SE b) -> f (Mix a) -> f (Mix b)
It applies the effect to the unmixed list of sounds:
> dac $ mix $ eff fader $ sco instr notes
Sometimes we can render the Csound file to sound files much faster then real-time rendering. It's more convenient to listen to the result in the media player. we can take a closer look at some details.
We can render the file without playing (the function csd
) and
specify the wav-file as the output in the options.
> csdBy (setOutput "tmp.wav") $ mix $ eff fader $ sco instr notes
It saves the file to the tmp.csd
with output set to tmp.wav
and renders it with csound
. The csound
then creates a wav-file.
And now we can listen to it in the player:
> :!mplayer tmp.wav
There are shortcut functions for Linux-users: mplayer
, totem
.
They save the output to file and invoke the given media-player on it.
> mplayer $ mix $ eff fader $ sco instr notes
Instruments can return a tuple of signals or a single signal or
a tuple signals wrapped in the type SE
. There are many different
variants of the output. But often we want to process the output as
a single signal. The output is always a container of signals.
To simplify this task there is a class SigSpace
:
class SigSpace a where
mapSig :: (Sig -> Sig) -> a -> a
bindSig :: (Sig -> SE Sig) -> a -> SE a
With method from this class we can easily apply the effects to the different types of the output. There is a very often used special case:
mul :: SigSpace a => Sig -> a -> a
mul k = mapSig ( * k)
It scales the output.
Sometimes our instruments are pure functions. But all functions
that invoke instruments require them to return a result that is wrapped
in the type SE
. Often we can lift the instrument on the fly
with methods from the special classes:
class Outs a where
type SigOuts a :: *
toOuts :: a -> SE (SigOuts a)
onArg :: Outs b => (a -> b) -> (a -> SE (SigOuts b))
class CpsInstr a where
type CpsInstrOut a :: *
onCps :: a -> (D, D) -> SE (CpsInstrOut a)
class AmpInstr a where
type AmpInstrOut a :: *
onamp :: a -> D -> SE (AmpInstrOut a)
The method onArg
unifies the pure and non pure instruments.
We can use it like this:
> let notes = temp (440 :: D)
> let instr cps = osc $ sig cps
> dac $ mix $ sco (onArg instr) notes
Now we don't need to wrap the output in the type SE
with the method return
.
The method onCps
defines the instruments that take an amplitude
and frequency as input. For example, we can convert to this type of instrument
a waveform:
> let notes = temp (0.5 :: D, 440 :: D)
> dac $ mix $ sco (onCps osc) notes
The method onAmp
defines the instruments that take only an amplitude.
They are drum sounds or noises. It can construct instruments from constants
by scaling the sound with the input amplitude.
> let notes = str 0.25 $ loop 4 $ mel [temp (0.5::D), rest 1]
> let instr = noise 1 0
> dac $ mix $ sco (onAmp instr) notes
We can find many predefined instruments in the package csound-catalog
.
Let's install it:
> cabal install csound-catalog
Let's try it in the interpreter:
> ghci
Prelude> :m +Csound
Prelude> :m +Csound.Catalog
Prelude Csound Csound.Catalog> vdac $ mul 0.3 $ midi $ onMsg (mul (fades 0.01 3) . vibraphone1)
or
> vdac $ mul 0.5 $ midi $ onMsg (mul (fades 0.01 3) . delayedString)
or
> vdac $ mul 0.15 $ fmap largeHall $ midi $ onMsg (mul (fades 1 2) . stringPad 1)
With Gui elements (Csound.control.Gui
) we can update
the synth paramters online. There are sliders, knobs, numeric fields, rollers.
The Gui element contains three parts:
SE (Gui, Input a, Output a)
Gui -- visual representation of the element
Input a = a -- reads the current value
Output a = a -> SE () -- writes the value to the element
Some elements can only read values (no output), some of the can only show the values (no input), some of them are static elements (no inputs and outputs).
Let's create a simple pure tone sound and update volume and frequency with sliders:
import Csound.Base
main = dac $ do
(gVol, vol) <- slider "volume" (linSpan 0 1) 0.5
(gCps, cps) <- slider "frequency" (expSpan 100 1000) 440
panel $ ver [gVol, gCps]
return $ vol * osc cps
The function slider expects a label, value diapason and initial value. It returns a pair of visual representation and the current value:
type Source a = SE (Gui, Input a)
slider :: String -> ValSpan -> Double -> Source a
First, we create to sliders for volume and frequency:
(gVol, vol) <- slider "volume" (linSpan 0 1) 0.5
(gCps, cps) <- slider "frequency" (expSpan 100 1000) 440
The first has linear diapason from 0 to 1 (linSpan) and the second has exponential diapason from (expSpan). Then we place our GUI elements on the screen. we create a panel that contains the elements with vertical placement:
panel $ ver [gVol, gCps]
The sliders are automatically aligned. We can find the other layout
function in the module Csound.Control.Gui.Layout
. We
can modify the appearance of the elements with functions
from the module Csound.Control.Props
.
At the end we return the signal that depends on the values of the GUI elements:
return $ vol * osc cps
We can listen for the keyboard events with functions: keyIn
, charOn
, charOff
.
data KeyEvt = Press Key | Release key
data Key = CharKey Char | F1 | F2 | ... | LeftShift | RightShift | ...
keyIn :: KeyEvt -> Evt Unit
charOn :: Char -> Evt Unit
charOff :: Char -> Evt Unit
charOn
and charOff
are handy shortcuts for
charOn = keyIn . Press . CharKey
charOff = keyIn . Release . CharKey
For example, we can trigger the note with key 'a':
instr _ = return $ mul (fades 0.5 1) $ osc 440
res = schedUntil instr (charOn 'a') (charOff 'a')
main = dac res
The schedUntil
function triggers the instrument with
the first event stream and holds the notes while
the second event stream is silent.