Showing posts with label synthdef. Show all posts
Showing posts with label synthdef. Show all posts

Sunday, October 03, 2010

Working with Buffers

Let's say you want to open a short audio file and play it back. You can do this with a Buffer.

 s.boot;
 b = Buffer.read(s, "sounds/a11wlk01.wav");

Don't run both of those lines at once. Wait for the server to finish booting before reading the Buffer. The reason you need to wit is because the Buffer actually lives with the server. The server plays the audio, so the server needs to have the audio in it's memory.

The first argument, is therefore the server that will hold the buffer. The second is the path to the buffer on your computer.

Because the Buffer lives with the server, the command to read it is asynchronous. We tell the server to load the Buffer and then carry on, without waiting to hear back if it's been loaded or not. For large files, it may take a few moments for it to fully load. One way to deal with tis is to load buffers into global variables (like b, above) and then manually wait to evaluate the next section.

PlayBuf

To play the Buffer, you can use the UGen PlayBuf:

(
 SynthDef(\playBuf, {| out = 0, bufnum = 0 |
  var scaledRate, player;
  scaledRate = BufRateScale.kr(bufnum);
  player = PlayBuf.ar(1, bufnum, scaledRate, doneAction:2);
  Out.ar(out, player)
 }).play(s, [\out, 0, \bufnum, b.bufnum]);
)

When Buffers are allocated on the server, they're given a unique ID number, so we can reference them later. This number is called the bufnum. It's the second argument to the SynthDef. This way, we know which buffer we're supposed to play.

Some Buffers may have unusual sample rates or otherwise be unusual. To make sure that they play at the speed we expect, we use a UGen to scale the rate. If we wanted to play at half speed, we could change the SynthDef:

(
 SynthDef(\playBuf, {| out = 0, bufnum = 0, rate = 1 |
  var scaledRate, player;
  scaledRate = rate * BufRateScale.kr(bufnum);
  player = PlayBuf.ar(1, bufnum, scaledRate, doneAction:2);
  Out.ar(out, player)
 }).play(s, [\out, 0, \bufnum, b.bufnum, \rate, 0.5]);
)

Note that when we play at half speed, it takes twice as long to play the sample and pitches are all an octave lower. This is just like working with magnetic tape. If we change the rate to 2, it will play back at half the speed of the original and be an octave higher. If you try a rate of -1, it will play backwards at the normal rate.

The PlayBuf UGen is the one that actually plays the buffer. The first argument is the number of channels. Our buffer is mono, so that number is 1. If it were stereo, we would change it to 2. You cannot pass in this number as an argument or change it while the Synth is running. The SynthDef must be defined with the right number of channels. If you give a stereo buffer to a mono PlayBuf, or vice versa, it will not play. If you have both mono and stereo buffers in one piece, you will need to have two SynthDefs:

(
 SynthDef(\playBufMono, {| out = 0, bufnum = 0, rate = 1 |
  var scaledRate, player;
  scaledRate = rate * BufRateScale.kr(bufnum);
  player = PlayBuf.ar(1, bufnum, scaledRate, doneAction:2);
  Out.ar(out, player)
 }).add;

 SynthDef(\playBufStereo, {| out = 0, bufnum = 0, rate = 1 |
  var scaledRate, player;
  scaledRate = rate * BufRateScale.kr(bufnum);
  player = PlayBuf.ar(1, bufnum, scaledRate, doneAction:2);
  Out.ar(out, player)
 }).add;
)

PlayBuf can handle N-channel files, so if you have some three channel or 8 channel files, you can also play those if you define synthdefs with the right number of channels.

To figure out which SynthDef to use, you can send the message numChannels to a buffer object:

(
 if((b.numChannels == 1), {
  Synth(\playBufMono, [\out, 0, \bufnum, b.bufnum, \rate, 1])
 }, {
  if((b.numChannels == 2), {
   Synth(\playBufStereo, [\out, 0, \bufnum, b.bufnum, \rate, 1])
  })
 });
)

