There are plenty of beautiful instruments designed for the library!
They can be found at the package csound-catalog
(available on Hackage).
Let's load the module:
> import Csound.Patch
Now we can play a patch:
> vdac $ mul 0.75 $ atMidi dreamPad
> vdac $ mul 0.75 $ atMidi vibraphone1
With vdac
we can play midi instruments on a virtual midi-keyboard.
If you have a real midi-keyboard attached to your computer you can
use the plain old dac
. This way we can play the patches with real MIDI-device:
> dac $ mul 0.75 $ atMidi toneWheelOrgan
There are many predefined instruments. Some goodies to try out: vibraphone1, dreamPad, razorPad, epianoBright, xylophone, scrapeDahina, noiz, mildWind, toneWheelOrgan, banyan, caveOvertonePad, flute, hulusiVibrato, shortBassClarinet, pwBass, pwEnsemble, albertClockBellBelfast etc.
The main type for the instrument is a Patch
. The patch is a data-type
that holds a complete set up for the instrument to be played live or
to apply it to musical scores.
Let's look at the definition of the Patch
:
data Patch a
= MonoSynt MonoSyntSpec (GenMonoInstr a)
| PolySynt PolySyntSpec (GenInstr D a)
| SetSkin SyntSkin (Patch a)
| FxChain [GenFxSpec a] (Patch a)
| SplitPatch (Patch a) D (Patch a)
| LayerPatch [(Sig, Patch a)]
We can
-
create monophonic (
MonoSynt
) synthesizers -
create polyphonic (
PolySynt
) synthesizers -
set generic parameters for synthesizers (
SetSkin
) such as filter type, -
add effects (
FxChain
) -
split keyboard on sections (
SplitPatch
). We can assign different patches to each section. -
play several patches at the same time (
LayerPatch
)
The Gen-prefix for instruments and effects refers to one peculiarity of the Patch data type. I'd like to be able to change some common parameters of the instrument after it's already constructed. Right now we can change only the type of the low-pass filter but some more parameters can be added in the future.
In Haskell we can do this with Reader
data type. Reader
lets us parametrize the values with some common arguments
and we can supply those arguments later.
The value: Reader a b
means that the result value b
depends on argument a
which we provide later.
type SyntSkin = ResonFilter
type GenInstr a b = Reader SyntSkin (Instr a b)
type GenFxSpec a = Reader SyntSkin (FxSpec a)
type GenMonoInstr a = Reader SyntSkin (MonoInstr a)
So the polyphonic and monophonic synthesizers and effects are parametrized with value of the type SyntSkin
.
The SyntSkin
should contain some common parameters. We can change the filter type with it. By default it's set
to mlp
(moog low pass filter). But we can set other types with the constructor SetSkin skin patch
.
Let's discuss each case of the Patch data type.
Patch can be a polyphonic synth:
PolySynt PolySyntSpec (GenInstr D a)
type GenInstr a b = Reader SyntSkin (Instr a b)
type CsdNote a = (a, a)
type Instr a b = CsdNote a -> SE b
data PolySyntSpec = PolySyntSpec
{ polySyntChn :: MidiChn }
It converts notes to signals. With polyphonic instrument we can play several notes at the same time. The note is a pair of amplitude and frequency.
With PolySyntSpec
we can specify midi channel to play the instrument.
The PolySyntSpec
has default instance with which we listen for midi messages on all channels.
Let's play a polyphonic patch:
> vdac $ atMidi whaleSongPad
We can change the volume by signal multiplication:
> vdac $ mul 0.5 $ atMidi whaleSongPad
We have played the predefined polyphonic synth. But we can also create our own! We can do it directly with constructor PolySynt
and with smart constructors:
polySynt :: (Instr D a) -> Patch a
polySyntFilter :: (ResonFilter -> Instr D a) -> Patch a
Let's create a simple polyphonic instrument. It' just plays pure sines waves with percussive shape of amplitude:
> instr (amp, cps) = return $ sig amp * xeg 0.01 4 0.001 2 * osc (sig cps)
> patch = polySynt instr
> vdac $ atMidi patch
Reminder: xeg
-- creates exponential ADSR-envelope, osc
is for pure sine wave, with sig
we convert
constant. We use return
to wrap the result in the SE-type.
We can do it in the regular Haskell-file:
import Csound.Base
instr :: CsdNote D -> SE Sig
instr (amp, cps) = return $ sig amp * env * wave
where
env = xeg 0.01 4 0.001 2
wave = osc (sig cps)
patch = polySynt instr
main = vdac $ atMidi patch
With polySyntFilter
we can let the user decide which type of filter is going to be used:
polySyntFilter :: (ResonFilter -> Instr D a) -> Patch a
We can create a simple instrument with generic filter:
import Csound.Base
instr :: ResonFilter -> CsdNote D -> SE Sig
instr resonFilter (amp, cps) = return $ filter $ sig amp * env * wave
where
env = xeg 0.01 4 0.001 2
wave = sqr (sig cps)
filter = resonFilter (200 + 1500 * env) 0.25
patch = polySyntFilter instr
main = vdac $ atMidi patch
We can alter the filter with SetSkin
constructor.
Patch can be a monophonic synth. The monophonic synth can play only one note at the time (like flute or voice). But it converts not just notes but signals of amplitude and frequency.
MonoSynt MonoSyntSpec (GenMonoInstr a)
type GenMonoInstr a = Reader SyntSkin (MonoInstr a)
type MonoInstr a = MonoArg -> SE a
data MonoArg = MonoArg
{ monoAmp :: Sig
, monoCps :: Sig
, monoGate :: Sig
, monoTrig :: Sig }
data MonoSyntSpec = MonoSyntSpec
{ monoSyntChn :: MidiChn
, monoSyntSlideTime :: Maybe D } -- portamento for amplitude and frequency
It looks like polyphonic synth but monophonic synt argument type is a bit more complicated. It contains four components:
-
amplitude signal
monoAmp
-
frequency signal
monoCps
-
mask of when instrument is on
monoGate
. It equals to1
when any note is played or0
otherwise. -
trigger signal. It equals to 1 when note is triggered and
0
otherwise.
The argument type is designed to be used with the function adsr140 it creates complex ADSR envelope which is retriggered when note is played. There is a shortcut to create the ADSR-function out of the arguments:
-- | ADSR that's used in monophonic instruments.
type MonoAdsr = Sig -> Sig -> Sig -> Sig -> Sig
monoAdsr :: MonoArg -> MonoAdsr
we can create monophonic instruments with smart constructors:
monoSynt :: (MonoInstr a) -> Patch a
monoSyntFilter :: (ResonFilter -> MonoInstr a) -> Patch a
adsrMono :: (MonoAdsr -> Instr Sig a) -> Patch a
adsrMonoFilter :: (ResonFilter -> MonoAdsr -> Instr Sig a) -> Patch a
The monoSynt
creates the synt out of raw value of type MonoArg
.
The adsrMono
converts the arguments to a simpler form. It provides
a retriggering adsr-envelope which is syncronized with notes and a pair of amplitude and frequency signals.
With Filter
suffix we can parametrize the insturment by low-pass filter.
Let's create a very basic mono-insturment:
instr adsrFun (amp, cps) = return $ amp * env * osc (port cps 0.007)
where env = adsrFun 0.01 4 0.001 2
patch = adsrMono instr
main = vdac $ atMidi patch
We add a portamento to frequency signal to make transition between the notes smooth.
Let's play some predefined monophonic synth:
> vdac $ atMidi nightPadm
By convention many polyphonic synthesizers have monophonic sibling
with an m
as a suffix.
Also we can apply a chain of effects to the patch:
FxChain [GenFxSpec a] (Patch a)
type GenFxSpec a = Reader SyntSkin (FxSpec a)
type DryWetRatio = Sig
data FxSpec a = FxSpec
{ fxMix :: DryWetRatio
, fxFun :: Fx a
}
type Fx a = a -> SE a
An effect is a function that transforms signals (can be as single signal or a tuple of signals). An effect unit comes with a main parameter that's called dry/wet ratio. It signifies the ratio of unprocessed (dry) and processed signal (wet). And the fx chain contains a list of pairs of ratios and effect functions. Note that the list is reversed (like in haskell dot notation). The first function in the list is going to be applied at the last moment.
We can create effects in the chain with smart constructors:
type Fx a = a -> SE a
fxSpec :: Sig -> Fx a -> GenFxSpec a
fxSpecFilter :: Sig -> (ResonFilter -> Fx a) -> GenFxSpec a
Almost all predefined patches have a bit of reverb. We can strip down the effect
with useful function dryPatch
:
> vdac $ atMidi $ dryPatch nightPadm
It throws away all the effects.
Also there are speciall functions to add effects to existing patch. We can add effects to the both ends of the chain:
addPreFx, addPostFx :: DryWetRatio -> Fx a -> Patch a -> Patch a
There are functions to add monophonic effects:
mapFx :: SigSpace a => (Sig -> Sig) -> Patch a -> Patch a
bindFx :: BindSig a => (Sig -> SE Sig) -> Patch a -> Patch a
There are variants to specify dry/wet ratio:
mapFx' :: SigSpace a => Sig -> (Sig -> Sig) -> Patch a -> Patch a
bindFx' :: BindSig a => Sig -> (Sig -> SE Sig) -> Patch a -> Patch a
Let's add a delay to our patch:
> vdac $ mapFx' 0.5 (echo 0.5 0.65) patch
Sometimes we want to play several instruments by the same key press. We can achieve it with ease by layered patches:
LayerPatch [(Sig, Patch a)]
This case contains a list of pairs. Each element contains a volume of the layer and the patch of the layer. The output is a mix of the outputs from all patches in the list.
Let's layer the vibraphone with pad-sound:
> vdac $ atMidi $ LayerPatch [(0.5, nightPad), (1, vibraphone1)]
On the modern synthesizer we can find a useful function that is called split
.
With split we can play two instruments at the same keyboard. We define a split point (frequency or pitch)
and two instruments. One is going to be active for all instruments below the pitch and another one
is going to be active for all notes above the given pitch.
SplitPatch (Patch a) D (Patch a)
We specify the split with the frequency value. We can use cpspch
to convert
frequencies to pitches. Here is how we can split the keyboard by the note C:
> SplitPatch instr1 (cpspch 8.00) instr2
Let's play a pad in the low register and electric piano in the upper one:
vdac $ atMidi $ SplitPatch dreamPad (cpspch 8.00) epiano2
Note that with split you can combine monophonic instruments with polyphonic ones. We can play even several monophonic instruments in the diferent areas of the keyboard.
There are many ways to play the patch. We have already discovered how to play with midi device, but we also can play it with other ways.
We can trigger a single note:
dac $ atNote overtonePad (0.5, 110)
We can apply the patch to scores:
> ns = fmap temp [(0.5, 220), (0.75, 330), (1, 440)]
> notes = str 0.25 $ mel [mel ns, har ns, rest 4]
> dac $ mul 0.75 $ mix $ atSco banyan notes
We can play the patch with an event stream:
> notes = cycleE [(1, 220), (0.5, 220 * 5/ 4), (0.25, 330), (0.5, 440)]
> dac $ atSched hammondOrgan (withDur 0.15 $ notes $ metro 6)
We can play a dry patch (throw away all the effects). Can be useful if you like the tone of the instruments but want to apply you own effect:
vdac $ atMidi $ dryPatch $ scrapeXylophone
We can transpose a patch two semitones up:
> vdac $ atMidi $ transPatch (semitone 2) $ dahina
or one octave down:
> vdac $ atMidi $ transPatch (octave (-1)) $ dahina
We can add an effect to the patch. Note that we can append from both ends of the fx-chain. Let's add a delay to the sound:
> vdac $ atMidi $ addPreFx 0.5 (return . (mapSig $ echo 0.25 0.75)) banyan
We can use the function addPostFx
to add the effect to the end of the chain
and the function addPreFx
to add effect to the beginning of the effect chain.
We can change the amount of dry-wet ratio for the last effect in the chain with function setFxMix
:
vdac $ atMidi $ setFxMix 0 $ addPostFx 0.5 (return . (mapSig $ echo 0.25 0.75)) banyan
In this example we add post-delay, but set the ratio to zero so we can hear no delay.
We can also set a list of ratios with function setFxMixes :: [Sig] -> Patch a -> Patch a
.
With deepPad
we can add a second note that plays an octave below to deepen the sound:
> vdac $ atMidi $ deepPad cathedralOrgan
There is a more generic function that let's us to add any number of harmonics that are played with the given patch:
harmonPatch :: (SigSpace b, Sigs b) => [Sig] -> [D] -> Patch b -> Patch b
We can quickly fuse two patches together with function mixInstr
:
mixInstr :: (SigSpace b, Num b) => Sig -> Patch b -> Patch b -> Patch b
We can create patches out of soundfonts! This way we can quickly turn our PC
into rompler. Check the soundfont section of the guide for the details on the type Sf
.
sfPatchHall :: Sf -> Patch2
sfPatch :: Sf -> Patch2
There other functions. See the full list at the module Csound.Air.Patch
.
We can set a midi channel for all instruments in the patch with function setMidiChn
:
setMidiChn :: MidiChn -> Patch a -> Patch a
Let's study some predefined patches. We should install the csound-catalog
package.
Then we need to import the module Csound.Patch
and try some goodies (you can use dac
instead of vdac
if you have a real midi device):
>:m +Csound.Patch
> vdac $ atMidi vibraphone1
> vdac $ atMidi dreamPad
> vdac $ atMidi $ deepPad razorPad
> vdac $ atMidi epianoBright
> vdac $ atMidi xylophone
> vdac $ atMidi scrapeDahina
> vdac $ atMidi noiz
> vdac $ atMidi mildWind
> vdac $ atMidi toneWheelOrgan
> vdac $ atMidi $ addPreFx 1 (at $ echo 0.35 0.65) banyan
> vdac $ atMidi caveOvertonePad
> vdac $ atMidi flute
> vdac $ atMidi fmBass2
> vdac $ atMidi hulusiVibrato
> vdac $ atMidi shortBassClarinet
> vdac $ atMidi $ withDeepBass 0.75 pwBass
> vdac $ atMidi pwEnsemble
> vdac $ atMidi albertClockBellBelfast
> vdac $ atMidi $ vibhu 65
There are 200+ of other instruments to try out! You can find the complete list
in the module Csound.Patch
.