Function tables, quick recap…
Function tables are used to store a sequence of numbers in your computer’s RAM. The sequence of numbers may represent a waveform such as an audio file, an envelope, a series of MIDI notes, etc. The data created/loaded and stored in a function table is for the most part determined by the GEN routine. For example, GEN10 creates composite waveforms while GEN01 loads sound files from disk. A full list of GEN routines can be found here. From hereon in we will deal with GEN01 function tables.
Opcodes that read function tables
The opcodes you choose to read data from a function table largely on what data is stored there. Oscillators are the go-to tool for reading function tables that contain composite wave forms. Oscillators can also be used for reading back large tables, but many people will prefer to use one of the so-called direct access table opcodes, such as the table family of opcodes. Direct access means we tell Csound exactly which sample to read at any particular time. It also means we can quickly skip to any place in the table quite easily.
The syntax for the tab opcode is as follows:
ar tab xndx, ifn[, ixmode]
xndx
is the table index. If we passed the number 10 as this parameter, the output variable ar
would be the value of the 10th sample from the table we are reading. It obviously doesn’t make much sense to pass constants, so instead we use an audio signal to specify which sample we want. (we’ll return to this later). ifn
is the table number we are trying to read from, and ixmode
determines how the table’s range is to be set. If you don’t set this parameter, then the table range will go from 0 the the number of samples in the table, ergo, your index will also need to be kept within this range. If you set this parameter to 1, then the table range will be from 0 to 1. This makes life a lot easier as you don’t have to know in advance how many samples are contained in your sound file.
Reading data from a function table
If you’ve looked through any Csound source code, you see quite a lot that uses a combination of phasor
and tab
. They make a great pair. phasor
outputs a signal that moves between 0 and 1 n times a second.
If we set our table opcode’s ixmode
to 1 then we use the output from a phasor to read samples from our table:
giSoundfile ftgen 1, 0, 0, 1, "06_Piano.wav", 0, 4, 1
instr 1
aPhs phasor 1
aTab tab aPhs, giSoundfile, 1
outs aTab, aTab
endin
As aPhs
moves continuously from 0 and 1, tab
will output all samples from the table in quick succession. Only there is a problem here, the playback frequency is not right. The piano sample used in this example is 3 minutes long. Yet we have our phasor set to a frequency of 1. This means out tab opcodes will be reading the entire table once every second. So we are actually reading the table about 180 times quicker than normal speed.
Setting the right frequency for our phasor
In order to set the correct frequency for playback we need to know how long the sound file is. We can use the ftlen opcode to find this out. ftlen
return the number of samples in a sound file. Once we know the number of samples in the file we can work out the correct frequency of the phasor by dividing the current sampling rate by the length of the table in samples.
Let’s consider for a moment that there are only 2 seconds worth of audio sample in our sound file. Assuming a sample rate of 44100, ftlen
will return 88200 when we query the table length. Frequency is defined as the number of cycles per second. A single cycle in this context is the entire table. If we set our phasor frequency to 1 we will play this entire table in a single second. A phasor with a frequency of .5 on the other hand will only get through half a table in a second. The following single line of code will work out what the frequency of your phasor should be, depending on the length of your sound file.
iFreq = sr/ftlen(giSoundfile)
Changing the speed of playback
Once you have the correct frequency for your phasor you can start to manipulate it quite easily. If for example you multiple the normal speed of playback by 2 for example you will increase the pitch of the sample by an octave. If you half it you will reduce the speed of playback by an octave. You can use any frequency ratio to alter the pitch. You could also use a slider as in the following example.
<Cabbage>
form caption("Untitled") size(400, 300), colour(58, 110, 182), pluginID("def1")
hslider bounds(12, 8, 200, 30), channel("speed"), range(0, 2, 1, 1, .1)
</Cabbage>
<CsoundSynthesizer>
<CsOptions>
-n -d -+rtmidi=NULL -M0 -m0d
</CsOptions>
<CsInstruments>
; Initialize the global variables.
sr = 44100
ksmps = 32
nchnls = 2
0dbfs = 1
giSoundfile ftgen 1, 0, 0, 1, "06_Piano.wav", 0, 4, 1
instr 1
iFreq = sr/ftlen(giSoundfile)
aPhs phasor iFreq*chnget:k("speed")
aTab tab aPhs, giSoundfile, 1
outs aTab, aTab
endin
</CsInstruments>
<CsScore>
i1 0 z
</CsScore>
</CsoundSynthesizer>
Note that the above instrument is reading only the left channel of the sound file. We can tell by looking at the last parameter passed to ftgen
when we created our GEN01 function table. A 1 instructs Csound to read the left channel, a 2 the right, 3, a third channel, etc. GEN01 can support any number of channels. 0 instructs Csound to read both channels and can be useful if using one of the oscillator opcodes that support stereo tables, such as loscil
. If you wish to preserve the stereo nature of the file you should use two function tables, one for the left and one for the right. You’ll also need to use two tab opcodes.
giSoundfile ftgen 1, 0, 0, 1, "06_Piano.wav", 0, 4, 0
giSoundfile ftgen 1, 0, 0, 1, "06_Piano.wav", 0, 4, 1
instr 1
iFreq = sr/ftlen(giSoundfile)
aPhs phasor iFreq*chnget:k("speed")
aTabL tab aPhs, giSoundfile, 1
aTabR tab aPhs, giSoundfile, 1
outs aTabL, aTabR
endin
Conclusion
Function tables are at the heart of thousands of Csound files, but they are not the only way to read data from disk. One can also use the soundin or diskin2 opcodes. diskin2
provides an input parameter that allows one to quickly change the play-pack speed. On top of that, it will always plays back the sound files at the correct pitch by default. Here’s a quick example of an instrument that will play back a sound file at its correct speed over and over for as long as the instrument is running.
instr 1
aOutL, aOutR diskin2 "06_Piano.wav", 1, 0, 1
outs aOutL, aOutR
endin
So why not use diskin2
? While it does provide a quick way of reading sounds from disk, it doesn’t provide the same level of control you can get from using a table opcode. On top of this, it only reads from disk, it doesn’t load the sound files into the computer’s RAM. Reading samples from RAM is usually quicker than reading from a hard disk, although the difference these days is getting smaller and smaller with the advent of high speed SSDs.