The second argument to PlayBuf is the bufnum. This tells it which Buffer to play.

The third argument is the rate. As mentioned above, it's a good idea to scale this rate with BufRateScale.kr

Buffers, like envelopes, can have a doneAction. 2 means to deallocate the synth from the server when the Buffer stops playing.

If you want to loop your buffer, that's also a possible argument:

(
 SynthDef(\playBufMonoLoop, {| out = 0, bufnum = 0, rate = 1 |
  var scaledRate, player;
  scaledRate = rate * BufRateScale.kr(bufnum);
  player = PlayBuf.ar(1, bufnum, scaledRate, loop: 1, doneAction:2);
  Out.ar(out, player)
 }).play(s, [\out, 0, \bufnum, b.bufnum, \rate, 1]);
)

1 means do loop and 0 means don't loop. the default is 0. You can change the loop value while the Synth is running, so it's a good idea to still have a doneAction.

You can trigger a PlayBuf to jump back to the start:

(
 SynthDef(\playBufMonoStutter, {| out = 0, bufnum = 0, rate = 1 |
  var trigs, scaledRate, player;
  trigs = Dust.ar(2);
  scaledRate = rate * BufRateScale.kr(bufnum);
  player = PlayBuf.ar(1, bufnum, scaledRate, trigs, loop: 1, doneAction:2);
  Out.ar(out, player)
 }).play(s, [\out, 0, \bufnum, b.bufnum, \rate, 1]);
)

Dust.ar returns impulses at random intervals. It's argument is the average number of impulses per second.

You can also tell the PlayBuf to start someplace else than the first sample:

(
 SynthDef(\playBufMonoLoopStart, {| out = 0, bufnum = 0, rate = 1, startPos |
  var scaledRate, player;
  scaledRate = rate * BufRateScale.kr(bufnum);
  player = PlayBuf.ar(1, bufnum, scaledRate, startPos: startPos, loop: 1, doneAction:2);
  Out.ar(out, player)
 }).play(s, [\out, 0, \bufnum, b.bufnum, \rate, 1, \startPos, b.numFrames.rand]);
)

startPos is the number of the sample to start from. If you have a one second long buffer with 44100 samples in it, and you want to start in the middle, you would set use 22050 for the startPos.

Dialogs

If you don't want to have to pick an audio file ahead of time, you can use Buffer.loadDialog:


s.boot;
 
(
 SynthDef(\playBufMono, {| out = 0, bufnum = 0, rate = 1 |
  var scaledRate, player;
  scaledRate = rate * BufRateScale.kr(bufnum);
  player = PlayBuf.ar(1, bufnum, scaledRate, doneAction:2);
  Out.ar(out, player)
 }).add;

 SynthDef(\playBufStereo, {| out = 0, bufnum = 0, rate = 1 |
  var scaledRate, player;
  scaledRate = rate * BufRateScale.kr(bufnum);
  player = PlayBuf.ar(1, bufnum, scaledRate, doneAction:2);
  Out.ar(out, player)
 }).add;

 b = Buffer.loadDialog(s);
)

(
 if((b.numChannels == 1), {
  Synth(\playBufMono, [\out, 0, \bufnum, b.bufnum, \rate, 1])
 }, {
  if((b.numChannels == 2), {
   Synth(\playBufStereo, [\out, 0, \bufnum, b.bufnum, \rate, 1])
  })
 });
)

Note that you cannot load a compressed audio format like mp3 or ogg directly into a Buffer.

More on Buffers to come!

Summary

  • Buffers live on the server
  • Every Buffer has a unique bufnum
  • You can change the playback rate of a Buffer. This should be scaled with BufRateScale
  • SynthDefs must be sent to the server already knowing how many channels PlayBuf.ar will play.
  • PlayBuf.ar will optionally take a doneAction
  • You can loop a Buffer, stuter it and tell it where to start
  • You can open a Buffer with a Dialog box
  • You cannot read an mp3 or ogg directly into a Buffer

