csound-expression-1.0.4: library to make electronic music

Safe HaskellNone

Csound.Base

Contents

Description

Basic types and functions.

WARNING (for Csound users): the maximum amplitude is 1.0. There is no way to alter it. don't define your amplitudes with 9000 or 11000. But the good news are: all signals are clipped by 1 so that you can not damage your ears and your speakers by a little typo.

Synopsis

Introduction to Csound for Haskell users

We are going to make electronic music. But what is Csound? And why should we use it?

Csound is a domain specific programming language. It helps you to define synthesizers and make some music with them (http://www.csounds.com). Csound was born in 1985 (a bit older than Haskell) at MIT by Barry Vercoe. It's widely used in the academia. It has a long history. So with Csound we get a lot of music dsp-algorithms ready to be used. It's written in C. So it's very efficient. It's driven by text, so we can generate it. Csound's community is very friendly (what a coincidence!). Csound is very well documented.

Making music with Csound

You don't need to know Csound to use this library. but it's helpful to know the main features of the Csound: how can you create music with Csound in general, what design choices were made, basic features and quirks. Csound belongs to the MUSIC N family of programming languages. What does it mean? It means that music is divided in two parts:

  1. Orchestra. User defines instruments
  2. Scores. User triggers instruments with a list of notes

Ab instrument is something that listens to notes and converts them to signals. Note is a tuple: (instrument name, start time, duration, parameters). Parameters cell is a tuple of primitive types: numbers (D), strings (Str) and tables or arrays of numbers (Csound.Tab).

Scores are very simple yet powerful. Csound handles polyphony for you. If you trigger several notes at the same time on the same instrument you get three instances of the same instrument running in parallel. It's very cool feature (not so easy thing to do with Pd).

But main strength lies in the Orchestra section. Here you can define the timbres for your musical journey. Csound is mostly for making strange sounds. How you can do it? You do it with instruments. An instrument is a sequence of statements that define a flow-graph for your sound waves. In instrument you can use predefined sound generators and transformers (Csound.Opcode and Csound.Air).

Score/Orchestra division stays in this library too. You define your instruments of the type

 (Arg a, Out b) => a -> b

An instrument is something that converts arguments-like things (tuple of primitive values) to output-like things (list of signals).

Later when you are done with orchestra section you can trigger the instruments with the function score

 score :: (Arg a, Out b) => (a -> b) -> [(Double, Double, a)] -> SigOut

It takes an instrument and the list of notes for this instrument. I've said that in Csound note contains four elements. But here it has only three because we define all notes at the time for one instrument. No need to label instrument with names explicitly. arguments-like thing is something that can be converted to the tuple of primitive values. There are a lot of predefined instances.

This library doesn't help you with score section that much. Scores are the same as you would write them with Csound. It's a list of events. Haskell can help you with powerful functions for lists but it's not so convenient as it can be. It's so on purpose. Csound-expression stays clear from score-generation libraries. But you can use your favourite library to create complex scores. You can use temporal-music-notation or Haskore or Euterpea. Any library that can generate the list of events will do.

Flags and options

Music is defined in two parts. They are Orchestra and Scores. But there is a third one. It's used to set the global settings like sample rate or control rate values (block size). In this library you can set the initial values with CsdOptions.

Features and quirks

Audio and control rates

Csound has made a revolution in electronic music technology. It introduced two types of signals. They are audio rate and control rate signals. The audio rate signals is what we hear and control rate signals is what changes the parameters of sound. Control rate is smaller then audio rate. It speeds up performance dramatically. Let's look at one of the sound units (they are called opcodes)

 ares buthp asig, kfreq [, iskip]

It's a butterworth high pass filter as it defined in the Csound. a-sig - means sig at audio rate. k-freq means freq at control rate (for historical reasons it is k not c). iskip means skip at i-rate. i-rate means init time rate. It is when an instruments instance is initialized to play a note. i-rate values stays the same for the whole note. So we can see that signal is filtered at audio rate but the center frequency of the filter changes at the control rate. In this library I've merged the two types together (Sig). If you plug a signal into kfreq we can infer that you want this signal to be control rate. In Csound some opcodes exist go in pairs. One that produces audio signals and one that produces control rate signals. By default if there is no constraint for the signal it is rendered at the audio rate except for those units that produce sound envelopes (like linseg).

You can change this behaviour with functions ar and kr. They set the signal-like things to audio or control rate. For instance if you want your envelope to run at control rate, write:

 env = ar $ linseg [0, idur/2, 1, idur/2, 0]

Constants are converted to signals with them also.

Table size

For speed table size should be the power of two or power of two plus one (all tables for oscillators). In this library you can specify the relative size (see Adoptions). I've tried to hide the size definition to make sings easier.

How to read the Csound docs

I'm to lazy to rewrite the Csound docs for all opcodes so you'd better get acquainted with Csound docs. Docs are very good. How to read them? For instance you want to use an oscillator with cubic interpolation so you dig into the Csound.Opcode.Basic and find the function:

 oscil3 :: Sig -> Sig -> Tab -> Sig

From Hackage we can guess that it takes two signals and table and returns a signal. It's a clue but a vogue one. Let's read along, in the docs you can see a short description (taken from Csound docs):

 oscil3 reads table ifn sequentially and repeatedly at a frequency xcps. 
 The amplitude is scaled by xamp. Cubic interpolation is applied for table look up from internal phase values. 

and here is the Csound types (the most useful part of it)

 ares oscil3 xamp, xcps, ifn [, iphs]
 kres oscil3 kamp, kcps, ifn [, iphs]

We see a two versions of the opcode. For audio and control rate signals. By default first is rendered if we don't plug it in something that expects control rates. It's all about rates, but what can we find out about the arguments?

First letter signifies the type of the argument and the rest is the name. We can see that first signal is amp with x rate. and the second one is cps with x rate. We can guess that amp is the amplitude and cps is cycles per second. This unit reads the table with given amplitude (it is a signal) and frequency (it is a signal too). Or we can just read about it in the docs if we follow the link that comes at the very last line in the comments:

I've said about a-, k- and i-rates. But what is the x-rate? Is it about X-files or something? X means a-rate or k-rate. You can use both of them for this argument. Let's go through all types that you can find:

  • asig -- audio rate (Sig)
  • ksig -- control rate (Sig)
  • xsig -- audio or control rate (Sig)
  • inum -- constant number (D)
  • ifn -- table (Tab). They are called functional tables in Csound.
  • Sfile -- string, probably a file name (Str)
  • fsrc -- spectrum (Spec). Yes, you can mess with sound in the space domain.

Often you will see the auxiliary arguments, user can skip them in Csound. So we can do it in Haskell too. But what if we want to supply them? We can use the function withInits for this purpose.

Example (a concert A)

 module Main where
 
 -- imports everything
 import Csound.Base
 
 -- Let's define a simple sound unit that 
 -- reads in cycles the table that contains a single sine partial.
 -- oscil1 is the standard oscillator with linear interpolation.
 -- 1 - means the amplitude, cps - is cycles per second and the last argument
 -- is the table that we want to read. 
 myOsc :: Sig -> Sig
 myOsc cps = oscili 1 cps (sines [1])
 
 -- Let's define a simple instrument that plays a sound on the specified frequency.
 -- We use kr to convert a constant value to signal and then plug it in the osc unit. 
 -- We make it a bit quieter by multiplying with 0.5.
 pureTone :: D -> Sig
 pureTone cps = 0.5 * (myOsc $ kr cps)
 
 -- Let's trigger the instrument from the score section.
 -- It plays a single note that starts at 0 and lasts for 1 second and 
 -- triggers the instrument 'instr' with frequency of 440 (Hz).
 res = score pureTone [(0, 1, 440)]
 
 -- Renders generated csd-file to the "tmp.csd".
 main :: IO ()
 main = writeFile "tmp.csd" $ renderCsd [res]

Now you can invoke Csound on tmp.csd and listen to the result with your favourite player.

 csound tmp.csd -o a.wav  

That's it csound is a separate program that we have to run to compile our csd-files to sounds. We can listen to the sound as it runs. It can be configured with flags.

References

Got interested in Csound? Csound is very well documented. There are good tutorials, read about it at:

Types

class Val a Source

Constants

A constant value doesn't change while instrument is playing a note. Only constants can be passed as arguments to the instruments.

data D Source

Doubles.

data Str Source

Strings.

withInits :: (Val a, CsdTuple inits) => a -> inits -> SigSource

Appends initialisation arguments. It's up to you to supply arguments with the right types. For example:

 oscil 0.5 440 sinWave `withInits` (0.5 :: D)

Tables

In Csound tables can be treated as primitive values. They can be passed to instruments in the score events. There are limited set of functions which you can use to make new tables. Look at the following module for details:

module Csound.Tab

Signals

Signals can be audio or control rate. Rate is derived from the code. If there are rate-collisions, values will be converted to the right rates. For example, if you are trying to apply an opcode that expects control rate signal to some audio rate signal, the signal will be downsampled behind the scenes.

data Sig Source

Audio or control rate signals.

data Spec Source

Spectrum of the signal (see FFT and Spectral Processing at Csound.Opcode.Advanced).

Instances

Booleans

Use functions from the module Data.Boolean to make boolean expressions.

data BoolSig Source

Boolean signals.

data BoolD Source

Boolean constants.

Instances

Side effects

data SE a Source

Csound's synonym for IO-monad. SE means Side Effect. You will bump into SE trying to read and write to delay lines, making random signals or trying to save your audio to file. Instrument is expected to return a value of SE [Sig]. So it's okay to do some side effects when playing a note.

Instances

Tuples

class CsdTuple a Source

Describes tuples of Csound values. It's used for functions that can return several results (such as soundin or diskin2). Tuples can be nested.

Instances

CsdTuple Tab 
CsdTuple Spec 
CsdTuple Str 
CsdTuple D 
CsdTuple Sig 
(CsdTuple a, CsdTuple b) => CsdTuple (a, b) 
(CsdTuple a, CsdTuple b, CsdTuple c) => CsdTuple (a, b, c) 
(CsdTuple a, CsdTuple b, CsdTuple c, CsdTuple d) => CsdTuple (a, b, c, d) 
(CsdTuple a, CsdTuple b, CsdTuple c, CsdTuple d, CsdTuple e) => CsdTuple (a, b, c, d, e) 
(CsdTuple a, CsdTuple b, CsdTuple c, CsdTuple d, CsdTuple e, CsdTuple f) => CsdTuple (a, b, c, d, e, f) 
(CsdTuple a, CsdTuple b, CsdTuple c, CsdTuple d, CsdTuple e, CsdTuple f, CsdTuple g) => CsdTuple (a, b, c, d, e, f, g) 
(CsdTuple a, CsdTuple b, CsdTuple c, CsdTuple d, CsdTuple e, CsdTuple f, CsdTuple g, CsdTuple h) => CsdTuple (a, b, c, d, e, f, g, h) 

Converters

class ToSig a whereSource

Values that can be converted to signals.

Methods

arSource

Arguments

:: a 
-> Sig

Forces signal to audio rate.

krSource

Arguments

:: a 
-> Sig

Forces signal to control rate.

ir :: Sig -> DSource

Converts signal to double.

double :: Double -> DSource

Converts Haskell's doubles to Csound's doubles

str :: String -> StrSource

Converts Haskell's strings to Csound's strings

Making a sound

Let's make some noise. Sound is build from list of tracks (SigOut).

class Out a Source

Output of the instrument.

Instances

Out Sig 
Out a => Out [a] 
Out a => Out (SE a) 
(Out a, Out b) => Out (a, b) 
(Out a, Out b, Out c) => Out (a, b, c) 
(Out a, Out b, Out c, Out d) => Out (a, b, c, d) 
(Out a, Out b, Out c, Out d, Out e) => Out (a, b, c, d, e) 
(Out a, Out b, Out c, Out d, Out e, Out f) => Out (a, b, c, d, e, f) 
(Out a, Out b, Out c, Out d, Out e, Out f, Out g) => Out (a, b, c, d, e, f, g) 
(Out a, Out b, Out c, Out d, Out e, Out f, Out g, Out h) => Out (a, b, c, d, e, f, g, h) 

data SigOut Source

The abstract type of musical tracks.

effect :: ([Sig] -> SE [Sig]) -> SigOut -> SigOutSource

Applies a global effect function to the signal. With this function we can add reverb or panning to the mixed signal. The argument function takes a list of signals. Each cell of the list contains a signal on the given channel.

Handy short-cuts

type Outs = SE [Sig]Source

type Sig2 = (Sig, Sig)Source

type Sig3 = (Sig, Sig, Sig)Source

type Sig4 = (Sig, Sig, Sig, Sig)Source

Scores

We can define an instrument and tell it to play some notes.

score :: (Arg a, Out b) => (a -> b) -> [(Double, Double, a)] -> SigOutSource

class Arg a whereSource

Describes all Csound values that can be used in the score section. Instruments are triggered with the values from this type class. Actual methods are hidden, but you can easily make instances for your own types with function makeArgMethods. You need to describe the new instance in terms of some existing one. For example:

 data Note = Note 
     { noteAmplitude    :: D
     , notePitch        :: D
     , noteVibrato      :: D
     , noteSample       :: S
     }
 
 instance Arg Note where
     argMethods = makeArgMethods to from
         where to (amp, pch, vibr, sample) = Note amp pch vibr sample
               from (Note amp pch vibr sample) = (amp, pch, vibr, sample)

Then you can use this type in an instrument definition.

 instr :: Note -> Out
 instr x = ...

Instances

Arg () 
Arg Tab 
Arg Str 
Arg D 
(Arg a, Arg b) => Arg (a, b) 
(Arg a, Arg b, Arg c) => Arg (a, b, c) 
(Arg a, Arg b, Arg c, Arg d) => Arg (a, b, c, d) 
(Arg a, Arg b, Arg c, Arg d, Arg e) => Arg (a, b, c, d, e) 
(Arg a, Arg b, Arg c, Arg d, Arg e, Arg f) => Arg (a, b, c, d, e, f) 
(Arg a, Arg b, Arg c, Arg d, Arg e, Arg f, Arg g) => Arg (a, b, c, d, e, f, g) 
(Arg a, Arg b, Arg c, Arg d, Arg e, Arg f, Arg g, Arg h) => Arg (a, b, c, d, e, f, g, h) 

data ArgMethods a Source

The abstract type of methods for the class Arg.

makeArgMethods :: Arg a => (a -> b) -> (b -> a) -> ArgMethods bSource

Defines instance of type class Arg for a new type in terms of an old one.

Midi

We can define a midi-instrument. Then we can trigger the instrument with a midi-keyboard.

data Msg Source

Midi messages.

massign :: Out a => Channel -> (Msg -> a) -> SigOutSource

pgmassign :: Out a => Maybe Channel -> Int -> (Msg -> a) -> SigOutSource

Rendering

Now we are ready to create a csound-file. The function renderCsd creates a String that contains the description of our music. We can save it to a file and compile it with our csound wizard.

renderCsd :: [SigOut] -> StringSource

Renders Csound file.

Opcodes

Some colors to paint our soundscapes.

Patterns

Frequently used combinations of opcodes.

module Csound.Air

Options

We can set some csound options.

renderCsdBy :: CsdOptions -> [SigOut] -> StringSource

Renders Csound file with options.

data CsdOptions Source

Csound options. The default value is

 instance Default CsdOptions where
     def = CsdOptions 
             { csdFlags = ""
             , csdRate  = 44100
             , csdBlockSize = 64
             , csdSeed = Nothing
             , csdInitc7 = []
             , csdEffect = mixing
             , csdKrate  = ["linseg", "expseg", "linsegr", "expsegr", "linen", "linenr", "envlpx"],
             , tabResolution = 8192 }  -- should be power of 2

Instances

mixing :: [[Sig]] -> OutsSource

Sums signals for every channel.

mixingBy :: ([Sig] -> Outs) -> [[Sig]] -> OutsSource

Sums signals for every channel and the processes the output with the given function.