RTcmix is a text-based program that generates or processes sound, usually in real time. You write a script (or score, as we usually call it) in a simple programming language, called “Minc,” and feed that to RTcmix, which then makes sound and/or writes a sound file. (“Minc” stands for “Minc is not C” and is pronounced either like the semiaquatic mammal, or as “min-C” — because Minc is a minimal subset of the C programming language.)
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 typical DAWs, with their timelines, virtual instruments, and effect plug-ins. RTcmix opens up a number of sonic possibilities and compositional ideas that would not otherwise be available.
RTcmix comes in two forms: (1) the original command-line program that you use in a Unix shell (such as bash or zsh, in the Terminal program in macOS), and (2) various applications that combine a text editor with the Minc language and the RTcmix sound engine, letting you run scores easily without having to know shell commands. This tutorial is focused on using RTcmix within the RTcmixShell application, which runs on macOS and Windows.
The source code for RTcmix (written in C and C++) is free and available on GitHub. Documentation is at rtcmix.org.
This tutorial is designed to get you started running and writing RTcmix scores.
The tutorial assumes you have downloaded and installed RTcmixShell. The tutorial also assumes that you have no prior programming experience.
You may find the RTcmix Quick Reference useful.
First we’ll learn how to view and run RTcmix scores within RTcmixShell.
Download RTcmix examples. If this ZIP file didn’t already unpack into a folder, double-click it to make that happen. Put this folder wherever you wish. The scores are in the “sco” subfolder. The “snd” folder contains sound files used by some of the scores.
Launch the RTcmixShell application.
If you’re using anything other than built-in audio, choose your audio interface using the Preferences menu command. Set the sampling rate to 48000 Hz and the number of output channels at 2.
CAUTION: When you’re running RTcmix scores, especially ones that you’re working on, you should turn your monitoring volume down. 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.
Find “fmalias.sco” in the “sco” folder, and drag it into the empty RTcmixShell window to see it. Okay, it looks like gibberish. The next section helps you understand scores like this.
Press the Play button. You should hear sound and see some stuff printed to the lower part of the window, beneath the movable divider.
If there were any errors that prevented the score from running, you will see them printed at the bottom of the window, in the area that is below the movable horizontal divider line. These error messages often contain an approximate line number for the error. Turn on line numbers (Edit > Show Line Numbers) to help you find the error.
NOTE: It is strongly recommended that you type in all the example code snippets, as this will help you learn the language faster.
Here is a very simple, and very boring, RTcmix score.
WAVETABLE(0, 10, 10000, 440, 0.5)
Press Play to hear a beautiful sine wave at A440 for ten seconds.
The score consists of a single function call. 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.
WAVETABLE is the name of an instrument that performs wavetable synthesis. An instrument is a piece of software that makes sound. Instrument names are always in upper case.
WAVETABLE 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 in place of the values, and their values can be modified in the score. For example, you could have...
homer = 10
marge = 20
bart = homer + marge
print(bart)
You will need to stop playback of this score manually by pressing the Stop button in RTcmixShell, because the score isn’t playing any sound. A sound-making score will stop running by itself when the sound is over.
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 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.
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.
What if you want to play more than one note? Just use WAVETABLE more than once. Play this score. You don’t have to worry about overlapping notes (as you would in Max) — a note will not cut off the previous one. In other words, RTcmix has implicit polyphony. You don’t have to think about it.
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 sound the same.
There’s one major problem with our score: it clicks. In DAW software we use fades to smooth out click-producing discontinuities at the boundaries of a waveform region. We do this in RTcmix by using an amplitude envelope table.
This note clicks, especially at its end:
dur = 8
amp = 8000
freq = 440
WAVETABLE(start=0, dur, amp, freq, pan=0.5)
So instead of using a simple constant value for the peak amplitude of the note, we create a curve for the peak amplitude to follow. It starts at zero, grows to the maximum, and then returns to zero.
env = maketable("line", size=1000, 0,0, 1,1, 9,1, 10,0)
WAVETABLE(0, dur, amp * env, freq, pan=0.5)
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 (the 8000 stored in
the variable amp
) as the amplitude for the note, we multiply that
number by the changing numbers stored in the table (amp * env
).
By default, these numbers are between 0 and 1, so they act as a variable
volume control, scaling between 0 and 8000 the peak amplitude value that
WAVETABLE sees while the note is playing.
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. For example...
env = maketable("line", size=1000, 0,0, 2,1, 9,1, 10,0)
We have 1000 numbers in the table. They start at 0 and increase to 1 over the first 2 seconds. 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.
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 powerful construction for algorithmic composition. Play this score.
amp = 8000
env = maketable("line", 1000, 0,0, 1,1, 9,1, 10,0)
start = 0
while (start < 10) {
freq = irand(100, 2000)
pan = irand(0.2, 0.8)
WAVETABLE(start, dur=1, amp * env, freq, pan)
start = start + 1
}
Although we hear ten notes, one after another, there is only one
WAVETABLE call in the score. But it’s embedded within a
loop, which repeats the WAVETABLE note several times, along with
the statements that randomly set freq
and pan
. All
this we enclose in curly braces ('{' and '}').
The while loop needs several parts to function:
start
),start = 0
),start = start + 1
), andstart < 10
).
We use one variable — in this case, start
— to
control the loop, starting from an initial value (0
). This value
increases, by whatever amount we want, at the bottom of the loop. We count
until the resulting value no longer satisfies the end condition, which appears
in parentheses right after the word, “while.” Then the loop ends.
Though it’s not necessary, you should indent the block of statements within the braces to make the loop easier to read.
What would happen if you omitted the end condition?
You would get an infinite loop: a loop that never stops cyling. This causes the program to hang, with the familiar spinning beachball as the symptom.
For people who haven’t programmed in C before, here is a step-by-step description of what the “while” section is asking RTcmix to do.
start
to 0
start
is less than 10
(start < 10
)
start
to the current value of start
plus 1
(start = start + 1
)
start
is less than 10
start
by one again
start
is less than 10
start
is no longer less
than 10
Computers are really dumb. They will do this sort of thing all day if you let them. That’s why we give them the drudge work: we can make RTcmix play thousands of notes, and we only have to type the WAVETABLE line once.
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. (“irand” stands for “interpolated random.”)
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
. (irand returns floating-point numbers, potentially
with numbers to the right of the decimal point.)
We can use the same type of table we used for the peak amplitude envelope to create a glissando. Play this score, which uses a glissando table.
env = maketable("line", size=1000, 0,0, 1,1, 9,1, 10,0)
gliss = maketable("line", "nonorm", size, 0,1, 1,2)
start = 0
while (start < 10) {
freq = irand(100, 2000)
pan = irand(0.2, 0.8)
WAVETABLE(start, dur=2, 8000 * env, freq * gliss, pan)
start = start + 1
}
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, which is the default behavior.
We’ve been playing sine waves. To get other waveforms, you need to provide the WAVETABLE instrument with a particular wavetable, created by maketable. You supply this table as the seventh argument to the instrument, following the pan argument. Play this score.
dur = 2
freq = 300
env = maketable("line", 1000, 0,0, 1,1, 3,1, 4,0)
pan = 0.5
wavetable = maketable("wave", 1000, "square")
WAVETABLE(start=0, dur, 10000 * env, freq, pan, wavetable)
wavetable = maketable("wave", 1000, "buzz")
WAVETABLE(start=2, dur, 10000 * env, freq, pan, wavetable)
wavetable = maketable("wave", 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:
Our third note uses the numeric option. Following the table size, there can be any number of arguments, which specify the amplitudes (from 0 to 1) of successively higher harmonic partials: the first is the fundamental, the second is the second harmonic partial, the third is the third harmonic partial, etc. Our waveform has the fundamental at full strength, the third partial at .5, the fifth partial at .3, the seventh partial at .1, the tenth partial at .7, and all the other partials at zero. If you want non-harmonic partials, use the “wave3” type of maketable, which lets you specify a partial using a decimal point (e.g., partial 2.1, for something a little higher than the second harmonic partial), and a specific amplitude and phase. Or simply play several WAVETABLE notes at the same time, with the right frequencies.
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.
total_dur = 10
dur = 0.08
env = maketable("line", 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)
start = 0
while (start < total_dur) {
p1 = random()
p2 = random()
p3 = random()
p4 = random()
p5 = random()
p6 = random()
p7 = random()
p8 = random()
wavetable = maketable("wave", 2000, p1, p2, p3, p4, p5, p6, p7, p8)
pan = irand(0, 1)
WAVETABLE(start, dur, amp * env, freq, pan, wavetable)
start = start + increment
}
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.
We’ve been specifying the pitch of WAVETABLE notes as frequencies in Herz. There are other ways. The traditional RTcmix way is to use octave-point-pitch-class notation. It works like this: you give a decimal number, where the number to the left of the decimal point indicates the octave, and the number to the right of the decimal point gives semitones. Middle C is 8.00. The D above that is 8.02. The B above that is 8.11. The C above that is either 8.12 or 9.00. You can be more precise: 8.015 is a quarter tone above the C# above middle C.
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.
The advantages of octave-point-pitch-class are that it's easy to read and some instruments accept it as a replacement for frequency. But because of trouble that arises when computing pitches using formulas (discussed below), sometimes MIDI note numbers are a better way to go. These are not so easy to relate to 12-tone equal-tempered pitches at a glance — quick, what’s the letter name for MIDI note number 77? — but there's no problem performing calculations with them. Just as with oct.pc, MIDI note numbers in RTcmix can have precision to the right of the decimal point, even though this is not part of the MIDI specification. So 60.5 is a quarter tone higher than middle C.
Sometimes it’s useful to convert octave-point-pitch-class or MIDI note numbers to frequency in Herz, or vice versa. In fact, you must do this with MIDI note numbers before feeding them as frequency values to instruments, because no instruments understand MIDI note numbers. 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 = cpsmidi(60) // convert MIDI note number to Hz
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.”) That puts the left format closer to the variable name that will hold the result of the conversion.
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 using oct.pc, 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, not the one we expect, which would be 7.11. (Keep in mind that 8.12 in oct.pc is an octave above 8.00, and 8.24 is two octaves above 8.00. So 7.99 is more than 8 octaves above the answer we want.)
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).
Or you could just use MIDI note numbers and not worry about this problem.
Here’s a score that uses pitch computation; the melodic line goes up a minor third or down a major second, depending on a random number.
env = maketable("line", 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
start = 0
while (start < 15) {
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)
start = start + increment
}
We start with a particular pitch: D below middle C (7.02). 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 (fred < 0.5) {
barney = 1.3
}
else {
barney = -99
}
If our test succeeds (the value of fred
is less than 0.5), then
RTcmix performs whatever is within the next set of curly braces (barney
= 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 to
The 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.
Sometimes random pitches are just too ... random. What if we want randomly selected pitches, but we want them to come from a specific scale, rather than from thin air? This is where lists come in handy. A list is just an ordered sequence of things, like your shopping list. (You’ll sometimes hear lists referred to as arrays in Minc, because that is the C language structure that they resemble.) Here’s a list of pitches...
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. List indices start from zero and go up to one less than the number of things in the list.
anote = notes[2]
anothernote = notes[7]
Now anote
is 8.04 and anothernote
is 9.00. You can
assign to a list in a similar way.
notes[2] = 8.03
notes[5] = 8.08
This turns the major scale into a harmonic minor scale. Notice that you use curly braces — { } — when defining the list and square brackets — [ ] — when accessing the list 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 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.
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 list
start = 0
while (start < 20) {
index = trand(numnotes) // get random integer for index
lettername = row[index] // access pitch list 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)
start = start + increment
}
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 list. (We got this earlier using the
len function.) We access the row
list at that index, and
then convert the result into oct.pc notation for use in WAVETABLE.
(Yes, trand might occasionally return an index that is one past the
right end of the list. Minc lists deliver the last list location in that
case.)
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!
RTcmix is not limited to synthesis. You can also read sound files into your score. The two most basic instruments for playing sound files are STEREO, which simply plays the file into two output channels, and TRANS, which lets you transpose the sound.
But first you need to tell RTcmix which sound file to open. Here’s a score that plays a sound file several times, using various offsets into the file.
rtinput("/Users/yourname/yoursoundfolder/yoursound.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. You give it, as an argument, a double-quoted string that is the full or relative path name for the sound file. A full path name starts from the top of the disk hierarchy, represented by the initial slash, and ends with the file name. The rest of the path probably will consist of folders in your home directory. On macOS, your home directory normally is “/Users/yourname/”. On Windows, it normally is “C:\Users\yourname\”, though RTcmixShell understands '/' as a path delimiter character there. (“Directory” and “folder” are synonymous.)
RTcmixShell lets you drag a WAVE or AIFF sound file into the edit window. When you drop it, the full path name of the file appears at the insertion point. This is a quick way to construct an rtinput line.
The best way to organize your RTcmix projects is to make a folder — let’s call this the “project” folder — that contains two sub-folders: one for your scores, and one for your sound files. Avoid using spaces and weird characters in your file and folder names when working with RTcmix. ('-' and '_' are good ways to separate parts of a name: e.g., “two-dogs-barking.wav”.)
Then refer to a file in your sounds folder by its relative path name. This kind of name starts from the directory the file is in, instead of from the top-level (or “root”) directory. The directory component “../” means to go up one level in the folder hierarchy. So if you have “scores” and “sounds” folders inside the same folder, one of your scores can refer to a sound this way:
rtinput("../sounds/mysound.wav")
This method has two advantages: (1) relative path names often are shorter than full path names, and (2) you can move the project folder to anywhere on your disk, or to another computer, and the relative path will still work.
The sampling rate of the file you give to rtinput must match the sampling rate you use to configure RTcmix. (In RTcmixShell, this is in the Audio pane of the Preferences dialog.)
With these preliminaries out of the way, let’s return to the score.
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.
CAUTION: This is a place where we can make a dreadful mistake: passing 20000 as the amplitude for a sound-input instrument, because we’re used to using peak amplitudes in the 0-32767 range as the amp argument for synthesis instruments like WAVETABLE. What will happen to the amplitude of a STEREO note if we do that?
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.
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)
start = 0
while (start < 10) {
inskip = irand(0, filedur - notedur)
STEREO(start, inskip, notedur, env, pan=random())
start = start + 0.15
}
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 that modifies the previous one to randomly transpose snippets.
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)
start = 0
while (start < 10) {
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())
start = start + 0.15
}
At some point we’d like to capture the audio we’re hearing into a sound file, for use in a DAW or elsewhere. In RTcmixShell you just press the Record button. This will give you a dialog for specifying the output sound file name; the default is to create a .wav file. The file will be a 32-bit floating point file, with values in the range -1 to 1.
After you dismiss the dialog, the score will begin playing, and its sound will be recorded into the file you specified. Playback and recording stop automatically when the score is over.
There are many other instruments to use in RTcmix. Here are two more: FMINST and STRUM2. FMINST is a simple two-operator FM synthesis instrument. It works similarly to WAVETABLE, except that you have to give additional arguments for the modulator frequency and the modulation index. The latter is kind of complicated: you set a minimum and maximum modulation index, and then make a table, the index guide, that describes the shape used to traverse between these extreme values. Here’s a sample score.
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.
dur = 1.2
decay_time = dur * 0.4
base_increment = 0.16
minfreq = 400
maxfreq = 435
increment = base_increment
start = 0
while (start < 16) {
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)
start = start + increment
}
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
), a thing we were wondering about earlier. Most of the
other parameters are randomized as well.
Here are a few other instruments to explore (see docs at rtcmix.org).
COMBIT MBANDEDWG REVMIX
DELAY MSAXOFONY STRUMFB
FILTERBANK MULTEQ SYNC
FREEVERB MULTIFM VWAVE
MBLOWBOTL NOISE WAVY
Often we want to process the sound of one instrument using another. RTcmix
implements a bus connection scheme that lets you route audio within a score.
You set this up using two or more calls to the bus_config function. The
following score plays WAVETABLE notes and runs them through a stereo
delay processor. The DELAY instrument plays one “note,”
which spans the total duration of the WAVETABLE phrase. An instrument
accepting input from a bus, such as DELAY below, must have an
inskip
of zero. Note that buses are numbered starting from zero.
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)
start = 0
while (start < total_dur) {
freq = irand(120, 2500)
WAVETABLE(start, dur, amp * env, freq, pan = random())
start = start + increment
}
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. (This is known as dual
mono effects processing.)
The bus scheme also lets us address more than two channels. It’s easy to write a score that flings out bits of sound to many speakers, treated as point sources (meaning that sound goes only to one speaker, not panned across multiple speakers).
Consider the following score.
// Note: 5 output channels used below
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(48000)
start = 0
while (start < total_dur) {
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)
start = start + increment
}
NOTE: This score will work only if you set the number of output channels in the RTcmixShell Audio preferences to 5. Your audio interface must be able to support this many channels.
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.
There’s a lot more to learn about RTcmix. The best place to turn is the documentation at rtcmix.org and example scores, including the collection mentioned above.
Here are some more advanced RTcmix topics.
Although RTcmix doesn’t care how you indent your code — how many spaces or tabs or line breaks you use — you should get in the habit of neatly indenting, so that you and others can read it more easily. If your code indentation gets messed up, you can try pasting it into this code beautifier to fix it. There are many such “pretty printer” programs. Indenting for the C programming language will get you close to a good result for Minc code.
Tables created with maketable normally are used to control some parameter that changes during the course of a single note. We’ve been using amplitude envelope tables in this way. But what if you have a long series of notes, and you want to make continuous dynamic changes across the entire duration of the series? Here is a technique you can use. Of course, you can adapt this technique to control any parameter, even ones that cannot change during the course of a note (such as its start time).
peakamp = 20000
note_env = maketable("line", 1000, 0,0, 1,1, 2,1, 3,0)
total_env = maketable("line", size=1000, 0,1, 1,0, 2,1)
start = 0
while (start < totdur) {
freq = irand(261, 440)
pan = irand(0, 1)
index = (start / totdur) * size
thisamp = samptable(total_env, index)
WAVETABLE(start, dur, thisamp * peakamp * note_env, freq, pan)
start += incr
}
(You will have to supply some missing variable assignments to make this score work. Look at the log at the bottom of the RTcmixShell window for hints after you try running the score.)
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 (stored in the size
variable in
this example). 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.)
We’ve been writing all our timing information in terms of seconds. It would be nice to have the option of working in beats, with the ability to change tempos. This requires a few extra steps and a few new functions.
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)
beat = 0
while (beat < totbeats) {
start = tb(beat) // return time value (seconds) for beat
freq = irand(100, 1200)
pan = irand(0, 1)
WAVETABLE(start, dur, amp, freq, pan)
beat += beat_incr
}
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 accelerando after five beats, and end with a tempo of 500 bpm. You also can 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.
You can process the audio output of one instrument by another. Of course, the second instrument must be one that is capable of processing audio, such as DELAY, MULTEQ, or FREEVERB. Moreover, it must be an instrument that does not require future input to process a current sample. (In DSP lingo, it requires that the instrument be a causal filter.) This requirement rules out TRANS, since, for any upward transposition, it consumes more than one input sample for every output sample. Most other instruments that have an inskip or input start time parameter after the start time will work.
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
start = 0
while (start < totdur) {
STEREO(start, inskip, dur, amp, pan)
start += incr
}
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.
An instrument that takes input from the outside world, either from a real-time input (e.g., mic) or from a sound file, uses “in 0” or “in 0-1” (etc.) as its source in the bus_config call. Confusingly, the channel range given is ignored for file input, and all channels of the sound file are available.
Most processing instruments take mono input only, so you call them twice, once for each input channel, as shown above for DELAY. (Remember, in our upside-down RTcmix world, “pan=1” means to pan to the left.)
inskip must be zero for any instrument that reads from an aux bus. This just means that the instrument reads sound starting now, the time given by the start time argument of the aux bus-reading instrument, which does not have to be zero.
Each STEREO note does not have its own dedicated DELAY; instead, the entire stream of overlapping STEREO notes enters DELAY, just as it would on a hardware mixer. (If you want each STEREO note to have its own DELAY, check out the CHAIN instrument and the makeinstrument function.)
You can simulate a series of insert effects in a DAW by using more than one
processing instrument connected by bus_config
calls. (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.
One of the more confusing things in RTcmix is the business of pitch computation and dynamic pitch changes (while an instrument plays a note — e.g., glissando).
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 functions that let you convert between pitch representations: 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”). The linear octave representation is a simple idea — represent the interval of an octave linearly between one middle-C integer and the next higher one. For example, 8.5 is the F# above middle C, because half of an octave is 6 semitones. But linear octaves are hard to read: we can see that 8.07 in oct.pc means 7 semitones above middle C, but its linear octave equivalent, 8.58333, looks more obscure.
A useful compromise between ease of computation and readability is the MIDI note number representation. It can be better to work in MIDI note numbers, and then convert to oct.pc or Hz, as needed. MIDI note numbers in RTcmix can have a decimal component. For example, 60.5 is halfway between middle C and the semitone above it. To get pitch into and out of MIDI note number format, use pitch conversion functions, such as midipch, pchmidi, midicps, and cpsmidi.Unfortunately, these conversion functions cannot help us when we use a table — or other source of changing values (e.g., 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 MIDI note numbers (gliss down a M2)
pitch = maketable("line", "nonorm", 1000, 0,60, 1,58)
// convert resulting stream of MIDI values to oct.pc for instrument
pitch = makeconverter(pitch, "pchmidi")
WAVETABLE(start=0, dur=4, amp=5000, pitch, pan=.5)
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.
NOTE: Do not use a MIDI note number to represent a semitone difference, such as what you would use to specify transposition. Using pchmidi to convert from 2 MIDI semitones to 0.02 in oct.pc will not work. Use linear octaves to express transposition curves instead.
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 OSC input, scale that to a range of MIDI note numbers, add offsets from a random number generator, convert to oct.pc, and pass the resulting stream of pitch data to a single WAVETABLE note.