Wednesday, May 26, 2010

Envelopes

In the last post, we wrote a function to play overtones, but it played them all at the same time. In order to write a piece of music, we need both to be able to tell notes when to start and for how long they should last. This section will be on the latter part of the problem.

"Just like human beings, sounds are born, reach their prime and die; but the life of a sound, from creation to evanescence last only a few seconds" Yamaha GX-1 Guide

We control this process with a type of UGen called an envelope.

(
 SynthDef.new("example3", {arg out = 0, freq = 440, amp = 0.2,
       dur = 1;
  var sin, env_gen, env;
  
  env = Env.triangle(dur, amp);
  env_gen = EnvGen.kr(env);
  sin = SinOsc.ar(freq, mul: env_gen);
  Out.ar(out, sin);
 }).load(s);
)

Synth.new("example3");

There are a few things that make that SynthDef different. One is that it uses variables. Variables are a good way to keep SynthDefs more readable. The easier it is to read, the easier it is to understand, to fix and to change.

The other obvious change is the addition of an envelope. Envelopes have two parts. One is Env. The Env class lets you describe what shape the envelope should have. In this case, we're using a fixed duration envelope shaped like a triangle. When we use a fixed duration envelope, we know the length of the envelope when we create an instance of the Synth. There are other envelopes that we will not know the envelope of until we decide to end the note, for instance, in response to a key up on a keyboard.

triangle is a constructor. It creates a new instance of an Env. The arguments it takes are the duration and the maximum amplitude.

The other part of the envelope is the EnvGen. Since EnvGen is a class, kr must be a constructor. The two most common UGen constructors are ar and kr. You may recall that ar stands for “audio rate.” kr stands for “control rate.” Because the EnvGen is not actually creating sound, but is controlling the amplitude of a sound, we want the control rate. Control rate signals change less often than audio rate signals, and so using kr reduces the load on our computer and makes our SynthDef more efficient.

EnvGen.kr generates the envelope based on the specifications of its first argument, an instance of Env. So, with Env, we define an Envelope. We pass that definition to an Envelop Generator, which plays back the envelope that we defined. We use that envelope to control the amplitude of a SinOsc. We send the output of the SinOsc to an output bus. When the envelope is done, the amplitude of it has returned to zero, so the SinOsc is no longer audible.

With our previous examples, the Synth never ended. The way to stop sound was to stop execution with apple-. . With our new SynthDef, the sound goes on for exactly the length of dur and then ends with no way to restart it. After we create a Synth and play it, there's nothing that can be done with it.

Try running Synth.new("example3"); several times. If you look at the localhost server window, you will see the number of UGens and Synths grow every time you run Synth.new. The Avg and Peak CPU may also increase.

Every time we create a new instance of Synth, we use up more system resources. Eventually we will either run out of memory or the CPU will climb over 100%. We can clear the Synths by hitting apple-., but this still would limit the number of sounds we could play in a row without stopping execution.

We need an automatic way to remove our synths from the server and deallocate their resources. That means that some new Synths would be able to use the resources the old ones were taking up.

Fortunately, EnvGens can both silence a Synth forever and lay it to rest so iti s deallocated. EnvGen takes an argument called “doneAction”. Adding a doneAction to your EnvGen would look like:

env_gen = EnvGen.kr(env, doneAction: 2);

Try changing adding a doneAction to the SynthDef and running Synth.new("example3"); a few times. Notice that now the Synths don’t accumulate. Now, after a Synth stops making sound, it has ended its useful life and so it gets removed from the server because of the doneAction: 2. However, note that the doneAction removes only instances of the Synth. The SynthDef persists and can be used as many times as we'd like.

Envelopes don’t have to only control amplitude, but can control (almost) any aspect of any UGen. You can’t set the bus on Out.ar with an Envelope, but you can control almost anything else. For example, we can cause the frequency to warble


