├── .DS_Store ├── LICENSE ├── README.md ├── audio ├── .DS_Store └── tutorial10_audio │ ├── .DS_Store │ ├── high │ ├── high0.aiff │ ├── high1.aiff │ ├── high2.aiff │ ├── high3.aiff │ └── high4.aiff │ ├── low │ ├── .DS_Store │ ├── low0.aiff │ ├── low1.aiff │ ├── low2.aiff │ └── low3.aiff │ └── mid │ ├── mid0.aiff │ ├── mid1.aiff │ ├── mid2.aiff │ ├── mid3.aiff │ └── mid4.aiff ├── full video scripts ├── .DS_Store ├── 01_script.scd ├── 02_script.scd ├── 03_script.scd ├── 04_script.scd ├── 05_script.scd ├── 06_script.scd ├── 07_script.scd ├── 08_script.scd ├── 09_script.scd ├── 10_script.scd ├── 11_script.scd ├── 12_script.scd ├── 13_script.scd ├── 14_script.scd ├── 15_script.scd ├── 16_script.scd ├── 17_script.scd ├── 18_script.scd ├── 19_script.scd ├── 20_script.scd ├── 21_script.scd ├── 22_script.scd ├── 23_script.scd ├── 24_script.scd ├── 25_script.scd ├── 26_script.scd ├── 27_script.scd ├── 28_script.scd ├── 29_script.scd ├── 30_script.scd └── 31_script.scd ├── tutorial15-16-17_buffers ├── .DS_Store └── buffers │ ├── .DS_Store │ ├── crotales │ ├── crotale01.aiff │ ├── crotale02.aiff │ ├── crotaleBrakeDrum01.aiff │ ├── crotaleBrakeDrum02.aiff │ ├── crotaleBrakeDrum03.aiff │ └── crotaleBrakeDrum04.aiff │ ├── deskBells │ ├── deskBell01_C.aiff │ ├── deskBell02_Db.aiff │ ├── deskBell03_D.aiff │ ├── deskBell04_Eb.aiff │ ├── deskBell05_E.aiff │ ├── deskBell06_F.aiff │ ├── deskBell07_Gb.aiff │ ├── deskBell08_G.aiff │ ├── deskBell09_Ab.aiff │ ├── deskBell10_A.aiff │ ├── deskBell11_Bb.aiff │ ├── deskBell12_B.aiff │ └── deskBell13_C.aiff │ └── shakers │ ├── shakerHit01.aiff │ ├── shakerHit02.aiff │ ├── shakerHit03.aiff │ ├── shakerHit04.aiff │ ├── shakerHit05.aiff │ ├── shakerSustain01.aiff │ ├── shakerSustain02.aiff │ └── shakerSustain03.aiff ├── tutorial28_extensions ├── .DS_Store ├── tutorial28_classes.sc └── tutorial28_methods.sc └── tutorial30_startupFile └── startup.scd /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/.DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Eli Fieldsteel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | I have been creating SuperCollider video tutorials since June 2013, which are available on YouTube: 2 | 3 | https://www.youtube.com/playlist?list=PLPYzvS8A_rTaNDweXe6PX4CXSGq4iEWYC 4 | 5 | This repository contains full transcripts of these videos, which include narration and code examples. The main purpose of this repository is to provide access to the code from these tutorial videos without requiring the user/viewer to manually type the code out themselves. 6 | 7 | Eli -------------------------------------------------------------------------------- /audio/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/audio/.DS_Store -------------------------------------------------------------------------------- /audio/tutorial10_audio/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/audio/tutorial10_audio/.DS_Store -------------------------------------------------------------------------------- /audio/tutorial10_audio/high/high0.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/audio/tutorial10_audio/high/high0.aiff -------------------------------------------------------------------------------- /audio/tutorial10_audio/high/high1.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/audio/tutorial10_audio/high/high1.aiff -------------------------------------------------------------------------------- /audio/tutorial10_audio/high/high2.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/audio/tutorial10_audio/high/high2.aiff -------------------------------------------------------------------------------- /audio/tutorial10_audio/high/high3.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/audio/tutorial10_audio/high/high3.aiff -------------------------------------------------------------------------------- /audio/tutorial10_audio/high/high4.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/audio/tutorial10_audio/high/high4.aiff -------------------------------------------------------------------------------- /audio/tutorial10_audio/low/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/audio/tutorial10_audio/low/.DS_Store -------------------------------------------------------------------------------- /audio/tutorial10_audio/low/low0.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/audio/tutorial10_audio/low/low0.aiff -------------------------------------------------------------------------------- /audio/tutorial10_audio/low/low1.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/audio/tutorial10_audio/low/low1.aiff -------------------------------------------------------------------------------- /audio/tutorial10_audio/low/low2.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/audio/tutorial10_audio/low/low2.aiff -------------------------------------------------------------------------------- /audio/tutorial10_audio/low/low3.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/audio/tutorial10_audio/low/low3.aiff -------------------------------------------------------------------------------- /audio/tutorial10_audio/mid/mid0.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/audio/tutorial10_audio/mid/mid0.aiff -------------------------------------------------------------------------------- /audio/tutorial10_audio/mid/mid1.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/audio/tutorial10_audio/mid/mid1.aiff -------------------------------------------------------------------------------- /audio/tutorial10_audio/mid/mid2.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/audio/tutorial10_audio/mid/mid2.aiff -------------------------------------------------------------------------------- /audio/tutorial10_audio/mid/mid3.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/audio/tutorial10_audio/mid/mid3.aiff -------------------------------------------------------------------------------- /audio/tutorial10_audio/mid/mid4.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/audio/tutorial10_audio/mid/mid4.aiff -------------------------------------------------------------------------------- /full video scripts/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/full video scripts/.DS_Store -------------------------------------------------------------------------------- /full video scripts/01_script.scd: -------------------------------------------------------------------------------- 1 | Hi everyone, and welcome to the first tutorial. In this video I'll discuss navigating the SuperCollider environment as well as some basic programming concepts. 2 | 3 | When you first open SuperCollider, this is what you'll see. On the left is your workspace. This is where you'll type and evaluate code, and this code is what will be written to a file when you save your work. On the lower right is the post window. This is where SuperCollider communicates with the user, by posting either the result of evaluated code, or an error message in response to invalid code. And on the top right is the help documentation. 4 | 5 | The post window and help documentation can both be moved and repositioned by clicking and dragging their top bar, like so. They can be undocked or detached by clicking the icon at the upper left of each component. If you choose to detach a component, then it becomes a standard window which can move between the foreground and background. If you close a component, it can be re-opened by clicking on the View menu and selecting Docklets. 6 | 7 | Unlike some previous versions of SuperCollider, there is now a preferences dialog under the SuperCollider menu. Here you can modify the appearance and behavior of the text editor, customize keyboard shortcuts, and a few other things. 8 | 9 | Let's take a look at the post window, where you can see that there's been some activity. Specifically, on startup, SuperCollider will try to compile the class library, and load the help files. Here we can see the library has been successfully compiled, and the help documents have been successfully indexed. Here, and in many other situations, you'll find it very useful to be able to clear the post window. To do this, right click on the post window, and you'll see an option to Clear. You'll probably also see a keyboard shortcut next to the word Clear. I suggest memorizing this keyboard shortcut, because you'll probably be using it a lot. 10 | 11 | Let's now move on to the workspace to start writing and evaluating code. 12 | 13 | The SuperCollider language is home to a library of classes, which represent different types of data. Inputs, outputs, computation, and other types of data manipulation are conceived as messages that are passed to objects. When an object receives a message, it is said to be the receiver of that message. The most commonly used syntax for this receiver-message paradigm is 14 | 15 | receiver-dot-message 16 | 17 | messages are sometimes called methods, and these two words are used interchangably. 18 | 19 | In the following example, 3 is the receiver of the message "cubed". In order to execute this statement, we simply place the mouse cursor anywhere on this line and press shift-return. The result of this evaluation is printed in the post window. 20 | 21 | To demonstrate error messages, let's suppose we've misspelled "cubed". If we try to evaluate, the post window displays an error message. It looks complicated, but if we scroll to the top, we can see that SuperCollider was unable to understand the message "cudeb". Let's clear the post window and imagine a different flavor of misspelling, like typing a comma instead of a period. In this case, SuperCollider reports a syntax error. There are several different types of error messages, some of which take some time to decipher and debug, but in most cases, it will be a simple misspelling, an extra character, or a forgotten parenthesis. 22 | 23 | Let's clear the post window and discuss the use of variables. A variable is a named container that's used to store a value, not necessarily a number, so that it can be referred to later. For instance, we can store the value of 3.cubed in the variable "x", like so. 24 | 25 | x = 3.cubed; 26 | 27 | Evaluate this line, and "x" now holds a value of 27. We can now continue to do operations with x, if we so choose. For instance, x+10 returns 37. Let's change our variable name from "x" to "number" 28 | 29 | and if we evaluate, we get an error message saying that the variable "number" is not defined. This brings up an important concept. In SuperCollider, there is a distinction made between local variables and global variables. Local variables must be declared before they are used, using a "var" statement like so: 30 | 31 | var number; 32 | number = 3.cubed; 33 | 34 | local variables must begin with a lowercase alphabetic character, but after the first character you can use uppercase letters, numbers, hyphens, and underscores. Notice that now we're dealing with multiple statements of code, so each statement must end with a semicolon. There are several ways to evaluate multiple statements simultaneously. You can highlight everything and press shift-return, or easier still, you can enclose your code in parentheses, place the mouse cursor anywhere between them, and press command-return. 35 | 36 | ( 37 | var number; 38 | number = 3.cubed; 39 | ) 40 | 41 | Notice that even though this code has compiled with no errors, the variable "number" is now lost, since it was a local variable. 42 | 43 | Notice also that we can't run these statements one-at-a-time; local variables must be declared and used within the same code execution. 44 | 45 | Global variables, on the other hand, persist after code evaluation. Lowercase a through z are reserved for use as global variables, or you can precede a local variable name with a tilde in order to turn it into a global variable. So we could set ~number equal to 3-cubed, and we'd have no errors. 46 | 47 | Technically speaking, global variables are actually environment variables, which means they are specific to a particular environment. But for the beginner, it's fine to conceive environment variables as being globally accessible. 48 | 49 | When dealing with many statements of code, you might often see a single variable being continually overwritten as a piece of data is continually manipulated, like so: 50 | 51 | Here, we declare a variable called foo, set it equal to three, cube it, add 100, and divide by two. For the last line, we simply output the foo's value. In each statement, we set "foo" equal to the result of the newly manipulated data. 52 | 53 | ( 54 | var foo; 55 | foo = 3; 56 | foo = foo.cubed; 57 | foo = foo + 100; 58 | foo = foo/2; 59 | foo; 60 | ) 61 | 62 | We see in the post window that the result is 63.5. If we were to remove these overwritings, like so (pause), we'd still be computing values, but foo would not keep track of these computations. Hence, when we evaluate this clump, we see foo's original value of three, which has not changed. 63 | 64 | ( 65 | var foo; 66 | foo = 3; 67 | foo.cubed; 68 | foo + 100; 69 | foo/2; 70 | foo; 71 | ) 72 | 73 | Notice that the original clump can be mathematically rewritten like so (pause), demonstrating that methods and operations can be strung together on a single line, always evaluated from left to right. Notice that there is no mathematical operator preference, that is to say, the addition is done before the division. Although this is syntactically legal, I would argue that the multi-line approach is more clear. 74 | 75 | 3.cubed + 100 / 2; 76 | 77 | As your code gets longer and longer, I strongly advise that you include comments. A comment is a piece of text that's invisible to SuperCollider, that's used to clarify your code in human terms, so that, for instance, someone you're collaborating with can understand what your code is attempting to do. Comments are also useful if you return to a complicated piece of code you've written several years ago and need to remind yourself what it's all about. 78 | 79 | A single-line comment is always preceded with two forward slashes, like so, 80 | 81 | and a multi-line comment is preceded by slash-asterisk, and appended with asterisk-slash. By default, comments are red. 82 | 83 | To close out this tutorial, I'll talk just a bit more about the help documentation. 84 | 85 | cubed 86 | 87 | To get help, place the cursor on the text you'd like to look up, click on the Help menu at the top, and choose look up documentation for cursor. The keyboard shortcut for this is command-d. In this case, SuperCollider searches its help documents for the word "cubed", and it comes up with a method called cubed, which is defined for several different classes, like SequenceableCollection and SimpleNumber. You can bring up a search bar by selecting "look up documentation" in the Help menu, and type whatever you like. The shortcut for this is shift-command-d. In this case, I've typed oscillator, and SuperCollider returns all the help files that contain the word oscillator. You can do a find for text within the current help document using the search bar in the upper right corner, and you can navigate forward and back through the help documentation, just like you would on a web browser. 88 | 89 | That's it for this tutorial. In the next video we'll start making some sound. Thanks for watching. 90 | 91 | -------------------------------------------------------------------------------- /full video scripts/02_script.scd: -------------------------------------------------------------------------------- 1 | Hey everyone, welcome to tutorial number 2, where we'll start working with sound. 2 | 3 | SuperCollider appears as one program, but actually exists as two programs. The language, called sclang, is home to an object-oriented programming language, a library of classes, and the language's interpreter, which reads and parses your code. The other half of SuperCollider is a real-time audio synthesis program called scsynth, which communicates over UDP or TCP using the Open Sound Control protocol, or OSC. The language and the server are abstractly networked and behave according to a "client-server" architecture. The user, operating in the language, acts as a client to the audio server, and makes requests of the audio server by transmitting OSC messages. Because the two programs are networked, one could theoretically manipulate audio on an instance of scsynth running on a laptop halfway around the world, or you could have several clients connected to one audio server, as is the case in some laptop ensemble performances. 4 | 5 | If you're interested in a brief introduction to the client-server architecture of SuperCollider, you can read the video description below, and there are some useful reference files in the help documentation, such as 6 | 7 | SuperCollider 3 Server Tutorial 8 | Client vs Server 9 | Server Architecture 10 | 11 | To make sound on your computer, we first have to boot the local audio server. There are several ways to do this. One is by evaluating 12 | 13 | Server.local.boot; 14 | 15 | As the server boots, the post window displays some information...Here we can see the available audio devices...the devices that are currently being used for input and output...and the sample rate. These can all be changed very easily, but I won't dive into that right now. At the bottom right of the screen is a small display that gives information about thet status of the language and the audio server. We can see that the interpreter is active, and that the server is humming along, using somewhere between .05 and .1 percent of the computer's CPU. 16 | 17 | To quit the audio server, we can evaluate 18 | 19 | Server.local.quit; 20 | 21 | And we can see in the corner that the server application is no longer running. As a convenience for the user, when SuperCollider starts up, the local server is automatically stored in the global variable s. If we evaluate s, we can see that indeed it keeps a reference to the local server. 22 | 23 | s; 24 | 25 | Therefore we can alternatively boot the server by evaluating 26 | 27 | s.boot; 28 | 29 | and quit scsynth by evaluating 30 | 31 | s.quit; 32 | 33 | s is by convention reserved for the local server, and it's convenient to have it available in a single-character global variable. So even though there's nothing stopping you from overwriting s, I'd recommend not overwriting it. 34 | 35 | Lastly, you can boot the server by going to the Language menu and clicking Boot Server. There's a keyboard shortcut for this which I recommend memorizing, since it's the quickest way to launch scsynth. Don't forget that you can customize all these shortcuts in the preferences dialog. If everything's green in the corner, you're ready to make sound. 36 | 37 | The quickest and easiest way to make sound in SuperCollider is to create a function, which is delineated by an enclosure of curly braces, fill it with one or more Unit Generators, and send it the "play" message. As a reminder from the previous tutorial, press shift-return to evaluate a single line of code. 38 | 39 | {SinOsc.ar}.play; 40 | 41 | Before we go any farther, it's absolutely critical that you know how to stop all sound. In the Language menu, there's an option labeled "Stop", but much quicker is the keyboard shortcut, which is command-period. This is by far the most important keyboard shortcut to memorize. 42 | 43 | Notice that the post window indicates that we've created a Synth. We'll talk more about Synths down the road, but for now it's important to know that Synths are inidividual entities that exist on the audio server, and that they can be destroyed using the message "free". Using this method, we can selectively remove sounds instead of terminating all sounds via command-period. For example, if we store our Synth in a global variable, we can free it, like so, which is slightly more elegant than command-period: 44 | 45 | x = {SinOsc.ar}.play; 46 | 47 | x.free; 48 | 49 | Here's a common mistake that I see from time to time. I'll first create and name a Unit Generator function, like this 50 | 51 | x = {SinOsc.ar}; 52 | 53 | play it, 54 | 55 | x.play; 56 | 57 | and then try to free it. 58 | 59 | x.free; 60 | 61 | But this doesn't work, so I need to hit command-period instead. The reason it doesn't work is that in the first statement, we store a Function in the variable x. We then play the function, but we don't store the resulting Synth in its own variable, so the Synth is inaccessible. When we evaluate x.free, SuperCollider tries to free the Function stored in x. But of course it's the Synth, the process that's actually running on the audio server, that needs to be freed. Therefore, the correct way to do this is to store x.play in its own variable, like so: 62 | 63 | y = x.play; 64 | 65 | and then we can free the Synth, which has been stored in the global variable y: 66 | 67 | y.free; 68 | 69 | Let's spend some time talking about Unit Generators, which are more often referred to as UGens. Let's start by looking at the help file for UGen. Remember that you can bring up a search bar by hitting shift-command-d. The description is fairly straightforward. "UGens represent calculations with signals. They are the basic building blocks of synth definitions on the server, and are used to generate or process both audio and control signals." Just below, we can see that UGens accept three messages: ar, kr, and ir, which stand for audio rate, control rate, and initialization rate. Audio rate UGens output values at the sample rate, and in this case that means 44100 samples per second. If you want to hear the output of a UGen, or if you want a high-resolution waveform, you should use .ar. In the previous example, I've used .ar for SinOsc because I want to hear the resulting sine wave that SinOsc produces. 70 | 71 | By default, UGens running at the control rate output one value for every 64 audio samples, which means they run 64 times slower than audio rate UGens, and therefore use less of your computer's CPU. The .kr method should be used for UGens that shape or control other UGens, for example, if you're controlling the amplitude or frequency of an audio rate oscillator. Ir UGens don't continuously output values, instead they calculate only one value when the Synth is first instantiated, and it's arguably the rarest of these three methods. 72 | 73 | The top of the UGen helpfile has references to three other help documents: Browse UGens, Tour of UGens, and Unit Generators and Synths, and these are all worth reading if you'd like to learn more. But for now, let's dig a little deeper into UGens. We'll begin by looking at the help file for SinOsc. Remember that you can place the cursor on any text and press command-D to bring up its helpfile, if it exists. 74 | 75 | As indicated immediately below the class name, SinOsc is an interpolating sine wavetable oscillator. In UGen help files, there's usually a brief description...the available methods...the arguments that the UGen expects...and some examples, which can be evaluated just like you would with ordinary code in the text editor. 76 | 77 | ahh, I love that sound. 78 | 79 | Let's talk about Arguments. As we can see in the help file, SinOsc needs four values to produce a signal: freq... phase... mul... and add. Respectively, these arguments represent the frequency of the oscillator (which of course determines the pitch of the tone produced), an inital phase offset into the wavetable in radians, a value to multiply by each sample, and a value to add to each sample. Mul translates to amplitude scaling and add translates to DC offset. Mul and add are in fact quite ubiquitous. Nearly every UGen's last two arguments are mul and add. 80 | 81 | If we don't specify values for these arguments, as has been the case so far, then the default values are used. We can see from the help file that the default values, in order, are 440Hz, 0 radians, amplitude scaling of 1, and DC offset of 0. 82 | 83 | We can specify our own values for these arguments by creating a parenthetical enclosure after the rate method. Notice the convenient pop-up text that tells you the names of the arguments and their default values, so you don't have to look up the help file every time. Here' I'll set the frequency to 700Hz and the amplitude to one-eighth full volume. 84 | 85 | {SinOsc.ar(700, 0, 0.125, 0)}.play 86 | 87 | If we specify values for less than all of the arguments, then the defaults are used for the rest. Here you can hear that the amplitude has returned to its default value of 1. 88 | 89 | {SinOsc.ar(700)}.play 90 | 91 | We can also skip arguments, or specify them in a custom order, by providing the name of the argument you want to specify, a colon, and the value. Here the first value is still interpreted as frequency, but I'm telling the interpreter that I want to skip phase and go straight to mul: 92 | 93 | {SinOsc.ar(700, mul:0.125)}.play 94 | 95 | UGens can perform mathematical computations like addition, subtraction, exponentiation, etc, so as an alternative for mul and add, you can simply multiply or add a constant value: 96 | 97 | {SinOsc.ar(700) * 0.125}.play 98 | 99 | I won't mess around with adding values, because DC offset on an audible signal isn't very healthy for speakers. 100 | 101 | ... 102 | 103 | When you create and play a function, you have the option to declare your own arguments, so that you can manipulate the sound as it's playing. Arguments are declared with an arg statement, just like a variable declaration from the previous tutorial. If you're using arguments and variables, then arguments must be declared first, then variables, then the rest of your code. 104 | 105 | ( 106 | z = { 107 | arg freq=440, amp=1; 108 | var sig; 109 | sig = SinOsc.ar(freq) * amp; 110 | }.play 111 | ) 112 | 113 | Just like the "free" message, you can use the "set" message to modify arguments of an active Synth. Just specify the argument as a symbol by preceding it with a backslash, a comma, and the value for that argument. We can change the frequency...We can change the amplitude... and we can change both together in one statement. And the order of the arguments in a set command doesn't have to match the order in which they were declared. 114 | 115 | z.set(\freq, 330) 116 | z.set(\amp, 0.125) 117 | z.set(\freq, 660, \amp, 1) 118 | z.set(\amp, 0.125, \freq, 550) 119 | z.free; 120 | 121 | Also, keep in mind that you can name your arguments and variables whatever you like, as long as they start with lowercase alphabetic characters. I've used freq, amp, and sig because they're short and meaningful, but you can call them whatever makes the most sense to you. 122 | 123 | The real fun comes from using UGens to control other UGens. For example, we can make a slightly more complex UGen function in which the frequency of a sine oscillator is controlled by a non-interpolating noise generator LFNoise0...Since we're using a noise generator instead of a constant numeric value, we'll use a variable for frequency instead of an argument...since LFNoise0 is controlling another UGen, we should use .kr. The first argument is the frequency with which the noise generator will out a new random value, let's say 8 random values per second. The default output of LFNoise0, like SinOsc, ranges between -1 and 1. I'll set mul equal to 400 and add to 600, so that the actual output of LFNoise0 is between 200 and 1000 Hz. Then, all we have to do is use our noise generator, which we've named "freq", as a frequency input to our sound source. 124 | 125 | ( 126 | z = { 127 | var freq, sig; 128 | freq = LFNoise0.kr(8, 400, 600); 129 | sig = SinOsc.ar(freq); 130 | }.play; 131 | ) 132 | 133 | z.free; 134 | 135 | A more intuitive way of specifying the output range of a UGen is to use the "range" message, like so: 136 | 137 | ( 138 | z = { 139 | var freq, sig; 140 | freq = LFNoise0.kr(8).range(200,1000); 141 | sig = SinOsc.ar(freq); 142 | }.play; 143 | ) 144 | 145 | z.free; 146 | 147 | But be sure you don't specify mul/add while Simultaneously using a range method, because they will conflict with one another. 148 | 149 | Since humans perceive frequency exponentially, it's usually more desirable to specify an exponential distribution for a range of frequency values, using the 'exprange' message. This will sound like a more equal distribution of frequency values, whereas the previous example tends more toward the higher end of the range: 150 | 151 | ( 152 | z = { 153 | var freq, sig; 154 | freq = LFNoise0.kr(8).exprange(200,1000); 155 | sig = SinOsc.ar(freq); 156 | }.play; 157 | ) 158 | 159 | z.free; 160 | 161 | All UGens understand range and exprange, and there are other useful methods that you can read about in the UGen help file. 162 | 163 | Let's control the amplitude as well, in this case with a linearly interpolating noise generator, LFNoise1: 164 | 165 | ( 166 | z = { 167 | var freq, amp, sig; 168 | freq = LFNoise0.kr(8).exprange(200,1000); 169 | amp = LFNoise1.kr(12).exprange(0.02,1); 170 | sig = SinOsc.ar(freq) * amp; 171 | }.play 172 | ) 173 | 174 | z.free; 175 | 176 | And just for fun, let's add an argument to this UGen function, which we'll use to control the frequency of LFNoise0 177 | 178 | ( 179 | z = { 180 | arg noiseHz=8; 181 | var freq, amp, sig; 182 | freq = LFNoise0.kr(noiseHz).exprange(200,1000); 183 | amp = LFNoise1.kr(12).exprange(0.02,1); 184 | sig = SinOsc.ar(freq) * amp; 185 | }.play 186 | ) 187 | 188 | Now we can change the frequency of LFNoise0 on-the-fly 189 | 190 | z.set(\noiseHz, 16); 191 | 192 | For even more fun, we can have SuperCollider generate a random value each time we evaluate this statement: 193 | 194 | z.set(\noiseHz, exprand(4,64)); 195 | z.free; 196 | 197 | That's all for this tutorial. I hope this is enough to help you get off the ground with some sound experimentation. In the next video, We'll talk in more depth about Synth, and it's parter in crime, SynthDef. Please leave any comments or questions on YouTube. Thanks for watching. -------------------------------------------------------------------------------- /full video scripts/03_script.scd: -------------------------------------------------------------------------------- 1 | Hey everyone, welcome to tutorial number 3. In the previous video we started making sound by creating a UGen function and sending it the "play" message. This process creates a Synth, which represents a single sound-producing unit on the audio server. However, the primary and more flexible procedure for creating sound is to first create a SynthDef and then execute the SynthDef by calling a Synth explicitly. A SynthDef is essentially a recipe for a particular sound, and a Synth is the execution of that recipe. As we can see in the help file for Function, 2 | 3 | "Function.play is often more convienent than SynthDef.play, particularly for short examples and quick testing. But where reuse and maximum flexibility are of greater importance, SynthDef and its various methods are usually the better choice." 4 | 5 | Likewise, in the SynthDef help file we see that "Methods such as Function-play, etc. are simply conveniences which automatically create a SynthDef" 6 | 7 | To demonstrate, I'll take our UGen function from the previous tutorial and convert it to a SynthDef. 8 | 9 | s.boot; 10 | 11 | ( 12 | z = { 13 | arg noiseHz=8; 14 | var freq, amp, sig; 15 | freq = LFNoise0.kr(noiseHz).exprange(200,1000); 16 | amp = LFNoise1.kr(12).exprange(0.02,1); 17 | sig = SinOsc.ar(freq) * amp; 18 | }.play 19 | ) 20 | 21 | As is the case with many objects in SuperCollider, we create a new instance using the "new" message. "new," in this case, takes 6 arguments, but generally, you'll only specify values for the first two arguments, name and ugenGraphFunc. 22 | 23 | The first argument is the name you want to give to your SynthDef, which can be specified as either a string...or a symbol. I prefer the symbol because it's one less character I have to type. 24 | 25 | The second argument is a UGen function, which is nearly identical to our function from above, so we can pretty much copy and paste. However, there's one additional thing to consider when building a SynthDef. If you want this SynthDef to output a signal, you must include an output UGen. The most basic of these UGens is simply called Out. Out needs an output bus index, and the signal to write to that bus. I'll deal with inputs, outputs, and busses in a future tutorial since there's a lot of information to cover, so for now, I'll just say that the audio bus with index 0 corresponds to your lowest-numbered hardware audio output. Audio bus 1 corresponds to the next-highest hardware output, and so on, until you run out of hardware outputs. So in the case of most laptop sound cards, this means audio bus 0 corresponds to your left speaker, and audio bus 1 corresponds to your right speaker. 26 | 27 | We'll close out the SynthDef, and the last thing we need to do is "add" the new SynthDef, which sends it to the audio server so that it can be used. There are other methods to make a SynthDef usable, such as load, send, and store, but add is probably the most flexible. 28 | 29 | ( 30 | SynthDef.new(\sineTest, { 31 | arg noiseHz=8; 32 | var freq, amp, sig; 33 | freq = LFNoise0.kr(noiseHz).exprange(200,1000); 34 | amp = LFNoise1.kr(12).exprange(0.02,1); 35 | sig = SinOsc.ar(freq) * amp; 36 | Out.ar(0, sig); 37 | }).add 38 | ) 39 | 40 | To execute the SynthDef, we create a new Synth and provide it with the name of a SynthDef. 41 | 42 | x = Synth.new(\sineTest); 43 | 44 | We terminate the Synth using the free method, just like we've done in the previous tutorial. 45 | 46 | x.free; 47 | 48 | The SynthDef above has one argument, noiseHz, whose default value is 8. If I wanted to create a Synth that starts with a different value for noiseHz, I can add a second argument to Synth.new. This is an array that contains the symbolic name of the argument, followed by a comma, and the value. 49 | 50 | x = Synth.new(\sineTest, [\noiseHz, 32]) 51 | 52 | As we saw in the previous tutorial, the set message can be used to update control arguments while the Synth is active. 53 | 54 | x.set(\noiseHz, 12) 55 | 56 | x.free; 57 | 58 | In the Synth help file, notice that the "new" method actually takes four arguments. In addition to the SynthDef name and argument array, there's also target and addAction. These are useful, but not relevant right now, so I'll deal with them in a tutorial down the road. For now it's fine to use the default values for target and addAction. 59 | 60 | Before I close out this video, I'll code another SynthDef from scratch to reinforce concepts. 61 | 62 | I'll call this one pulseTest. 63 | 64 | ( 65 | SynthDef.new(\pulseTest, { 66 | 67 | }).add; 68 | ) 69 | 70 | I'll send one signal to the left speaker, and another to the right speaker. 71 | 72 | ( 73 | SynthDef.new(\pulseTest, { 74 | var sig1, sig2; 75 | Out.ar(0, sig1); 76 | Out.ar(1, sig2); 77 | }).add; 78 | ) 79 | 80 | I'll use pulse waves for both audio signals. 81 | 82 | ( 83 | SynthDef.new(\pulseTest, { 84 | var sig1, sig2; 85 | sig1 = Pulse.ar(); 86 | sig2 = Pulse.ar(); 87 | Out.ar(0, sig1); 88 | Out.ar(1, sig2); 89 | }).add; 90 | ) 91 | 92 | I'll control the frequency of these pulse waves with non-interpolated noise, with a new value chosen 4 times per second. LFNoise0 is a randm value generator, so even though I'm using the same UGen twice, they will both generate a unique stream of values. 93 | 94 | ( 95 | SynthDef.new(\pulseTest, { 96 | var sig1, sig2, freq1, freq2; 97 | freq1 = LFNoise0.kr(4).exprange(); 98 | freq2 = LFNoise0.kr(4).exprange(); 99 | sig1 = Pulse.ar(); 100 | sig2 = Pulse.ar(); 101 | Out.ar(0, sig1); 102 | Out.ar(1, sig2); 103 | }).add; 104 | ) 105 | 106 | I want to be able to change the frequency as the Synth is playing, so I'll create some arguments. I'll define a fundamental frequency and a maximum partial number, and have LFNoise range between them. 107 | 108 | ( 109 | SynthDef.new(\pulseTest, { 110 | arg fund=40, maxPartial=4; 111 | var sig1, sig2, freq1, freq2; 112 | freq1 = LFNoise0.kr(4).exprange(fund, fund * maxPartial); 113 | freq2 = LFNoise0.kr(4).exprange(fund, fund * maxPartial); 114 | sig1 = Pulse.ar(); 115 | sig2 = Pulse.ar(); 116 | Out.ar(0, sig1); 117 | Out.ar(1, sig2); 118 | }).add; 119 | ) 120 | 121 | And I'll introduce a message we haven't seen yet, called "round", which simply rounds the output to the nearest multiple of a number. So in this case, LFNoise0 will output random overtones of a given fundamental. 122 | 123 | ( 124 | SynthDef.new(\pulseTest, { 125 | arg fund=40, maxPartial=4; 126 | var sig1, sig2, freq1, freq2; 127 | freq1 = LFNoise0.kr(4).exprange(fund, fund * maxPartial).round(fund); 128 | freq2 = LFNoise0.kr(4).exprange(fund, fund * maxPartial).round(fund); 129 | sig1 = Pulse.ar(); 130 | sig2 = Pulse.ar(); 131 | Out.ar(0, sig1); 132 | Out.ar(1, sig2); 133 | }).add; 134 | ) 135 | 136 | Just for a little extra subtlety, I'll use LFPulse to add some octave jumps. LFPulse normally ranges from 0 to 1, so I'll add 1 in order to have it range from 1 to 2. And I'm just using different frequency values here for the sake of variety. 137 | 138 | ( 139 | SynthDef.new(\pulseTest, { 140 | arg fund=40, maxPartial=4; 141 | var sig1, sig2, freq1, freq2; 142 | freq1 = LFNoise0.kr(4).exprange(fund, fund * maxPartial).round(fund); 143 | freq2 = LFNoise0.kr(4).exprange(fund, fund * maxPartial).round(fund); 144 | freq1 = freq1 * LFPulse.kr(8)+1; 145 | freq2 = freq2 * LFPulse.kr(6)+1; 146 | sig1 = Pulse.ar(); 147 | sig2 = Pulse.ar(); 148 | Out.ar(0, sig1); 149 | Out.ar(1, sig2); 150 | }).add; 151 | ) 152 | 153 | I'll use LFPulse again to control the amplitude and transform what would otherwise be a steady tone into regular pulses of sound. I'll specify a duty cycle that's close to zero, so that the pulses are fairly short. I'll set the phase of the 2nd amplitude control to 0.5 so that the two pulse waves are out of phase with one another. This way, the sound will alternate between the left and right speakers. And, I'll also multiply by 0.75 just to take down the volume a bit. 154 | 155 | ( 156 | SynthDef.new(\pulseTest, { 157 | arg fund=40, maxPartial=4; 158 | var amp1, amp2, sig1, sig2, freq1, freq2; 159 | amp1 = LFPulse.kr(4,0,0.12) * 0.75; 160 | amp2 = LFPulse.kr(4,0.5,0.12) * 0.75; 161 | freq1 = LFNoise0.kr(4).exprange(fund, fund * maxPartial).round(fund); 162 | freq2 = LFNoise0.kr(4).exprange(fund, fund * maxPartial).round(fund); 163 | freq1 = freq1 * LFPulse.kr(8)+1; 164 | freq2 = freq2 * LFPulse.kr(6)+1; 165 | sig1 = Pulse.ar(); 166 | sig2 = Pulse.ar(); 167 | Out.ar(0, sig1); 168 | Out.ar(1, sig2); 169 | }).add; 170 | ) 171 | 172 | In fact, let's use another argument for the amplitude pulse frequency. 173 | 174 | ( 175 | SynthDef.new(\pulseTest, { 176 | arg ampHz=4, fund=40, maxPartial=4; 177 | var amp1, amp2, sig1, sig2, freq1, freq2; 178 | amp1 = LFPulse.kr(ampHz,0,0.12) * 0.75; 179 | amp2 = LFPulse.kr(ampHz,0.5,0.12) * 0.75; 180 | freq1 = LFNoise0.kr(4).exprange(fund, fund * maxPartial).round(fund); 181 | freq2 = LFNoise0.kr(4).exprange(fund, fund * maxPartial).round(fund); 182 | freq1 = freq1 * LFPulse.kr(8)+1; 183 | freq2 = freq2 * LFPulse.kr(6)+1; 184 | sig1 = Pulse.ar(); 185 | sig2 = Pulse.ar(); 186 | Out.ar(0, sig1); 187 | Out.ar(1, sig2); 188 | }).add; 189 | ) 190 | 191 | I'll add one last argument for the width of the pulse wave output, and fill in the parentheses at the bottom. 192 | 193 | ( 194 | SynthDef.new(\pulseTest, { 195 | arg ampHz=4, fund=40, maxPartial=4, width=0.5; 196 | var amp1, amp2, sig1, sig2, freq1, freq2; 197 | amp1 = LFPulse.kr(ampHz,0,0.12) * 0.75; 198 | amp2 = LFPulse.kr(ampHz,0.5,0.12) * 0.75; 199 | freq1 = LFNoise0.kr(4).exprange(fund, fund * maxPartial).round(fund); 200 | freq2 = LFNoise0.kr(4).exprange(fund, fund * maxPartial).round(fund); 201 | freq1 = freq1 * LFPulse.kr(8)+1; 202 | freq2 = freq2 * LFPulse.kr(6)+1; 203 | sig1 = Pulse.ar(freq1, width, amp1); 204 | sig2 = Pulse.ar(freq2, width, amp2); 205 | Out.ar(0, sig1); 206 | Out.ar(1, sig2); 207 | }).add; 208 | ) 209 | 210 | Last, I'll add some reverb using FreeVerb to make it sound nice. 211 | 212 | ( 213 | SynthDef.new(\pulseTest, { 214 | arg ampHz=4, fund=40, maxPartial=4, width=0.5; 215 | var amp1, amp2, sig1, sig2, freq1, freq2; 216 | amp1 = LFPulse.kr(ampHz,0,0.12) * 0.75; 217 | amp2 = LFPulse.kr(ampHz,0.5,0.12) * 0.75; 218 | freq1 = LFNoise0.kr(4).exprange(fund, fund * maxPartial).round(fund); 219 | freq2 = LFNoise0.kr(4).exprange(fund, fund * maxPartial).round(fund); 220 | freq1 = freq1 * (LFPulse.kr(8)+1); 221 | freq2 = freq2 * (LFPulse.kr(6)+1); 222 | sig1 = Pulse.ar(freq1, width, amp1); 223 | sig2 = Pulse.ar(freq2, width, amp2); 224 | sig1 = FreeVerb.ar(sig1, 0.7, 0.8, 0.25); 225 | sig2 = FreeVerb.ar(sig2, 0.7, 0.8, 0.25); 226 | Out.ar(0, sig1); 227 | Out.ar(1, sig2); 228 | }).add; 229 | ) 230 | 231 | Let's give it a try. 232 | 233 | x = Synth.new(\pulseTest); 234 | 235 | We can change the width 236 | 237 | x.set(\width, 0.05); 238 | x.set(\width, 0.25); 239 | 240 | the fundamental frequency 241 | 242 | x.set(\fund, 50); 243 | x.set(\fund, 60); 244 | x.set(\fund, 80); 245 | x.set(\fund, 160); 246 | x.set(\fund, 30); 247 | 248 | The maximum partial number 249 | 250 | x.set(\maxPartial, 8); 251 | x.set(\maxPartial, 20); 252 | 253 | And last, we can change the rate of amplitude pulsing 254 | 255 | x.set(\ampHz, 2); 256 | x.set(\ampHz, 1); 257 | x.set(\ampHz, 0.25); 258 | 259 | x.free; 260 | 261 | If I want to initialize this Synth with arguments that are different from the default values, I can do so by providing an array of symbol-value pairs as Synth's second argument, like this 262 | 263 | x = Synth.new(\pulseTest, [\ampHz, 3.3, \fund, 48, \maxPartial, 4, \width, 0.15]); 264 | 265 | x.free; 266 | 267 | That's it for tutorial number 3. From this point on, I'll use function.play for short simple examples, and SynthDef for more complicated examples. Stay tuned for number 4, where I'll talk about envelopes and doneActions. Thanks for watching! -------------------------------------------------------------------------------- /full video scripts/04_script.scd: -------------------------------------------------------------------------------- 1 | s.boot; 2 | 3 | Hey everyone, welcome to tutorial number 4. So far, all the sound we've been producing is generated indefinitely until we either free the Synth or press command period. 4 | 5 | x = {PinkNoise.ar * 0.5}.play 6 | x.free; 7 | 8 | But both of these options are hard stops: they release the sound instantaneously and usually produce a click. There are many cases when you'd want a Synth to fade in and fade out, and to free itself after the fade out is complete instead of the user having to free the Synth manually. For this purpose, there are several envelope UGens available, which we can find in the Documentation by going to Browse...UGens...and Envelopes. The term 'envelope' in many cases refers to the well-known "adsr" envelope, which I'll discuss later in this video, but there are many kinds of envelopes. More generally, 'envelope' refers to a custom signal shape that controls one or more parameters of a sound. Envelopes most often control amplitude, but they can just as easily control other parameters, like frequency, duty cycle, playback speed, etc. 9 | 10 | Let's start by looking at Line, which is the simplest of these envelopes.\ 11 | 12 | Line.kr(); 13 | 14 | Line takes a start value, an end value, and generates a signal that travels linearly from start to end over a duration given in seconds. Like almost all UGens, Line also takes an optional mul and add, and then there's an argument we haven't seen yet, called doneAction. 15 | 16 | doneActions are found with UGens that are inherently finite. For instance, an oscillator has no inherent end point, it simply generates a recurring wave shape until we tell it to stop. On the other hand, a line and other envelopes have a definitive end point. Therefore, when a finite UGen is part of an active Synth, SuperCollider wants to know what kind of action to take once the UGen has finished. doneAction allows the user to specify this action by supplying an integer. 17 | 18 | In this help file, there's a link to a reference file called UGen done-actions. You can also find a link to this reference file at the bottom of the UGen category in the document browser. Here we can see the available doneActions and the integers associated with them. To be perfectly honest, although there are 15 options, the only doneActions I've ever used are 0 and 2, which are "do nothing" and "free the enclosing synth". 19 | 20 | Let's return to our Line example and see how doneAction works. But first, to make these concepts more clear, let's bring up a visualization of the audio server, which is actually pretty useful in many contexts. We can do this by evaluating 21 | 22 | Server.local 23 | 24 | which, remember, is also stored in the global variable s 25 | 26 | s.plotTree; 27 | 28 | In the following example, we're controlling the amplitude of a pulse wave using a control-rate line that goes from 1 to 0 over 1 second. We haven't specified a value for doneAction, so the default value 0 is used. This means SuperCollider will take no action when the line is complete. 29 | 30 | ( 31 | x = { 32 | var sig, env; 33 | env = Line.kr(1, 0, 1); 34 | sig = Pulse.ar(ExpRand(50,300)) * env; 35 | }.play 36 | ) 37 | 38 | A Synth appears on the visual server window when we run this code. After 1 second, the line is complete, and because we've chosen doneAction:0, no further action is taken by scsynth. This means that even though we don't hear anything, the Synth is still running, as we can see on the visual representation, and it is outputting zeros at the audio rate, which means CPU cycles are being used. In addition to the audio server visualization, we can also see a "1s" on the status bar, which means there is one synth currently active. 39 | 40 | The only way to free this Synth is to do it manually with 41 | 42 | x.free; 43 | 44 | or command period. 45 | 46 | Suppose we evaluate the code above several times in a row: 47 | 48 | ( 49 | x = { 50 | var sig, env; 51 | env = Line.kr(1, 0, 1); 52 | sig = Pulse.ar(ExpRand(50,300)) * env; 53 | }.play 54 | ) 55 | 56 | Not only do these Synths pile up on the server, but we are overwriting the global variable x with each evaluation. This means that x.free will only free the most recently created synth: 57 | 58 | x.free; 59 | 60 | If we try x.free again, SuperCollider complains that the Synth we're trying to free doesn't exist anymore: 61 | 62 | So now, the only option is to free everything from the server using either 63 | 64 | s.freeAll; 65 | 66 | or command period. 67 | 68 | Let's do the same thing, only this time, we'll specify doneAction:2, which means SuperCollider will automatically free the enclosing Synth when the UGen is finished. Now we can run this code as much as we like, and with each Synth that's created, the server will take care of freeing it once the Line is complete. 69 | 70 | ( 71 | x = { 72 | var sig, env; 73 | env = Line.kr(1, 0, 1, doneAction:2); 74 | sig = Pulse.ar(ExpRand(50,300)) * env; 75 | }.play 76 | ) 77 | 78 | In fact, we don't even really need to give this Synth a name anymore. Previously, we needed to name our Synth so that we could free it later, but now SuperCollider is now handling that for us. 79 | 80 | ( 81 | { 82 | var sig, env; 83 | env = Line.kr(1, 0, 1, doneAction:2); 84 | sig = Pulse.ar(ExpRand(50,300)) * env; 85 | }.play 86 | ) 87 | 88 | Let's move on to XLine, which is an exponential version of Line and works in very much the same way. However, it's very important to remember that it's mathematically impossible to interpolate exponentially when including or crossing zero in the output range. So even though a Synth appears on the server when using an XLine from 1 to 0, we don't hear the expected result. 89 | 90 | ( 91 | { 92 | var sig, env; 93 | env = XLine.kr(1, 0, 1, doneAction:2); 94 | sig = Pulse.ar(ExpRand(50,300)) * env; 95 | }.play 96 | ) 97 | 98 | Instead, we need to constrain XLine's start and end points to either the positive or negative domain. 99 | 100 | ( 101 | { 102 | var sig, env; 103 | env = XLine.kr(1, 0.01, 1, doneAction:2); 104 | sig = Pulse.ar(ExpRand(50,300)) * env; 105 | }.play 106 | ) 107 | 108 | Notice that XLine sounds a little more natural than Line. This is because we perceive amplitude exponentially. We hear an amplitude of 0.5 as half as loud as 1, we hear 0.25 as half as loud as 0.5, and so on. So the exponential line makes for a nicer-sounding fade. If we were using decibels, on the other hand, we'd probably want to use Line, since the decibel is a linear measure of loudness. 0dB is twice as loud as -6dB, which is twice as loud as -12dB, and so on. We can convert from decibels to amplitude using dbamp. Here the line dips from normalized output to -40dB 109 | 110 | ( 111 | { 112 | var sig, env; 113 | env = Line.kr(0, -40, 1, doneAction:2); 114 | sig = Pulse.ar(ExpRand(50,300)) * env.dbamp; 115 | }.play 116 | ) 117 | 118 | And for those who are curious, we can convert back to decibels from amplitude using ampdb. 119 | 120 | 0.5.ampdb; 121 | 0.25.ampdb; 122 | 0.125.ampdb; 123 | 124 | Just to demonstrate that Line and XLine aren't restricted to amplitude control, let's use another XLine to control the frequency of the pulse wave. Again, XLine is a sensible choice because like amplitude, we also perceive frequency exponentially. 200Hz is an octave above 100Hz, 400 is an octave about 200, and so on. In this case, both our XLines have the same duration, so it doesn't really matter which one has doneAction:2, as long as one of them has it. 125 | 126 | ( 127 | { 128 | var sig, env, freq; 129 | env = XLine.kr(1, 0.01, 1, doneAction:2); 130 | freq = XLine.kr(880, 110, 1, doneAction:2); 131 | sig = Pulse.ar(freq) * env; 132 | }.play 133 | ) 134 | 135 | Suppose our XLines had different durations and both had doneAction:2. In this case, whichever finishes first will free the Synth: 136 | 137 | ( 138 | { 139 | var sig, env, freq; 140 | env = XLine.kr(1, 0.01, 1, doneAction:2); 141 | freq = XLine.kr(880, 110, 5, doneAction:2); 142 | sig = Pulse.ar(freq) * env; 143 | }.play 144 | ) 145 | 146 | In this case, we hear that the 5 second XLine doesn't have time to get all the way down to 110Hz, because the one-second XLine frees the Synth after it finishes. If the roles were reversed, 147 | 148 | ( 149 | { 150 | var sig, env, freq; 151 | env = XLine.kr(1, 0.01, 5, doneAction:2); 152 | freq = XLine.kr(880, 110, 1, doneAction:2); 153 | sig = Pulse.ar(freq) * env; 154 | }.play 155 | ) 156 | 157 | Then the sound stops abruptly before it has time to fade out all the way to 0.01. One way to fix this is to change the doneAction on the shorter XLine to zero 158 | 159 | ( 160 | { 161 | var sig, env, freq; 162 | env = XLine.kr(1, 0.01, 5, doneAction:2); 163 | freq = XLine.kr(880, 110, 1, doneAction:0); 164 | sig = Pulse.ar(freq) * env; 165 | }.play 166 | ) 167 | 168 | Or remove the doneAction entirely, which has the same effect since the default value is zero. 169 | 170 | But now there's a different sort of problem. After 1 second, the amplitude envelope is still in progress, and the shorter XLine gets down to 110Hz after only 1 second and sits there for the remaining 4 seconds, and this may not be the sound you want. So the real solution is to conceive of all your envelope UGens together, as one sound-producing unit, and make sure that their durations are harmonious with one another. In other words, if you want a ten second sound, don't put doneAction:2 on a one second UGen. 171 | 172 | Let's move on to a more sophisticated envelope generator, called EnvGen. EnvGen makes use of a class of objects called Env. Env is a specification for a breakpoint envelope shape, and has functionality in both the language and on the server. Also, unlike Line and XLine, EnvGen has a gate argument, which means EnvGen can be sustained indefinitely and can also be re-triggered. 173 | 174 | EnvGen's first argument expects an instance of Env, so let's take a look at that class first. The most generalized and all-purpose method for Env is ".new". In the language, an Env can be visualized using the plot method. If we don't provide any arguments to Env.new, SuperCollider uses the defaults, which results in a simple triangle envelope: 175 | 176 | Env.new.plot; 177 | 178 | The first three arguments for Env.new, levels, times, and curve, are probably the most significant. The first argument, levels, should be an Array of numbers representing ordered values that EnvGen will output. The default value is the Array [0, 1, 0], which means the envelope signal will start at zero, rise to a value of 1, and return to zero. The second argument is an array of times. The size of this array is almost always one item smaller than the levels array, because the number of connecting segments is one fewer than the number of level points. In other words, if you have three level points, there are two connecting segments. The default value is the array [1, 1], which means the EnvGen will take 1 second to travel from 0 to 1, and another second to travel from 1 back to zero. The default value for curve is the symbol 'lin', which means the EnvGen will linearly interpolate between level points. If you scroll down, you can see the other options for curve. But for now, let's hear the default Env.new in action: 179 | 180 | ( 181 | { 182 | var sig, env; 183 | env = EnvGen.kr(Env.new, doneAction:2); 184 | sig = Pulse.ar(ExpRand(50,300)) * env; 185 | }.play 186 | ) 187 | 188 | Triangle envelopes are all well and good, but let's provide our own arguments for Env.new. We'll start by changing the levels array. The envelope will start at zero, rise to 1, fall to 0.2, and then fall all the way back to zero from 0.2. Since there are now four level points, I'll need three durations in the second array. I'll use 0.5, 1, and 2 seconds. I'll leave the curve argument alone for now and plot the Env, so that we can see that we have four level values, with linear interpolation, with durations equal to 0.5, 1, and 2. 189 | 190 | Env.new([0,1,0.1,0],[0.5,1,2]).plot; 191 | 192 | Suppose we want exponential interpolation. Changing \lin to \exp in this example won't work, as I mentioned earlier, since you can't interpolate exponentially when zero is part of the output range. So to use \exp, we'd have to change our zero levels to a very small positive number. 193 | 194 | Env.new([0,1,0.1,0],[0.5,1,2], \exp).plot; 195 | 196 | Env.new([0.01,1,0.1,0.01],[0.5,1,2], \exp).plot; 197 | 198 | But a more flexible option is to use a third array of numbers to specify segment curvatures. Positive values make the segment change slowly at first, then quickly, while negative values make the segment change quickly at first, then level off. The size of this array should be equal to the size of the second array, since we need one curvature value for each breakpoint segment. 199 | 200 | In this example 201 | 202 | Env.new([0,1,0.1,0], [0.5,1,2], [3,-3,0]).plot; 203 | 204 | the first curvature value is positive, so the first segment changes slowly, then quickly. The second segment has a negative curvature, so it changes quickly, then levels off. 205 | 206 | The farther away from zero the curvature values get, the more extreme the curvature will be. 207 | 208 | Env.new([0,1,0.1,0], [0.5,1,2], [10,-10,0]).plot; 209 | 210 | If we reverse the first two curvature values, we see that now the first segment changes quickly at first, then levels off, while the second segment changes slowly, then drops more quickly. 211 | 212 | Env.new([0,1,0.1,0], [0.5,1,2], [-3,3,0]).plot; 213 | 214 | We can even replace individual numbers with valid symbols, like this: 215 | 216 | Env.new([0,1,0.1,0], [0.5,1,2], [\sine,\sine,0]).plot; 217 | 218 | Let's hear one of our custom envelopes in action: 219 | 220 | ( 221 | { 222 | var sig, env; 223 | env = EnvGen.kr(Env.new([0,1,0.1,0], [0.5,1,2], [3,-3,0]), doneAction:2); 224 | sig = Pulse.ar(ExpRand(50,300)) * env; 225 | }.play 226 | ) 227 | 228 | I'll now move on to EnvGen's second argument, "gate." In the case of fixed-length envelopes, such as the one we've been dealing with, gate can be used as a trigger, which will reset the envelope. To re-trigger the envelope, gate must change from a non-positive value to a positive value. Generally it's a good idea to just use zero and one. 229 | 230 | ( 231 | x = { 232 | arg gate=0; 233 | var sig, env; 234 | env = EnvGen.kr(Env.new([0,1,0.1,0], [0.5,1,2], [3,-3,0]), gate); 235 | sig = Pulse.ar(LFPulse.kr(8).range(600,900)) * env; 236 | }.play 237 | ) 238 | 239 | Notice, in the EnvGen help file, that gate's default argument is 1, but I'm overwriting it with a value of zero. So the Synth has been created, but the envelope hasn't been triggered yet. All we need to do is set gate equal to 1: 240 | 241 | x.set(\gate, 1); 242 | 243 | A trigger occurs when the value changes from non-positive to positive. Therefore, evaluating this line again won't do anything, since gate has already been set to one. So, one option is to set gate back to zero, then set it to 1 again to retrigger. 244 | 245 | x.set(\gate, 0); 246 | x.set(\gate, 1); 247 | 248 | x.free; 249 | 250 | But it's kind of stupid to do this manually, which is why there is a special trigger argument to take care of this kind of thing. To create a trigger argument, all you have to do is precede a normal argument with t-underscore: 251 | 252 | ( 253 | x = { 254 | arg t_gate=0; 255 | var sig, env; 256 | env = EnvGen.kr(Env.new([0,1,0.1,0], [0.5,1,2], [3,-3,0]), t_gate); 257 | sig = Pulse.ar(LFPulse.kr(8).range(600,900)) * env; 258 | }.play 259 | ) 260 | 261 | t-underscore arguments, according to the SynthDef help file, "will be made as a TrigControl. Setting the argument will create a control-rate impulse at the set value." This means that if you set t_gate equal to 1, it will automatically return to zero in the next control cycle, or approximately 64 samples later. This means we can re-trigger the envelope in a more intuitive way, like this: 262 | 263 | x.set(\t_gate, 1) 264 | 265 | x.free; 266 | 267 | Maybe we want our Synth to trigger its envelope as soon as we create it. In this case, it makes more sense to set gate's default argument to 1 instead of zero. 268 | 269 | ( 270 | x = { 271 | arg t_gate=1; 272 | var sig, env; 273 | env = EnvGen.kr(Env.new([0,1,0.1,0], [0.5,1,2], [3,-3,0]), t_gate); 274 | sig = Pulse.ar(LFPulse.kr(8).range(600,900)) * env; 275 | }.play 276 | ) 277 | 278 | and of course the envelope is still re-triggerable. 279 | 280 | x.set(\t_gate, 1); 281 | 282 | x.free; 283 | 284 | It's important to use the correct doneAction when dealing with a re-triggerable envelope. In this case, if we use doneAction:2, then the envelope will be retriggerable so long as it does not reach the end. If the envelope is allowed to finish, then the synth will free itself and will no longer be accessible, as we can see from the message in the post window. When using doneAction:0, once the envelope finishes, it will output its last value until it is retriggered, and the Synth will remain on the server untill we free it. 285 | 286 | ( 287 | x = { 288 | arg t_gate=1; 289 | var sig, env; 290 | env = EnvGen.kr(Env.new([0,1,0.1,0], [0.5,1,2], [3,-3,0]), t_gate, doneAction:2); 291 | sig = Pulse.ar(LFPulse.kr(8).range(600,900)) * env; 292 | }.play 293 | ) 294 | 295 | x.set(\t_gate, 1); 296 | 297 | If you want a fixed-length envelope to be re-triggerable, it's best to use doneAction:0 and to re-trigger using a t-underscore argument. If you want a one-shot sound with a fixed-length envelope, it's better to use doneAction:2 and to create multiple synths. It all depends on the nature of the sound you're trying to make. 298 | 299 | The last thing I'll discuss in this tutorial in the adsr envelope. We've looked at Env.new, but there's also Env.adsr. adsr stands for attack-decay-sustain-release. The fundamental difference between adsr and the previous examples is that adsr has a sustain portion, which means it can be sustained indefinitely, so long as the gate argument remains positive. 300 | 301 | adsr takes 7 arguments: an attack time, a decay time, a sustain level, a release time, a peak level, a curvature, and a bias, and these are all just numbers, as opposed to Env.new which expects arrays of numbers. Here I'll just use the default values, but if you've been following this series so far, you should have no problem putting in your own values for adsr. 302 | 303 | ( 304 | x = { 305 | arg gate=0; 306 | var sig, env; 307 | env = EnvGen.kr(Env.adsr, gate); 308 | sig = VarSaw.ar(SinOsc.kr(16).range(500,1000)) * env; 309 | }.play 310 | ) 311 | 312 | gate is zero by default, so all we have to do is open the gate to trigger the adsr envelope 313 | 314 | x.set(\gate, 1); 315 | 316 | there's a quick attack to an amplitude of 1, and a 0.3 second decay to a sustain amplitude of 0.5. The sound will sit at this level until the gate is set to zero, which will trigger a 1 second release. 317 | 318 | x.set(\gate, 0); 319 | 320 | Because there's no doneAction:2, the Synth is still hanging around, so it can be retriggered. 321 | 322 | x.free; 323 | 324 | Notice that it makes less sense to use a t-underscore argument for a gate when dealing with a sustainable envelope. If we were to use a trigger argument, then as soon as the envelope is triggered, t_gate will almost immediately return to zero, which will trigger the release phase of the adsr envelope. 325 | 326 | ( 327 | x = { 328 | arg t_gate=0; 329 | var sig, env; 330 | env = EnvGen.kr(Env.adsr, t_gate); 331 | sig = VarSaw.ar(SinOsc.kr(16).range(500,1000)) * env; 332 | }.play 333 | ) 334 | 335 | x.set(\t_gate, 1) 336 | 337 | x.free; 338 | 339 | So with t_gate and an adsr envelope, there's actually no way to sustain the sound. Therefore it's better to use normal gate arguments for sustaining envelopes. 340 | 341 | And of course, if we use doneAction:2, then the Synth will be removed after the envelope finishes. 342 | 343 | ( 344 | x = { 345 | arg gate=0; 346 | var sig, env; 347 | env = EnvGen.kr(Env.adsr, gate, doneAction:2); 348 | sig = VarSaw.ar(SinOsc.ar(20).range(500,1000)) * env; 349 | }.play 350 | ) 351 | 352 | x.set(\gate, 1); 353 | x.set(\gate, 0); 354 | 355 | Last, here's an example of using a second adsr envelope to control the frequency modulation of the oscillator sound source. I'll make use of EnvGen's 3rd and 4th arguments, levelScale and levelBias, which are almost exactly like mul and add. I'll increase the attack time of the frequency control so that the effect is more audible. Also, since these two envelopes have the same gate argument and the same release time, they will end simultaneously, so it doesn't matter which envelope has the doneAction:2 356 | 357 | ( 358 | x = { 359 | arg gate=0; 360 | var sig, env, freq; 361 | env = EnvGen.kr(Env.adsr, gate, doneAction:2); 362 | freq = EnvGen.kr(Env.adsr(1), gate, 200, 0.1); 363 | sig = VarSaw.ar(SinOsc.ar(freq).range(300,500)) * env; 364 | }.play 365 | ) 366 | 367 | x.set(\gate, 1); 368 | x.set(\gate, 0); 369 | 370 | In the Env help file, you can find many other class methods in addition to .new and .adsr. For fixed-duration envelopes, there's a triangle shape, a sinusoid shape, a percussive shape, and others. For sustained envelopes, there's also dadsr, which has an initial delay time, and asr, which does not have a decay segment. EnvGen, especially considering its combination with Env, is a pretty deep UGen with a lot of potential that I haven't discussed in this video. But hopefully this material is enough to clarify the basic concepts. 371 | 372 | That's it for tutorial number 4. In the next video, I'll talk about multi-channel expansion, which is an incredibly powerful and convenient shortcut for creating rich and complex sounds. If you've been enjoying this series so far, please consider giving a thumbs up and subscribing to my channel. Thanks for watching. 373 | 374 | -------------------------------------------------------------------------------- /full video scripts/05_script.scd: -------------------------------------------------------------------------------- 1 | s.boot; 2 | 3 | Hey everyone, welcome to tutorial number 5, where I'll talk about how SuperCollider deals with multichannel sound. You might have noticed that most of the sound we've been producing so far is monophonic, in other words, a single channel of sound being sent to one speaker. Multichannel expansion is a convenient feature of SuperCollider in which an Array of UGens is translated into multiple channels of audio. 4 | 5 | Since we're going to be talking about multichannel audio, let's bring up the level meters for inputs and outputs, which we can create by evaluating s.meter or the default shortcut, command-M. 6 | 7 | s.meter; 8 | 9 | It's probably hard to see since it's so small, but on the left half of this window, there are 8 level meters corresponding to input signals, and on the right, 8 level meters corresponding to output signals. I'm using a separate microphone for recording this tutorial, which is why you don't see the level meters mirroring my voice. You can independently change the number of inputs and outputs, but we don't need to deal with that right now, so I'll save it for another tutorial. 10 | 11 | The last thing I'll point out before we dig into multichannel expansion is that there's a help document called Multichannel expansion, which you can read if you want more information. 12 | 13 | The essence of multichannel expansion is that whereas a single UGen produces a single channel of audio, an Array of UGens will produce multiple channels of audio. When asked to play a multichannel signal, SuperCollider will output the channels on busses with consecutive, ascending indices. And just to review, an Array is an ordered collection of items, delineated by an enclosure of square brackets, with items separated by commas, such as this. 14 | 15 | [1, 2, 3]; 16 | 17 | For example, we've seen this before. A function like this, with a single UGen will produce a monophonic output on bus zero, as we can see on the levels window. 18 | 19 | x = {SinOsc.ar}.play; 20 | x.free; 21 | 22 | But if we play an Array of two UGens, SuperCollider translates the array into a two-channel output signal. Because we don't specify otherwise, SuperCollider plays the first signal on output bus 0, and the second on output bus 1. 23 | 24 | x = {[SinOsc.ar, SinOsc.ar]}.play; 25 | x.free; 26 | 27 | To hear this effect more clearly, we can have these two oscillators run at different frequencies: 28 | 29 | x = {[SinOsc.ar(300), SinOsc.ar(500)]}.play; 30 | x.free; 31 | 32 | We hear 300Hz in the left speaker, and 500Hz in the right. 33 | 34 | This example can be written even more efficiently, by using an internal Array of arguments instead of an Array of UGens. In the following example, we use the Array [300, 500] as the frequency argument, and SuperCollider expands this expression into an Array of two SinOscs in which the frequency values are consecutively distributed, which is essentially identical to the example above: 35 | 36 | x = {SinOsc.ar([300, 500])}.play; 37 | x.free; 38 | 39 | If we perform mathematical operations involving two multichannel UGens, then the arguments for one UGen will correspond with the arguments for the other UGen. For example, I'll add a two-channel amplitude control signal and multiply it by the audio output. 40 | 41 | ( 42 | x = { 43 | var amp, sig; 44 | amp = SinOsc.kr([7,1]).range(0,1); 45 | sig = SinOsc.ar([300,500]); 46 | sig = sig * amp; 47 | }.play; 48 | ) 49 | 50 | x.free; 51 | 52 | When we multiply these two signals together, the first channel of the amplitude control signal is multiplied by the first channel of the audio signal, and the same goes for the 2nd channel of each signal. So as a result, the amplitude of the 300Hz tone in the left speaker fluctuates 7 times per second, while the frequency of the 500Hz tone in the right speaker fluctuates once per second. 53 | 54 | If we perform mathematical operations with two multichannel signals that don't have the same number of channels, then the resulting signal will have as many channels as the longer array. The shorter array will wrap back around to the beginning to account for the longer array. So for example, if we multiply a two-channel signal by a one-channel signal, then the one-channel signal will be applied to each of the two-channels in the other signal. In this example, the 7Hz amplitude pulsing is applied to both the 300Hz oscillator and the 500Hz oscillator. 55 | 56 | ( 57 | x = { 58 | var amp, sig; 59 | amp = SinOsc.kr(7).range(0,1); 60 | sig = SinOsc.ar([300,500]); 61 | sig = sig * amp; 62 | }.play; 63 | ) 64 | 65 | x.free; 66 | 67 | Of course, we could make arrays that have size greater than two, but if you've only got two speakers, there's not much point since the extra channels have nowhere to go. With this example, even though we can see that SuperCollider is producing five channels, we only hear the first two. 68 | 69 | ( 70 | x = { 71 | var amp, sig; 72 | amp = SinOsc.kr([7, 1, 2, 0.2, 6]).range(0,1); 73 | sig = SinOsc.ar([300, 500, 700, 900, 1100]); 74 | sig = sig * amp; 75 | }.play; 76 | ) 77 | 78 | x.free; 79 | 80 | There is, however, a UGen called Mix that takes a UGen Array of arbitrary size and mixes the discrete signals down to a single channel. In this case I'm also going to scale the amplitude down to 1/4th to avoid clipping. 81 | 82 | ( 83 | x = { 84 | var amp, sig; 85 | amp = SinOsc.kr([7, 1, 2, 0.2, 6]).range(0,1); 86 | sig = SinOsc.ar([300, 500, 700, 900, 1100]); 87 | sig = sig * amp; 88 | sig = Mix(sig) * 0.25; 89 | }.play; 90 | ) 91 | 92 | x.free; 93 | 94 | Since we're listening to the output of the Mix UGen, we're back to dealing with monophonic sound, so you could consider applying multichannel expansion to the mixed signal, like this 95 | 96 | ( 97 | x = { 98 | var amp, sig; 99 | amp = SinOsc.kr([7, 1, 2, 0.2, 6]).range(0,1); 100 | sig = SinOsc.ar([300, 500, 700, 900, 1100]); 101 | sig = sig * amp; 102 | sig = [Mix(sig), Mix(sig)] * 0.25; 103 | }.play; 104 | ) 105 | 106 | x.free; 107 | 108 | Now's a good time to introduce the ".dup" method, which when applied to any object, returns an array of duplicates, like this: 109 | 110 | 60.dup(4); 111 | 112 | This means that the above example could be rewritten in the following way: 113 | 114 | 115 | ( 116 | x = { 117 | var amp, sig; 118 | amp = SinOsc.kr([7, 1, 2, 0.2, 6]).range(0,1); 119 | sig = SinOsc.ar([300, 500, 700, 900, 1100]); 120 | sig = sig * amp; 121 | sig = Mix(sig).dup(2) * 0.25; 122 | }.play; 123 | ) 124 | 125 | x.free; 126 | 127 | The exclamation point is available as a syntactical shortcut for .dup 128 | 129 | 60!4; 130 | 131 | So the last line of the above example can also be written like this: 132 | 133 | ( 134 | x = { 135 | var amp, sig; 136 | amp = SinOsc.kr([7, 1, 2, 0.2, 6]).range(0,1); 137 | sig = SinOsc.ar([300, 500, 700, 900, 1100]); 138 | sig = sig * amp; 139 | sig = Mix(sig)!2 * 0.25; 140 | }.play; 141 | ) 142 | 143 | x.free; 144 | 145 | Splay is a UGen somewhat similar to Mix, but instead of mixing down to a single channel, Splay will spread an arbitrarily large array of channels across a stereo field, resulting in a more complex sound. You'll hear it most clearly if you use headphones. 146 | 147 | ( 148 | x = { 149 | var amp, sig; 150 | amp = SinOsc.kr([7, 1, 2, 0.2, 6]).range(0,1); 151 | sig = SinOsc.ar([300, 500, 700, 900, 1100]); 152 | sig = sig * amp; 153 | sig = Splay.ar(sig) * 0.25; 154 | }.play; 155 | ) 156 | 157 | x.free; 158 | 159 | I want to take a brief detour from this example to illustrate an important nuance of UGen duplication. In the following example, I'm creating pink noise at half-amplitude, and then duplicating this UGen. As a result, an exact copy of the instance of PinkNoise is created, and so we hear the exact same audio in both channels, and we can see this clearly on the output level meters. 160 | 161 | x = {PinkNoise.ar(0.5)!2}.play; 162 | x.free; 163 | 164 | However, if duplication occurs on an argument within the UGen, then the argument is duplicated, but SuperCollider creates a unique instance of the UGen for each argument. Therefore we can see and hear that the output of each channel is unique. This nuance is less obvious in deterministic UGens like SinOsc, but it's important to bear in mind with noise generators like this. 165 | 166 | x = {PinkNoise.ar(0.5!2)}.play; 167 | x.free; 168 | 169 | Let's return to the previous example, and I'll convert it into a SynthDef in order to illustrate another common pitfall. Remember that in a SynthDef, if you want to output a signal, you need to include an output UGen. ... So here an important question arises: what do we supply for the bus argument of Out.ar? 170 | 171 | ( 172 | SynthDef.new(\multi, { 173 | var amp, sig; 174 | amp = SinOsc.kr([7, 1, 2, 0.2, 6]).range(0,1); 175 | sig = SinOsc.ar([300, 500, 700, 900, 1100]); 176 | sig = sig * amp; 177 | sig = Splay.ar(sig) * 0.25; 178 | Out.ar(0, sig); 179 | }).add; 180 | ) 181 | 182 | x = Synth.new(\multi); 183 | x.free; 184 | 185 | If we specify bus 0, SuperCollider writes the first channel to this bus, and correctly assumes that we want the remaining channels on consecutive, ascending busses. So this is the correct approach. 186 | 187 | It's not uncommon to try something like this, and in fact, it looks pretty reasonable. We've got a stereo signal, and we want to write it to busses 0 and 1: 188 | 189 | ( 190 | SynthDef.new(\multi, { 191 | var amp, sig; 192 | amp = SinOsc.kr([7, 1, 2, 0.2, 6]).range(0,1); 193 | sig = SinOsc.ar([300, 500, 700, 900, 1100]); 194 | sig = sig * amp; 195 | sig = Splay.ar(sig) * 0.25; 196 | Out.ar([0,1], sig); 197 | }).add; 198 | ) 199 | 200 | x = Synth.new(\multi); 201 | x.free; 202 | 203 | When we create the Synth, it doesn't necessarily sound wrong, but there's something unusual happening with the level meters. 204 | 205 | Here's what's actually happening here. By specifying an array of busses for Out.ar, we are accidentally invoking multichannel expansion on the output UGen, which is already processing a multichannel signal. So as a result, the stereo signal is written to bus 0, which causes it to appear on busses 0 and 1, but it is ALSO being written to bus 1, causing the stereo signal to appear on busses 1 and 2 as well. This means there's signal overlap on bus 1, which is why that level indicator is higher than on bus 0 or 2. So this isn't right at all. The correct approach is to not invoke multichannel expansion on output UGens. Instead, just specify the lowest numbered bus for output UGens, usually zero, and let SuperCollider handle the consecutive distribution of audio channels. 206 | 207 | ( 208 | SynthDef.new(\multi, { 209 | var amp, sig; 210 | amp = SinOsc.kr([7, 1, 2, 0.2, 6]).range(0,1); 211 | sig = SinOsc.ar([300, 500, 700, 900, 1100]); 212 | sig = sig * amp; 213 | sig = Splay.ar(sig) * 0.25; 214 | Out.ar(0, sig); 215 | }).add; 216 | ) 217 | 218 | x = Synth.new(\multi); 219 | x.free; 220 | 221 | Let's look at duplication of randomly generated numbers. Like the previous example with pink noise, there's a similar but different nuance to keep in mind. In the following example, first, a random number is chosen between 50 and 1200, and then that number is duplicated into an array of size four. So, everytime we evaluate this line, we'll get an array of 4 copies of a randomly generated value 222 | 223 | rrand(50,1200)!4; 224 | 225 | However, if we surround rrand with curly braces, we create a function, and functions respond to duplication in a different way. The difference is that the contents of the function are evaluated each time it's duplicated, so here we will get an array of uniquely generated random numbers: 226 | 227 | {rrand(50,1200)}!4; 228 | 229 | Again, it's a subtle difference in syntax, but an important one. 230 | 231 | Let's try adding some multichannel randomness to our example. Instead of arrays of fixed values, i'll randomize the amplitudes and frequencies using the technique I just demonstrated. I'll use curly braces to delineate functions, and I'll duplicate them each 8 times. 232 | 233 | ( 234 | SynthDef.new(\multi, { 235 | var amp, sig; 236 | amp = SinOsc.kr({exprand(0.2,12)}!8).range(0,1); 237 | sig = SinOsc.ar({exprand(50,1200)}!8); 238 | sig = sig * amp; 239 | sig = Splay.ar(sig) * 0.25; 240 | Out.ar(0, sig); 241 | }).add; 242 | ) 243 | 244 | x = Synth.new(\multi); 245 | x.free; 246 | 247 | If I take away the curly braces, SuperCollider will create arrays of eight identical numbers, so the complexity of the sound will be greatly reduced. In fact, in this case, each of the 8 channels will be exactly the same, so the whole point of multichannel expansion is lost. 248 | 249 | ( 250 | SynthDef.new(\multi, { 251 | var amp, sig; 252 | amp = SinOsc.kr(exprand(0.2,12)!8).range(0,1); 253 | sig = SinOsc.ar(exprand(50,1200)!8); 254 | sig = sig * amp; 255 | sig = Splay.ar(sig) * 0.25; 256 | Out.ar(0, sig); 257 | }).add; 258 | ) 259 | 260 | x = Synth.new(\multi); 261 | x.free; 262 | 263 | But, using the language operator exprand, even with curly braces, isn't the best option for SynthDefs. Using lowercase exprand in a SynthDef chooses random values when the SynthDef is compiled, and these random values remain fixed, for every instance of Synth that's created. So here, even though I'm creating several Synths, you can hear that the randomly chosen values are the same each time. 264 | 265 | ( 266 | SynthDef.new(\multi, { 267 | var amp, sig; 268 | amp = SinOsc.kr({exprand(0.2,12)}!8).range(0,1); 269 | sig = SinOsc.ar({exprand(50,1200)}!8); 270 | sig = sig * amp; 271 | sig = Splay.ar(sig) * 0.25; 272 | Out.ar(0, sig); 273 | }).add; 274 | ) 275 | 276 | x = Synth.new(\multi); 277 | x.free; 278 | 279 | The best option is to use the UGen ExpRand, with capital E capital R. While lowercase exprand picks values when the SynthDef is compiled, uppercase ExpRand chooses random values when the Synth is created. I'll create multiple synths just like I did a second ago, but here, listen to the unique frequencies and amplitudes of each generated Synth: 280 | 281 | ( 282 | SynthDef.new(\multi, { 283 | var amp, sig; 284 | amp = SinOsc.kr({ExpRand(0.2,12)}!8).range(0,1); 285 | sig = SinOsc.ar({ExpRand(50,1200)}!8); 286 | sig = sig * amp; 287 | sig = Splay.ar(sig) * 0.25; 288 | Out.ar(0, sig); 289 | }).add; 290 | ) 291 | 292 | x = Synth.new(\multi); 293 | x.free; 294 | 295 | You can find more random number generator UGens by looking in Browse, UGens, ... Random. 296 | 297 | Just to make things sound extra nice, I'll add an envelope to this SynthDef for a nice smooth 10 second attack and 10 second release. I'll use doneAction:2 so each Synth will free itself when the envelope is complete. Now we can easily create a rich, complex texture of sine waves. 298 | 299 | ( 300 | SynthDef.new(\multi, { 301 | var amp, sig, env; 302 | env = EnvGen.kr( 303 | Env.new([0,1,0],[10,10],[1,-1]), 304 | doneAction:2 305 | ); 306 | amp = SinOsc.kr({ExpRand(0.2,12)}!8).range(0,1); 307 | sig = SinOsc.ar({ExpRand(50,1200)}!8); 308 | sig = sig * amp * env; 309 | sig = Splay.ar(sig) * 0.25; 310 | Out.ar(0, sig); 311 | }).add; 312 | ) 313 | 314 | x = Synth.new(\multi); 315 | 316 | That's it for tutorial number 5. There are lots of other multichannel UGens, which can be found under Browse, UGens, and Multichannel. There are additional categories at the top of the list of Multichannel UGens. Multichannel expansion with Arrays is very powerful, but it can also be difficult to conceptualize. So to conclude, I suggest the best way to grasp multichannel expansion is to experiment with it, and make sure to use the level indicators so you can see what's happening. In the next video I'll talk about iteration in SuperCollider and how it can be used for iterative synthesis. Thanks for watching. -------------------------------------------------------------------------------- /full video scripts/06_script.scd: -------------------------------------------------------------------------------- 1 | Hey everyone, welcome to tutorial number 6. In this video I'll talk about iteration and how it can be used for audio synthesis. Iteration is a topic in and of itself, but essentially, iteration is a repeating process. It enables the user to execute a block of code many times over, usually until some condition is met or a certain number of iterations have been performed. 2 | 3 | It's very common to iterate over a collection of items. And by this I mean you take a collection, and one-by-one, pass the individual items into a function as input arguments. In the class library there's an object called Collection, which is the parent of many useful subclasses, listed here at the top. There's also a file called Collections, that outlines the subclass hierarchy. One of these subclasses is Array, which we're already sort of familiar with. 4 | 5 | Back in the Collection help file, we can do a search for the word "iteration", which takes us to a list of relevant methods. There are quite a few, but the most generalized method is called "do", which simply evaluates a function for each item in the collection. 6 | 7 | To iterate over a collection, we first need to create a collection. So here's an array of several numbers 8 | 9 | [6, 4, 0.5, 10, 7] 10 | 11 | and to iterate over this array, we'll send it the message "do", and provide a function. I'll keep things fairly simple, and just print "hello" for each iteration. 12 | 13 | [6, 4, 0.5, 10, 7].do{"hello".postln}; 14 | 15 | Because there are five items in the array, we see the word 'hello,' 5 times. We also see the original array in the post window, and this is because do always returns its receiver. This is usually irrelevant, and it's probably best to ignore this or just think of it as a side effect of using do. 16 | 17 | More importantly, you might have noticed that the same result would have been produced from an array of ANY five items, so this is not a particularly instructive example. 18 | 19 | In many cases, you'll want to incorporate somehow the collection's items into the function. For example, if you have a list of 200 file names, you might want to change the extension on all of these files names, and this is where the real strength of iteration comes into play. To pass items into a function, we declare an input argument, whose value will range over the contents of the collection. In this particular function, we're going to square each item and post the result. 20 | 21 | ( 22 | [6, 4, 0.5, 10, 7].do{ 23 | arg item; 24 | item.squared.postln; 25 | } 26 | ) 27 | 28 | In some cases, you might also want to keep track of how many times the function has been evaluated, or in other words, how many iterations have occured. This is done by adding a second argument to the function, like this. Now, on each iteration, I'll instead post an array containing the current number of iterations, and the current item, squared. 29 | 30 | ( 31 | [6, 4, 0.5, 10, 7].do{ 32 | arg item, count; 33 | [count, item.squared].postln; 34 | } 35 | ) 36 | 37 | You might notice that we're not actually storing the results of this data manipulation anywhere, we're just posting information. Using do, if you'd actually like to return and store a modified collection, you'd have to change this array manually within the function. First, it's probably a good idea to create a new empty array to hold our resulting calculations, since it's generally risky and perhaps undesirable to overwrite data. The function squares every item in the array and stores each result at the corresponding index in the array x. 38 | 39 | ( 40 | x = Array.newClear(5); 41 | 42 | [6, 4, 0.5, 10, 7].do{ 43 | arg item, count; 44 | x[count] = item.squared; 45 | }; 46 | ) 47 | 48 | The last evaluated statement in this clump of code is a do statement, which, as we've seen, returns its receiver, but sure enough, if we query the global variable x, we can see it is the collection of our new data. 49 | 50 | x; 51 | 52 | But 'do' isn't the best method to use for something like this. It's much easier to use a method called collect, which returns a new collection instead of just returning its receiver. While we've explicitly stored new data in the function above, collect does this automatically. So all we'd have to type is this; 53 | 54 | ( 55 | z = [6, 4, 0.5, 10, 7].collect{ 56 | arg item; 57 | item.squared; 58 | }; 59 | ) 60 | 61 | And for anyone who has a sweet tooth for syntactic sugar, you can even use this condensed underscore notation. 62 | 63 | z = [6, 4, 0.5, 10, 7].collect(_.squared); 64 | 65 | One more thing before we move on to audio, I'll point out that positive integers respond to "do" by iterating from 0 up to and not including their value. So something like 66 | 67 | 5.do; 68 | 69 | is effectively the same as the array 0 through 4 .do 70 | 71 | [0,1,2,3,4].do 72 | 73 | and just to prove this, I'll iterate over the array, and post each item, 74 | 75 | [0,1,2,3,4].do{arg item; item.postln}; 76 | 77 | and then execute 5.do and post each item: 78 | 79 | 5.do{arg item; item.postln}; 80 | 81 | We're using different receivers, but the results of these two functions are indistinguishable. 82 | 83 | s.boot; 84 | 85 | So now, let's move on to iterative synthesis in order to see how we can use "do" with UGens. Take, for instance, this simple variable duty sawtooth wave. 86 | 87 | x = {VarSaw.ar(40!2, 0, 0.05)}.play; 88 | x.free; 89 | 90 | It's nice, but, uh, you know, it's not particularly interesting or anything. But with iteration, we can layer many of these sounds together to create something much richer. In the following SynthDef I'll declare two variables, temp and sum. Temp will be used within the iteration block to hold a temporary signal, and sum will be the eventual output signal. So to start, I'll initialize sum to zero. Within the iteration block, which will be evaluated 10 times, I'll create an audio rate VarSaw, but I'll randomize the frequency a little bit. The second argument is the phase, and the third argument is the duty cycle. Then, I'll add this signal to the output signal and store the result. So what'll happen here is that this function will be evaluated ten times, and on each iteration, we create a stereo VarSaw whose frequency is slightly and uniquely offset from 40Hz, and then add this signal to a running total. By the end, we've accumulated 10 unique sawtooth waves, summed together. Because of this, it's a good idea to scale down this signal since it's probably going to clip. And last but not least, we need an output UGen. 91 | 92 | ( 93 | SynthDef.new(\iter, { 94 | var temp, sum; 95 | sum = 0; 96 | 10.do{ 97 | temp = VarSaw.ar( 98 | 40 * {Rand(0.99,1.02)}!2, 99 | 0, 100 | 0.05 101 | ); 102 | sum = sum + temp; 103 | }; 104 | sum = sum * 0.05; 105 | Out.ar(0, sum); 106 | }).add; 107 | ) 108 | 109 | Before we dive into this example, I just want to point out what happens if we don't initialize sum. Without a starting value, sum is equal to 'nil' which is a special value given to variables that don't have a value. SuperCollider doesn't know how to add things to nil (and quite frankly it doesn't make sense to me either), so it runs into trouble on this line. Sure enough, SuperCollider tells us that it couldn't make heads or tails of the plus operator. So that's why we need to initialze our output signal. 110 | 111 | x = Synth.new(\iter); 112 | x.free; 113 | 114 | Right off the bat, this sounds a lot more interesting than the single VarSaw. But as you might have heard, there's a huge pop at the beginning of this sound. And it's pretty ugly. and that's because these VarSaws all have the same initial phase offset, zero. So if we randomize the phase as well, we can smooth out the sound a bit. 115 | 116 | ( 117 | SynthDef.new(\iter, { 118 | var temp, sum; 119 | sum = 0; 120 | 10.do{ 121 | temp = VarSaw.ar(40 * {Rand(0.99,1.02)}!2, {Rand(0.0,1.0)}!2, 0.05); 122 | sum = sum + temp; 123 | }; 124 | sum = sum * 0.05; 125 | Out.ar(0, sum); 126 | }).add; 127 | ) 128 | 129 | x = Synth.new(\iter); 130 | x.free; 131 | 132 | And, you know, while we're at it, we could randomize the duty cycle as well. 133 | 134 | ( 135 | SynthDef.new(\iter, { 136 | var temp, sum; 137 | sum = 0; 138 | 10.do{ 139 | temp = VarSaw.ar(40 * {Rand(0.99,1.02)}!2, {Rand(0.0,1.0)}!2, {ExpRand(0.005, 0.05)}!2); 140 | sum = sum + temp; 141 | }; 142 | sum = sum * 0.05; 143 | Out.ar(0, sum); 144 | }).add; 145 | ) 146 | 147 | x = Synth.new(\iter); 148 | x.free; 149 | 150 | I'm gonna throw a doneAction:2 on this SynthDef so I don't have to worry about freeing these Synths anymore. 151 | 152 | ( 153 | SynthDef.new(\iter, { 154 | var temp, sum, env; 155 | env = EnvGen.kr( 156 | Env.perc(0.01, 5, 1, -2), 157 | doneAction:2 158 | ); 159 | sum = 0; 160 | 10.do{ 161 | temp = VarSaw.ar(40 * {Rand(0.99,1.02)}!2, {Rand(0.0,1.0)}!2, {ExpRand(0.005, 0.05)}!2); 162 | sum = sum + temp; 163 | }; 164 | sum = sum * 0.05 * env; 165 | Out.ar(0, sum); 166 | }).add; 167 | ) 168 | 169 | Synth.new(\iter); 170 | 171 | 172 | Here's a frequency argument, so that now, we can specify any frequency we like when we instantiate or set the Synth. 173 | 174 | ( 175 | SynthDef.new(\iter, { 176 | arg freq=40; 177 | var temp, sum, env; 178 | env = EnvGen.kr( 179 | Env.perc(0.01, 5, 1, -2), 180 | doneAction:2 181 | ); 182 | sum = 0; 183 | 10.do{ 184 | temp = VarSaw.ar(freq * {Rand(0.99,1.02)}!2, {Rand(0.0,1.0)}!2, {ExpRand(0.005, 0.05)}!2); 185 | sum = sum + temp; 186 | }; 187 | sum = sum * 0.05 * env; 188 | Out.ar(0, sum); 189 | }).add; 190 | ) 191 | 192 | Synth.new(\iter, [\freq, 400]); 193 | Synth.new(\iter, [\freq, 300]); 194 | Synth.new(\iter, [\freq, 250]); 195 | Synth.new(\iter, [\freq, 224]); 196 | 197 | If you prefer, you can deal in midi note numbers, using the method 'midicps' 198 | 199 | Synth.new(\iter, [\freq, 66.midicps]); 200 | Synth.new(\iter, [\freq, 73.midicps]); 201 | Synth.new(\iter, [\freq, 80.midicps]); 202 | Synth.new(\iter, [\freq, 75.midicps]); 203 | 204 | And hey, get this. We can even use iteration again to generate multiple synths at the same time: 205 | 206 | ( 207 | [53,59,63,68].do{ 208 | arg midinote; 209 | Synth.new(\iter, [\freq, midinote.midicps]); 210 | }; 211 | ) 212 | 213 | Although, to be honest, I would generally not recommend using language-side looping and iteration to create simultaneous Synths, since you run the risk of losing sample-accuracy if you're using a lot of complicated or heavy-duty UGens. But as you heard, even if these four synths aren't technically simultaneous down to the sample-level, it still sounds pretty damn close. 214 | 215 | Here, is another example. Unlike the previous SynthDef, this time I'll actually incorporate the iteration count into the audio manipulation. So I'll define an argument within the iteration block, call it 'count'. Notice that I don't actually need a second argument for the iteration count, because when using integer dot do, the items in the collection and the iteration count are actually the same stream of numbers, so using two arguments is redundant. I'll create an overtone of a fundamental pitch by multiplying the frequency by the iteration count. I have to add one because the iteration count starts at zero, and we don't want an oscillator with a frequency of 0Hz. The rest of this example is no different from the previous example. 216 | 217 | ( 218 | SynthDef.new(\iter2, { 219 | arg freq=200; 220 | var temp, sum; 221 | sum = 0; 222 | 10.do{ 223 | arg count; 224 | temp = SinOsc.ar(freq * (count + 1)); 225 | sum = sum + temp; 226 | }; 227 | sum = sum * 0.05; 228 | Out.ar(0, sum); 229 | }).add; 230 | ) 231 | 232 | x = Synth(\iter2); 233 | x.free; 234 | 235 | What we hear is a stack of 10 partials with a fundamental of 200Hz. Now, granted, there's already a UGen that does this, called blip. 236 | 237 | x = {Blip.ar(200, 10, 0.5)}.play 238 | x.free; 239 | 240 | But with iteration, we have a lot more control over the individual partials than we do with blip. For instance, we can have the frequency of each overtone meander very slightly using LFNoise1. I'll also invoke multichannel expansion on LFNoise so that we have two unique channels of audio. 241 | 242 | ( 243 | SynthDef.new(\iter2, { 244 | arg freq=200; 245 | var temp, sum; 246 | sum = 0; 247 | 10.do{ 248 | arg count; 249 | temp = SinOsc.ar(freq * (count + 1) * LFNoise1.kr({Rand(0.05,0.2)}!2).range(0.98,1.02)); 250 | sum = sum + temp; 251 | }; 252 | sum = sum * 0.05; 253 | Out.ar(0, sum); 254 | }).add; 255 | ) 256 | 257 | x = Synth(\iter2); 258 | x.free; 259 | 260 | We can add another statement within the iteration block to have the amplitude of each partial fluctuate randomly. 261 | 262 | ( 263 | SynthDef.new(\iter2, { 264 | arg freq=200; 265 | var temp, sum; 266 | sum = 0; 267 | 10.do{ 268 | arg count; 269 | temp = SinOsc.ar(freq * (count + 1) * LFNoise1.kr({Rand(0.05,0.2)}!2).range(0.98,1.02)); 270 | temp = temp * LFNoise1.kr({Rand(0.5,8)}!2).exprange(0.01, 1); 271 | sum = sum + temp; 272 | }; 273 | sum = sum * 0.05; 274 | Out.ar(0, sum); 275 | }).add; 276 | ) 277 | 278 | x = Synth(\iter2); 279 | 280 | and don't forget that we can change the frequency since we have declared an argument for it: 281 | 282 | x.set(\freq, 50); 283 | x.free; 284 | 285 | Maybe you'd want to control the amount of frequency deviation in addition to frequency. We can declare an argument, and then set the range of LFNoise1 like this. 286 | 287 | ( 288 | SynthDef.new(\iter2, { 289 | arg freq=200, dev=1.02; 290 | var temp, sum; 291 | sum = 0; 292 | 10.do{ 293 | arg count; 294 | temp = SinOsc.ar( 295 | freq * 296 | (count + 1) * 297 | LFNoise1.kr({Rand(0.05,0.2)}!2).range(dev.reciprocal, dev) 298 | ); 299 | temp = temp * LFNoise1.kr({Rand(0.5,8)}!2).exprange(0.01, 1); 300 | sum = sum + temp; 301 | }; 302 | sum = sum * 0.05; 303 | Out.ar(0, sum); 304 | }).add; 305 | ) 306 | 307 | x = Synth(\iter2); 308 | 309 | x.set(\dev, 1.05); 310 | x.set(\dev, 1.1); 311 | x.set(\dev, 1.4); 312 | 313 | x.free; 314 | 315 | Here's a problem you might run into. Let's say we want even more partials. A natural instinct is to replace the integer receiver of 'do' with an argument, so that we could change it just like we'd change the frequency or frequency deviation. 316 | 317 | ( 318 | SynthDef.new(\iter2, { 319 | arg freq=200, dev=1.02, num=10; 320 | var temp, sum; 321 | sum = 0; 322 | num.do{ 323 | arg count; 324 | temp = SinOsc.ar( 325 | freq * 326 | (count + 1) * 327 | LFNoise1.kr({Rand(0.05,0.2)}!2).range(dev.reciprocal, dev) 328 | ); 329 | temp = temp * LFNoise1.kr({Rand(0.5,8)}!2).exprange(0.01, 1); 330 | sum = sum + temp; 331 | }; 332 | sum = sum * 0.05; 333 | Out.ar(0, sum); 334 | }).add; 335 | ) 336 | 337 | x = Synth(\iter2, [\num, 10]); 338 | x.free; 339 | 340 | But this doesn't work, as you can hear from this strange result, and I'll tell you why. The reason is that the receiver of 'do' is no longer an integer. It sure looks like an integer, but it's not. It's actually an instance of a class called Control, which is a type of UGen that SuperCollider creates automatically whenever you declare an argument in a SynthDef. The key fact is that Control is not a collection, it's just one thing. So when we iterate over a Control, the Control just passes itself into the function, once... In this case the actual output value of the signal is the default value 10. So 10, plus one, is multiplied by the fundamental frequency, 200Hz, and then there's some slight frequency deviation. What we end up hearing is basically just a 2.2kHz tone, which, coincidentally, is not my favorite sound. 341 | 342 | Just to prove this, I'll start up the sound again, and I'll set the argument 'num' equal to some different floating point values just to show that it serves no other purpose than as a frequency multiplier. 343 | 344 | x = Synth(\iter2, [\num, 10]); 345 | x.set(\num, 8.7); 346 | x.set(\num, 5.4); 347 | x.free; 348 | 349 | So in that example, there's really no iteration happening at all. Changing the number of iterations in a SynthDef in real-time essentially boils down changing the code in a SynthDef in real-time, which, from what I understand about how SuperCollider is designed, is pretty much impossible. So, the best way to change the number of partials is to just manually change the iteration receiver and then re-evaluate the SynthDef. 350 | 351 | ( 352 | SynthDef.new(\iter2, { 353 | arg freq=200, dev=1.02; 354 | var temp, sum; 355 | sum = 0; 356 | 20.do{ 357 | arg count; 358 | temp = SinOsc.ar( 359 | freq * 360 | (count + 1) * 361 | LFNoise1.kr({Rand(0.05,0.2)}!2).range(dev.reciprocal, dev) 362 | ); 363 | temp = temp * LFNoise1.kr({Rand(0.5,8)}!2).exprange(0.01, 1); 364 | sum = sum + temp; 365 | }; 366 | sum = sum * 0.05; 367 | Out.ar(0, sum); 368 | }).add; 369 | ) 370 | 371 | x = Synth(\iter2); 372 | x.free; 373 | 374 | That's it for tutorial number 6. If you'd like to see more, I've got an older video on this channel that deals with creating an infinite reverb effect, which also includes some SynthDef iteration. But keep in mind these are just a few improvised examples in a sea of possibilities. What you end up putting in an iteration block is limited only by your imagination, so I absolutely encourage you to experiment. 375 | 376 | What I'd like to do next is talk about the architecture of the audio server in more detail. So far we've just been dumping Synths onto the server with reckless abandon, but our examples have been simple enough that we can get away with this. But when you develop a more sophisticated and interconnected collection of audio generators and processors, there's a whole family of server-related classes that come into play, such as Nodes, Groups, Busses, and additionally, some important concepts arise, such as inputs & outputs and order-of-execution. The latter is actually described as "one of the most critical and seemingly difficult aspects of using SuperCollider." I might not put it that strongly, but I will say that Server Architecture is probably one of the most extensive and important topics, so I'll probably break it down into at least two videos, possibly three. After a thorough tutorial on Server Architecture, I hope I'll have provided a fairly comprehensive introduction, and from there I only have vague ideas of what to discuss next. I'd definitely like to cover Tasks & Routines, Patterns, and GUI, but I'm also open to suggestions. But anyway, thanks for watching this tutorial, and see you next time. -------------------------------------------------------------------------------- /full video scripts/07_script.scd: -------------------------------------------------------------------------------- 1 | Hey everyone, welcome to tutorial number 7. In this video I'll talk about the architecture of the audio server and take a more detailed look at how it works. The audio examples so far in this series have been pretty simple, usually just a few UGens within a single Synth. But as your SuperCollider work grows and becomes more complex, it's more likely you'll want to have many Synths running simultaneously, possibly passing signals to one another, some generating audio and others processing audio. To handle projects like this, it's very important to have a clear understanding of the basic design of the server. 2 | 3 | First things first, let's boot the server and bring up the server visualization, just like we did in tutorial number 4, and I'll also bring up the level meters as we did in tutorial number 5. 4 | 5 | s.boot; 6 | s.plotTree; 7 | s.meter 8 | 9 | There are three concepts I'll discuss in this tutorial. Nodes, Busses, and Order of execution. Node is an abstract class of objects representing modules on the audio server. We don't deal with Node directly, instead we use its two subclasses, Synth and Group. Busses are used to pass signals between Synths, in other words, you can send a signal to a bus, and then use that signal as an input to anther Synth by reading from that bus. And last, there's Order of execution, which has to do with the specific order of nodes on the server. 10 | 11 | As you might imagine, these concepts are closely related to one another, which makes it difficult to talk about one without talking about the others. So in order to explain these three topics, I'll use a simple example of passing audio between two Synths, in this case, sending audio from a generative Synth to be processed by a reverb Synth. 12 | 13 | The first thing we need to do is create two SynthDefs. I'll create a sine wave with a frequency that jumps randomly between the first four partials of a 300Hz fundamental. And I'll transform this sine wave into short blips using a combination of Dust and Decay2. 14 | 15 | ( 16 | SynthDef.new(\blip, { 17 | var freq, trig, sig; 18 | freq = LFNoise0.kr(3).exprange(300,1200).round(300); 19 | sig = SinOsc.ar(freq) * 0.25; 20 | trig = Dust.kr(2); 21 | sig = sig * EnvGen.kr(Env.perc(0.01, 0.2), trig); 22 | Out.ar(0, sig); 23 | }).add; 24 | ) 25 | 26 | I pointed out in tutorial number 3 that audio bus 0 corresponds to your lowest-numbered hardware output, in this case, my left speaker. But we don't want to send this output to the speakers, we want to send it to another synth. So what I'll do here is declare an argument for the bus index, so that I can specify the output bus when I create the Synth. And, in general, it's always a good idea to declare arguments for bus indices, so that you always have the option to re-route the output signal. 27 | 28 | ( 29 | SynthDef.new(\blip, { 30 | arg out; 31 | var freq, trig, sig; 32 | freq = LFNoise0.kr(3).exprange(300,1200).round(300); 33 | sig = SinOsc.ar(freq) * 0.25; 34 | trig = Dust.kr(2); 35 | sig = sig * EnvGen.kr(Env.perc(0.01, 0.2), trig); 36 | Out.ar(out, sig); 37 | }).add; 38 | ) 39 | 40 | In order to receive a signal from another Synth, we can use a UGen called In. In reads a signal from an input bus, and also needs to know how many channels to read. 41 | 42 | ( 43 | SynthDef.new(\reverb, { 44 | var sig; 45 | sig = In.ar(); 46 | }).add; 47 | ) 48 | 49 | I'll declare an argument for the input bus. And again, I'll decide the specific bus assignment when I create the Synth, but whatever bus I end up using, It'll be the same as the output bus for the sound source. 50 | 51 | ( 52 | SynthDef.new(\reverb, { 53 | arg in; 54 | var sig; 55 | sig = In.ar(in); 56 | }).add; 57 | ) 58 | 59 | In this case, the incoming signal is monophonic, so I'm specifying 1 channel. 60 | 61 | ( 62 | SynthDef.new(\reverb, { 63 | arg in; 64 | var sig; 65 | sig = In.ar(in, 1); 66 | }).add; 67 | ) 68 | 69 | Next, I'll apply reverb using FreeVerb...and I'll duplicate the signal so that we hear something in both speakers. 70 | 71 | ( 72 | SynthDef.new(\reverb, { 73 | arg in; 74 | var sig; 75 | sig = In.ar(in, 1); 76 | sig = FreeVerb.ar(sig, 0.5, 0.8, 0.2)!2; 77 | }).add; 78 | ) 79 | 80 | Last, we output the processed signal. For the sake of consistency, I'll specify another argument for the output bus. But since this is the sound we actually want to hear, I'll set the default value to be 0. 81 | 82 | ( 83 | SynthDef.new(\reverb, { 84 | arg in, out=0; 85 | var sig; 86 | sig = In.ar(in, 1); 87 | sig = FreeVerb.ar(sig, 0.5, 0.8, 0.2)!2; 88 | Out.ar(out, sig); 89 | }).add; 90 | ) 91 | 92 | Before we instantiate these Synths, let's talk about busses. When it boots, the audio server has a fixed number of audio busses. You can get this number by evaluating 93 | 94 | s.options.numAudioBusChannels; 95 | 96 | The default value, as we can see, is 128. SuperCollider reserves a number of these busses for hardware outputs and inputs. These values can be found by evaluating 97 | 98 | s.options.numOutputBusChannels; 99 | s.options.numInputBusChannels; 100 | 101 | The default for each of these is 8 busses. This means, that by default, busses 0 through 7 are reserved for hardware output, 8 through 15 are reserved for inputs, and 16 through 127 are so-called private busses, in effect, "safe" choices for internally routing audio signals between Synths. Now, there's nothing stopping you from using hardware busses for internal routing, but it's not a good idea, because you can run into feedback if you're not careful. 102 | 103 | There are situations where you might want to change the number of hardware inputs and outputs that SuperCollider reserves for you. Maybe you have an audio interface with 2 inputs and 4 outputs. You can change these values by setting the server attributes equal to new values, like this: 104 | 105 | s.options.numOutputBusChannels = 4; 106 | s.options.numInputBusChannels = 2; 107 | 108 | But the main thing to remember is that you need to reboot the server for these changes to take effect. 109 | 110 | s.reboot; 111 | 112 | This change will be reflected in the level meters, once you close and reopen the window. 113 | 114 | s.meter; 115 | 116 | So now, busses 0 through 3 correspond to your hardware outputs, while 4 and 5 correspond to your hardware inputs, and now, 6 through 127 are private busses, available to you however you want to use them. 117 | 118 | So let's call up our reverb synth. For the duration of this video, I'm going to use the global variable y for the reverb Synth, and x for the sound source. I'll specify input bus 6, but of course we could use any integer from 6 up to 127. 119 | 120 | y = Synth.new(\reverb, [\in, 6]); 121 | 122 | We can the Synth appear on the server, but we don't hear anything yet because there's no signal being written to bus 6. So let's call up our sound source, and specify bus 6 as its output destination. 123 | 124 | x = Synth.new(\blip, [\out, 6]); 125 | 126 | Here's a quick demonstration of how bus routing works. Let's imagine a more complex setup in which I had a different effect synth reading signal from bus 25. I could re-route the sound source by changing the output bus, like this: 127 | 128 | x.set(\out, 25); 129 | 130 | The sound stops, because there's no longer any signal being sent to bus 6, which is what the reverb synth is listening to. And there's nothing processing signal from bus 25, so the sound source has reached a dead end. We could, however, re-route the reverb synth as well, like this, 131 | 132 | y.set(\in, 25); 133 | 134 | And the synthesis chain is intact once again. 135 | 136 | Another nice consequence of dividing the synthesis chain into component nodes, instead of having one big Synth handling everything, is that we can dismantle the synthesis chain piece-by-piece. If we free the sound source but leave the reverb node alone, 137 | 138 | x.free; 139 | 140 | then the sound stops in a very natural-sounding manner. The source is removed, but the reverb effect remains for as long as it needs to completely decay the sound. And now we can free the reverb synth at leisure. 141 | 142 | y.free; 143 | 144 | On the other hand, if the sound source and reverb effect were part of the same synth, we'd be forced to free them together, and the entire sound would stop much more suddenly, like this: 145 | 146 | y = Synth.new(\reverb, [\in, 6]); 147 | x = Synth.new(\blip, [\out, 6]); 148 | 149 | s.freeAll; 150 | 151 | Let's talk about busses some more. Generally, it can be problematic to specify a bus with an integer, since, by doing this, we're hard-coding that value. For example, if you're using a new audio interface, it might not have the same number of inputs and outputs as you're used to, and so bus 6 might conflict with a hardware channel. In this case, you'd have to go back into your code and change numbers around, and that's kind of annoying. 152 | 153 | For this reason, you should use the Bus object, in order to let SuperCollider handle the allocation of busses for you. We can grab a reference to a private audio bus by evaluating Bus.audio and by storing the result in a global variable, I'll call it reverbBus. This method needs to know what server the bus belongs to, in this case it's the local server stored in the variable s, and it also needs to know how many channels of audio it's dealing with. And as I mentioned, at this point we're just dealing with a monophonic signal. 154 | 155 | ~reverbBus = Bus.audio(s, 1); 156 | 157 | According to the post window, this is an audio bus with index 6, it expects one channel of audio, and it belongs to the localhost server. When using the Bus object to allocate an audio bus, SuperCollider will always choose the lowest available bus that doesn't conflict with hardware outputs. Currently, we have 4 hardware outputs and 2 hardware inputs, so the server assumes busses 0 through 5 are unavailable, and 6 is the first available private bus. If we hadn't changed the default values, we'd still have 8 outputs and 8 inputs, and in that case, bus 16 would have been the first available private bus. 158 | 159 | We can us the 'index' method on a bus to return its integer index 160 | 161 | ~reverbBus.index; 162 | 163 | So we can now revisit the previous example, and replace the integer with the bus index, like this: 164 | 165 | y = Synth.new(\reverb, [\in, ~reverbBus.index]); 166 | x = Synth.new(\blip, [\out, ~reverbBus.index]); 167 | 168 | x.free; 169 | y.free; 170 | 171 | Turns out you don't even need to use dot index. When SuperCollider receives a Bus as an argument value, it gets tranlated into the index of that bus, automatically. 172 | 173 | y = Synth.new(\reverb, [\in, ~reverbBus]); 174 | x = Synth.new(\blip, [\out, ~reverbBus]); 175 | 176 | x.free; 177 | y.free; 178 | 179 | You won't always be dealing with 1-channel signals, so I'll change these SynthDefs in order to demonstrate how SuperCollider deals with bussing multichannel signals. 180 | 181 | First I'll add a UGen called Pan2, which pans a monophonic signal across a stereo field, and I'll control the pan position with a noise generator. Multichannel UGens like Pan2 can be a little deceptive because there are no Arrays or exclamation points to suggest multichannel expansion, but that's just how these UGens work. If we send the output of this Synth straight to the hardware outputs by specifying bus 0, we can see that, sure enough, there are two channels: 182 | 183 | ( 184 | SynthDef.new(\blip, { 185 | arg out; 186 | var freq, trig, sig; 187 | freq = LFNoise0.kr(3).exprange(300,1200).round(300); 188 | sig = SinOsc.ar(freq) * 0.25; 189 | trig = Dust.kr(2); 190 | sig = sig * EnvGen.kr(Env.perc(0.01, 0.2), trig); 191 | sig = Pan2.ar(sig, LFNoise1.kr(10)); 192 | Out.ar(out, sig); 193 | }).add; 194 | 195 | SynthDef.new(\reverb, { 196 | arg in, out=0; 197 | var sig; 198 | sig = In.ar(in, 1); 199 | sig = FreeVerb.ar(sig, 0.5, 0.8, 0.2)!2; 200 | Out.ar(out, sig); 201 | }).add; 202 | ) 203 | 204 | x = Synth.new(\blip, [\out, 0]); 205 | x.free; 206 | 207 | We need to change the reverb synth too. Since we're outputing a stereo signal, there's no longer any reason to multichannel expand the processed output. We also need to change the input UGen to read 2 channels instead of just 1. 208 | 209 | ( 210 | SynthDef.new(\blip, { 211 | arg out; 212 | var freq, trig, sig; 213 | freq = LFNoise0.kr(3).exprange(300,1200).round(300); 214 | sig = SinOsc.ar(freq) * 0.25; 215 | trig = Dust.kr(2); 216 | sig = sig * EnvGen.kr(Env.perc(0.01, 0.2), trig); 217 | sig = Pan2.ar(sig, LFNoise1.kr(10)); 218 | Out.ar(out, sig); 219 | }).add; 220 | 221 | SynthDef.new(\reverb, { 222 | arg in, out=0; 223 | var sig; 224 | sig = In.ar(in, 2); 225 | sig = FreeVerb.ar(sig, 0.5, 0.8, 0.2); 226 | Out.ar(out, sig); 227 | }).add; 228 | ) 229 | 230 | And last, we should also allocate a two channel bus, like this: 231 | 232 | ~reverbBus2 = Bus.audio(s, 2); 233 | 234 | And take a look at the post window here. Because we've already allocated a one channel audio bus, SuperCollider remembers this, and assumes we're still going to use it for something, so bus 7 was the lowest available private bus. 235 | 236 | This brings up a very important point, and it's that in SuperCollider there's no such thing as a multichannel bus. Instead, one bus corresponds to one channel of signal. The Bus help file puts it very clearly, pointing out that 237 | 238 | "using the Bus class to allocate a multichannel bus does not 'create' a multichannel bus, but rather simply reserves a series of adjacent bus indices" 239 | 240 | This means that the bus we've just allocated, ~reverbBus2, is not actually a stereo bus. Instead, SuperCollider has set aside two busses, with indices 7 and 8. And just to prove this, I'll allocate another 1-channel bus, and we can see in the post window, that the audio bus with index 9, not 8, is the next available private audio bus. 241 | 242 | ~reverbBus3 = Bus.audio(s, 1); 243 | 244 | If we use dot index on our so-called 2-channel bus, SuperCollider returns the lowest numbered index in the series of adjacent busses -- in this case, 7. 245 | 246 | ~reverbBus2.index 247 | 248 | But, as we saw in tutorial number 5, remember what happens when we output a multichannel signal to a single bus. SuperCollider will distribute the remaining audio channels on consecutive, ascending busses. 249 | 250 | So, returning to our modified example, the first channel of the sound source is sent to bus 7, and the second channel to bus 8. The reverb synth reads the stereo signal split between these two busses, applies reverb, and sends the result to busses 0 and 1. 251 | 252 | y = Synth.new(\reverb, [\in, ~reverbBus2]); 253 | x = Synth.new(\blip, [\out, ~reverbBus2]); 254 | 255 | x.free; 256 | y.free; 257 | 258 | But, Notice, that if we instead used our original 1-channel bus, everything still works exactly the same 259 | 260 | y = Synth.new(\reverb, [\in, ~reverbBus]); 261 | x = Synth.new(\blip, [\out, ~reverbBus]); 262 | 263 | x.free; 264 | y.free; 265 | 266 | Even though this bus is supposedly a one-channel bus, SuperCollider doesn't know or care. All it does is determine the index associated with this bus allocation, which happens to be 6, which means the stereo sound source is now being written to busses 6 and 7, instead of 7 and 8. But hopefully, you can see how this might be problematic. For instance, we might be using busses 7 and 8 for something else, and with a stereo signal being written to bus 6, we risk having signal overlap on bus 7, and that can lead to other unintended consequences. 267 | 268 | So the bottom line with busses is that there's nothing magical or sophisticated about using the Bus object to allocate audio busses, other than the fact that it avoids hardware busses. It's entirely the user's responsibility to make sure that the number of channels matches the number of busses, and that there are no conflicts or overlaps. And, in fact, the Bus help file warns the reader of exactly the same thing. 269 | 270 | I'll move away from busses for now, and turn the discussion to order of execution, but I'll stick with the same example in order to demonstrate a common pitfall. Let's say, innocently enough, we create these two Synths in the reverse order. 271 | 272 | x = Synth.new(\blip, [\out, ~reverbBus2]); 273 | y = Synth.new(\reverb, [\in, ~reverbBus2]); 274 | 275 | Well, where's the sound? We've got both Synths on the server, and they're sharing the same audio bus. The silence we're hearing is a consequence of order of execution. There's a help document called order of execution, and it describes the issue in a very straightforward way: 276 | 277 | "if you have a synth on the server (i.e. an "effect") that depends on the output from another synth (the "source"), the effect must appear later in the chain of nodes on the server than the source." 278 | 279 | When you've got more than 1 Synth on the server, their outputs are not calculated simultaneously, but instead, they're calculated from top to bottom, or as it's more commonly called, head to tail. According to the current node order, the reverb synth is at the head of the node tree, which means, on every control cycle, its output is calculated first. But the sound source hasn't been calculated yet, so there's no input to the reverb synth. Next, the sound source is calculated and sent to busses 7 and 8. But the reverb synth that's listeting to these busses has already calculated its output, so we hear nothing. 280 | 281 | s.freeAll; 282 | 283 | Before we talk about how to place nodes in a specific order, we should have a more complete discussion about nodes themselves, in particular, we haven't talked about Groups yet. As I mentioned, Node has two subclasses, Synth and Group. We're already very familiar with Synths, so let's talk about Groups. A Group is essentially a collection of nodes in a specific order. The nodes inside of a Group can be both Synths and other Groups. 284 | 285 | When we add a Synth node to the server, it appears as a white rectangle on the node tree; 286 | 287 | x = Synth.new(\blip, [\out, 0]); 288 | 289 | and when we add a Group node to the server, it appears as a gray rectangle, like this; 290 | 291 | g = Group.new; 292 | 293 | If you look closely at the node tree, you'll see that these two nodes we just created are actually contained within another group node. This larger gray rectangle represents the default group, which is created for you whenever you boot the server. 294 | 295 | We already know that Synths can be removed by using the free method 296 | 297 | x.free; 298 | 299 | and, turns out, the same applies to groups 300 | 301 | g.free; 302 | 303 | With this very brief introduction to groups, I'm going to revisit the Synth help file, which I briefly touched on in tutorial number 3. As we saw in that video, the third and fourth arguments of Synth.new are target and addAction 304 | 305 | A target is ultimately a Synth or a Group, but we also have the option to specify a server, or nil, meaning we don't specify anything for a target. If we specify a server as a target, SuperCollider translates this into the default group on that server. And if we specify nil, this is translated to the default group of the default server. We also have the option to specify an integer as a target, which corresponds to a node ID, but we haven't talked about node IDs, and it's probably too much of a distraction right now. The other argument, addAction, specifies where to place the node relative to the target. 306 | 307 | So let's get back to our reverb example and put these two arguments to work. When we created these two Synths one after the other, 308 | 309 | x = Synth.new(\blip, [\out, ~reverbBus2]); 310 | y = Synth.new(\reverb, [\in, ~reverbBus2]); 311 | 312 | they ended up in the wrong order. This is because the default value for addAction is addToHead. So in this case, 313 | 314 | x = Synth.new(\blip, [\out, ~reverbBus2]); 315 | 316 | the blip synth is added to the head of the default group, 317 | 318 | y = Synth.new(\reverb, [\in, ~reverbBus2]); 319 | 320 | and then the reverb synth is added to the head of the default group, therefore ending up BEFORE the sound source. 321 | 322 | s.freeAll; 323 | 324 | There are many ways to do this correctly, since the only requirement is that the reverb synth ends up after the sound source. One possible approach is to specify addToTail on the reverb synth. And we might as well add targets to these Synths while we're at it, just to be extra diligent. 325 | 326 | I'm specifying the local server as a target, which means these Synths will end up in the default group of the local server. And now the order in which we create these nodes doesn't matter, because the effect Synth will always be added to the very end of the default group. 327 | 328 | x = Synth.new(\blip, [\out, ~reverbBus2], s); 329 | y = Synth.new(\reverb, [\in, ~reverbBus2], s, \addToTail); 330 | 331 | y = Synth.new(\reverb, [\in, ~reverbBus2], s, \addToTail); 332 | x = Synth.new(\blip, [\out, ~reverbBus2], s); 333 | 334 | Here's another way to place these nodes in the correct order. Let's create the sound source first 335 | 336 | x = Synth.new(\blip, [\out, ~reverbBus2]); 337 | 338 | And we'll specify this synth, called x, as a target for the effect Synth, and we'll use \addAfter as our addAction 339 | 340 | y = Synth.new(\reverb, [\in, ~reverbBus2], x, \addAfter); 341 | 342 | Which places the reverb synth immediately after the sound source in the node chain. I'll free the sound source for a second, 343 | 344 | x.free; 345 | 346 | in order to demonstrate one of several alternatives to Synth.new. In the help file, there are several convenience methods, one corresponding to each addAction. 347 | 348 | So, since our reverb synth still exists, we can re-instantiate the sound source using Synth.before 349 | 350 | x = Synth.before(y, \blip, [\out, ~reverbBus2]); 351 | 352 | x.free; 353 | y.free; 354 | 355 | You can use Groups to your advantage, too. Since a sound source should always appear before an effect, you can create a Group for your sources and a group for your effects. The five convenience methods in the Synth help file are also available for Groups, so I'll use group.after for the effects group. 356 | 357 | ~sourceGrp = Group.new; 358 | ~fxGrp = Group.after(~sourceGrp); 359 | 360 | As long as these groups are in the proper order, you don't have to worry about the order of your synth nodes, as long as they get added to the correct groups. 361 | 362 | x = Synth.new(\blip, [\out, ~reverbBus2], ~sourceGrp); 363 | y = Synth.new(\reverb, [\in, ~reverbBus2], ~fxGrp); 364 | 365 | x.free; 366 | 367 | I'm going to add a few arguments to the blip SynthDef to demonstrate another important advantage of using Groups. 368 | 369 | ( 370 | SynthDef.new(\blip, { 371 | arg out, fund=300, dens=2, decay=0.2; 372 | var freq, trig, sig; 373 | freq = LFNoise0.kr(3).exprange(fund, fund*4).round(fund); 374 | sig = SinOsc.ar(freq) * 0.25; 375 | trig = Dust.kr(dens); 376 | sig = sig * EnvGen.kr(Env.perc(0.01, decay), trig); 377 | sig = Pan2.ar(sig, LFNoise1.kr(10)); 378 | Out.ar(out, sig); 379 | }).add; 380 | ) 381 | 382 | Now, using iteration, I'll create 8 instances of this SynthDef, making sure to add them to the correct Group 383 | 384 | 8.do{Synth.new(\blip, [\out, ~reverbBus2, \fund, exprand(60,300).round(30)], ~sourceGrp)} 385 | 386 | I haven't given these Synths any names, but it doesn't matter because they're contained within a Group that I can refer to by name. We know that we can use the set message to change a control argument of a single Synth. But we can alternatively send a set message to a Group, which causes the group to relay that message to all the nodes within it. 387 | 388 | ~sourceGrp.set(\decay, 0.05); 389 | ~sourceGrp.set(\dens, 12); 390 | ~sourceGrp.set(\dens, 0.25); 391 | ~sourceGrp.set(\decay, 1); 392 | 393 | It's a very convenient way of sending messages to many nodes at once. And instead of having to free these Synths with eight individual statements, we can just tell the group to free all of the nodes that it contains, like this 394 | 395 | ~sourceGrp.freeAll; 396 | 397 | That's all for tutorial number 7. As always, one of the best ways to understand these concepts is to experiment, by practicing adding and removing nodes from the server. I encourage you to read the help files for Synth, Group, and Node more closely, and maybe take a look at the documents on Order of execution and Server Architecture to get a better idea of what's going on. I hope this video helps clarify the design of the audio server and points you in the right direction. Please leave any comments or questions on YouTube, and thanks for watching. -------------------------------------------------------------------------------- /full video scripts/09_script.scd: -------------------------------------------------------------------------------- 1 | Hey everyone, welcome to tutorial number 9. Here I'll show you the basics of getting SuperCollider to communicate with other MIDI devices. 2 | 3 | Just a few words before we begin. If you're watching this video, then I'm assuming you've already got a general understanding of what MIDI is, so I'm not gonna go into a ton of depth here. But, if you don't know much about MIDI, then in short, MIDI, which stands for Musical Instrument Digital Interface, is a communication protocol for allowing musical devices, such as synthesizers, computers, etc, to talk to each other. 4 | 5 | I suggest taking a look at the guide called "Using MIDI", which gives a very broad overview of the MIDI classes in the SuperCollider library. There's also a guide simply titled "MIDI", which gives an overview from a slightly different perspective. 6 | 7 | So how do we use MIDI in SuperCollider? Well, first things first, SuperCollider needs to connect with the MIDI functionality of your operating system, and this is done with 8 | 9 | MIDIClient.init; 10 | 11 | In doing so, SC touches base with your operating system and its knowledge of available MIDI devices, and comes back with a list of MIDI sources and destinations. Right now, as you can see, I don't have any. So I'm going to plug in my audio MIDI interface...clear the post window...and run this line again. 12 | 13 | MIDIClient.init; 14 | 15 | and now you can see that SuperCollider has detected my interface. For this tutorial, I've got a Yamaha PSR 620 keyboard, which is sending MIDI to the interface. 16 | 17 | After detecting MIDI devices, the next step is to connect SuperCollider with a MIDI device. The easiest way to do this is to just connect with all available devices, using 18 | 19 | MIDIIn.connectAll; 20 | 21 | And I find there's no disadvantage to doing it like this. However, if you have multiple devices, but for some reason don't want to connect with all of them, you can first get an array of available source devices, with 22 | 23 | MIDIClient.sources; 24 | 25 | Of course in this case I just have one source. And then use 26 | 27 | MIDIIn.connect(); 28 | 29 | And then specify the device by its index in the source array. 30 | 31 | MIDIIn.connect(0); 32 | MIDIIn.connect(1); 33 | MIDIIn.connect(2); 34 | 35 | But, just to reiterate, I find that there's absolutely no problem with just using MIDIIn.connectAll. 36 | 37 | So what's next? Let's now get some MIDI data into the language. To actually interact with MIDI data, you'll use one of two very similar classes. There's 38 | 39 | MIDIFunc 40 | 41 | and 42 | 43 | MIDIdef 44 | 45 | These two classes have a slightly different syntax, but they perform the same function. In fact, MIDIdef is a subclass of MIDIFunc. They both allow you to register an action to be taken when a certain type of MIDI message is received. It would actually be fairly redundant to demonstrate both classes, since they really are quite similar, so for the purposes of this video, I'm going to use MIDIdef. Now I like MIDIdef, because there are several "def" type classes in SuperCollider, most prominently SynthDef, but also Tdef, Pdef, Ndef, and so forth, so there's a certain homogeneity of syntax with MIDIdef that I find very appealing. 46 | 47 | Since I'm using a keyboard controller, then I'm probably going to be dealing with note-on and note-off messages. So, to start, I'll create a MIDIdef to respond to note-ons, with MIDIdef.noteOn, and remember that capitalization does matter. 48 | 49 | MIDIdef.noteOn(); 50 | 51 | At minimum, MIDIdef expects two things: a symbol, which serves as the name of the def, and a function to be evaluated when a note-on message is received. In this case I'll just print the words "key down" every time a note-on message is received. 52 | 53 | MIDIdef.noteOn(\noteOnTest, {"key down".postln}); 54 | 55 | Once created, you can temporarily deactivate a MIDIdef with the 'disable' message 56 | 57 | MIDIdef.noteOn(\noteOnTest).disable; 58 | 59 | Andy you can re-activate with 'enable' 60 | 61 | MIDIdef.noteOn(\noteOnTest).enable; 62 | 63 | To destroy a MIDIdef, we use the free message, just like we do with Synths and Groups. 64 | 65 | MIDIdef.noteOn(\noteOnTest).free; 66 | 67 | And finally, if you have multiple MIDIdefs floating around, rather than free them individually, you can destroy them all at once using the class method 'freeAll' 68 | 69 | MIDIdef.freeAll; 70 | 71 | I also want to point out that by default, a MIDIdef will be destroyed by pressing command-period. This can be specified explicitly by spetting the MIDIdef's permament attribute to true or false. If true, when I hit command preiod, the MIDIdef persists. 72 | 73 | MIDIdef.noteOn(\noteOnTest, {"key down".postln}).permanent_(true); 74 | 75 | If permanent is set to false, then command period will remove the MIDIdef. 76 | 77 | MIDIdef.noteOn(\noteOnTest).permanent_(false); 78 | 79 | Ok, so with that out of the way, let's acknowledge that printing the text "key down" is not particularly useful. So instead, let's use MIDIdef to print the actual incoming MIDI data. Here I'm talking about things like note number, velocity, MIDI channel, and so forth. This is done by declaring arguments within the MIDIdef function. The number of arguments and what they represent varies, depending on which type of MIDI message you're dealing with. In the MIDIdef help file, we can read about this in detail. under the new method, in the description of the function, we can read about the types of expected arguments. When evaluated for note on, note off, control, and poloyhponic aftertouch messages, the function will be passed the arguments val, num, chan, and source, in that order. So in our case, dealing with note on mesages, these arguments represent note velocity, note number, MIDI channel, and and identifying number associated with the source device. 80 | 81 | Aftertouch, program change, and pitch bend messages are different. In this case the funciton is passed only three arguments: value, channel, and source. So, since we're dealing with note on, I'll declare four arguments in the MIDIdef function. I can't change the order in which the function expects these vaules, but I can call them whahtever I want. 'vel' for velocity, 'nn' for note number, 'chan', and 'src'. And in this case I'll simply print an array of these four values. 82 | 83 | ( 84 | MIDIdef.noteOn(\noteOnTest, { 85 | arg vel, nn, chan, src; 86 | [vel, nn, chan, src].postln; 87 | }); 88 | ) 89 | 90 | And in the post window, here's the data for these five notes I've just played, each starting with velocity, followed by note number. And then third we have MIDI channel, which is zero. So just a quick note about how SC interprets MIDI channels: you proably already know that one MIDI cable accommodates 16 unique MIDI channels, and that MIDI messages are sent and received on one or more of these channels. Most MIDI software, and perhaps, most human beings, think of these channels as 1 through 16. However, by convention of other programming languages, and for the sake of consistency with other indexing methods, SuperCollider understands these channels as 0 through 15. You can see how this might be a point of confusion if you're not expecting it. But the point is, this numbering convention is very impontant to keep in mind if you're going to be dealing with multiple channels, or if you're transferring MIDI between SuperCollider and some other software. Last is source ID, and I don't really ever use this number, but I guess it can be useful to distinguish identical data coming from two different devices. 91 | 92 | Let's say in this case we don't actually care about channel and source ID. The nice thing about MIDIdef is that you can change the function, but keep the same symbolic name, re-evaluate, and the def is overwritten. 93 | 94 | ( 95 | MIDIdef.noteOn(\noteOnTest, { 96 | arg vel, nn, chan, src; 97 | [vel, nn].postln; 98 | }); 99 | ) 100 | 101 | And now we only see velocity and note number. 102 | 103 | So, now that we have the basic syntax and functionality out of the way, let's use this MIDI data to make some sound. probably the quickest and easiest way is to just put a UGen function within the MIDIdef function. And I'm gonna go real simple here. 104 | 105 | For the source sound, I'll use a sine wave, making sure to convert MIDI note numbers into cycles per second for the frequency argument. Exclam 2 to create a stereo output signal. Since I'm not incorporating note off messages, I should make sure that the sine waves will turn themselves off somehow. I'll use a default percusisve envelope for this, with doneAction:2. Of course, this means I'll have no way of sustaining the sound, but like I said, we're starting simple. I'm gonna use velocity in the traditional sense, to control amplitude. Of course, we're going to have to convert velocity numbers to a reasonable range, since we don't want a sine wave with an amplitude of 127, that would be...unpleasasnt. So I'm gonna use a method called 'linexp'. linexp is one of several methods which maps a number from one range to another, in this case a linear range to an exponential range. So, we provide the input range, in this case, 1 to 127...and the output range, and since we're talking about raw amplitude here, I'll say a minimum of 0.01 and a maximum of 0.3. And while I could go with a maximum of 1, we'd probably end up clipping the output signal if we play multiple notes simultaneously. You can read more about linexp and similar mapping methods in the help file for SimpleNumber. Make sure to add '.play' at the end of the sound funtion, boot the audio server, and we should be good to go. I'm also going ot turn down the volume on my synthesizer, because synthesis is now being handled by SuperCollider instead. 106 | 107 | ( 108 | MIDIdef.noteOn(\noteOnTest, { 109 | arg vel, nn, chan, src; 110 | [vel, nn].postln; 111 | { 112 | var sig, env; 113 | sig = SinOsc.ar(nn.midicps)!2; 114 | env = EnvGen.kr(Env.perc, doneAction:2); 115 | sig = sig * env * vel.linexp(1,127,0.01,0.3); 116 | }.play; 117 | }); 118 | ) 119 | 120 | s.boot; 121 | 122 | Changing the type of sound simply amounts to changing the UGen function, and then re-evaluating the MIDIdef. For example, here's a triangle wave. 123 | 124 | ( 125 | MIDIdef.noteOn(\noteOnTest, { 126 | arg vel, nn, chan, src; 127 | [vel, nn].postln; 128 | { 129 | var sig, env; 130 | sig = LFTri.ar(nn.midicps)!2; 131 | env = EnvGen.kr(Env.perc, doneAction:2); 132 | sig = sig * env * vel.linexp(1,127,0.01,0.3); 133 | }.play; 134 | }); 135 | ) 136 | 137 | Like I said, this is the quick and easy way of using MIDI to generate sound. If you want to get more complex, like maybe using note-off to release a sustained note, or maybe incorporating pitch bend, then this approach is probably not going to cut it. You'll remember from tutorial number 3 that the more formal and flexible way of making sound is to create a SynthDef. So, let's do that. I'll copy and paste, and make the necessary changes... 138 | 139 | ( 140 | SynthDef.new(\tone, { 141 | arg freq=440, amp=0.3; 142 | var sig, env; 143 | sig = LFTri.ar(freq)!2; 144 | env = EnvGen.kr(Env.perc, doneAction:2); 145 | sig = sig * env * amp; 146 | Out.ar(0, sig); 147 | }).add; 148 | ) 149 | 150 | ...and there we go. Let's quickly test to make sure this works. 151 | 152 | Synth.new(\tone, [\freq, 700, \amp, 0.5]) 153 | 154 | For starters, we can now simplify our MIDIdef function. Instead of playing a UGen function, we can just instantiate a Synth with the correct parameters. 155 | 156 | ( 157 | MIDIdef.noteOn(\noteOnTest, { 158 | arg vel, nn, chan, src; 159 | [vel, nn].postln; 160 | Synth.new(\tone, [\freq, nn.midicps, \amp, vel.linexp(1,127,0.01,0.3)]); 161 | }); 162 | ) 163 | 164 | Let's do away with this percussive envelope, and instead use an ADSR envelope, so that we can initiate and sustain a pitch with a note-on, and then release the note with a corresponding note-off. First things first, we need to make a few changes to our SynthDef. I'm just gonna keep things simple and go with the default ADSR envelope. Since ADSR is a sustaining envelope, we need a gate argument. The attack phase is triggered when gate = 1, and the release phase is triggered when gate = 0. 165 | 166 | ( 167 | SynthDef.new(\tone, { 168 | arg freq=440, amp=0.3, gate=0; 169 | var sig, env; 170 | sig = LFTri.ar(freq)!2; 171 | env = EnvGen.kr(Env.adsr, gate, doneAction:2); 172 | sig = sig * env * amp; 173 | Out.ar(0, sig); 174 | }).add; 175 | ) 176 | 177 | And again, I'm just gonna do a quick test to make sure it works. 178 | 179 | x = Synth.new(\tone, [\gate, 1]) 180 | 181 | x.set(\gate, 0); 182 | 183 | Perfect. But, we're not quite done yet. We can't just put x = Synth.new into our note-on MIDIdef function, because *if* we play a second note, while the first note is being sustained, then x will be overwritten with a new Synth, and we'll have no way of communicating with the old one. This means we'll have no way of releasing the note, and we'll get a whole bunch of stuck notes. So, the way I like to deal with MIDI polyphony is to create an empty array of size 128, since there are 128 possible note numbers. When I play a note, I create a Synth, and store that Synth in the array, at an index equal to the incoming note number. When I release a note, I free the Synth at the index equal to the incoming note number. In this way, I'm visualizing the piano keyboard as an array of possible notes, and I think, conceptually, this works very well. 184 | 185 | So, here's our initial empty array of size 128. 186 | 187 | ~notes = Array.newClear(128); 188 | 189 | Next, I'll modify the MIDIdef for note-on messages. Instead of just creating a Synth, I'm also going to store the Synth in the global array of notes, at the index determined by the incoming note number. 190 | 191 | ( 192 | MIDIdef.noteOn(\noteOnTest, { 193 | arg vel, nn, chan, src; 194 | [vel, nn].postln; 195 | ~notes[nn] = Synth.new( 196 | \tone, 197 | [ 198 | \freq, nn.midicps, 199 | \amp, vel.linexp(1,127,0.01,0.3), 200 | \gate, 1 201 | ] 202 | ); 203 | }); 204 | ) 205 | 206 | Ok, so that's that. And, if we were to play any notes right now, they get stuck...totally stuck. So, I'm just gonna hit command-period, which also removes the MIDIdef, so I'm gonna re-evaluate that. 207 | 208 | Ok so what we've gotta do is make another MIDIdef to handle note-off messages. In this function, I'll address the Synth at the correct index, corresponding to its note number, and set the gate argument of that Synth to zero, then replace the Synth with a nil value, so that it's like the Synth was never there. 209 | 210 | ( 211 | MIDIdef.noteOn(\noteOnTest, { 212 | arg vel, nn, chan, src; 213 | [vel, nn].postln; 214 | ~notes[nn] = Synth.new( 215 | \tone, 216 | [ 217 | \freq, nn.midicps, 218 | \amp, vel.linexp(1,127,0.01,0.3), 219 | \gate, 1 220 | ] 221 | ); 222 | }); 223 | 224 | MIDIdef.noteOff(\noteOffTest, { 225 | arg vel, nn; 226 | [vel, nn].postln; 227 | ~notes[nn].set(\gate, 0); 228 | ~notes[nn] = nil; 229 | }); 230 | ) 231 | 232 | And just like that, we've got a polyphonic synthesizer. As a finishing touch, let's incorporate pitch bend. And this means we've gotta make a third MIDIdef. I always like to start by just printing the incoming values, to make sure we know exactly what kind of numbers we're getting. 233 | 234 | ( 235 | MIDIdef.noteOn(\noteOnTest, { 236 | arg vel, nn, chan, src; 237 | [vel, nn].postln; 238 | ~notes[nn] = Synth.new( 239 | \tone, 240 | [ 241 | \freq, nn.midicps, 242 | \amp, vel.linexp(1,127,0.01,0.3), 243 | \gate, 1 244 | ] 245 | ); 246 | }); 247 | 248 | MIDIdef.noteOff(\noteOffTest, { 249 | arg vel, nn; 250 | [vel, nn].postln; 251 | ~notes[nn].set(\gate, 0); 252 | ~notes[nn] = nil; 253 | }); 254 | 255 | MIDIdef.bend(\bendTest, { 256 | arg val, chan, src; 257 | [val, chan, src].postln; 258 | }); 259 | ) 260 | 261 | Create the MIDIdef, and adjusting the pitch wheel on my keyboard, we can see that the values range from 0 to 16383, and that its 8192 at rest position. 262 | 263 | Uh, ok, this is interesting. Take a look at this. My Yamaha seems to be sending out three identical messages on channels 0, 10 and 1...for some reason. This just goes to show that it's always good to check your data before you start using it. You know, this might actually cause some problems, so what I'm gonna do is only pay attention to pitch wheel messages coming in on channel 0. In the help file for MIDIdef, you can see that it has an argument called 'chan', which is nil by default, and this means the def will respond to messages on any channel. But, if we specify an integer instead of nil, then the MIDIdef will only respond to messages on that particular channel. 264 | 265 | ( 266 | MIDIdef.noteOn(\noteOnTest, { 267 | arg vel, nn, chan, src; 268 | [vel, nn].postln; 269 | ~notes[nn] = Synth.new( 270 | \tone, 271 | [ 272 | \freq, nn.midicps, 273 | \amp, vel.linexp(1,127,0.01,0.3), 274 | \gate, 1 275 | ] 276 | ); 277 | }); 278 | 279 | MIDIdef.noteOff(\noteOffTest, { 280 | arg vel, nn; 281 | [vel, nn].postln; 282 | ~notes[nn].set(\gate, 0); 283 | ~notes[nn] = nil; 284 | }); 285 | 286 | MIDIdef.bend(\bendTest, { 287 | arg val, chan, src; 288 | [val, chan, src].postln; 289 | }, chan:0); 290 | ) 291 | 292 | And now, you can see we only get pitch messages on channel zero. And this looks much better. 293 | 294 | Let's use the pitch wheel in its most traditional sense: to bend the pitch. First, we need to change our SynthDef again, in particular we need to add a new argument for pitch bend. I'm going to conceive of bend as a number of semitones, so it's zero by default. But to use this with a frequency measured in cycles per second, I need to use the midiratio method to convert from semitones to a frequency ratio. 295 | 296 | ( 297 | SynthDef.new(\tone, { 298 | arg freq=440, amp=0.3, gate=0, bend=0; 299 | var sig, env; 300 | sig = LFTri.ar(freq * bend.midiratio)!2; 301 | env = EnvGen.kr(Env.adsr, gate, doneAction:2); 302 | sig = sig * env * amp; 303 | Out.ar(0, sig); 304 | }).add; 305 | ) 306 | 307 | And now, I'll change the MIDIdef receiving pitch bend messages. On synthesizers, the pitch wheel usually acts globally, on all notes, so it might be wise to use a global variable to keep track of the pitch wheel position. I'll initialize this variable to 8192, and have the MIDIdef update this value every time the pitch wheel is moved. 308 | 309 | ( 310 | ~bend = 8192; 311 | 312 | MIDIdef.noteOn(\noteOnTest, { 313 | arg vel, nn, chan, src; 314 | [vel, nn].postln; 315 | ~notes[nn] = Synth.new( 316 | \tone, 317 | [ 318 | \freq, nn.midicps, 319 | \amp, vel.linexp(1,127,0.01,0.3), 320 | \gate, 1 321 | ] 322 | ); 323 | }); 324 | 325 | MIDIdef.noteOff(\noteOffTest, { 326 | arg vel, nn; 327 | [vel, nn].postln; 328 | ~notes[nn].set(\gate, 0); 329 | ~notes[nn] = nil; 330 | }); 331 | 332 | MIDIdef.bend(\bendTest, { 333 | arg val, chan, src; 334 | [val, chan, src].postln; 335 | ~bend = val; 336 | }, chan:0); 337 | ) 338 | 339 | So, if I move the pitch wheel away from rest position...and then check the global bend value... 340 | 341 | ~bend; 342 | 343 | ...you can see that the MIDIdef is keeping track of it. But this MIDIdef also needs to update any notes which might currently be playing. So I'm going to use 'do', as we saw in tutorial number 6 on iteration, to iterate over the ~notes array and update the ~bend argument for each Synth. I'm going to map the raw bend value onto a transposition value ranging from down a whole step (-2) to up a whole step (+2). I'm using linlin, since both of these ranges are distributed linearly. 344 | 345 | ( 346 | ~bend = 8192; 347 | 348 | MIDIdef.noteOn(\noteOnTest, { 349 | arg vel, nn, chan, src; 350 | [vel, nn].postln; 351 | ~notes[nn] = Synth.new( 352 | \tone, 353 | [ 354 | \freq, nn.midicps, 355 | \amp, vel.linexp(1,127,0.01,0.3), 356 | \gate, 1 357 | ] 358 | ); 359 | }); 360 | 361 | MIDIdef.noteOff(\noteOffTest, { 362 | arg vel, nn; 363 | [vel, nn].postln; 364 | ~notes[nn].set(\gate, 0); 365 | ~notes[nn] = nil; 366 | }); 367 | 368 | MIDIdef.bend(\bendTest, { 369 | arg val, chan, src; 370 | [val, chan, src].postln; 371 | ~bend = val; 372 | ~notes.do{arg synth; synth.set(\bend, val.linlin(0,16383,-2,2))}; 373 | }, chan:0); 374 | ) 375 | 376 | You might be thinking: "Well...hold on. Aren't many of the items in the ~notes array nil?" And, you'd be right. At any given point, ~notes contains some mixture of nils and Synths. But, fortunately for us, nil understands the 'set' message, and responds to it by doing nothing, which is exactly what we want it to do. Here's your proof: 377 | 378 | nil.set(\freq, 880); 379 | 380 | We're almost done, but there's one last thing. What happens if we move the pitch wheel away from rest, and *then* strike a key on the synthesizer? 381 | 382 | You can see and hear that even though I'm moving the pitch wheel away from rest position, the initial frequency of the tone is not being affected. To fix this, the note-on MIDIdef needs to acknowledge the global position of the pitch wheel, and apply that position to the initial frequency of the tone. Fortunately, this is as simple as just adding one last argument to the code where the Synth is instantiated. 383 | 384 | ( 385 | ~bend = 8192; 386 | 387 | MIDIdef.noteOn(\noteOnTest, { 388 | arg vel, nn, chan, src; 389 | [vel, nn].postln; 390 | ~notes[nn] = Synth.new( 391 | \tone, 392 | [ 393 | \freq, nn.midicps, 394 | \amp, vel.linexp(1,127,0.01,0.3), 395 | \bend, ~bend.linlin(0,16383,-2,2), 396 | \gate, 1 397 | ] 398 | ); 399 | }); 400 | 401 | MIDIdef.noteOff(\noteOffTest, { 402 | arg vel, nn; 403 | [vel, nn].postln; 404 | ~notes[nn].set(\gate, 0); 405 | ~notes[nn] = nil; 406 | }); 407 | 408 | MIDIdef.bend(\bendTest, { 409 | arg val, chan, src; 410 | [val, chan, src].postln; 411 | ~bend = val; 412 | ~notes.do{arg synth; synth.set(\bend, val.linlin(0,16383,-2,2))}; 413 | }, chan:0); 414 | ) 415 | 416 | And that's really all there is to it. There are so many directions you can go from here. For example, you might try redefining your SynthDef and MIDIdefs so that the pitch wheel creates some other effect, like stereo beating, amplitude modulation...of course you can also change your sound source to be something other than a triangle wave...you could also create more MIDIdef, such as MIDIdef.cc, to respond to continuous controller messages. And this would enable the use of knobs, buttons, faders, a sustain pedal, an expression pedal, really all sorts of things. 417 | 418 | Anyway, that's it for tutorial number 9. I hope this has been helpful, and I hope you have some fun with this. Please leave any comments or questions on YouTube, and thanks for watching. -------------------------------------------------------------------------------- /full video scripts/11_script.scd: -------------------------------------------------------------------------------- 1 | //on iPad, quit all apps, make sure on wifi 2 | 3 | //video description 4 | opensoundcontrol.org 5 | 6 | Hey, welcome to tutorial 11. In the next few videos, we're gonna take a look at how SuperCollider integrates with the Open Sound Control protocol, more commonly known as OSC. In contrast to the MIDI protocol, OSC is faster, more flexible, more time-accurate, and doesn't include a hardware specification. OSC messages are most commonly sent over the internet, taking advantage of established net transmission protocols such as UDP and TCP. The syntax of an OSC message consists of a URL-style address, followed by some number of arguments. Often these arguments are numbers, but not always. OSC messages are sent over a network to a specific IP address on a specific port. 7 | ////////////////////// 8 | 9 | In this video, I'll be using TouchOSC, which is a multi-touch interface designer for iOS and Android. You can find some links and resources in the video description. 10 | 11 | //OSC DISABLED on TouchOSC 12 | 13 | So let's open TouchOSC. Right now we're looking at the main settings page, where you can set up your OSC connection, choose an interface layout, etc. In the layout list, you can view the available layouts, tap the one you want, I'll choose Jog-On, and then hit 'Done' in the upper right corner to bring it up. All the controls on this layout send OSC messages, but they won't do anything right now because I haven't set up an OSC connection yet. You can return to the main settings page by tapping the button in the upper right corner. In many cases, you'll probably want to make your own custom interface. You can't actually create or edit layouts within the TouchOSC app, instead, this is done in a separate application called TouchOSC editor. 14 | 15 | Since I'm using an iPad, I'm going to select the iPad layout size, and choose a horizontal orientation. Right-click or control-click on the canvas to bring up a list of available controls, and I'm going to keep things simple and just drop a few things onto the canvas. I'll grab a toggle button, two H faders, and an H rotary. You can move and resize controls by clicking and dragging, holding shift while dragging preserves the height-width ratio, and you can also copy and paste just as you would with a text editor. If you click on one of these controls, a panel appears on the left side of the editor, where you can change the color, the numerical output range, and various other parameters specific to that control. For example, I can change the rotary to be centered. If I click on the canvas, we can see that the name of this particular page is simply the number 1. Clicking on the toggle button, for example, you can see that the address of this control is /1/toggle1. This is the URL-style address I mentioned earlier. You can uncheck 'auto' and give your controls custom OSC addresses if you want, but they must start with a slash and look like a URL address. If I click on the page and change its name, then the controls' addresses are automatically updated to reflect the new page name. Here the new address is /main/toggle1. To add a new page, right click on the canvas top bar. The name of the new page defaults to the number 2, so if we add a control here, it's URL address is /2/fader3. (delete) 16 | 17 | Let's stop here and save this interface, I'll name it 'basic'. Make sure to save it in the TouchOSC layouts folder. After saving, we need to synchronize this layouts folder with the TouchOSC app. At the top, click 'sync,' and a window with step-by-step instructions appears. The first thing we need to do is make sure the computer and mobile device are both on the same network. I can see in the menu bar that my computer is on my home network. And I can verify that the iPad is connected to this same network. So that's the first step out of the way. The next thing we need to do is go into the TouchOSC app and navigate to Layout >> Add. On this screen, sometimes available TouchOSC hosts will pop up automatically, and sometimes they wont. (In this case, I can just tap my computer's name. In this case, it looks like the host computer isn't showing up). If you don't see your host computer, touch 'edit' in the upper right corner, then the plus sign in the upper left corner, and you can enter the computer's local IP address manually. On Mac OS, in System Preferences, under Network, I can check my local IP address, which happens to be [???.???.???.???]. So, back in TouchOSC, I simply enter this IP address exactly as it appears, hit enter, and tap the IP address that appears. And now, we can see that our 'basic' layout has been added to the layout list. Select it, and touch 'done'. 18 | 19 | Ok, so with that out of the way, let's read this OSC data into SuperCollider, and just print it in the post window. For this, we use one of two objects: OSCFunc, or OSCdef. Since I used MIDIdef in tutorial 9, I'll use OSCdef for the sake of consistency. But first, we need to boot the server, since IT is the program which actually sends and receives OSC data. 20 | 21 | s.boot; 22 | 23 | Before we move forward, I want to draw your attention to the help documentation, in particular, under Browse >> External Control >> OSC, and here we have a handful of relevant files. In particular, I suggest reading the guide called OSC Communication. 24 | 25 | I'm going to make an OSCdef that'll respond to OSC messages coming from the toggle button on the TouchOSC interface. The first argument is a symbolic name, and next we need a function to be evaluated when an OSC message is received. As was the case with MIDIdef functions, an OSCdef function also allows for a specific collection of input arguments. The OSCdef help file reads that 'When the function is evaluated it will be passed the arguments msg, time, addr, and recvPort'. So let's create four arguments for this function, enclose them in an Array, and just post them. 26 | 27 | ( 28 | OSCdef.new( 29 | \toggle, 30 | { 31 | arg msg, time, addr, port; 32 | [msg, time, addr, port].postln; 33 | } 34 | ) 35 | ) 36 | 37 | An OSCdef also needs the URL-style address corresponding to the source of the OSC message. We can see from the TouchOSC Editor that the toggle's path is /main/toggle1. We provide this address to the OSCdef as a Symbol. But, if we create a symbol using the backslash approach like usual, SuperCollider has trouble with this, syntactically. Instead we forgo the backslash syntax and use the alternate Symbol syntax of enclosing the address in single quotes. 38 | 39 | ( 40 | OSCdef.new( 41 | \toggle, 42 | { 43 | arg msg, time, addr, port; 44 | [msg, time, addr, port].postln; 45 | }, 46 | '/main/toggle1' 47 | ) 48 | ) 49 | 50 | 51 | 52 | This is generally enough data to get OSCdef to behave the way you want. But there's one last step. We have to jump back to TouchOSC and establish the OSC connection from that side. First things first, on the main settings page, we need make sure OSC communication is enabled. Next we need specify the IP address of the host computer, which we know to be [???.???.???.???]. The outgoing port should match the port that SuperCollider is listening to. By default, SuperCollider listens for incoming OSC data on port 57120, we can verify this by evaluating 53 | 54 | NetAddr.langPort; 55 | 56 | Next we need the incoming OSC port, and since we're not planning on sending data to TouchOSC, this doesn't really apply. And last is the local IP of the mobile device. In some cases you might have data coming from multiple OSC sources, in which case it might be necessary to take note of these different IP addresses, so you can separate out different streams of data. But in this case, it's not really necessary to know. 57 | 58 | Let's go to our saved basic layout, jump back to SuperCollider, and finally, let's evaluate the OSCdef. 59 | 60 | ( 61 | OSCdef.new( 62 | \toggle, 63 | { 64 | arg msg, time, addr, port; 65 | [msg, time, addr, port].postln; 66 | }, 67 | '/main/toggle1', 68 | ); 69 | ) 70 | 71 | If I now tap the toggle button on TouchOSC, we'll see the corresponding OSC data appear in the post window. So, the hard part's over. We're looking at all the data from the toggle button, including the address, the time received, the IP address of the source device... and maybe we don't want to see all that data. If we only want to see the OSC message proper, we can just remove the other arguments from the function and re-evaluate. 72 | 73 | ( 74 | OSCdef.new( 75 | \toggle, 76 | { 77 | arg msg, time, addr, port; 78 | msg.postln; 79 | }, 80 | '/main/toggle1', 81 | ); 82 | ) 83 | 84 | Notice that the message argument is, itself, an array, which contains the OSC address, followed by some number of numerical values (in this case, just the one toggle value). So if we only want the numerical arguments, but not the message address, we can ask for the item at index 1 within the message array, like this. 85 | 86 | ( 87 | OSCdef.new( 88 | \toggle, 89 | { 90 | arg msg, time, addr, port; 91 | msg[1].postln; 92 | }, 93 | '/main/toggle1', 94 | ); 95 | ) 96 | 97 | So that's looking pretty good. I'll make three more OSCdefs for the remaining three controls. One for the first fader...one for the second fader...and one for the knob. 98 | 99 | ( 100 | OSCdef.new( 101 | \toggle1, 102 | { 103 | arg msg, time, addr, port; 104 | ("toggle: "++msg[1]).postln; 105 | }, 106 | '/main/toggle1', 107 | ); 108 | 109 | OSCdef.new( 110 | \fader1, 111 | { 112 | arg msg, time, addr, port; 113 | ("fader 1: "++msg[1]).postln; 114 | }, 115 | '/main/fader1', 116 | ); 117 | OSCdef.new( 118 | \fader2, 119 | { 120 | arg msg, time, addr, port; 121 | ("fader 2: "++msg[1]).postln; 122 | }, 123 | '/main/fader2', 124 | ); 125 | OSCdef.new( 126 | \rotary1, 127 | { 128 | arg msg, time, addr, port; 129 | ("rotary 1: "++msg[1]).postln; 130 | }, 131 | '/main/rotary1', 132 | ); 133 | ) 134 | 135 | Back on the TouchOSC interface, here's the toggle again, fader 1... fader 2... and the knob. So everything's working, and now we're in a good position to start using this data to make sound. 136 | 137 | So I'm gonna quickly contstruct a SynthDef. An adsr envelope will control the amplitude of the sound, and the gate will be closed by default. The source of the sound will come from Blip.ar, which generates harmonic partials. I'll set a fundamental frequency, and to create a detuning feature, I'll multiply the frequency by a slow moving LFNoise1, ranging between negative and positive detune values. I'm envisioning detune as a number of semitones, so I'll use midiratio to make the output values suitable for frequency scaling. I also want to add a second argument to Blip for the number of harmonic partials to generate. I'm going to multichannel expand the noise generator, which means the enclosing Blip UGen is expanded to an array of 16 Blip generators each with a uniquely meandering frequency. 138 | 139 | For additional complexity, I'll also vary the amplitude with another sixteen unique noise generators. 140 | 141 | But since we don't have sixteen speakers, I'll use Splay to mix the 16 audio channels down to stereo. Next I want to be able to balance the left and right channels independently. But I can't use Pan2, because Pan2 expects a monophonic signal. Instead, I'll use Balance2. Balance2 expects the left channel, which I can specify as sig at 0, the right channel, sig at 1, and the pan control. Apply the envelope, apply the master amplitude argument, and output the signal. 142 | 143 | ( 144 | SynthDef.new(\tone, { 145 | arg freq=40, nharm=12, detune=0.2, gate=0, pan=0, amp=1, out=0; 146 | var sig, env; 147 | env = EnvGen.kr(Env.adsr(0.05,0.1,0.5,3), gate); 148 | sig = Blip.ar( 149 | freq * LFNoise1.kr(0.2!16).bipolar(detune).midiratio, 150 | nharm 151 | ); 152 | sig = sig * LFNoise1.kr(0.5!16).exprange(0.1,1); 153 | sig = Splay.ar(sig); 154 | sig = Balance2.ar(sig[0], sig[1], pan); 155 | sig = sig * env * amp; 156 | Out.ar(out, sig); 157 | }).add; 158 | ) 159 | 160 | Let's test it to make sure everything's working 161 | 162 | x = Synth.new(\tone, [\gate, 1]) 163 | 164 | x.set(\freq, 50) 165 | x.set(\pan, 0.0) 166 | x.set(\nharm, 12) 167 | x.set(\detune, 0.25) 168 | x.set(\amp, 1) 169 | x.set(\gate, 0) 170 | 171 | Ok cool. So using TouchOSC, I'd like to be able to control these parameters. gate, fundamental frequency, detune, number of harmonics, panning, and overall amplitude. So that's six parameters, but I've only got four controls on the interface. Fortunately, modifying layouts is really quick and easy. Switch over to the editor, make the desired changes, save the layout, click Sync, and on TouchOSC, go to the layout list, tap Add, select the host device, and it'll ask you if you want to overwrite your previous layout, tap ok, go back to the main settings page, and tap 'done'. 172 | 173 | Now all we need to do is add two more OSCdefs, and modify the functions of all 6 OSCdefs to control our Synth. I'll have the toggle control the gate, the first fader will control the frequency, but right now the fader ranges between 0 and 1, and these aren't suitable frequency values. So I have map this normalized output to a new range, suitable for audible frequency values. You can do this in TouchOSC or SuperCollider, whichever your prefer. I'll have the 2nd fader control the number of harmonics, the last fader, the overall amplitude. Technically I don't need to map these values, but I prefer an exponential range of amplitude values. The centered rotary will control panning, and the last rotary will control detuning, with a maximum of 12 semitones. 174 | 175 | ( 176 | OSCdef.new( 177 | \toggle1, 178 | { 179 | arg msg; 180 | x.set(\gate, msg[1]); 181 | }, 182 | '/main/toggle1', 183 | ); 184 | OSCdef.new( 185 | \fader1, 186 | { 187 | arg msg; 188 | x.set(\freq, msg[1].linexp(0,1,20,400)); 189 | }, 190 | '/main/fader1', 191 | ); 192 | OSCdef.new( 193 | \fader2, 194 | { 195 | arg msg; 196 | x.set(\nharm, msg[1].linlin(0,1,1,50)); 197 | }, 198 | '/main/fader2', 199 | ); 200 | OSCdef.new( 201 | \fader3, 202 | { 203 | arg msg; 204 | x.set(\amp, msg[1].linexp(0,1,0.005,1)); 205 | }, 206 | '/main/fader3', 207 | ); 208 | OSCdef.new( 209 | \rotary1, 210 | { 211 | arg msg; 212 | x.set(\pan, msg[1].linlin(0,1,-1,1)); 213 | }, 214 | '/main/rotary1', 215 | ); 216 | OSCdef.new( 217 | \rotary2, 218 | { 219 | arg msg; 220 | x.set(\detune, msg[1].linexp(0,1,0.01,12)); 221 | }, 222 | '/main/rotary4', 223 | ); 224 | ) 225 | 226 | And that's about it for tutorial number 11. In the next few videos I'd like to demonstrate how to use SuperCollider with the Nintendo Wiimote, as well as the Xbox Kinect, so stick around for more OSC tutorials. I also want to say a very genuine thank you to all the viewers and subscribers. I've gotten a lot of positive feedback on these videos, and I'm very happy to know that people find them useful and enjoyable. So, thanks for watching! See you next time. -------------------------------------------------------------------------------- /full video scripts/12_script.scd: -------------------------------------------------------------------------------- 1 | Hey everybody, welcome to tutorial 12. In this video, I'm gonna continue with OSC and show you the basics of getting wiimote data into SuperCollider, and using it to control sound. Out of necessity, this video is gonna be fairly specific to Mac OS. So if you're on another operating system, you might not find this video quite as helpful. But I'll try to put some useful information in the video description. Also, if you haven't watched the previous tutorial, I recommend you do so before watching this one, because it covers some fundamentals on the OSC protocol which in this video I'll sort of gloss over. 2 | 3 | The Wiimote is a bluetooth device, so the basic process is to connect one or more wiimotes to your computer via bluetooth, translate that data into OSC messages, and then receive these messages in SuperCollider. 4 | 5 | There is, actually, a WiiMote object in the class library. But from personal experience, and from what I've read on the SC listserv, I don't think this object works properly on Mac OS. Fortunately there's a bunch of software out there designed for the same purpose. In this video, I'm going to be using a program called OSCulator to translate wiimote data into OSC messages, which is available at OSCulator.net. At the time of making this video, the initial OSCulator download is free, but while it's running, OSCulator will periodically interrupt the flow of data to ask you to pay for the software. So this means the free version isn't really usable in a performance context, but it's fine for just messing around at home. The full version isn't very expensive, and more importantly it's very reliable and easy to use. In addition, the user manual for OSCulator is clear and thorough. I bought the full version awhile back and I've been very happy with it. 6 | 7 | A quick word of advice about wiimotes-- make sure you have a wiimote which is actually manufactured by nintendo. Cheap knock-off versions of the wiimote do exist, and a student of mine purchased one, and then found out the hard way that it would not connect with osculator. So just to be safe, I do recommend that you buy the real thing. 8 | 9 | Before opening OSCulator, you want to go to your bluetooth settings in system preferences and make sure Bluetooth is on and discoverable. My bluetooth is already on because I'm using a bluetooth keyboard and mouse. Then, open OSCulator, and a new default document should appear. In order to receive wiimote data, first we need to pair the wiimote with the computer. Click the wiimote button in the upper right to open the wiimote drawer. Now what you want to do is click the button that says 'start pairing', and while OSCulator scans for nearby wiimotes, you want to press and hold the small red button inside the wiimote battery compartment...like this. A green check mark should appear in the wiimote drawer, and the LED on the wiimote should light up. It's worth noting that if you're connecting a wiimote to your computer for the first time, you probably need to use the red button in the back to hard-sync the device to the computer. But if you've connected this wiimote to your computer before, I'm pretty sure you can just simultaneously press buttons 1 and 2 instead. But the red button should work in all cases. 10 | 11 | One of many convenient features of OSCulator is that whenever OSC or wiimote data is received, that data is automatically added to the list on the main routing view, and automatically given an OSC address. Since I've waved the wiimote around a little bit after pairing, OSCulator has detected incoming data for pitch, roll, yaw, and accelerometer. (and it looks like i've accidentally pressed the B button as well). As soon as I press a button on the wiimote, a corresponding OSC address appears in the list, as you can see. OSCulator generates these addresses automatically, but you can change them if you want. A yellow light next to the address means that OSCulator is receiving data from that particular input. 12 | 13 | Another nice feature is that you can click on a particular incoming message, let's click on pitch, then click the "quick look" button that appears on the top of the document, alternatively you can just hit spacebar. This brings up a live display of of the data for that message, and the data is normalized, so it ranges between zero and one....here's the roll data...and...yaw gives...shall we say, interesting results. I don't know this for sure, but I think the yaw data relies on the infrared sensor bar that sits above or below your screen when you're actually playing the wii. And I'm guessing that with respect to yaw data, without the infrared bar, the device just has no idea which way it's pointing. So I haven't really used yaw in any of my work, but I guess it makes a semi decent random number generator. Or maybe my wiimote is just broken. Who knows. Anyway here's the accelerometer data, which works just fine. Note that at rest position, values hover around 0.2. Quick motions tend to spike the data toward 1, and when the device is in freefall, the data dives close to zero. There's a small delay in the camera feed, but you get the picture. Buttons send a single value of 1 when depressed, and a single 0 when released. 14 | 15 | So, let's take this data and send it to SuperCollider as a stream of OSC messages. I'm gonna start with pitch data. So next to pitch, in the event type column, select "OSC routing", and in the value column, select "new". 16 | 17 | This brings up the parameters window, and there are two main views. On the top we have targets, where we identify the devices to which OSCulator sends OSC messages-- in this case, SuperCollider is our desired target. In the previous video, I mentioned that OSC messages are sent to a specific IP address, on a specific port. Since these messages are being sent to and from the same physical computer, I'm gonna use the loopback IP address 127.0.0.1. And remember that SuperCollider receives OSC messages on port 57120. You can double click and enter the IP address and port using the following syntax: 127.0.0.1:57120. 18 | 19 | On the bottom view, we have a list of routes. This D on the left stands for default target, and the default target is determined by these radio buttons in the target list above. Right now of course we only have one target, and it is set as the default, so the D is fine, but alternatively you could select the number 1, since SuperCollider is listed as target number one. Click on the rewrite address column, and we can see that the default OSC message to be routed consists of the original address, automatically assigned by OSCulator, and all the arguments that make up this message-- of course in the case of pitch, it's just one numerical value corresponding to the pitch orientation. I'm going to leave the message as is for now, close the parameter window, and make sure the default target is selected for the pitch message. Notice that now we see a green light next to the pitch message, which indicates that data is not only being received by OSCulator, but also being routed elsewhere. 20 | 21 | Now we're ready to receive this data in SuperCollider. Boot the server, and let's create a simple OSCdef to print the incoming data. As we saw in the previous tutorial, there are other arguments that can be included in the argument declaration, but right now I'm only interested in the message itself. Remember that the message is actually an array, where the 0th item is the address, and subsequent items are the message arguments. So I'm going to print the pitch data, which is at the 1st index. And we also need the incoming OSC address, specified as a symbol. We can see in the OSCulator document that the address for pitch is /wii/1/accel/pry, which stands for pitch roll yaw, and then we append /0, because we can see the zero just to the left of the word 'pitch'. 22 | 23 | s.boot; 24 | 25 | ( 26 | OSCdef.new( 27 | \pitch, 28 | { 29 | arg msg; 30 | msg[1].postln; 31 | }, 32 | '/wii/1/accel/pry/0' 33 | ); 34 | ) 35 | 36 | And there you have it, pitch data flowing into SC like tap water. 37 | 38 | OSCdef(\pitch).disable; 39 | 40 | It is possible to receive pitch, roll, yaw, and accelerometer data as four arguments in a single OSC message. If we go back to OSCulator, we can remove the routing for pitch, and add a routing for the parent message. Now, we just create a slightly different OSCdef, most importantly changing the incoming OSC address, but also displaying all four arguments. 41 | 42 | ( 43 | OSCdef.new( 44 | \prya, 45 | { 46 | arg msg; 47 | msg[1..4].postln; 48 | }, 49 | '/wii/1/accel/pry' 50 | ); 51 | ) 52 | 53 | And we see an array of pitch, roll, yaw, and accelerometer data, in that order. 54 | 55 | OSCdef(\prya).disable; 56 | 57 | To process data from the buttons, the procedure is basically the same. Although I'm going to use this opportunity to show you how to customize the incoming OSC addresses. Let's say we want to get data from the A button. Choose OSC routing, and in the value column, click 'new'. Click the plus button to create a second routing (otherwise we'd overwrite the routing for the motion data). Then click the rewrite address for the new routing, and at the top of the editor, enter your preferred OSC address. I'm gonna keep things mega simple and just call this address /a. ) On the main routings page, make sure this new address is selected in the value column. 58 | 59 | So now I should be able to make a new OSCdef to process this new incoming message, like so. 60 | 61 | ( 62 | OSCdef.new( 63 | \abutton, 64 | { 65 | arg msg; 66 | msg[1].postln; 67 | }, 68 | '/a' 69 | ); 70 | ) 71 | 72 | So that's that essence of getting wiimote data into SuperCollider. The only thing left to do is actually make some sound. So we'll start with a SynthDef. The essence of this sound will be a very low frequency sawtooth wave, sent through a bandpass filter. A plain vanilla sawtooth wave at a subaudio frequency just sounds like a sequence of pops. But when sent through a high quality bandpass filter, we emphasize a narrow range of the frequency spectrum while attenuating the rest of the spectrum. In this way we can draw perceptable tones out of these otherwise noisy pops. The amplitude is shaped by an adsr envelope. The fundamental frequency deviates to some degree, and I'm multichannel expanding the UGen by 8, so we'll have 8 unique meandering sawtooth waves. Before going through the bandpass filter, I amplify these 8 waveforms by some amount, and then fold the waveforms back into a normalized range, iteratively, five times. Folding. Alright what's folding? If I had to put this technique in some sort of category, I'd say it's a form of distortion, kind of like clipping, but instead of flattening the portion of the signal that's out of range, that portion is folded back into range, so the +1 and -1 amplitude boundaries act sort of like a mirror. A sample with an amplitude of 1.1 would end up at 0.9, 1.2 would end up at 0.8, etc. And so a sawtooth wave, which ordinarily has these sharp corners, takes on even more of a jagged appearance after folding. Essentially this technique introduces some noise into an otherwise periodic signal. Moving on to the bandpass filter, both the center frequency and quality of the filter are randomized to some degree. And Splay distributes the array of 8 signals into 2 channels. 73 | 74 | ( 75 | SynthDef(\foldsaw, { 76 | arg freq=4, detune=1, atk=0.01, dec=0.3, rel=3, c1=1, c2=(-1), gate=1, 77 | mincf=40, maxcf=12000, minrq=0.002, maxrq=0.2, boost=1.25, amp=1, out=0; 78 | var sig, env; 79 | env = EnvGen.kr(Env.adsr(atk,dec,0.5,rel), gate, doneAction:2); 80 | sig = Saw.ar( 81 | freq + 82 | LFNoise1.kr(LFNoise1.kr(0.5!8).range(0.1,1)).bipolar(detune); 83 | ); 84 | 5.do{sig = (sig*boost).fold(-1.0,1.0)}; 85 | sig = BPF.ar( 86 | sig, 87 | LFNoise1.kr({ExpRand(0.005,0.05)}!8).exprange(mincf, maxcf), 88 | LFNoise1.kr({ExpRand(0.1,0.75)}!8).range(minrq, maxrq) 89 | ); 90 | sig = Splay.ar(sig) * env * amp * 2; 91 | Out.ar(out, sig); 92 | }).add; 93 | ) 94 | 95 | using the default argument values I've provided, the SynthDef sounds like this: 96 | 97 | x = Synth.new(\foldsaw); 98 | x.set(\gate, 0); 99 | 100 | So, big picture, here's what I'm going to do: The a button will add a Synth to an array, the b button will remove a synth from the array, assuming the array isn't empty. And the motion data will control sound parameters for the existing synths. 101 | 102 | I'm going to create a group, and whenever I create a synth, i'll put it in this group. This will make it very easy to set parameters for all existing Synths, beacuse I can just address the group itself. I'll also create an empty array, in which Synths will be stored. I'm gonna make three OSCdefs- one for motion data, one for the a button, and one for the b button. 103 | 104 | The OSCdef for the motion data will send a set message to the group, in effect this will set argument values for all synths in the group. the detune amount will be controlled by pitch orientation, exponentially mapped from 1 thousandth to 80Hz. But remember that the fundamental frequency of the sound is initially very low, so we won't hear a change in perceivable pitch, but rather we'll hear a change in the rate of filtered pops. And when the wiimote is pointed upward, the high detune value will cause the fundamental frequency to be above 20Hz, so we might hear the discrete oscillations of the sawtooth wave fuse into a perceivable tone. Roll will control the filter quality-- so when the wiimote is rolled to the left, we'll have a high quality filter, and a low quality filter when rolled to the right. high acceleration will increase the boost argument, which means the folding distortion will be more pronounced when the wiimote moves quickly. 105 | 106 | Ok next OSCdef-- the a button. When the button is depressed, in other words, if the message from the A button equals 1, a new Synth is created and appended to the existing array, with a random frequency, attack time, and release time. For the b button, if the button is depressed and the array is not empty, fade out the oldest synth, and remove it from the array. 107 | 108 | for extra clarity, I'm going to bring up the visual server tree, so we can actually watch as Synths are created and released. 109 | 110 | And finally, I'm going to jump back to OSCulator to set up a routing for the B button, and double check to make sure everything is configured correctly. 111 | 112 | s.plotTree; 113 | 114 | ( 115 | g = Group.new; 116 | a = []; 117 | 118 | OSCdef.new(\prya, { 119 | arg msg; 120 | g.set( 121 | \detune, msg[1].linexp(0,1,0.001,80), 122 | \maxrq, msg[2].linexp(0,1,0.01,0.5), 123 | \boost, msg[4].linlin(0.2,1,1,16), 124 | ); 125 | }, '/wii/1/accel/pry' 126 | ); 127 | 128 | OSCdef.new(\a, { 129 | arg msg; 130 | if( 131 | msg[1]==1, 132 | { 133 | a = a.add( 134 | Synth.new( 135 | \foldsaw, 136 | [ 137 | \freq, exprand(1,8), 138 | \atk, exprand(2,8), 139 | \rel, exprand(2,8), 140 | ], g 141 | ) 142 | ) 143 | } 144 | ); 145 | }, '/a' 146 | ); 147 | 148 | OSCdef.new(\b, { 149 | arg msg; 150 | if( 151 | (msg[1]==1) && (a.size>0), 152 | { 153 | a[0].set(\gate, 0); 154 | a.removeAt(0); 155 | } 156 | ); 157 | }, '/b' 158 | ); 159 | ) 160 | 161 | And there you have it. Wiimotes and supercollider. It's worth noting that OSCulator is capable of connecting to eight wiimotes simultaneously, which gives you a lot more data to play with. And OSCulator doesn't just translate wiimote data to OSC-- it's a fully featured program when it comes to OSC routing, so you could have OSC messages coming from any number of sources, routed to any number of targets. You can also have multiple OSCulator documents open and running simultaneously, so the possibilities are endless. I hope you enjoyed this tutorial, and thanks for watching. -------------------------------------------------------------------------------- /full video scripts/21_script.scd: -------------------------------------------------------------------------------- 1 | Hey everyone, welcome to Tutorial 21. In this video I've decided to cover some introductory approaches to frequency modulation synthesis, usually just called 'FM' for short. FM refers to a synthesis configuration in which the output signal of one oscillator, called the modulator, is used to offset the frequency of another oscillator, called the carrier. This is a really simple concept, but as we'll see in this video, FM is capable of creating a really impressive variety of sound that ranges from pure sine waves to dense, complex, and chaotic spectra, all of which can be generated from as few as two oscillators. In classic FM, the carrier and the modulator are both sine waves, so that's a good place for us to start. 2 | 3 | Let's boot the server, and also launch our scope and frequency analyzer utilities, so that we can see what we're dealing with as we hear it. 4 | 5 | s.boot; 6 | s.scope; 7 | FreqScope.new; 8 | 9 | Here's a sound we've heard many times in this tutorial series: a lovely sine wave 10 | 11 | {SinOsc.ar(500) * 0.2!2}.play; 12 | 13 | with a fixed frequency of 500 Hz, amplitude scaled down by 0.2, and invoking multichannel expansion so that we hear it in both speakers. This will be our carrier oscillator, and the conventional way of introducing FM is to simply add another audio rate oscillator to the carrier frequency, like this. This'll be our modulator. Initially I'll set its frequency to be 1 Hz, and for right now, that's the only thing I'll specify: 14 | 15 | {SinOsc.ar(500 + SinOsc.ar(1)) * 0.2!2}.play; 16 | 17 | Right off the bat, this sounds like basically the exact same thing we just heard. But, we can see just the tiniest bit of left-right movement on our visuals, indicating that the frequency of the carrier is oscillating. If we don't specify otherwise, the output range of a SinOsc UGen is between negative 1 and positive 1, which means the frequency of the tone we're listening to is fluctuating between 499 and 501 Hz. And this is, like, *just* enough deviation for our ears to notice a change. It's sort of right on the threshold of perception. But, to get more substantial variation in the carrier frequency, all we have to do is amplify the modulating oscillator. The simplest way to do this is to specify a value for its 'mul' argument. With mul at 20, the carrier ranges from 480 to 520 Hz: 18 | 19 | {SinOsc.ar(500 + SinOsc.ar(1, mul:20)) * 0.2!2}.play; 20 | 21 | definitely wide enough to notice. And, with mul at 400, we get a very generous range, sweeping from 100 all the way up to 900 Hz. 22 | 23 | {SinOsc.ar(500 + SinOsc.ar(1, mul:400)) * 0.2!2}.play; 24 | 25 | So, technically we're already doing FM, but we haven't gotten into the really interesting stuff yet. All we've done for now is just make this wacky exaggerated vibrato effect. So, let's increase the modulator frequency little by little. So, instead of 1 Hz, here's 2 Hz...4 Hz...8 Hz. 26 | 27 | {SinOsc.ar(500 + SinOsc.ar(2, mul:400)) * 0.2!2}.play; 28 | {SinOsc.ar(500 + SinOsc.ar(4, mul:400)) * 0.2!2}.play; 29 | {SinOsc.ar(500 + SinOsc.ar(8, mul:400)) * 0.2!2}.play; 30 | 31 | Now, I'm gonna go ahead and replace this static value with MouseX, so that the horizontal screen position of the cursor controls the modulator frequency, starting at 1 Hz on the left edge, and 2 kHz at the right edge, and one for the third argument of MouseX specifies an exponential mapping. We'll also poll these values, so we can see them in the post window. And listen carefully, because there's a very interesting transformation that occurs as the modulator frequency crosses into the audible spectrum, somewhere around 20 Hz, give or take a little. 32 | 33 | {SinOsc.ar(500 + SinOsc.ar(MouseX.kr(1,2000,1).poll, mul:400)) * 0.2!2}.play; 34 | 35 | It's a pretty wild sound! This line of code is similar to one of the examples in the SinOsc help file. The syntax is a little different, but the concept is exactly the same: 36 | 37 | {SinOsc.ar(SinOsc.ar(XLine.kr(1,1000,9), 0, 200, 800), 0, 0.25)}.play; 38 | 39 | This was actually one of the first sounds I ever made in SuperCollider, like 12 years ago, when all I knew how to do was boot the server and run helpfile examples, and I remember it just completely broke my brain. I had no idea what I was hearing, or that sounds like this could exist — no clue what was going on. It was great. 40 | 41 | So, if you are similarly excited by this kind of sound, and just want to mess around, you can, for example, substitute some more unit generators and just see what happens. For example, lets use MouseY for the carrier frequency, and poll that as well: 42 | 43 | {SinOsc.ar(MouseY.kr(200,5000,1).poll + SinOsc.ar(MouseX.kr(1,2000,1).poll, mul:400)) * 0.2!2}.play; 44 | 45 | And let's use a non-interpolating noise generator for modulator amplitude, generating new values eight times a second, which can range from 20 to 10,000: 46 | 47 | {SinOsc.ar(MouseY.kr(200,5000,1).poll + SinOsc.ar(MouseX.kr(1,2000,1).poll, mul:LFNoise0.kr(8).range(20,10000))) * 0.2!2}.play; 48 | 49 | So that's really fun. Endless hours of entertainment. But this line of code is kind of long, not very readable, hard to tell at a glance what's going on, so real quick, for good practice, let's revisit concepts from tutorial three and convert this function into a SynthDef, so that we can treat it more like a blueprint for a sound, rather than a sort of fixed sound object. 50 | 51 | For arguments, we need a carrier frequency, a modulator frequency, and a modulator amplitude. I'm just gonna give these some arbitrary but sensible default values. We also need variables to keep a reference to our carrier and modulator oscillators. Our modulator is a sine wave, running at modHz, scaled by modAmp, and our carrier (also a sine wave) is running at a frequency determined by carHz plus the modulator signal. And for now, we'll keep our fixed amplitude scaling like we had before, and our multichannel expansion. Make sure to add an Out unit generator, and add the SynthDef to the audio server. 52 | 53 | ( 54 | SynthDef.new(\fm, { 55 | arg carHz=500, modHz=100, modAmp=200; 56 | var car, mod; 57 | mod = SinOsc.ar(modHz, mul:modAmp); 58 | car = SinOsc.ar(carHz + mod) * 0.2!2; 59 | Out.ar(0, car); 60 | }).add; 61 | ) 62 | 63 | With default argument values it sounds like this: 64 | 65 | Synth(\fm) 66 | 67 | Visiting concepts from tutorial 4, it would make a lot of sense to add an amplitude envelope, so that every FM sound we create has a beginning and an end, so it doesn't just go on forever. I'm just gonna do a basic percussive envelope, with an attack and release. 68 | 69 | ( 70 | SynthDef.new(\fm, { 71 | arg carHz=500, modHz=100, modAmp=200, atk=0.01, rel=1; 72 | var car, mod, env; 73 | env = EnvGen.kr(Env.perc(atk, rel), doneAction:2); 74 | mod = SinOsc.ar(modHz, mul:modAmp); 75 | car = SinOsc.ar(carHz + mod) * env * 0.2!2; 76 | Out.ar(0, car); 77 | }).add; 78 | ) 79 | 80 | Synth(\fm); 81 | 82 | Let's turn this 0.2 into an actual amplitude argument, so that we have some basic volume control, if we want to use it. And for fun, let's add a pan argument so that we can position each FM sound in the stereo field. So, we'll remove our multichannel expansion here, and instead run this now mono signal through a Pan2. 83 | 84 | ( 85 | SynthDef.new(\fm, { 86 | arg carHz=500, modHz=100, modAmp=200, 87 | atk=0.01, rel=1, amp=0.2, pan=0; 88 | var car, mod, env; 89 | env = EnvGen.kr(Env.perc(atk, rel), doneAction:2); 90 | mod = SinOsc.ar(modHz, mul:modAmp); 91 | car = SinOsc.ar(carHz + mod) * env * amp; 92 | car = Pan2.ar(car, pan); 93 | Out.ar(0, car); 94 | }).add; 95 | ) 96 | 97 | And now let's just check a few things: so we have hard left: 98 | 99 | Synth(\fm, [\pan, -1]); 100 | 101 | hard right: 102 | 103 | Synth(\fm, [\pan, 1]); 104 | 105 | in the middle: 106 | 107 | Synth(\fm, [\pan, 0]); 108 | 109 | and just confirming that amplitude works: 110 | 111 | Synth(\fm, [\pan, 0, \amp, 0.7]); 112 | Synth(\fm, [\pan, 0, \amp, 0.1]); 113 | 114 | and some of our other arguments: 115 | 116 | Synth(\fm, [\pan, 0, \amp, 0.3, \carHz, 200]); 117 | Synth(\fm, [\pan, 0, \amp, 0.3, \carHz, 1000]); 118 | Synth(\fm, [\pan, 0, \amp, 0.3, \carHz, 1000, \modHz, 200]); 119 | Synth(\fm, [\pan, 0, \amp, 0.3, \carHz, 1000, \modHz, 200]); 120 | Synth(\fm, [\pan, 0, \amp, 0.3, \carHz, 1000, \modHz, 300]); 121 | Synth(\fm, [\pan, 0, \amp, 0.3, \carHz, 1000, \modHz, 360]); 122 | 123 | Yeah, so, let's just go nuts here and dump a full plate of randomness all over these arguments. And, for carrier & modulator frequency, and modulator amplitude, there are really no, you know, "dangerous" values — it's just one oscillator controlling the frequency of another oscillator. So, extreme values might give you some glitchy results, but for these three it's basically anything goes. For overall amplitude, envelope, and pan, we'll just do some sensible random ranges here. Feel free to copy along with me, or just chart your own path. 124 | 125 | ( 126 | Synth(\fm, [ 127 | \carHz, exprand(20, 10000), 128 | \modHz, exprand(20, 10000), 129 | \modAmp, rrand(0, 10000), 130 | \amp, exprand(0.1, 0.5), 131 | \atk, exprand(0.001, 0.05), 132 | \rel, exprand(0.05, 1.2), 133 | \pan, rrand(-1.0, 1.0), 134 | ]); 135 | ) 136 | 137 | And, already, we're getting a very pleasant variety here. And then finally, to really put the SuperCollider platform to good use, let's use Pbind from tutorial 10 to make an FM sequencer, specifying SynthDef name and delta time, let's do an eighth of a second for each event, and then just using what we already have. But, if we use these random number generator as they appear, then the first Synth that Pbind generates will initially be random, but these random values will then persist for every Synth, so it’ll sound like this: 138 | 139 | ( 140 | p = Pbind( 141 | \instrument, \fm, 142 | \dur, 1/8, 143 | \carHz, exprand(20, 10000), 144 | \modHz, exprand(20, 10000), 145 | \modAmp, rrand(0, 10000), 146 | \amp, exprand(0.1, 0.5), 147 | \atk, exprand(0.001, 0.05), 148 | \rel, exprand(0.05, 1.2), 149 | \pan, rrand(-1.0, 1.0), 150 | ).play; 151 | ) 152 | 153 | p.stop; 154 | 155 | And that's not really what we want. So, the correct thing to do is convert these language-side random number generators into their pattern-equivalent objects. 156 | 157 | ( 158 | p = Pbind( 159 | \instrument, \fm, 160 | \dur, 1/8, 161 | \carHz, Pexprand(20, 10000), 162 | \modHz, Pexprand(20, 10000), 163 | \modAmp, Pwhite(0, 10000), 164 | \amp, Pexprand(0.1, 0.5), 165 | \atk, Pexprand(0.001, 0.05), 166 | \rel, Pexprand(0.05, 1.2), 167 | \pan, Pwhite(-1.0, 1.0), 168 | ).play; 169 | ) 170 | 171 | p.stop; 172 | 173 | So, this is what I'd consider to be a very entry-level introduction to FM in SuperCollider. Feel free to go absolutely crazy on these numbers, and see what kind of sounds and patterns and textures you can make, you know, maybe stretch out the duration of the envelope to create a drone texture, or use more predictable patterns instead of these random ones. Maybe you can also very the time between events, so it's not always 1/8th of a second. But, one thing we cannot do very easily with this code is play a tune — you know, like, create a specific melody. This is because, in FM synthesis, neither the carrier frequency nor the modulator frequency directly corresponds to the pitch that our brain perceives. The pitch we perceive, and also whether we actually perceive a clear pitch at all, is the result of a more complex relationship between carrier frequency modulator frequency and modulator amplitude. So, we'll continue this conversation in the next video, where we'll back up a few steps, dive little deeper, unpack all these numbers, figure out what they mean, and give ourselves a more robust understanding of the principles behind classic FM, and how to implement them in SuperCollider. So, thanks for watching this video, I hope you have some fun making these FM bleeps and bloops, and see you next time. -------------------------------------------------------------------------------- /full video scripts/27_script.scd: -------------------------------------------------------------------------------- 1 | Hey everybody, welcome to tutorial 27. This video's gonna be a little different than usual—I'm gonna show you how you can help contribute to the development of SuperCollider using git and github. 2 | 3 | Full disclosure, for all the SuperCollider users out there who are primarily programmers dabbling in music, or if you're a regular github user, there's a good chance this video won't teach you anything new. I'm imagining this tutorial is much more useful for users who are primarily musicians dabbling in programming. And if the idea of helping to develop SuperCollider seems overwhelming, remember that development contributions come in many different forms. It doesn't necessarily mean a deep dive into C++, or fixing elusive bugs, rather, it might be as simple as fixing a type in one of the help files, or maybe refining a code example in the documentation that isn't quite as clear or instructive as perhaps it could be. These are examples of bite-size helpful changes you can propose, that don't require an extensive programming background. And as for why you should consider helping with development, it's my opinion that having more active users making good faith contributions, means SuperCollider becomes more robust, more user-friendly, and has better prospects for longevity. So remember, no contribution is too small, and every little bit helps. 4 | 5 | If you're not familiar with git and github, you're not alone, because I'm a relatively new user myself. In fact, to prepare for this video, I sat down with one of the SuperCollider developers, Brian Heim, and he very graciously walked me through the process from start to finish. So, big thank you to Brian for the help, and I'm very pleased to be able to pass his knowledege on to you, in hopes that you'll put it to good use. 6 | 7 | Out of necessity, this tutorial was developed with macOS in mind, but I'm cautiously optimistic that most if not all concepts can be easily extrapolated and applied to other operating systems. 8 | 9 | The first step is to identify some kind of contribution you'd like to make. So before making this video, I rummaged through a lot of the help files, and noticed that in the guide file titled 'Glossary,' in the entry for server, the word "see" accidentally appears twice in a row in the last sentence. So, let's fix it. 10 | 11 | In this video we're going to be working at the command line a fair amount, so find your operating system's command line interface, and open it up. On macOS, this is called Terminal, located in your Applications folder...and the Utilities subfolder. If you've never used the command line before, think of it as a text-based interface for using your computer, harder-to-learn than the graphical interface you're accustomed to, with the mouse and folder icons and all that, but much more powerful. 12 | 13 | Next, make sure you have Git installed. In short, Git is version control software that helps track changes during the process of software development, it's free and open source. To see if you've already got git installed, type git version and hit enter. If you see a version number, you're good to go. If git is not installed, this action may trigger an installation prompt that you can follow, or alternatively, you can go to git-scm.com/downloads and follow the instructions there. 14 | 15 | In addition, you'll also need a github account, which is also free. GitHub is a website that integrates with Git and allows people from all over the world to collaborate on software development projcets. Once you've created an account, log in. The SuperCollider github account is here, at github.com/supercollider, and the main supercollider code repository, or "repo" for short, is here at github.com/supercollider/supercollider. 16 | 17 | In terms of the big picture, what you're gonna do is make a copy of this repo for yourself, edit that copy locally, on your personal machine, and then request to have your changes merged into the main SuperCollider project. 18 | 19 | So, the next step is to decide where to store this local copy of the SuperCollider project. Doesn't really matter where it goes, as long as you're aware of where you're putting it. I'm gonna go to my home folder, then into Documents, and make a new folder called GitHub. And then, at the command line, navigate into this folder. We do this which a change directory command, which is 'cd'. So, cd, space, and then the absolute path to this folder. If you're not sure what that is, a quick option is to just click and drag the folder into the command line, and hit enter. If you like, you can follow this up with a pwd command, which is 'print working directory'. This is not a necessary step, but it's a good command to be familiar with, useful for confirming your current location within a hierarchy of folders. 20 | 21 | Back on github, we're gonna make a copy of the supercollider repo for ourselves, and this is called forking. On the supercollider repo page, click the button in the upper right that says fork. This'll take a few seconds, and when it's done, github should automatically take you to your page for the fork you just created. For me, that is github.com/elifieldsteel/supercollider. 22 | 23 | So we've got a copy of the repo on our github page, now we need to make yet another copy that resides locally, on your computer. This is called cloning, and at the command line, the command for this action is git ...space... clone ...space... the URL to the fork you just created...we'll copy and paste this from your browser ... then one more space and 'dash dash recursive', and this last bit ensures that all of the submodules in the supercollider repo get cloned as well. Hit enter, this process should take a about a minute or so, so just hang out while it works. I'm gonna fast forward here...once it's done, you'll be able to find the local clone, in whatever folder you chose as your working directory. 24 | 25 | Back at the command line, cd supercollider forward slash to navigate into the repo itself, and now, sort of a technical step, we want to manage the remote repos on github that this local repo is tracking, so that it stays up-to-date with other developments. So first, the command git remote dash-v, and you should see something like this, which means the local repo is tracking your personal fork as a remote. What you want to do, is add the *original* repo as a second remote, and we do this with git remote add upstream ...and then the URL to the main supercollider repo, you can type it in or just copy and paste from the browser. 26 | 27 | Now git remote -v once again, and we can confirm that we've successfully added the main repo as a remote. 28 | 29 | With that step completed, the next sensible thing to do is update your fork. Now, if this is your first time doing all of this, forking, cloning, etc, then this next step is probably not necessary, since you've *just* now forked, and your copy is almost certainly up-to-date. Updating your fork is a much more important thing to do if, for example, you forked a year ago and haven't touched it since then, in which case, probably, a lot of changes have happened since then and your fork is out of date. No matter the case, updating your fork is a good thing to know how to do, and it goes like this: 30 | 31 | git fetch upstream 32 | (which downloads everything from the upstream remote) 33 | 34 | git checkout develop 35 | (this makes sure we're on the correct branch of the project, and develop is the name of the default branch for the main repo) 36 | 37 | and then 38 | git pull upstream develop 39 | (which brings your develop branch up-to-date with the main repo's develop branch) 40 | 41 | I want use this moment to point out that on the main repo page, there's a wiki, and at the bottom of that wiki, a bunch of links, one of which is titled Creating Pull Requests. And this page contains a lot of the same information I'm covering here, like forking, cloning, keeping things up-to-date, etc. 42 | 43 | We're almost ready to correct that typo, but if we run git branch we can see that we're currently on the develop branch, and this is not where we want to be for making a simple typo correction, so just a quick discussion about branches--on the main repo page on github, 'develop' is the default branch of the project. conceptually, 'develop' is the branch where all of the primary, substantial things take place for improving, evolving the platform, creating the next stable release of supercollider, but if all changes took place on this one branch, big, medium, and small, then it'd just be a big mess, and just not a very clean way of working. So the accepted practice is to create a new branch, specifically for your contribution. And in fact, if you scroll down on this list of branches, you can see that there are already several topic branches, dedicated for individual purposes. 44 | 45 | So, back at the command line we create a new branch with git...checkout...dash-b, and then the name of the branch we're creating, so this needs to be a single string without spaces, and by convention encouraged by the supercollider developers, this shuold start with topic/ followed by some short but meaningful descriptor of why this branch is going to exist. so i'll say topic slash glossary dash typo...and hit enter. 46 | 47 | Run git branch again, and you can see we've created a new branch, and switched onto it. 48 | 49 | Alright, time to fix that typo. Somehow, we've got to locate the relevant file or files in the local repo, in my case, the Glossary guide document. That...could take awhile if we just mindlessly poke around, so one thing you can do is go back to the project wiki, and go to the link for Project Structure at the bottom, this gives an hierarchical overview of where project assets are located, so the HelpSource directory contains all the help docs, and alternatively, since we're editing a help document, we could view to that document in the SuperCollider IDE, scroll all the way to the bottom, and there's a path that contains info on the location of this file. So we can see it should be in HelpSource/Guides. So let's track it down...here it is...and we just want to open this in some text editor, really any text editor is fine, you could even do this using SuperCollider. So, make the change, and save the file. 50 | 51 | At the command line, run git status, and this confirms that we have in fact made changes to our branch, specifically to the Glossary dot schelp file. So the first step is to add this change to the staging area, which we do with git...add...and then we could just put the local path to the file we changed, so that would be HelpSource slash Guides slash Glossary dot schelp, but that's a lot to type, and you can imagine this could be really tedious if your contribution involves multiple files, so instead we can take advantage of the fact that add is recursive by default, so we can simply add a parent folder, like HelpSource, or even simpler, git add ... period, and this adds any changes within the entire working directory, in other words the entire local repo. 52 | 53 | Git status again, and that change is now green, which means it is staged for inclusion in the next commit. After we're done adding changes, we're ready to commit those changes, essentially a commit is like saving a snapshot of the the state of the repo at a point in time. So that command is git... commit...dash-m to add a commit message, and then the commit message itself, in quotes, and similar to the branch name, this should be a short, meaningful message that describes the nature of this commit. Specifically, Brian recommends the following convention: the broad project component, so in thi scase that would be docs (or maybe helpdocs, or helpsource, something like that), colon, and then a short descriptor phrase as if it were a simple computer command, so i'd go with fix glossary typo. 54 | 55 | Run that, and then git status... confirms we've got no more changes to commit. 56 | 57 | From here, we want to push this new branch to our personal fork on github, so that this new branch is visible there as well, we do that with git ... push... --set-upstream origin, which means the local clone will track changes in your remote fork, and then the name of the branch, which is topic/glossary-typo 58 | 59 | Run this, which takes a few seconds...and once it's done, go back to your fork on github, and you should be able to see confirmation of your recent push, right at the top. Usually this happens pretty quick but in some cases it might take a minute or so to show up. 60 | 61 | The final step is to make a pull request, which essentially means asking the SC developers to incorporate your changes into the develop branch, so that your contributions will be reflected in the next release. Click this green button that says 'compare and pull request', and complete the form on this page, in particular, the purpose and motivation of the pull request. In this case it's just a simple typo, so something brief is fine, though I imagine for more substantial contributions you might want to put some detailed information here. For types of changes, delete the lines that aren't applicable. This is strictly a documentation change, and not any of these things. And then check off the to-do list by putting a lowercase x in each set of brackets. So, we've updated documentation, and this pull request is ready for review, and even though this change doesn't involve any code testing, Brian recommends checking off items that don't apply, just to show that, you know, all tests are passing because...there's no code to be tested. 62 | 63 | Preview your PR if you like, and if it looks good, click on create pull request. This completes the process of initiating a pull request, so from hree, at some point a developer will come along and review your pull request, now that might be a couple minutes, but more realistically probably a day or two at least. But you can come back to this PR page whenever you like, and it'll give you live status updates, including tags, comments, and whether the PR was approved or not. I'm gonna jump forward in time here, just to show you that this PR was approved. 64 | 65 | And that's all there is to it. As a reminder, the wiki page for the supercollider repo page is a good source of information for making contributions and otherwise interacting with the SC community. I know we usually talk about signal processing and UGens and all that, and this tutorial topic is a bit far from the usual path, but nonetheless I hope you find it useful, so the next time you come across something in SuperCollider that looks like it could be fixed or improved, I hope you'll consider making that change yourself, and contributing to the supercollider community. Thanks for watching, and I will see you next time. -------------------------------------------------------------------------------- /tutorial15-16-17_buffers/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/tutorial15-16-17_buffers/.DS_Store -------------------------------------------------------------------------------- /tutorial15-16-17_buffers/buffers/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/tutorial15-16-17_buffers/buffers/.DS_Store -------------------------------------------------------------------------------- /tutorial15-16-17_buffers/buffers/crotales/crotale01.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/tutorial15-16-17_buffers/buffers/crotales/crotale01.aiff -------------------------------------------------------------------------------- /tutorial15-16-17_buffers/buffers/crotales/crotale02.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/tutorial15-16-17_buffers/buffers/crotales/crotale02.aiff -------------------------------------------------------------------------------- /tutorial15-16-17_buffers/buffers/crotales/crotaleBrakeDrum01.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/tutorial15-16-17_buffers/buffers/crotales/crotaleBrakeDrum01.aiff -------------------------------------------------------------------------------- /tutorial15-16-17_buffers/buffers/crotales/crotaleBrakeDrum02.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/tutorial15-16-17_buffers/buffers/crotales/crotaleBrakeDrum02.aiff -------------------------------------------------------------------------------- /tutorial15-16-17_buffers/buffers/crotales/crotaleBrakeDrum03.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/tutorial15-16-17_buffers/buffers/crotales/crotaleBrakeDrum03.aiff -------------------------------------------------------------------------------- /tutorial15-16-17_buffers/buffers/crotales/crotaleBrakeDrum04.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/tutorial15-16-17_buffers/buffers/crotales/crotaleBrakeDrum04.aiff -------------------------------------------------------------------------------- /tutorial15-16-17_buffers/buffers/deskBells/deskBell01_C.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/tutorial15-16-17_buffers/buffers/deskBells/deskBell01_C.aiff -------------------------------------------------------------------------------- /tutorial15-16-17_buffers/buffers/deskBells/deskBell02_Db.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/tutorial15-16-17_buffers/buffers/deskBells/deskBell02_Db.aiff -------------------------------------------------------------------------------- /tutorial15-16-17_buffers/buffers/deskBells/deskBell03_D.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/tutorial15-16-17_buffers/buffers/deskBells/deskBell03_D.aiff -------------------------------------------------------------------------------- /tutorial15-16-17_buffers/buffers/deskBells/deskBell04_Eb.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/tutorial15-16-17_buffers/buffers/deskBells/deskBell04_Eb.aiff -------------------------------------------------------------------------------- /tutorial15-16-17_buffers/buffers/deskBells/deskBell05_E.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/tutorial15-16-17_buffers/buffers/deskBells/deskBell05_E.aiff -------------------------------------------------------------------------------- /tutorial15-16-17_buffers/buffers/deskBells/deskBell06_F.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/tutorial15-16-17_buffers/buffers/deskBells/deskBell06_F.aiff -------------------------------------------------------------------------------- /tutorial15-16-17_buffers/buffers/deskBells/deskBell07_Gb.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/tutorial15-16-17_buffers/buffers/deskBells/deskBell07_Gb.aiff -------------------------------------------------------------------------------- /tutorial15-16-17_buffers/buffers/deskBells/deskBell08_G.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/tutorial15-16-17_buffers/buffers/deskBells/deskBell08_G.aiff -------------------------------------------------------------------------------- /tutorial15-16-17_buffers/buffers/deskBells/deskBell09_Ab.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/tutorial15-16-17_buffers/buffers/deskBells/deskBell09_Ab.aiff -------------------------------------------------------------------------------- /tutorial15-16-17_buffers/buffers/deskBells/deskBell10_A.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/tutorial15-16-17_buffers/buffers/deskBells/deskBell10_A.aiff -------------------------------------------------------------------------------- /tutorial15-16-17_buffers/buffers/deskBells/deskBell11_Bb.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/tutorial15-16-17_buffers/buffers/deskBells/deskBell11_Bb.aiff -------------------------------------------------------------------------------- /tutorial15-16-17_buffers/buffers/deskBells/deskBell12_B.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/tutorial15-16-17_buffers/buffers/deskBells/deskBell12_B.aiff -------------------------------------------------------------------------------- /tutorial15-16-17_buffers/buffers/deskBells/deskBell13_C.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/tutorial15-16-17_buffers/buffers/deskBells/deskBell13_C.aiff -------------------------------------------------------------------------------- /tutorial15-16-17_buffers/buffers/shakers/shakerHit01.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/tutorial15-16-17_buffers/buffers/shakers/shakerHit01.aiff -------------------------------------------------------------------------------- /tutorial15-16-17_buffers/buffers/shakers/shakerHit02.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/tutorial15-16-17_buffers/buffers/shakers/shakerHit02.aiff -------------------------------------------------------------------------------- /tutorial15-16-17_buffers/buffers/shakers/shakerHit03.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/tutorial15-16-17_buffers/buffers/shakers/shakerHit03.aiff -------------------------------------------------------------------------------- /tutorial15-16-17_buffers/buffers/shakers/shakerHit04.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/tutorial15-16-17_buffers/buffers/shakers/shakerHit04.aiff -------------------------------------------------------------------------------- /tutorial15-16-17_buffers/buffers/shakers/shakerHit05.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/tutorial15-16-17_buffers/buffers/shakers/shakerHit05.aiff -------------------------------------------------------------------------------- /tutorial15-16-17_buffers/buffers/shakers/shakerSustain01.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/tutorial15-16-17_buffers/buffers/shakers/shakerSustain01.aiff -------------------------------------------------------------------------------- /tutorial15-16-17_buffers/buffers/shakers/shakerSustain02.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/tutorial15-16-17_buffers/buffers/shakers/shakerSustain02.aiff -------------------------------------------------------------------------------- /tutorial15-16-17_buffers/buffers/shakers/shakerSustain03.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/tutorial15-16-17_buffers/buffers/shakers/shakerSustain03.aiff -------------------------------------------------------------------------------- /tutorial28_extensions/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Tutorials/4624bc18dc69495f341bc7379597c4459ab954ad/tutorial28_extensions/.DS_Store -------------------------------------------------------------------------------- /tutorial28_extensions/tutorial28_classes.sc: -------------------------------------------------------------------------------- 1 | VerbEF { 2 | 3 | *ar { 4 | arg in, dec=3.5, mix=0.08, lpf1=2000, lpf2=6000, predel=0.025, mul=1, add=0; 5 | var dry, wet, sig; 6 | dry = in; 7 | wet = in; 8 | wet = DelayN.ar(wet, 0.5, predel.clip(0.0001,0.5)); 9 | wet = 16.collect{ 10 | var temp; 11 | temp = CombL.ar( 12 | wet, 13 | 0.1, 14 | LFNoise1.kr({ExpRand(0.02,0.04)}!2).exprange(0.02,0.099), 15 | dec 16 | ); 17 | temp = LPF.ar(temp, lpf1); 18 | }.sum * 0.25; 19 | 8.do{ 20 | wet = AllpassL.ar( 21 | wet, 22 | 0.1, 23 | LFNoise1.kr({ExpRand(0.02,0.04)}!2).exprange(0.02,0.099), 24 | dec 25 | ); 26 | }; 27 | wet = LeakDC.ar(wet); 28 | wet = LPF.ar(wet, lpf2, 0.5); 29 | sig = dry.blend(wet, mix); 30 | sig = sig * mul + add; 31 | ^sig; 32 | } 33 | 34 | } 35 | 36 | ColorEF { 37 | var 0) 104 | { 105 | var files, bufArray; 106 | files = contents.select({ |n| n.isFile }); 107 | files = files.select({ |n| 108 | ["aif", "aiff", "wav"].includesEqual(n.extension) 109 | }); 110 | 111 | if (event[dir.folderName.asSymbol] != nil) 112 | { "duplicate subfolder name ignored".warn; } 113 | { 114 | bufArray = files.collect({ |n| 115 | if(mono) 116 | { Buffer.readChannel(s, n.fullPath, channels:[0]) } 117 | { Buffer.read(s, n.fullPath) }; 118 | }); 119 | event[dir.folderName.asSymbol] = bufArray; 120 | }; 121 | }; 122 | 123 | if (contents.select({ |n| n.isFolder }).size > 0) 124 | { 125 | var folders = contents.select({ |n| n.isFolder }); 126 | folders.do({ |n| ~makeBuffers.(n.fullPath, event, mono) }); 127 | }; 128 | 129 | event; 130 | }; 131 | 132 | b = ~makeBuffers.("/Users/eli/Music/Compositions/Boneyard Festival 2019/amb/", ()); 133 | b = ~makeBuffers.("/Users/eli/Sounds/scaudio/shades/", b); 134 | b = ~makeBuffers.("/Users/eli/Sounds/scaudio/glitch/", b, true); 135 | b = ~makeBuffers.("/Users/eli/Sounds/scaudio/groh/metal/flowerPot/", b, true); 136 | b = ~makeBuffers.("/Users/eli/Sounds/scaudio/miscellaneous/pno/", b, true); 137 | 138 | }); 139 | ) --------------------------------------------------------------------------------