K509 — Computer Music Seminar: RTcmix Tutorial
The advantage of this approach is that you can design an algorithm — a set of specific instructions that perform a task — and have the algorithm generate sound. This is a very different way of working than the way you work with GUI-based software such as Pro Tools and Absynth, and it opens up a number of sonic possibilities that would not otherwise be available.
RTcmix runs on any computer that uses either the Mac OS X or Linux operating systems. The source code for the program (written in C and C++) is free. The code, as well as documentation, is available at rtcmix.org.
This tutorial is designed to get you started running and writing RTcmix scripts (or “scores,” as we usually call them).
The tutorial assumes
The tutorial assumes that you have no prior programming experience.
If you see an error message like “no such file or directory,” try it again, and be careful about all the slashes and letters. The only space is right after “cd.”cd rtcmix/sco
CAUTION: When you’re running RTcmix scores, especially ones that you’re working on, you should turn your monitoring volume down a bit. Sometimes you might get unexpected loud sounds that could damage the speakers and your ears. Turn it up once you’re sure of what will come out.
You should hear some sound and see some stuff printed to the screen.cmix < fmalias.sco
When the score is finished, you’ll see a new command prompt. If you want to stop playback before the score is finished, type control-c.
If you’re wondering about the use of the ‘<’ character in the command above, you can learn all about it here. Or you can think of it as telling the shell, “feed the ‘fmalias.sco’ file to the cmix program.”
Okay, it looks like gibberish. The next section helps you understand scores like this.edit fmalias.sco
The shell maintains a history of all the commands you type. If you want to go back in the history — to issue the “cmix < score” command again — just use the up and down arrows to find the command you want to give, and then press the return key. Also, typing “!!” (bang, bang), followed by return, at the shell will cause it to repeat the most recent command you gave.
rtsetparams(44100, 2)
load("WAVETABLE")
WAVETABLE(0, 10, 10000, 440, 0.5)
This plays a beautiful sine wave at A440 for ten seconds.
(All the scores in this tutorial are in your rtcmix/sco/tut directory. You should cd into that directory and run them while following the tutorial. The first one is called “tut1.sco.”)
The score consists of three function calls. A function is a chunk of computer code that performs an action, often taking arguments (or parameters) that specify details of the action. The arguments are in parentheses following the function name and are separated by commas.
Let’s look at this score line by line.
rtsetparams(44100, 2)The rtsetparams function sets up the sampling rate (44100) and the number of output channels (2) for this score.
load("WAVETABLE")
The load function specifies an instrument plug-in that we will
use. An instrument is a piece of software that makes sound. Instrument
names are always in upper case. The name must appear in double quotes as an
argument to the load function.
WAVETABLE(0, 10, 10000, 440, 0.5)Here’s the part that makes the sound. The WAVETABLE instrument performs wavetable synthesis. It takes several arguments that specify the start time in seconds of the note (0), the duration in seconds (10), the amplitude (10000), the frequency in Hz (440) and the stereo location (0.5). These arguments must appear in this order, from left to right. Amplitudes for WAVETABLE and other synthesis instruments fall between 0 and 32767, which is the maximum amplitude for a 16-bit audio signal (even though the internal sample word length is 32 bits). Pan is from 0 to 1.
Most scores make use of variables. These are names to which a value is assigned. Then the names can be used and their values modified in the score. For example, you could have...
homer = 10 marge = 20 bart = homer + marge print(bart)We assign values to the homer and marge variables. Then we add together the values of those variables and store the result into the bart variable. The print function prints the value of bart (30) to the screen.
The variable names are arbitrary: they can be nearly anything, as long as they begin with a letter and don’t contain weird characters. They probably shouldn’t be the same as names of RTcmix functions, just to avoid confusion.
We can use variables to make our first score easier to read and understand.
rtsetparams(44100, 2)
load("WAVETABLE")
start = 0
dur = 10
amp = 10000
freq = 440
pan = 0.5
WAVETABLE(start, dur, amp, freq, pan)
In the WAVETABLE call, RTcmix substitutes the values of the variables
for the names that we see in the score. The result is identical to the first
score above. Although this one is longer, the clarity is worth the extra
typing.
You can also assign values to variables right at the point when they’re used.
WAVETABLE(start=0, dur=10, amp=10000, freq=440, pan=0.5)This combines the advantages — compactness and clarity — of the previous two approaches.
rtsetparams(44100, 2)
load("WAVETABLE")
WAVETABLE(start=0, dur=6, amp=8000, freq=440, pan=0.5)
WAVETABLE(start=0, dur=6, amp=8000, freq=550, pan=1)
WAVETABLE(start=0, dur=6, amp=8000, freq=660, pan=0)
WAVETABLE(start=2, dur=4, amp=12000, freq=220, pan=0.3)
WAVETABLE(start=3, dur=3, amp=10000, freq=330, pan=0.7)
WAVETABLE(start=4, dur=2, amp=10000, freq=800, pan=0.4)
You could even write the lines with their start times out of order, and it
would still work.
Now let’s have some fun. Instead of specifying the notes one by one, let’s get RTcmix to choose them randomly. We’ll also play them using a loop, a very powerful construction for algorithmic composition. Play this score (“tut3.sco”).
rtsetparams(44100, 2)
load("WAVETABLE")
for (start = 0; start < 10; start = start + 1) {
freq = irand(100, 2000)
pan = irand(0.2, 0.8)
WAVETABLE(start, dur=2, 8000, freq, pan)
}
Although we hear ten notes, one after another, there is only one
WAVETABLE call in the score. But it’s embedded within a for
loop, which repeats the WAVETABLE note several times. A for
loop comprises a description of how the loop should work and a block of
statements enclosed in curly braces — ‘{’ and
‘}’. Though it’s not necessary, you should indent the block
of statements within the braces to make the loop easier to read. The text in
parentheses following “for” tells RTcmix how to perform the loop.
for (start = 0; start < 10; start = start + 1) {
block of statements...
}
For people who haven’t programmed in C before, here is a description of
what the “for” line is asking RTcmix to do...
Within the block of statements performed by the loop, we set the value of some variables to random numbers. This is a way of letting the computer make some of the decisions about how the notes will sound. We use the irand function to generate a random number within a given range, and then assign this number to a variable.
freq = irand(100, 2000)Here we ask irand to return a random value between 100 and 2000. We assign that as the value of freq, which we later feed to the WAVETABLE instrument call. We do something similar for pan.
rtsetparams(44100, 2)
load("WAVETABLE")
env = maketable("line", size=1000, 0,0, 1,1, 9,1, 10,0)
for (start = 0; start < 10; start = start + 1) {
freq = irand(100, 2000)
pan = irand(0.2, 0.8)
WAVETABLE(start, dur=2, 8000 * env, freq, pan)
}
You create an envelope table using the maketable function. This just
writes a bunch of numbers into a table, in such a way as to describe a shape
that changes over time. Our shape is a simple attack / release ramp. We store
the table into the env variable, and then use it within the call to
WAVETABLE. Instead of using a constant number (8000) as the amplitude
for the note, we multiply that number by the changing numbers stored in the
table (8000 * env). By default, these numbers are between 0 and 1, so
they act as a variable volume control, scaling between 0 and 8000 the amplitude
that WAVETABLE sees.
Here’s how the maketable function works in detail.
env = maketable("line", table_size, time1,value1, time2,value2, ...)
The type of table we want is called “line.” (There are many other
types.) You specify the size of the table — the number of values in the
table. Then you give two or more time-and-value pairs that set the position of
breakpoints in the envelope — points connected by straight line segments.
The times are in seconds and must be ascending, from left to right.
env = maketable("line", size=1000, 0,0, 1,1, 9,1, 10,0)
We have 1000 numbers in the table. They start at 0 and increase to 1 over the
first second. Then at 9 seconds, the numbers decrease to 0 until the table
ends at second 10.
IMPORTANT: When you give a table to an instrument, the time it takes is scaled to fit the duration of the note played by the instrument. Our envelope lasts ten seconds, but our note lasts only two, so the envelope shrinks to fit two seconds.
We can use the same type of table to create a glissando. Play this score (“tut5.sco”), which is the same as the last, except for an added glissando table.
rtsetparams(44100, 2)
load("WAVETABLE")
env = maketable("line", size=1000, 0,0, 1,1, 9,1, 10,0)
gliss = maketable("line", "nonorm", size, 0,1, 1,2)
for (start = 0; start < 10; start = start + 1) {
freq = irand(100, 2000)
pan = irand(0.2, 0.8)
WAVETABLE(start, dur=2, 8000 * env, freq * gliss, pan)
}
We multiply the frequency by a table that represents a straight line from 1 to
2, which results in a glissando from the initial randomly-generated frequency,
at the beginning of the note, to an octave above that, at the end of the note.
The extra “nonorm” argument for the gliss table is
required to keep RTcmix from normalizing (scaling) the table so that it fits
between 0 and 1.
rtsetparams(44100, 2)
load("WAVETABLE")
dur = 2
freq = 300
env = maketable("line", 1000, 0,0, 1,1, 3,1, 4,0)
pan = 0.5
wavetable = maketable("wave", size=1000, "square")
WAVETABLE(start=0, dur, 10000 * env, freq, pan, wavetable)
wavetable = maketable("wave", size=1000, "buzz")
WAVETABLE(start=2, dur, 10000 * env, freq, pan, wavetable)
wavetable = maketable("wave", size=1000, 1, 0, .5, 0, .3, 0, .1, 0, 0, .7)
WAVETABLE(start=4, dur, 10000 * env, freq, pan, wavetable)
Here we play three consecutive notes, each with a different wavetable. You
create a wavetable using the “wave” type for maketable.
There are two options:
We can change the wavetable within a loop, and we can specify the partial amplitudes using random numbers. Each note then has a slightly different wavetable. Play the next score (“tut7.sco”).
rtsetparams(44100, 2)
load("WAVETABLE")
total_dur = 10
dur = 0.08
env = maketable("line", size=1000, 0,0, .01,1, dur-.01,1, dur,0)
amp = 16000
freq = 250
increment = dur * 1.3
control_rate(15000) // increase rate of envelope updates (15000 times per sec)
for (start = 0; start < total_dur; start = start + increment) {
p1 = random()
p2 = random()
p3 = random()
p4 = random()
p5 = random()
p6 = random()
p7 = random()
p8 = random()
wavetable = maketable("wave", size=2000, p1, p2, p3, p4, p5, p6, p7, p8)
pan = irand(0, 1)
WAVETABLE(start, dur, amp * env, freq, pan, wavetable)
}
We use the random function, which returns random numbers between 0 and
1, to supply the amplitudes for the wavetable that is recreated during each
iteration of the loop. Each call to random returns a new random number.
Note that the random function takes no arguments, but it still needs the
empty parentheses.
Notice that we compute the time increment for the loop — the amount of time between successive notes — in a fancier way than we did before. Now it depends on the note duration. Can you figure out how to make the increment variable change randomly while the loop executes? This would give us irregularly timed notes, instead of the robotically quantized notes we now have.
This score has a few other new things. When creating the amplitude envelope, we use the note duration to specify some of the times in the time-and-value pairs. There is also a comment in the score. A comment is any text following two slashes (//) on a line. You use comments to help you (and other readers) understand and remember things about the score. RTcmix ignores them when computing and playing the score.
The control_rate function requires more explanation than the comment in the score provides. The control rate is the rate at which control information, such as envelopes, is calculated. Usually, this is much slower than the sampling rate, because it doesn’t need to be as fast, and it saves computer processing power to keep it slow. The normal control rate in RTcmix is 2000 times per second. For short synthetic sounds like WAVETABLE notes, this is too slow. So we bump it up to 15,000 times per second. If you delete the control_rate line and run the score, you’ll hear the difference: there’s a click at the note boundaries.
WAVETABLE accepts octave-point-pitch-class (aka “oct.pc” or “pch”) numbers directly, as an alternative to frequencies in Herz.
WAVETABLE(start, dur, amp, pitch = 9.07, pan)This WAVETABLE note plays the G that is a twelfth above middle C.
Sometimes it’s useful to convert octave-point-pitch-class to frequency in Herz, or vice versa. RTcmix has many pitch conversion functions. Here are some.
freq = cpspch(9.07) // convert pch to Hz (cps, or cycles per second)
pitch = pchcps(freq) // convert back from cps to pch
pitch = pchmidi(60) // convert MIDI note number to pch
freq = cpslet("C#5") // convert letter/octave name to Hz
freq = cpslet("B2 +23") // letter names can have inflection in cents
With all of these, notice that the conversion function name is composed of
abbreviations of the two pitch formats, with the left one indicating the format
to which you’re converting. (In other words, read “cpspch”
as “convert to cps from pch.”)
Sometimes we want to specify pitch directly, as we’ve been doing. But you can also construct an algorithm that computes pitches according to a formula. It turns out that when doing this, we need to be careful when subtracting one pitch from another. For example, what will happen if we try to subtract 0.02 (two semitones) from a pitch whose oct.pc value is 8.01? We would get 7.99, which, as an oct.pc value, is a very high pitch. (Keep in mind that 8.12 in oct.pc is an octave above 8.00, and 8.24 is two octaves above 8.00.) The right answer is 7.11, two semitones lower than 8.01. There is an alternative pitch representation, called “linear octaves,” that solves this problem. Unfortunately, linear octaves are cumbersome and unintuitive to use. Instead, we use the handy pchadd function to perform addition or subtraction of two pitches in oct.pc notation. Here’s how it works.
result = pchadd(8.01, -.02) print(result)This prints “7.11” to the screen. Notice that in order to subtract a positive number from another one, you need to put the minus sign in front of the second number (with no intervening space).
Here’s a score that uses pitch computation (“tut8.sco”); the melodic line goes up a minor third or down a major second, depending on a random number.
rtsetparams(44100, 2)
load("WAVETABLE")
env = maketable("line", size=1000, 0,0, 1,1, 30,0)
control_rate(20000) // need faster envelope updates to avoid clicks
srand(1) // seed the random number generator
increment = 0.14
dur = increment * 0.8
pitch = 7.02 // starting pitch
for (start = 0; start < 15; start = start + increment) {
if (random() < 0.5) { // if random number is less than 0.5
transp = 0.03 // set transposition to minor 3rd up
}
else { // otherwise...
transp = -0.02 // set transp to major 2nd down
}
pitch = pchadd(pitch, transp)
decibel = irand(70, 92) // get random amp between 70 and 92 dB
amp = ampdb(decibel) // convert dB to linear amplitude
pan = irand(0.1, 0.9) // get random pan
WAVETABLE(start, dur, amp * env, pitch, pan)
}
We start with a particular pitch (D below middle C). Each time through the
loop, we get a random number between 0 and 1. If that number is less than 0.5,
then we transpose the current pitch up a minor third; if the random number is
0.5 or above, then we transpose the current pitch down a major second. Over
the long haul, the resulting melodic line trends upward, but it does so in an
irregular way.
The score above includes a few unfamiliar features. The first is the srand function, which seeds the random number generator. Computer-generated random numbers are not really random, in the sense that if we start with the same seed each time, we’ll get the same series of random numbers. Changing the seed to a different integer gives us a different series. Try replacing the seed (1) in our score with another integer. You’ll hear a different melodic line.
We set the interval of transposition using a conditional — a test, and a series of actions to perform depending on the result of the test. Study the syntax of the test below.
if (homer < 0.5) {
bart = 1.3
}
else {
bart = -99
}
If our test succeeds (the value of homer is less than 0.5), then
RTcmix performs whatever is within the next set of curly braces (bart =
1.3). Otherwise, it performs whatever is within the set of braces
following “else.” Indenting the statements that are within curly
braces makes for easier reading. The tests you can use are...
> greater than < less than >= greater than or equal to <= less than or equal to == equals (note: 2 equal signs! Or else you might assign by mistake.) != not equal toThe last new feature in the score above has to do with setting the amplitude of each note. We set each amplitude to a random value in decibels. This gives us a smoother result than using linear amplitudes. But instruments generally do not understand decibels, so we have to convert them to linear amplitudes. We do this with the ampdb function, which operates similarly to the pitch conversion functions.
notes = { 8.00, 8.02, 8.04, 8.05, 8.07, 8.09, 8.11, 9.00 }
To access one of these notes, you need to use its index. Array indices
start from zero and go up to one less than the number of things in the array.
anote = notes[2] anothernote = notes[7]Now anote is 8.04 and anothernote is 9.00. You can assign to an array in a similar way.
notes[2] = 8.03 notes[5] = 8.08This turns the major scale into a harmonic minor scale. Notice that you use curly braces when defining the array and square brackets when accessing the array with an index.
You can use arrays inside of a loop to get a randomly-selected pitch from a collection of pitches you define in advance. Here we make an array out of the pitches in the tone row of Berg’s Violin Concerto Just out of familiarity, we use letter name notation for the pitches, which requires that we use the pchlet function to convert the names to oct.pc in the loop. This score (“tut9.sco,” on next page) uses a triangle wave, and it sets the loop increment and note duration so that the notes overlap. We play each note using two calls to WAVETABLE; the second call plays with slightly detuned pitch, to make for a richer sound.
rtsetparams(44100, 2)
load("WAVETABLE")
wavetable = maketable("wave", 1000, "tri")
env = maketable("line", 1000, 0,0, 1,1, 2,0)
control_rate(20000) // need faster envelope updates to avoid clicks
srand(3) // seed the random number generator
increment = 0.3
dur = increment * 5
amp = 8000
row = { "G4", "Bb4", "D4", "F#4", "A4", "C5",
"E5", "G#4", "B4", "C#5", "Eb5", "F5" }
numnotes = len(row) // returns number of elements in array
for (start = 0; start < 20; start = start + increment) {
index = trand(numnotes) // get random integer for index
lettername = row[index] // access pitch array at that index
pitch = pchlet(lettername) // convert letter name to oct.pc
pan = irand(0, 1)
WAVETABLE(start, dur, amp * env, pitch, pan, wavetable)
WAVETABLE(start, dur, amp * env, pitch + 0.001, pan, wavetable)
}
We call the trand function to get a random number for use as an index.
This function returns a random integer between zero and the supplied
argument, which can be the size of the array. (We got this earlier using the
len function.) We access the row array at that index, and then
convert the result into oct.pc notation for use in WAVETABLE.
With what you know now, you would be able to add logic to the loop for varying the octave of the notes, either randomly or based on some kind of condition. (For example, if the start time is greater than 10, add 1 to the pitch.) Try it!
But first you need to tell RTcmix which sound file to open. Here’s a score (“tut10.sco”) that plays a sound file several times, using various offsets into the file.
rtsetparams(44100, 2)
load("STEREO")
rtinput("../../snd/carol.aif")
STEREO(start=0, inskip=0.2, dur=0.4, amp=1.0, pan=0.4)
STEREO(start=0.7, inskip=0.6, dur=0.8, amp=1.0, pan=0.9)
STEREO(start=1.0, inskip=3.7, dur=0.8, amp=0.8, pan=0.1)
STEREO(start=1.5, inskip=1.7, dur=1.0, amp=1.0, pan=0.6)
You open a sound file with the rtinput function. The sampling rate of
this file must match that given in the rtsetparams call. Above, we give
the relative path name of the file. The score is in the
“rtcmix/sco/tut” directory, and that’s our current working
directory. The sound file is in the “rtcmix/snd” directory, so
the “../../snd” path component takes us up two levels (../..), into
the rtcmix directory, and then down into the snd directory.
The second argument to STEREO is the amount of time in seconds to skip into the input sound file before reading from it — thus the variable name “inskip.”
Amplitude for instruments that take sound input works differently than for synthetic instruments like WAVETABLE. For the former, you use an amplitude multiplier rather than a peak amplitude value. So in the third call to STEREO above, we’re asking RTcmix to multiply each sample point in the sound file by a factor of 0.8, reducing the volume of the sound slightly.
We’ve been using a pan argument all along. You may have noticed that it works differently than you might expect. In RTcmix, a pan value of 1 means to pan completely to the left channel. Think of pan as meaning “the percentage of sound (from 0 to 1) to place in the left channel.” So a pan value of 0 means to place the sound hard right. Go figure.
A common thing to do in RTcmix is to read random bits of sound from a file within a loop, as in this score (“tut11.sco”).
rtsetparams(44100, 2)
load("STEREO")
rtinput("../../snd/carol.aif")
filedur = DUR() // get duration of most recently opened sound file
notedur = 0.2
env = maketable("line", 1000, 0,0, 1,1, 9,1, 10,0)
for (start = 0; start < 10; start = start + 0.15) {
inskip = irand(0, filedur - notedur)
STEREO(start, inskip, notedur, env, pan=random())
}
We get a random inskip from 0 to a point in time that is one note’s
duration shy of the end of the file. This is to avoid “running off the
end of the file” when reading the input sound.
Transposing an input file works similarly, but uses the TRANS instrument. The interval of transposition is specified in oct.pc format. The sort of transposition used here does not maintain duration, so it works like a variable speed tape deck. Here’s a score (“tut12.sco”) that modifies the previous one to randomly transpose snippets.
rtsetparams(44100, 2)
load("TRANS")
rtinput("../../snd/carol.aif")
filedur = DUR() // get duration of most recently opened sound file
notedur = 0.2
env = maketable("line", 1000, 0,0, 1,1, 9,1, 10,0)
for (start = 0; start < 10; start = start + 0.15) {
inskip = irand(0, filedur - notedur)
transp = irand(-.12, .12) // random transposition up or down an octave
TRANS(start, inskip, notedur, env, transp, inchan=0, pan=random())
}
At some point we’d like to capture the audio we’re hearing into a
sound file, for use in Pro Tools or elsewhere. You do this with the
rtoutput function, which lets you specify an output sound file name and
a data format, such as 16-bit, 24-bit or 32-bit floating point. The type of
sound file header used is automatically taken from the file name suffix
(“.wav” or “.aif,” usually).
rtsetparams(44100, 2)
load("WAVETABLE")
set_option("play = off") // don’t play while writing file
rtoutput("/Users/yournamehere/totallycoolsound.aif") // change this
env = maketable("line", 1000, 0,0, 1,1, 9,1, 10,0)
for (start = 0; start < 10; start = start + 0.06) {
freq = irand(60, 6000)
WAVETABLE(start, dur=0.1, 8000 * env, freq, pan=random())
}
You’ll have to change the file name so that it points to a place where
you have permission to write. Be careful not to give the name of an existing
file, like your score!
rtsetparams(44100, 2)
load("FMINST")
amp = 10000
env = maketable("line", 1000, 0,0, 1,1, 5,1, 7,0)
carfreq = 150 // carrier frequency
modfreq = carfreq * 2 // modulator frequency
min_index = 0 // modulation index range
max_index = 25
pan = maketable("line", 1000, 0,0, 1,1)
wavetable = maketable("wave", 1000, "sine")
index_guide = maketable("line", 1000, 0,0, 1,1, 2,0)
FMINST(start=0, dur=10, amp * env, carfreq, modfreq, min_index, max_index,
pan, wavetable, index_guide)
Play around with the computation of modfreq. Try different minimum and
maximum modulation index values, and specify different shapes for the various
tables used in the score. Incidentally, this score shows how to pan during the
course of a single note. Previously, we’ve had only static pan
locations.
STRUM2 is a synthetic plucked string instrument. It sounds like a cross between a harpsichord and a hammer dulcimer, but more artificial. The nice thing about it is that you can vary the thickness of the plectrum, via the “squish” parameter. Here’s a sample score (“tut15.sco”).
rtsetparams(44100, 2)
load("STRUM2")
dur = 1.2
decay_time = dur * 0.4
base_increment = 0.16
minfreq = 400
maxfreq = 435
increment = base_increment
for (start = 0; start < 16; start = start + increment) {
amp = irand(8000, 32000)
freq = irand(minfreq, maxfreq)
if (start > 10) {
maxfreq = maxfreq + 150
}
squish = irand(0, 8) // how squishy is the guitar pick?
pan = random()
STRUM2(start, dur, amp, freq, squish, decay_time, pan)
increment = base_increment + irand(-0.01, 0.01)
}
The score begins with unison pitch (around G4), but widely detuned. Then after
ten seconds, the tessitura rapidly rises, due to maxfreq increasing.
This score shows how to randomize the time between successive notes (increment). Most of the other parameters are randomized as well.
Here are a few other instruments to explore (see docs at rtcmix.org).
COMBIT MBLOWBOTL REVMIX DELAY MBANDEDWG STRUMFB FILTERBANK MSAXOFONY SYNC FREEVERB MULTEQ VWAVE GRANULATE NOISE WAVY
rtsetparams(44100, 2)
load("WAVETABLE")
load("DELAY")
bus_config("WAVETABLE", "aux 0-1 out") // send WAVETABLE to stereo bus
bus_config("DELAY", "aux 0-1 in", "out 0-1") // read bus into DELAY, then out
total_dur = 10
dur = 0.1
increment = 0.6
amp = 15000
env = maketable("line", 2000, 0,0, 1,1, 10,1, 30,0)
for (start = 0; start < total_dur; start = start + increment) {
freq = irand(120, 2500)
WAVETABLE(start, dur, amp * env, freq, pan = random())
}
deltime = 0.2
feedback = 0.6
ringdur = 2.8 // seconds to ring out delay line after note is finished
DELAY(start=0, inskip=0, total_dur, amp=1, deltime, feedback, ringdur,
inchan=0, pan=1)
deltime += 0.02 // shorthand for "deltime = deltime + 0.02"
DELAY(start=0, inskip=0, total_dur, amp=1, deltime, feedback, ringdur,
inchan=1, pan=0)
Many instruments that take input accept only mono input. These instruments
have an inchan argument that lets you tell it which channel to read.
Since we want to retain the random stereo panning done in the WAVETABLE
loop, we need to read each WAVETABLE output channel into its own
mono-input DELAY instrument.
Consider the following score.
rtsetparams(44100, 5) // Note: 5 output channels
load("WAVETABLE")
total_dur = 20
dur = 0.1
increment = 0.16
minfreq = 100
maxfreq = 1500
minamp = 50 // in dB
maxamp = 90
env = maketable("curve", 5000, 0,0,1, 1,1,-8, 40,0)
wavet = maketable("wave", 2000, 1, .3, .1)
control_rate(44100)
for (start = 0; start < total_dur; start = start + increment) {
amp = ampdb(irand(minamp, maxamp))
freq = irand(minfreq, maxfreq)
chan = pickrand(0, 1, 2, 3, 4)
bus_config("WAVETABLE", "out " + chan)
WAVETABLE(start, dur, amp * env, freq, pan=0, wavet)
}
By changing the bus configuration before every note, we can direct notes to
speakers randomly on a per-note basis. The pickrand function chooses
randomly from the list of numbers you give it. (These numbers do not have to be
consecutive or in any particular order, so you can address just channels 1, 2,
and 4, if you wish.) Then we construct a bus string by appending the channel
number to the “out ” string, using a plus sign. So, for example,
the bus_config function will see “out 2” at some point. We
have to give a value for pan in the WAVETABLE call, even though
the instrument is using mono output, because we’re also using the
optional wavet argument, and this must be the sixth argument.
By default, RTcmix uses “out 0-1” for instruments in stereo-output scores. But you can override this with a bus_config statement.
bus_config("STRUM2", "out 2-3")
STRUM2(start, dur, amp, freq, squish, decay, pan)
This sends STRUM2 output to the third and fourth speakers.
RTcmix supports several different pitch representations: octave-point-pitch-class (oct.pc or pch), frequency in Hz (cps), linear octaves (oct), letter names, and MIDI note numbers. Instruments expect pitch to be specified in a particular way, using one or two of the representations above. For example, WAVETABLE understands pch or cps; TRANS understands only pch. No instrument understands letter names or MIDI note numbers, so these must be converted to another form prior to calling the instrument. There are many pitch conversion functions that do this: cpspch, pchcps, octcps, pchmidi, pchlet, etc.
The tutorial explains the hazard of doing arithmetic — specifically subtraction — on oct.pc: subtracting a major second (.02) from 8.01 gives 7.99, a very high pitch in oct.pc, instead of 7.11. The pchadd function is the safe way to perform arithmetic on oct.pc values: pchadd(8.01, -.02) gives the right answer.
If it weren’t for the pchadd function, we would need to use the pitch representation known as “linear octaves” (“oct”). But even with pchadd, it’s still useful to understand how linear octaves work. As with oct.pc, a linear octave is a decimal number that has the octave number to the left of the decimal point. But to the right of the decimal point is a fraction of the intervallic distance between the given octave number and the octave above. For example, 8.00 is middle C, and 8.5 is halfway between middle C and the C above — so it’s an F#. Because linear octaves use the first two numbers after the decimal point as 10ths and 100ths of an octave — rather than as a representation of semitones — you can perform arithmetic safely using this pitch format. Unfortunately, linear octaves aren’t as easy for us to translate at sight into familiar pitch names. We can see that 8.07 is 7 semitones above middle C, but its linear octave equivalent, 8.58333, looks more obscure.
To get pitch into and out of linear octave format, use pitch conversion functions, such as octpch and pchoct. Without pchadd, you would need to convert oct.pc to linear octaves, do the arithmetic, and then convert the result back to oct.pc. (This is exactly what pchadd does internally.)
linoct1 = octpch(8.01) linoct2 = octpch(0.02) pitch = pchoct(linoct1 - linoct2) // pitch is now 7.11 in oct.pcUnfortunately, the conversion functions cited above cannot help us when we use a table — or other source of changing values (e.g., MIDI, OSC, random, or LFO streams) — to specify dynamic pitch changes in oct.pc. The conversion functions operate on a single value, instead of a list or stream of values. Instead, we use a different function, makeconverter.
// gliss table with values expressed in linear octaves (gliss down a M2)
gliss = maketable("line", "nonorm", 1000, 0,0, 1,octpch(-.02))
// initial pitch in linear octave format
init_pitch = octpch(8.01)
// do the arithmetic in linear octaves, adding gliss line to constant pitch
pitch_linoct = init_pitch + gliss
// convert resulting stream of linear octave values to oct.pc for instrument
pitch = makeconverter(pitch_linoct, "pchoct")
WAVETABLE(start, dur, amp, pitch, pan)
The makeconverter function is a kind of filter that takes a stream of
values in one format and converts them to a stream of values in another format.
All of the single-value conversion functions (e.g., cpspch, ampdb, etc.) have
makeconverter analogs.
A simpler solution to the gliss problem above would be to work only with frequency in Hz, but then it’s harder to make a glissando that moves evenly across a specific musical interval.
There is a whole system of control-stream processing — operations on streams of data, such as tables, random and low-frequency oscillators — that work on the model illustrated above for makeconverter. For example, you can take real-time MIDI input, scale that to a range of linear octave values, add offsets from a random number generator, convert to oct.pc, and pass the resulting stream of pitch data to a single WAVETABLE note.
peakamp = 20000
note_env = maketable("line", 1000, 0,0, 1,1, 2,1, 3,0)
total_env = maketable("line", table_length = 1000, 0,1, 1,0, 2,1)
for (start = 0; start < totdur; start += incr) {
freq = irand(261, 440)
pan = irand(0, 1)
index = (start / totdur) * table_length
thisamp = samptable(total_env, index)
WAVETABLE(start, dur, thisamp * peakamp * note_env, freq, pan)
}
The samptable function takes a sample of a table at a particular index.
The index can be a decimal number, in which case samptable interpolates between
two adjacent table values. The trick is to calculate the index as a percentage
of the table length. For example, if the table has 1000 values, and our loop
is halfway through the total duration spanning all notes, then we want the
table value that is at index 500 — or 50% of the way through the table.
The expression (start / totdur) gives us a “percentage,”
between 0 and 1, that locates the current start time within the total duration.
We multiply this expression by the table length to get the index for use by
samptable.
The result, in this case, is that the series of notes begins at full volume, diminuendos to silence, and then crescendos to full volume at the end. (It would be better to use decibels or a curve table, instead of straight line segments and linear amplitude.)
totbeats = 50
tempo(0, 200) // at beat 0, set tempo to 200 bpm
beat_incr = 0.5
dur = 0.1
control_rate(10000)
amp = 20000 * maketable("line", 1000, 0,0, 1,1, 4,0)
for (beat = 0; beat < totbeats; beat += beat_incr) {
start = tb(beat) // return time value (seconds) for beat
freq = irand(100, 1200)
pan = irand(0, 1)
WAVETABLE(start, dur, amp, freq, pan)
}
The tempo function sets tempo in terms of beat, bpm pairs. Above, we
just set a static tempo, but you can create a tempo curve.
tempo(0,200, 5,200, 10,500)This tempo would start at 200 bpm, begin an accellerando after five beats, and end with a tempo of 500 bpm. You can also have instantaneous tempo changes by giving two beat, bpm pairs for the same beat.
tempo(0,90, 8,90, 8,180, 20,180, 20,90)This tempo suddenly enters double time at the eighth beat, and returns abruptly to the initial tempo after twelve beats of double time.
Use the tb function to retrieve a time value from a given beat value. (Read tb as “time from beat.”) To make use of the tempo information, we construct our loop in terms of beats, rather than seconds, converting from the current beat to its value in seconds before passing this to the instrument. You might also want to express duration in terms of beats.
As in a typical hardware mixer, you connect instruments using buses. In RTcmix, these are called “aux,” although the comparison with aux buses in a mixer is somewhat misleading. You set up the connections using the bus_config function before calling the instruments.
bus_config("WAVETABLE", "aux 0-1 out")
bus_config("FREEVERB", "aux 0-1 in", "out 0-1")
This routes stereo output from WAVETABLE into FREEVERB’s
stereo input; FREEVERB sends its output to the outside world.
The next (incomplete) score example illustrates a few more wrinkles.
bus_config("STEREO", "in 0", "aux 0-1 out")
bus_config("DELAY", "aux 0-1 in", "out 0-1")
incr = 0.5
dur = incr * 2
for (start = 0; start < totdur; start += incr) {
STEREO(start, inskip, dur, amp, pan)
}
inskip = 0 // inskip must be zero for aux bus readers
DELAY(0, inskip, totdur, amp, deltime, fdbck, rngdur, inchan=0, pan=1)
DELAY(0, inskip, totdur, amp, deltime, fdbck, rngdur, inchan=1, pan=0)
Here are some crucial things to keep in mind while working with buses.
You can have a chain comprising more than one processing instrument. (See “docs/sample_scores/longchain.sco” in the rtcmix application folder.) Read more about the RTcmix bus scheme on the bus_config help page at rtcmix.org.