(
 SynthDef.new("example3a", {arg out = 0, freq = 440, amp = 0.2,
       dur = 1;

  var sin, env_gen, env, freq_env;
  
  env = Env.triangle(dur, amp);
  env_gen = EnvGen.kr(env, doneAction: 2);
  freq_env = EnvGen.kr(Env.sine(dur, 50));

  sin = SinOsc.ar(freq + freq_env, mul: env_gen);
  Out.ar(out, sin);
 }).load(s);
)

Synth.new("example3a");

Note that we only need one of the envelopes to have a doneAction.

Summary

  • Envelopes can be used to control how the amplitude of a Synth changes over time.
  • Env is used to describe the shape of an enevelope.
  • EnvGen.kr is what actually plays the envelope.
  • Synths can be deallocated by using the doneAction argument of EnvGen.
  • Envelopes can also be used to control frequency and many other parameters of SynthDefs.

Problems

  1. Try out the different fixed duration envelopes and try them out with different Oscillators. Especially try out the Env.perc envelope with different noise generators.

Saturday, May 22, 2010

SynthDefs

Last time, I promised we would cover sound-making. First, make sure to boot the localhost server. The localhost server is one of the two small gray boxes at the bottom left of your screen. Press the “Boot” button. When it's booted, the window will say "running" in it and the "Boot" button will turn green and change to say "Quit."

Then, select this code and press enter (not return!). You can select it by double-clicking to the right of the open parenthesis.

 (

  var syn, sound;

  syn = SynthDef.new("example1", {
   Out.ar(0, SinOsc.ar(440));
  });
 
  syn.load(s);

  sound = Synth.new("example1");
 )

When you want to stop the program, press apple-period.

What's going on here? Our program first declares a variable, and then it defines a SynthDef and loads it on the server, then creates a Synth object that plays the sound. This is a complicated example, but I wanted to get you making sounds as soon as possible. Let's look at the example line by line.

The first line is var syn, sound; We are declaring two variables. One is called syn and the other is called sound.

The next line, translated into English, means "I want to create a new SynthDef called 'example1' and store it in the variable syn." Whoa, what's a SynthDef? The SynthDef help file told me that a SynthDef is a "definition of a synth architecture." I got to the help file by highlighting the word SynthDef and pressing apple-d. When I see a term like that, which I don't understand, I can always get help by highlighting it and typing apple-d.

Recall that SuperCollider is not just one program; it is two. One part of the program is the interpreter, which is what we've been talking about so far. The other part of the program is audio server, which is what actually makes the sound. It does not know anything but what it needs to know. This makes it run much faster and more efficiently. You can tell it to make certain kinds of sounds, but in order to run quickly, it wants to know what kinds of sounds you want before you actually make them. You write a description of what you want the server to do. This description, or definition, is called a SynthDef.

When we run SynthDef.new, we get back a new SynthDef, which we're stroing in the variable named syn. Inside the parenthesis are some arguments. SynthDef is the class. new is the message. And "example1" and the stuff in between the curly brackets are the two arguments.

The stuff in the curly brackets is telling the SynthDef what to play. Things between curly brackets {} are called functions. A function is a special type of object that is made up of a code block that you can run. We'll come back to this later.

Out is a UGen. The help file for UGens says, "A UGen or unit generator is an object for generating or processing audio or control signals." UGens exist in SynthDefs. They are what make up a SynthDef. Out is a UGen that writes a signal to a bus, which, in this case, sends it's output to the left channel. .ar is a type of constructor. So Out.ar creates a new instance of a an Out UGen running at the audio rate. ar stands for "audio rate" and is a common constructor name for UGens.

Looking at the arguments to Out.ar, 0 means left channel. If that were a 1, it would mean right channel. And the next thing is what gets sent out, which is a sine tone generator. The argument to SinOsc.ar, 440, is the frequency to play. So SinOsc.ar(440) creates a sine wave at 440 Hz. Out.ar takes that sine wave and sends it out to the left channel, which is channel 0.

The next line, is a closing curly bracket, a close paren and a semicolon. It's the end of the function, the end of the SynthDef and the end of a statement. We've created a new SynthDef that includes in it a function describing what the SynthDef should do when it gets instantiated.

