Table of Contents

  1. Running Scores
  2. Writing Scores
  3. Playing Multiple Notes
  4. Envelope Tables
  5. Looping and Randomness
  6. Making a Glissando
  7. Specifying Waveforms
  8. Pitch Specification
  9. Pitch Computation
  10. Random Seeds and Conditionals
  11. Lists
  12. Sound File Input
  13. Recording Your Sound
  14. Other Instruments
  15. Audio Buses
  16. Moving Beyond Stereo
  17. Where to Go from Here
  18. Appendix: Code Indentation
  19. Appendix: Tables Across Notes
  20. Appendix: Tempo Changes
  21. Appendix: More About Audio Buses
  22. Appendix: More About Pitch Computation

RTcmix Tutorial

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.

Running Scores

First we’ll learn how to view and run RTcmix scores within RTcmixShell.

  1. Download RTcmix examples. If this ZIP file didn’t already unpack into a folder, double-click it to make that happen. Put this folder whereever you wish. The scores are in the “sco” subfolder. The “snd” folder contains sound files used by some of the scores.
  2. Launch the RTcmixShell application.
  3. 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.

  4. 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.
  5. 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.

Writing Scores

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.

Playing Multiple Notes

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.

Envelope Tables

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.

Looping and Randomness

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:

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.

  1. set start to 0
  2. make sure that the value of start is less than 10 (start < 10)
  3. if it’s not, then exit the loop
  4. perform the block of statements following the open curly brace
  5. set start to the current value of start plus 1 (start = start + 1)
  6. make sure that start is less than 10
  7. if it’s not, then exit the loop
  8. perform the block of statements following the open curly brace
  9. increase the value of start by one again
  10. make sure that start is less than 10
  11. perform the block of statements following the open curly brace
  12. keep doing this until the value of start is no longer less than 10
  13. exit the loop

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.)

Making a Glissando

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.

Specifying Waveforms

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:

  1. give the name of a common waveform, such as “square,” “saw” or “buzz”; or
  2. give a numerical specification of the amplitudes of harmonic partials.

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.

Pitch Specification

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.

Pitch Computation

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.

Random Seeds and Conditionals

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.

Lists

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!

Sound File Input

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
   }

Recording Your Sound

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.

Other Instruments

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

Audio Buses

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.)

Moving Beyond Stereo

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.

Where to Go from Here

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.


Appendix

Here are some more advanced RTcmix topics.

Code Indentation

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 Across Notes

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.)

Tempo Changes

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.

More About Audio Buses

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.

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.

More About Pitch Computation

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.

Copyright ©2009-2017 John Gibson