The next line says, take that SynthDef and load it on the server. We're sending a message to syn, saying load. The argument, s, is the server to send it to. In SuperCollider, a lowercase s by itself refers to the audio server.

The next line asks to create a new Synth. The interpreter sends a message to the server, saying, "Hey, could you make a synth that plays the SynthDef called 'example1'?" The server looks and says, "oh yeah, I have a SynthDef called that" and makes a new instance of a Synth, which is running the function that we defined.

We get a new Synth object back from this and store it in "sound."

Let's say we don't want to play an A. Let's say we want to play an E. We can change 440 to 660.


(
 var sound;
 SynthDef.new("example1a", {
 
  Out.ar(0, SinOsc.ar(660));
 }).load(s);
 
 sound = Synth.new("example1a");
)

You'll note that we have one fewer variable in that example. Messages are evaluated left to right. So the interpreter first figures out the result of SynthDef.new("example1a" ... ). Then, it takes that new SynthDef and sends it the message load. We can keep adding on messages at the end as long as we'd like:

object.message1.message2(arg1).message3.message4(arg2)

Of course, this gets silly and difficult to read after a while, but it's legal.

When we're writing a piece, we don't want to have to write a new SynthDef for every note that we're going to play. We can create our own argument, which will tell the SynthDef what frequency to play.


(

 var sound;

 SynthDef.new("example1", { arg freq;
   Out.ar(0, SinOsc.ar(freq));
  }).load(s);

  sound = Synth.new("example1", [\freq, 440]);
)

We call that argument “freq.” An argument is a special kind of variable. You declare then at the top of a code block by using the reserved word “arg.” So the arg freq; part of syn = SynthDef.new("example1", { arg freq;tells SuperCollider that our SynthDef function takes a single argument called freq. We can then use freq like we would any other variable. Here, we’re passing to SinOsc.ar, to use for the frequency.

Passing variables to Synths is a bit different than normal variable passing. The interpreter has to communicate with the audio server. They use a protocol called OSC. The Synth object will handle the OSC for you, but the server does not know which argument you’re trying to pass a value to unless you specifically tell it. So Synth.new takes an optional second argument, which is an Array. An array is a list separated by commas and surrounded by square brackets. When you are passing arguments to the server via a synth, the array must be made up of pairs. The first item in a pair is a symbol or a string and the second is a value. The symbol or string must match the name of the argument. So for an argument called freq, we use the symbol \freq or the string “freq”. If we had an argument called amp, we would use the symbol \amp or the string "amp". A symbol starts with a forward slash: ‘\’. So to pass a value of 440 to the argument freq, our array contains [\freq, 440]. If we had two arguments, one freq and the other amp, we could pass values to freq and amp with an array that looks like [\freq, 440, \amp,0.2]. The array is made up of pairs, to the 440 is paired with freq and 0.2 is paired with amp. The symbols or strings must match the names of the arguments taken by the SynthDef function.

Summary

  • You must boot the server in order to play sounds.
  • There is a variable called "s" which always refers to the server
  • The sounds you want to play must be defined on the server before you play them. They are defined in SynthDefs.
  • Get help by highlighting a class name with the mouse and typing apple-d
  • Blocks of code surrounded by curly braces are functions
  • You play SynthDefs by using Synth
  • You can declare arguments to a function with the word arg: { arg agrument1, argument2;
  • Comma-separated lists surrounded by square brackets are Arrays
  • Synths use arrays to pass arguments: Synth.new(name, [\symbol, argument])

Problems

When you are writing code, if you want to syntax colorize it, so that reserved words are blue, strings are grey, symbols are green, and so on, you can find that in the Format menu or just type apple-‘ (apple-single quote).

  1. Write your own version of “hello world.”
  2. Write your own SynthDefs, using some of the oscillator UGens. To find a list of them, highlight the word “UGens” and type apple-d. Some oscillators to try are Saw and Pulse.