├── .DS_Store ├── SCmini00_script.scd ├── SCmini09_script.scd ├── SCmini04_script.scd ├── SCmini05_script.scd ├── SCmini01_script.scd ├── SCmini03_script.scd ├── SCmini08_script.scd ├── SCmini07_script.scd ├── SCmini06_script.scd ├── SCmini02_script.scd ├── SCmini12_script.scd ├── SCmini10_script.scd └── SCmini11_script.scd /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elifieldsteel/SuperCollider-Mini-Tutorials/HEAD/.DS_Store -------------------------------------------------------------------------------- /SCmini00_script.scd: -------------------------------------------------------------------------------- 1 | Hey everyone, welcome to this SuperCollider mini tutorial series. These videos will run in parallel with my main SuperCollider tutorials, in order to cover bite-size topics, like tips and tricks for improving your workflow, how to avoid common mistakes, syntax alternatives, understanding error messages, etc. In contrast to the main tutorial videos, which sometimes run pretty long, I'm going to commit to making each one of these mini tutorials 5 minutes or less. I'll keep an eye on the comments, so feel free to post thoughts or questions. I hope you find these mini tutorials super useful, if you do, please like and subscribe, and also, at the time of making this video, I've got a Patreon page for those who want to support these videos directly, which is patreon.com/elifieldsteel. Link in the video description. Thanks, and I'll see you in the next video. -------------------------------------------------------------------------------- /SCmini09_script.scd: -------------------------------------------------------------------------------- 1 | In many situations, duration and musical pitch are inseparably linked. For example, consider this excerpt from one of my compositions. 2 | 3 | s.boot; 4 | 5 | b = Buffer.read(s, "/Users/eli/Desktop/sax.aiff"); 6 | 7 | {PlayBuf.ar(2, b, rate: 1)}.play; 8 | 9 | If playback speed is reduced by two-thirds, 10 | 11 | {PlayBuf.ar(2, b, rate: 2/3)}.play; 12 | 13 | the duration of the sample increases, but pitch is also affected. This happens because pitch is a perceptual experience based primarily on frequency, and frequency is a measure of vibrational cycles per second. So, if we stretch the sample, cycles get longer, thus frequency and our sense of pitch are lower. 14 | 15 | But, we can unlink these two parameters to do things like pitch-neutral time-stretching and spectral freezing by applying the Fast Fourier Transform, or FFT, to perform a spectral analysis and resynthesis of a sound. Specifically, we'll use PV_RecordBuf and PV_PlayBuf, which are part of the SC3-plugins extensions package, which needs to be downloaded and installed separately, instructions in the video description if you need them. These UGens behave a lot like regular RecordBuf and PlayBuf, but the buffer they access contains FFT analysis data instead of data that directly represents the waveform. 16 | 17 | So, first we set a few important FFT parameters. The FFT size is the number of samples in one analysis window, has to be a power of two. For time stretching, a relatively high value like 8192 works well, but you can drop down to 4096 or 2048 and it'll probably still sound ok. I like 0.25 for the hop size, this means the analysis windows overlap by a factor of four which helps produce a smooth resynthesis, and for the windowing envelope, 0 is a sine window, 1 is a Hann window, slightly different but both reasonable choices, you can experiment and decide which one you like. 18 | 19 | ~fftsize = 8192; 20 | ~hop = 0.25; 21 | ~win = 0; 22 | 23 | Next, allocate a buffer that will contain analysis data. We calculate the appropriate buffer size based on the original sample duration, using the 'calcPVRecSize' method, also part of the same extensions package, and provide FFT size and hop size. 24 | 25 | f = Buffer.alloc(s, b.duration.calcPVRecSize(~fftsize, ~hop)); 26 | 27 | If your source file is monophonic, you can run this line as-is, but I've got a stereo file, so I need an array of two analysis buffers, one for each channel, so I'm gonna wrap this line in curly braces and duplicate with exclam 2. 28 | 29 | b.numChannels; 30 | 31 | f = { Buffer.alloc(s, b.duration.calcPVRecSize(~fftsize, ~hop)) } ! 2; 32 | 33 | And finally, a pair of UGen functions, one using PV_RecordBuf to analyze and record the FFT data, and the other using PV_PlayBuf to read and resynthesize an audio signal from that data. In the first function, we begin with an audio signal of the original sample, LocalBuf provides a dedicated space for the FFT object to actually perform its calculations, once again we need two of them because we're working in stereo, and PV_RecordBuf writes the complete analysis into the two buffers stored in f. Zero at the end to make sure the output is silent. 34 | 35 | ( 36 | { 37 | var sig, chain, localbuf; 38 | sig = PlayBuf.ar(2, b, BufRateScale.kr(b), doneAction: 2); 39 | localbuf = { LocalBuf.new(~fftsize) } ! 2; 40 | chain = FFT(localbuf, sig, ~hop, ~win); 41 | chain = PV_RecordBuf(chain, f, run: 1, hop: ~hop, wintype: ~win); 42 | 0; 43 | }.play; 44 | ) 45 | 46 | Keep in mind this analysis is a preparatory step that doesn't happen instantaneously, in fact in this particular case we actually need to read through the entire sample in real-time to complete the analysis, but you can monitor the progress on the node tree to see when it's finished. 47 | 48 | Plot f, and we can clearly see that the stored data does not represent the waveform itself, but instead it's FFT analysis data, representing the same sound in a different format. 49 | 50 | f.do(_.plot(minval: 0, maxval: 100)); 51 | 52 | In the second function, another pair of LocalBufs for FFT calculations, PV_PlayBuf reads the data from f, including a rate argument for real-time control, and IFFT is the inverse operation that resynthesizes a waveform from the data, create a signal we can send to our speakers. 53 | 54 | ( 55 | x = { 56 | var sig, chain, localbuf; 57 | localbuf = { LocalBuf.new(~fftsize) } ! 2; 58 | chain = PV_PlayBuf(localbuf, f, \rate.kr(1), loop: 1); 59 | sig = IFFT(chain, ~win); 60 | }.play; 61 | ) 62 | 63 | The lines below change the rate during playback. 0 creates a spectral freeze effect, positive values close to zero will creep along slowly, producing a smeared and dreamy effect, and negative numbers read through the data backward. 64 | 65 | x.set(\rate, 0); 66 | 67 | x.set(\rate, 1/10); 68 | 69 | x.set(\rate, -2/3); 70 | 71 | x.release(2); 72 | 73 | Lots of variations to be explored, but that's the basic technique of pitch-neutral time-stretching, and hopefully this tutorial gives you a good starting point. Shoutout to Josh Parmenter, who describes himself as a composer and performer distracted by a day job, also a long-time SC developer and creator of these and many other excellent UGen extensions. And a very special shoutout and thanks to my supporters on Patreon who help make these videos possible, thank you all so very much, hope you enjoyed this mini tutorial, if you did, please like and subscribe. Thanks for watching. 74 | 75 | Patreon.thanks; -------------------------------------------------------------------------------- /SCmini04_script.scd: -------------------------------------------------------------------------------- 1 | Figuring out the nuts and bolts of using patterns to express certain musical ideas can sometimes be tricky. As an example, let's consider the following sequence that plays four random low notes, followed by four random high notes, again and again indefinitely: 2 | 3 | s.boot; 4 | 5 | ( 6 | p = Pbind( 7 | \midinote, Pseq([ 8 | Pxrand([51,53,55,58],4), 9 | Pxrand([72,75,77,79],4) 10 | ],inf), 11 | \sustain, 0.02, 12 | \dur, 0.15, 13 | \amp, 0.5, 14 | ); 15 | 16 | q = p.play; 17 | ) 18 | 19 | q.stop; 20 | 21 | And, let's imagine we want the number of *low* notes to be different every time this pattern is embedded, like, instead of always four, a random integer between 2 and 5. And feel free to pause here and see if you can figure out a way to do this. There are multiple solutions, which is like, welcome to patterns, but for this video there's a specific sort of solution I want to focus on. 22 | 23 | You might be tempted to use rrand, which seems reasonable, but it won't work, — or at least, it won't work correctly. The number of low notes will be random, but it'll be the *same* random number every time we roll back around to this Pxrand. 24 | 25 | ( 26 | p = Pbind( 27 | \midinote, Pseq([ 28 | Pxrand([51,53,55,58],rrand(2,5)), 29 | Pxrand([72,75,77,79],4) 30 | ],inf), 31 | \sustain, 0.02, 32 | \dur, 0.15, 33 | \amp, 0.5, 34 | ); 35 | 36 | q = p.play; 37 | ) 38 | 39 | q.stop; 40 | 41 | In that case, we happened to roll a 2, and so that value gets baked into this Pbind and we're stuck with it. So, one option that *does* work here is to enclose the random expression in curly braces, so that it becomes a function. And by doing so, in a sense, we're deferring the evaluation of this code until the Stream that plays this Pbind actually needs to know what the value is, at which point it'll evaluate the function and obtain a new, random number each time: 42 | 43 | ( 44 | p = Pbind( 45 | \midinote, Pseq([ 46 | Pxrand([51,53,55,58],{rrand(2,5)}), 47 | Pxrand([72,75,77,79],4) 48 | ],inf), 49 | \sustain, 0.02, 50 | \dur, 0.15, 51 | \amp, 0.5, 52 | ); 53 | 54 | q = p.play; 55 | ) 56 | 57 | q.stop; 58 | 59 | So that's one solution to this problem, and it works perfectly well in cases where the algorithm can be expressed using one or more language methods, like rrand, exprand, array.choose, etc. But — what if we want something more complex than just pick a random number? For example, let's say we want 1 low note, followed by 4 high notes, then 2 low notes followed by 4 high, then 3 then 4, then 4 then 4, etc, just adding a new low note each time. You might very reasonably think to yourself, aha, I know the perfect pattern for this, and go grab a Pseries, that starts at one, adds one each time, goes on forever. 60 | 61 | ( 62 | p = Pbind( 63 | \midinote, Pseq([ 64 | Pxrand([51,53,55,58],Pseries(1,1,inf)), 65 | Pxrand([72,75,77,79],4) 66 | ],inf), 67 | \sustain, 0.02, 68 | \dur, 0.15, 69 | \amp, 0.5, 70 | ); 71 | 72 | q = p.play; 73 | ) 74 | 75 | q.stop; 76 | 77 | But this does not work, it's just stuck on the low notes somehow. The reason this doesn't work is related to one of *~the most~* central ideas of patterns, and that is the distinction between a pattern and a stream. 78 | 79 | A pattern, like this one here, defines some sequence, but it is not the sequence itself. Instead, a Pattern is a recipe, or a blueprint for that sequence. And if all we have is a Pattern, then there is no way for us to actually...get...the output directly from the pattern... 80 | 81 | x = Pseries(1,1,inf); 82 | x.next; 83 | x.value; 84 | x.please; 85 | 86 | ...and that's why this Pseries doesn't work here. Pxrand needs a number of repeats, and a Pseries is not a number. A stream, on the other hand, is a tangible execution of some pattern, and that's what we need, a thing that actually says here's the next number, here's the next number. So we can convert a pattern to a stream using asStream..., I know it says Routine here, but technically a Routine is a type of stream...and then we can call value, or next, on the result, and we actually get the values. 87 | 88 | x = Pseries(1,1,inf).asStream; 89 | x.value; 90 | x.next; 91 | 92 | So, back to our Pbind, the solution here is to use a stream, derived from a pattern, to control the number of repeats: 93 | 94 | ( 95 | ~reps = Pseries(1,1,inf).asStream; 96 | 97 | p = Pbind( 98 | \midinote, Pseq([ 99 | Pxrand([51,53,55,58],~reps), 100 | Pxrand([72,75,77,79],4) 101 | ],inf), 102 | \sustain, 0.02, 103 | \dur, 0.15, 104 | \amp, 0.5, 105 | ); 106 | 107 | q = p.play; 108 | ) 109 | 110 | q.stop; 111 | 112 | There you go. Something important to keep in mind, though, is that the event stream, q, is *independent* from the Pseries stream ~reps. If we stop the event stream player, and even if you also reset the EventStreamPlayer, doing so does not also reset ~reps, so in a sense that internal stream "remembers" where it left off and will continue from there if we restart the event stream: 113 | 114 | q.reset; 115 | q.play; 116 | q.stop; 117 | q.reset; 118 | 119 | If you want to reset everything, then the internal stream needs its own reset message, or, even easier, just create the entire thing again and start over. 120 | 121 | ~reps.reset; 122 | 123 | ( 124 | ~reps = Pseries(1,1,inf).asStream; 125 | p = Pbind( 126 | \midinote, Pseq([ 127 | Pxrand([51,53,55,58],~reps), 128 | Pxrand([72,75,77,79],4) 129 | ],inf), 130 | \sustain, 0.02, 131 | \dur, 0.15, 132 | \amp, 0.5, 133 | ); 134 | 135 | q = p.play; 136 | ) 137 | 138 | q.stop; 139 | 140 | This can be a nice trick to keep in the back pocket, and easy to overlook opportunities to take advantage of pattern/stream subtleties in situations like this. In some cases it can drastically simplify pattern expression of musical ideas, and it's something that I did not really get the hang of until fairly late into my own SuperCollider journey. 141 | 142 | Patreon.thanks; 143 | 144 | So, special thanks to my patrons, for supporting these tutorials and also for all the lively conversations that have been happening on Patreon about new tutorial ideas, really really appreciate it. And to everyone, hope this helps, thanks for watching. -------------------------------------------------------------------------------- /SCmini05_script.scd: -------------------------------------------------------------------------------- 1 | Back in like 2010 I discovered a SCtweet by Nathaniel Virgo, posted as @headcube that caught my attention, and sounds like this: 2 | 3 | play{GVerb.ar(VarSaw.ar(Duty.ar(1/5,0,Dseq(x=[[4,4.5],[2,3,5,6]];flat(x*.x allTuples(x*.x x)*4).clump(2)++0)),0,0.9)*LFPulse.ar(5),99,5)/5} 4 | 5 | Link in the video description, go check it out, listen to some of the other ones too, they're all fantastic. As an exercise, I like to deconstruct these to understand them better, feel free to pause and study this exploded version at your own pace: 6 | 7 | ( 8 | var freq; 9 | x = [ [4,4.5], [2,3,5,6] ]; 10 | freq = x *.x x; 11 | freq = allTuples(freq); 12 | freq = x *.x freq; 13 | freq = freq * 4; 14 | freq = freq.flat; 15 | freq = freq.clump(2); 16 | freq = freq ++ 0; 17 | 18 | { 19 | var demand, sig; 20 | demand = Duty.kr(1/5, 0, Dseq(freq)); 21 | sig = VarSaw.ar(demand, 0, 0.9); 22 | sig = sig * LFPulse.ar(5); 23 | sig = GVerb.ar(sig, 99, 5); 24 | sig = sig / 5; 25 | }.play; 26 | ) 27 | 28 | Back then, there was one part of this tweet I could. not. figure out, and it was this here: "x space asterisk period x space x." My reaction at the time was "well, this is complete nonsense" forgot about it for awhile until like like a decade later, I posted a question on scsynth.org, and mystery solved, turns out it's pretty interesting. 29 | 30 | This x and this x refer to this array of arrays of numbers. This asterisk means multiplication, like it usually does, but this .x here is called an adverb, it's an additional argument provided to the binary operator that changes its behavior — a binary operator being some symbol or method applied to a pair of values, like this plus that, this to the power of that, etc. 31 | 32 | [Operators HF] 33 | 34 | 17 + 3 35 | 17 ** 3 36 | 37 | Adverbs are documented in a file called "Adverbs for Binary Operators," and, this concept also appears in a file called "J Concepts in SC". 38 | 39 | And...by the way, the fact the array and the adverb are both named x, is a total coincidence, which is kind of confusing, so you can rename the array, if you like. 40 | 41 | ( 42 | var freq; 43 | z = [ [4,4.5], [2,3,5,6] ]; 44 | freq = z *.x z; 45 | freq = allTuples(freq); 46 | freq = z *.x freq; 47 | freq = freq * 4; 48 | freq = freq.flat; 49 | freq = freq.clump(2); 50 | freq = freq ++ 0; 51 | 52 | { 53 | var demand, sig; 54 | demand = Duty.kr(1/5, 0, Dseq(freq)); 55 | sig = VarSaw.ar(demand, 0, 0.9); 56 | sig = sig * LFPulse.ar(5); 57 | sig = GVerb.ar(sig, 99, 5); 58 | sig = sig / 5; 59 | }.play; 60 | ) 61 | 62 | So here's the deal. In SC, a binary operation with a number and an array is defined such that the operation is applied to the number and each item in the array, resulting in a new array of the same size. 63 | 64 | 100 + [1, 2, 3, 4, 5, 6, 7]; 65 | 66 | For an operation with two arrays, the default behavior is to apply the operation to corresponding pairs, and if the sizes of the arrays are different, wrap to the beginning of the the short array and start over as many times as needed to accommodate the long array. In this case we get 100 plus 1, 200 plus 2, 300 plus 3, then wrap the shorter one, and we continue with 100 plus 4, 200 + 5, etc. 67 | 68 | [100, 200, 300] + [1, 2, 3, 4, 5, 6, 7]; 69 | 70 | Adverbs change this default behavior. .s stands for short, which means the shorter array determines the length of the result. 71 | 72 | [100, 200, 300] +.s [1, 2, 3, 4, 5, 6, 7]; 73 | 74 | .f means folding, instead of wrapping, so when we get to the end of the short array, we bounce back and go the opposite direction, bouncing back and forth as many times as needed to accommodate the long array, so here notice the hundredths place goes 1232123 75 | 76 | [100, 200, 300] +.f [1, 2, 3, 4, 5, 6, 7]; 77 | 78 | .t is "table," and this is where things get interesting, the result is a multidimensional array, so in this case an array containing three arrays, each containing seven numbers, and the operation is applied to every possible pair of values from the first and second array. 79 | 80 | [100, 200, 300] +.t [1, 2, 3, 4, 5, 6, 7]; 81 | 82 | dot-x, which I think means "cross", almost the same as table, except it removes the inner brackets and the the result is just one large array 83 | 84 | [100, 200, 300] +.x [1, 2, 3, 4, 5, 6, 7]; 85 | 86 | keep in mind this syntax works for any binary operator with a symbolic representation, like subtraction...division...etc. 87 | 88 | [100, 200, 300] -.x [1, 2, 3, 4, 5, 6, 7]; 89 | [100, 200, 300] /.x [1, 2, 3, 4, 5, 6, 7]; 90 | [100, 200, 300] %.x [1, 2, 3, 4, 5, 6, 7]; 91 | 92 | For operators that don't have symbolic representation, for example the least common multiple, 93 | 94 | lcm(6, 8) 95 | 96 | the syntax needs to be a little different, you can either put method colon dot adverb between the arrays: 97 | 98 | [100, 200, 300] lcm:.x [1, 2, 3, 4, 5, 6, 7]; 99 | 100 | Or receiver.method style, and in parentheses, comma after the second array, followed by a symbol representing the adverb. 101 | 102 | [100, 200, 300].lcm([1, 2, 3, 4, 5, 6, 7], \x); 103 | 104 | So, cool that you can modify this behavior, but not immediately clear why it's useful, and where I think this trick really shines is generating pitch collections. Let's say you want to generate a list of all the MIDI notes on the 88-key piano keyboard that belong to the key of A minor. We can get the raw scale degrees like this: 105 | 106 | Scale.minor.degrees; 107 | 108 | Note numbers on the piano range from 21 to 108. So here's one way to do it, maybe not the most optimal, but it works. Start with an array of a bunch of octave transpositions, and cross-add with the minor scale, and then add 21 to start on the correct note. 127 is higher than we need, so we can use reject to iterate and remove anything greater than 108. 109 | 110 | ( 111 | f = (0,12..96) +.x Scale.minor.degrees + 21; 112 | f = f.reject({ |n| n>108 }); 113 | ) 114 | 115 | And there you go. So hopefully you can imagine all the complex interesting data collections you can concisely generate with these adverbs. And by the way if one day you find yourself with a lot of caffeine and nothing else to do, I recommend you browse through this J Concepts guide, because some of this stuff is...pretty cool. 116 | 117 | So, thanks to Nathaniel Virgo @headcube for letting me feature this awesome tweet, and very special shoutout to my Patrons for the support, huge thanks, much appreciated, and to everyone, hope this helps, thanks for watching. -------------------------------------------------------------------------------- /SCmini01_script.scd: -------------------------------------------------------------------------------- 1 | Did you know that it's possible to record SuperCollider's output and render that sound as an audio file? It's a useful trick, especially if you're the kind of person who likes using SuperCollider for *generating* sound, but prefers a DAW for assembly and fine-tuning. Let's say you've got some code you're happy with, like this here: 2 | 3 | s.boot; 4 | 5 | ( 6 | x = { 7 | var sig, freq, amp, reverb; 8 | freq = LFNoise0.ar(8!8).exprange(60,1500).round(60); 9 | amp = VarSaw.ar(8,0,0.004).range(0,1).pow(4); 10 | sig = LFTri.ar(freq); 11 | sig = sig * amp * 0.4; 12 | sig = Splay.ar(sig); 13 | }.play(fadeTime:0); 14 | ) 15 | 16 | x.release(1); 17 | 18 | One of the simplest ways to turn this code into an audio file is to run 19 | 20 | s.makeWindow; 21 | 22 | which gives you a little status window for the audio server, click the record button, run your code and let it play for as long as you like, stop the sound, and then click that same button again to stop recording. 23 | 24 | When you stop recording, ............the file path appears in the post window. You can also get the location of your recordings directory by evaluating 25 | 26 | Platform.recordingsDir; 27 | 28 | Navigate to the folder, and here's our new audio file. Open it up in a waveform editor...and there it is. Inevitably, with this approach, there's going to be some time that passes between pressing the record button and running your code, that's why we have this chunk of silence at the beginning here. Of course you can trim this off using editing software, but if this bothers you, you can handle the recording process using code instead. All you have to do is wrap your sound code in a Routine with .play at the end, and put s.record at the beginning of the Routine. Now, when we call s.record, the server does a tiny bit of setup in preparation for writing audio into a file, and that takes a small amount of time, so it's kind of necessary, actually, to include a short wait between these two steps. Hard to say precisely how much time is needed but I find that 0.02 seconds usually works pretty well. 29 | 30 | ( 31 | Routine.new({ 32 | 33 | s.record; 34 | 35 | 0.02.wait; 36 | 37 | x = { 38 | var sig, freq, amp, reverb; 39 | freq = LFNoise0.ar(8!8).exprange(60,1500).round(60); 40 | amp = VarSaw.ar(8,0,0.004).range(0,1).pow(4); 41 | sig = LFTri.ar(freq); 42 | sig = sig * amp * 0.4; 43 | sig = Splay.ar(sig); 44 | }.play(fadeTime:0); 45 | 46 | }).play; 47 | ) 48 | 49 | When you've had enough, stop the sound...and then evaluate s.stopRecording. 50 | 51 | x.release(1); 52 | 53 | s.stopRecording; 54 | 55 | [open recording] 56 | 57 | Once again, here...is our recording, and it looks we've got roughly 0.01 seconds of silence at the beginning. So that's pretty good, probably good enough, but, y'know, go ahead and mess with this 0.02 value if you feel like it. 58 | 59 | The default format for these files is 32-bit float, AIFF format, 2-channels. You can get these settings by evaluating 60 | 61 | s.recSampleFormat; 62 | s.recHeaderFormat; 63 | and 64 | s.recChannels; 65 | 66 | In many cases these settings are fine, but *if* you'll need to change something, most likely it'll be downgrading the bit depth to something lower than 32, because most but not all audio software can read 32-bit audio files. To drop the bit depth, set this expression equal to the string int24 or int16 depending on what bit depth you want. 67 | 68 | s.recSampleFormat = "int24"; 69 | 70 | Similarly, you can set the file format to wav like this 71 | 72 | s.recHeaderFormat = "wav"; 73 | 74 | though I don't think I've ever encountered a situation where wav and aiff are not interchangeable. 75 | 76 | So, let's run this Routine again... 77 | 78 | ( 79 | Routine.new({ 80 | 81 | s.record; 82 | 83 | 0.02.wait; 84 | 85 | x = { 86 | var sig, freq, amp, reverb; 87 | freq = LFNoise0.ar(8!8).exprange(60,1500).round(60); 88 | amp = VarSaw.ar(8,0,0.004).range(0,1).pow(4); 89 | sig = LFTri.ar(freq); 90 | sig = sig * amp * 0.4; 91 | sig = Splay.ar(sig); 92 | }.play(fadeTime:0); 93 | 94 | }).play; 95 | ) 96 | 97 | stop the sound, and stop recording... 98 | 99 | x.release(1); 100 | s.stopRecording; 101 | 102 | here's our file, we can see that it's a 24-bit wav file. 103 | 104 | s.record actually takes a few arguments that can make this process a little easier. 105 | 106 | ( 107 | Routine.new({ 108 | 109 | s.record(); 110 | 111 | 0.02.wait; 112 | 113 | x = { 114 | var sig, freq, amp, reverb; 115 | freq = LFNoise0.ar(8!8).exprange(60,1500).round(60); 116 | amp = VarSaw.ar(8,0,0.004).range(0,1).pow(4); 117 | sig = LFTri.ar(freq); 118 | sig = sig * amp * 0.4; 119 | sig = Splay.ar(sig); 120 | }.play(fadeTime:0); 121 | 122 | }).play; 123 | ) 124 | 125 | Two in particular I want to bring to your attention: The first argument, path, is a string representing where you want to put the new file, which is nice for being able to store the recording somewhere else. And the last argument, duration, is the desired length of the recording, in seconds. This is nice because you no longer have to stop the recording process manually, and you'll actually see "recording stopped" in the post window when it's finished. 126 | 127 | ( 128 | Routine.new({ 129 | 130 | s.record(path:"/Users/eli/Desktop/test/mySound.wav", duration:4); 131 | 132 | 0.02.wait; 133 | 134 | x = { 135 | var sig, freq, amp, reverb; 136 | freq = LFNoise0.ar(8!8).exprange(60,1500).round(60); 137 | amp = VarSaw.ar(8,0,0.004).range(0,1).pow(4); 138 | sig = LFTri.ar(freq); 139 | sig = sig * amp * 0.4; 140 | sig = Splay.ar(sig); 141 | }.play(fadeTime:0); 142 | 143 | }).play; 144 | ) 145 | 146 | x.free; 147 | 148 | And, on the desktop, here's my test folder, with mySound.wav in there. Something to watch out for — make sure this path is unique each time you run it. Otherwise, you're gonna end up overwriting a previous audio file with a new one, you won't get a warning message or anything like that, and the old recording will be unrecoverable. 149 | 150 | For more info, check out the Server help file, under the section titled Recording Support. Related to this, the help file for Recorder, which is the actual class that does the work in the background when we call s.record. 151 | 152 | So that's it for this mini tutorial, I want to give a special shoutout to my supporters on Patreon, huge thanks, you all are so awesome, and to everyone, hope this is helpful, and thanks for watching. -------------------------------------------------------------------------------- /SCmini03_script.scd: -------------------------------------------------------------------------------- 1 | When a Synth is running, we often use a 'set' message to change a control value, and when we do, that change happens more or less instantaneously... But maybe that's not what you want, or maybe at some point you asked yourself, is there an easy way to slide from one value to the next over a period of time? 2 | 3 | s.boot; 4 | 5 | ( 6 | SynthDef(\gliss, { 7 | arg freq=440, gate=1, amp=0.3, out=0; 8 | var sig, env; 9 | env = EnvGen.kr(Env.asr, gate, doneAction:2); 10 | sig = SinOsc.ar(freq)!2; 11 | sig = sig * amp; 12 | sig = sig * env; 13 | Out.ar(out, sig); 14 | }).add; 15 | ) 16 | 17 | x = Synth(\gliss); 18 | x.set(\freq, 72.midicps); 19 | x.set(\freq, 74.midicps); 20 | x.set(\gate, 0); 21 | 22 | In fact, there is. Lag is a UGen, essentially a type of lowpass filter that causes a signal to interpolate between changes over a period of time. In the case of pitch, it creates a glissando effect, and in more general terms, Lag makes a signal behave sluggishly, like it's moving through molasses. But the best way to understand how it works is to hear it. 23 | 24 | This frequency argument here, all we have to do is pass it through a Lag, and provide the desired lag time, in seconds. Once the sound is running, whenever that value changes, it takes that much time to actually get there. 25 | 26 | ( 27 | SynthDef(\gliss, { 28 | arg freq=440, gate=1, amp=0.3, out=0; 29 | var sig, env; 30 | freq = Lag.kr(freq, 1); 31 | env = EnvGen.kr(Env.asr, gate, doneAction:2); 32 | sig = SinOsc.ar(freq)!2; 33 | sig = sig * amp; 34 | sig = sig * env; 35 | Out.ar(out, sig); 36 | }).add; 37 | ) 38 | 39 | x = Synth(\gliss); 40 | x.set(\freq, 72.midicps); 41 | x.set(\freq, 74.midicps); 42 | x.set(\gate, 0); 43 | 44 | As a slightly shorter alternative, we can use the convenience method, dot-lag. 45 | 46 | ( 47 | SynthDef(\gliss, { 48 | arg freq=440, gate=1, amp=0.3, out=0; 49 | var sig, env; 50 | freq = freq.lag(1); 51 | env = EnvGen.kr(Env.asr, gate, doneAction:2); 52 | sig = SinOsc.ar(freq)!2; 53 | sig = sig * amp; 54 | sig = sig * env; 55 | Out.ar(out, sig); 56 | }).add; 57 | ) 58 | 59 | x = Synth(\gliss); 60 | x.set(\freq, 74.midicps); 61 | x.set(\gate, 0); 62 | 63 | Keep in mind this lag time doesn't have to be fixed — you can add a new argument for the lag time, and set it as fast or slow as you like. 64 | 65 | ( 66 | SynthDef(\gliss, { 67 | arg freq=440, freqlag=1, gate=1, amp=0.3, out=0; 68 | var sig, env; 69 | freq = freq.lag(freqlag); 70 | env = EnvGen.kr(Env.asr, gate, doneAction:2); 71 | sig = SinOsc.ar(freq)!2; 72 | sig = sig * amp; 73 | sig = sig * env; 74 | Out.ar(out, sig); 75 | }).add; 76 | ) 77 | 78 | x = Synth(\gliss); 79 | x.set(\freq, 76.midicps, \freqlag, 0.25); 80 | x.set(\freq, 71.midicps, \freqlag, 3); 81 | x.set(\freq, 80.midicps, \freqlag, 0); 82 | x.set(\gate, 0); 83 | 84 | And, of course, you can effectively bypass a lag by setting the lag time to zero...but don't use negative numbers for lag times — or use them at your own risk because...weird stuff starts to happen. 85 | 86 | Ok, so Lag gives us control over the duration of the interpolation, but it doesn't give any control over the shape of the interpolation curve, which, in the case of Lag, is always exponential. And it's for this reason that I usually prefer VarLag, which has a similar convenience method. In addition to a lag time, varlag accepts a curve value. And the behavior here is exactly like curve values for envelopes: 0 is linear, positive values bend the shape so that the value changes slowly at first, then quickly toward the end, negative values bend in the opposite way, and as this value gets further away from zero, the shape of the curve becomes more extreme. 87 | 88 | ( 89 | SynthDef(\gliss, { 90 | arg freq=440, freqlag=1, freqcrv=0, gate=1, amp=0.3, out=0; 91 | var sig, env; 92 | freq = freq.varlag(freqlag, freqcrv); 93 | env = EnvGen.kr(Env.asr, gate, doneAction:2); 94 | sig = SinOsc.ar(freq)!2; 95 | sig = sig * amp; 96 | sig = sig * env; 97 | Out.ar(out, sig); 98 | }).add; 99 | ) 100 | 101 | x = Synth(\gliss); 102 | 103 | So, here's our tone going down an octave over 2 seconds, linearly: 104 | x.set(\freq, 220, \freqlag, 2, \freqcrv, 0); 105 | 106 | Going up an octave, with a positive curve: 107 | x.set(\freq, 440, \freqlag, 2, \freqcrv, 15); 108 | 109 | And down an octave once more with a negative curve: 110 | x.set(\freq, 220, \freqlag, 2, \freqcrv, -15); 111 | 112 | x.set(\gate, 0); 113 | 114 | When applied to pitch, negative curves tend to be more useful, because when a value changes, a negative curve moves it close to its target quickly and then levels off, which is kind of the natural most common way to perform a glissando, whereas positive curves cause the value to stay mostly where it is until the very end of the lag time, so there's kind of this awkward delay. 115 | 116 | Lag is not just useful for frequency, it can be applied to just about anything. For example, let's get rid of this envelope, and instead, lag the amplitude, creating a sort of dynamic envelope that can be controlled stage by stage, using set messages: 117 | 118 | ( 119 | SynthDef(\gliss, { 120 | arg freq=440, freqlag=1, freqcrv=0, amplag=2, ampcrv=0, gate=1, amp=0, out=0; 121 | var sig; 122 | freq = freq.varlag(freqlag, freqcrv); 123 | sig = SinOsc.ar(freq)!2; 124 | sig = sig * amp.varlag(amplag, ampcrv); 125 | Out.ar(out, sig); 126 | }).add; 127 | ) 128 | 129 | x = Synth(\gliss); 130 | x.set(\amp, 0.3, \amplag, 1, \ampcrv, -2); 131 | x.set(\amp, 0, \amplag, 3, \ampcrv, -8); 132 | x.free; 133 | 134 | Another use for Lag, if you have an external physical controller, like a motion sensor or something, that kind of data tends to be jumpy, and typically you don't want to plug that straight into your synthesis algorithm, because it usually makes your sound all wobbly and unstable. Lag is very handy here, capable of smoothing out that data once it is mapped onto some SynthDef argument, softening all the hard edges and becoming much friendlier for musical applications. 135 | 136 | A quick note about varlag, though it is useful and flexible, it can exhibit weird behavior if applied to an audio-rate signal, as discussed in this warning at the top of the help file. 137 | 138 | But, Lag has a few other siblings, Lag2 and Lag3 are shortcuts for double- and triple-nested Lags, so to wrap up, here's one more example to help visualize these behaviors. Here, sig is a sample & hold noise generator, basically a random staircase shape, producing 50 random values per second, and we're gonna plot half a second of five different signals: the original noise, that noise passed through lag, lag2, lag3, and finally, two lag3s in series, which would be the equivaleant of lag6, each with a lag time of 0.01, and as you can see, all these signals get progressively smoother as we lag them more aggressively. 139 | 140 | ( 141 | { 142 | var sig; 143 | sig = LFNoise0.kr(50); 144 | [ 145 | sig, 146 | sig.lag(0.01), 147 | sig.lag2(0.01), 148 | sig.lag3(0.01), 149 | sig.lag3(0.01).lag3(0.01) 150 | ]; 151 | }.plot(0.5, bounds:Rect(100,200,1700,750)); 152 | ) 153 | 154 | So that's most of the Lag family in a nutshell, pretty handy, almost kind of essential in some cases. Use it well, and have fun smoothing out all your signals. Shoutout to my awesome patrons, you all are the best, thank you so much for supporting these tutorials. To everyone, hope this helps, thanks for watching. -------------------------------------------------------------------------------- /SCmini08_script.scd: -------------------------------------------------------------------------------- 1 | Randomness is a big part of computer music, you can check out the guide file called Randomness for an overview. When we generate random numbers in sclang, we get a convincingly random sequence. And if we go get a cup of coffee, come back five minutes later and do it again, we're virtually guaranteed to get a different sequence. 2 | 3 | rrand(0,99); 4 | 5 | But, it *is* possible to reproduce a specific random sequence, because, fun fact, computer-generated randomness is not random at all — it's completely deterministic, but the algorithm is complex enough that the human brain just can't see it, so for us, if it "feels" random, then for all intents and purposes, it is. 6 | 7 | A random number generator, or RNG, begins with a seed, some integer that provides a starting point for the algorithm. The simplest way to seed sclang's RNG is "thisThread", dot randSeed, equals some integer, let's do 24. 8 | 9 | thisThread.randSeed = 24; 10 | 11 | Then, generate some random numbers 12 | 13 | rrand(0,99); 14 | 15 | ...seems random enough. But, reseed, 16 | 17 | thisThread.randSeed = 24; 18 | 19 | rrand(0,99); 20 | 21 | we'll get the exact same sequence every time. And this applies to any random choice that sclang makes, like choosing from an array... 22 | 23 | ( 24 | thisThread.randSeed = 24; 25 | 4.do({ [1,2,3,4,5].choose.postln }); 26 | ) 27 | 28 | flipping a coin... 29 | 30 | ( 31 | thisThread.randSeed = 24; 32 | 4.do({ 0.5.coin.postln }); 33 | ) 34 | 35 | or using a different method, like exprand. 36 | 37 | ( 38 | thisThread.randSeed = 24; 39 | 4.do({ exprand(80, 8000).postln }); 40 | ) 41 | 42 | And, if we change the seed, even by the smallest possible amount, the results are completely different. 43 | 44 | ( 45 | thisThread.randSeed = 25; 46 | 4.do({ exprand(80, 8000).postln }); 47 | ) 48 | 49 | ( 50 | thisThread.randSeed = 26; 51 | 4.do({ exprand(80, 8000).postln }); 52 | ) 53 | 54 | So, let's unpack what "thisThread" actually means. In the SuperCollider language, the context in which code runs is called a thread, and right now, we're in the main, top-level thread, so at the moment, that's what thisThread refers to. 55 | 56 | thisThread 57 | 58 | There's a class called Thread, which mainly exists to provide a framework for its subclass, Routine, a special type of function that can pause and resume. Routines inherit the RNG seed from their parent thread, in other words, the thread in which they were created. 59 | 60 | So if the main thread seed is 24, 61 | 62 | thisThread.randSeed = 24; 63 | 64 | then this Routine has the same seed, and produces the same sequence from before — 27, 72, 99 65 | 66 | ( 67 | r = Routine.new({ 68 | loop{ 69 | rrand(0,99).postln; 70 | 0.4.wait; 71 | }; 72 | }); 73 | 74 | r.play; 75 | ) 76 | 77 | r.stop; 78 | 79 | But, a Routine can have its own seed. One option is to use the randSeed method on the routine, before it plays, and just like that, different numbers. 80 | 81 | thisThread.randSeed = 24; 82 | 83 | ( 84 | r = Routine.new({ 85 | loop{ 86 | rrand(0,99).postln; 87 | 0.4.wait; 88 | }; 89 | }); 90 | r.randSeed = 25; 91 | r.play; 92 | ) 93 | 94 | r.stop; 95 | 96 | Or, you can use thisThread *inside* the Routine, in which case it refers to the new child thread, instead of the main parent thread. 97 | 98 | thisThread.randSeed = 24; 99 | 100 | ( 101 | r = Routine.new({ 102 | thisThread.randSeed = 25; 103 | loop{ 104 | rrand(0,99).postln; 105 | 0.4.wait; 106 | }; 107 | }); 108 | 109 | r.play; 110 | ) 111 | 112 | r.stop; 113 | 114 | In this case, resetting the routine has the side effect of reseeding the RNG, because reseeding takes place *in* the routine. 115 | 116 | r.reset.play; 117 | 118 | r.stop; 119 | 120 | And just to clarify the subtleties, if the top-level seed is 24 and we grab a couple numbers, 27, 72, 121 | 122 | thisThread.randSeed = 24; 123 | rrand(0,99) 124 | 125 | we already know the next one is gonna be 99. Even if we let this routine blow through a few numbers 126 | 127 | ( 128 | r = Routine.new({ 129 | thisThread.randSeed = 25; 130 | loop{ 131 | rrand(0,99).postln; 132 | 0.4.wait; 133 | }; 134 | }); 135 | 136 | r.play; 137 | ) 138 | 139 | r.stop; 140 | 141 | It has no effect on the RNG of the parent thread — case in point, 99, still next in line. 142 | 143 | rrand(0,99); 144 | 145 | Ok, let's make some sound. Here's a routine called ~soundMaker, that plays a UGen function, and waits, and loops these two steps. The UGen function picks 8 random frequencies, sums 8 sine waves, and applies an envelope. Sounds like this... 146 | 147 | ( 148 | ~soundMaker = Routine.new({ 149 | loop{ 150 | { 151 | var sig, freq; 152 | freq = {exprand(80, 8000)}.dup(8); 153 | sig = 8.collect({ 154 | arg i; 155 | SinOsc.ar( 156 | freq: freq[i] * [-0.1, 0.1].midiratio, 157 | mul: 0.05 158 | ); 159 | }).sum; 160 | sig = sig * EnvGen.ar( 161 | Env.perc(0.002, 0.1), 162 | doneAction:2 163 | ); 164 | }.play(fadeTime:0); 165 | 166 | 1.wait; 167 | }; 168 | }); 169 | ) 170 | 171 | And a second looping routine to create a musical sequence. The first thing it does is seed the first routine, we'll give it a value in just a second. Then, jumps into an iteration block, six times in row calls 'next' on the first routine, and then waits for point 12 seconds. calling 'next' on a routine tells it to start evaluating, and come to a full stop if runs into a wait. So this 1 is totally arbitrary, it could be any number, and you can also replace 'wait' with 'yield,' same result. So, when using next, this line is like a traffic cop that says "stop, and I will tell you when you can go." 172 | 173 | ( 174 | ~player = Routine.new({ 175 | loop{ 176 | ~soundMaker.randSeed_(~seed); 177 | 6.do{ 178 | ~soundMaker.next; 179 | 0.12.wait; 180 | }; 181 | }; 182 | }); 183 | ) 184 | 185 | Prepare a seed value, 186 | 187 | ~seed = 44; 188 | 189 | And play the second routine, you'll notice that we get 6 random bloops that repeat indefinitely: 190 | 191 | ~player.play; 192 | 193 | ~player.stop; 194 | 195 | And because ~seed is a "global" variable inside a continually re-evaluated process, we can swap it out in real-time, but the seed only updates at the beginning of a six-note cycle, so the rhythm is preserved. 196 | 197 | ~player.reset.play; 198 | ~seed = 45; 199 | ~seed = 44; 200 | ~seed = 46; 201 | ~seed = 44; 202 | ~player.stop; 203 | 204 | Lots of variations to explore. But, an important observation, all these techniques are exclusively language-side, so it only applies to RNG in sclang, not on the server. And to demonstrate, if we swap the exprand method for the ExpRand UGen: 205 | 206 | ( 207 | ~soundMaker = Routine.new({ 208 | loop{ 209 | { 210 | var sig, freq; 211 | freq = {ExpRand(80, 8000)}.dup(8); 212 | sig = 8.collect({ 213 | arg i; 214 | SinOsc.ar( 215 | freq: freq[i] * [-0.1, 0.1].midiratio, 216 | mul: 0.05 217 | ); 218 | }).sum; 219 | sig = sig * EnvGen.ar( 220 | Env.perc(0.002, 0.1), 221 | doneAction:2 222 | ); 223 | }.play(fadeTime:0); 224 | 225 | 1.wait; 226 | }; 227 | }); 228 | ) 229 | 230 | The behavior's different — the randomness doesn't repeat, because now the RNG lives on the server, an entirely different program with a separate RNG mechanism. It is possible to do seeded randomness on the server, but, too much for a five minute video, so I'm gonna save that for a future tutorial, but if you want a push in the right direction, take a look at RandID and RandSeed. 231 | 232 | So that's it for this tutorial, big thanks to my Patrons, love you all, truly appreciate the support, thank you so so much, and to everyone, hope this helps, thanks for watching. -------------------------------------------------------------------------------- /SCmini07_script.scd: -------------------------------------------------------------------------------- 1 | Here's a problem pretty much everybody runs into at some point, it has to do with using conditional logic inside of a SynthDef. Documented in a reference file called Control Structures, conditional logic refers to things that control the flow of information in a program, like if. Here's an example that works correctly: We have a function with an argument. If it's 0, play a tone, otherwise, play some noise. 2 | 3 | s.boot; 4 | 5 | ( 6 | f = { 7 | arg num=0; 8 | if( 9 | num == 0, 10 | { {SinOsc.ar(500) * 0.2!2 }.play }, 11 | { {PinkNoise.ar(1) * 0.2!2}.play } 12 | ); 13 | }; 14 | ) 15 | 16 | f.value([0,1].choose.postln); 17 | 18 | And, it seems reasonble to rewrite this code as a SynthDef with the conditional logic inside, 19 | 20 | ( 21 | SynthDef.new(\if, { 22 | arg num=0; 23 | var sig; 24 | if( 25 | num == 0, 26 | { sig = SinOsc.ar(500) }, 27 | { sig = PinkNoise.ar(1) } 28 | ); 29 | Out.ar(0, sig * 0.2!2); 30 | }).add; 31 | ) 32 | 33 | but it will not work correctly. In fact, it's such a common pitfall, it has a dedicated page on the SuperCollider website. 34 | 35 | https://supercollider.github.io/tutorials/If-statements-in-a-SynthDef.html 36 | 37 | No matter how we manipulate the argument... 38 | 39 | x = Synth(\if, [\num, 0]); 40 | x.set(\num, 1); 41 | x.set(\num, 0); 42 | x.free; 43 | 44 | we always get noise. 45 | 46 | If we change the conditional expression to num less than 1, then the SynthDef fails outright, and says the expression is non-boolean, which -- seems outrageous, because num is 0, and we want to know, is it less than 1...? 47 | 48 | ( 49 | SynthDef.new(\if, { 50 | arg num=0; 51 | var sig; 52 | if( 53 | num < 1, 54 | { sig = SinOsc.ar(500) }, 55 | { sig = PinkNoise.ar(1) } 56 | ); 57 | Out.ar(0, sig * 0.2!2); 58 | }).add; 59 | ) 60 | 61 | So, something fishy is going on here. The key to understanding stems from the fact that SynthDef arguments automatically become instances of a class called Control, it's basically what lets us interact with sounds using set messages. Going back to the version that checks equality with 0, this *if* construct is language-side, 62 | 63 | ( 64 | SynthDef.new(\if, { 65 | arg num=0; 66 | var sig; 67 | if( 68 | num == 0, 69 | { sig = SinOsc.ar(500) }, 70 | { sig = PinkNoise.ar(1) } 71 | ); 72 | Out.ar(0, sig * 0.2!2); 73 | }).add; 74 | ) 75 | 76 | which means, before the server even gets involved, the language says, "oh, it's an if, I need to evaluate this." and it does. So it says is num equal to 0? and to us humans, it looks obviously true, but what's actually being evaluated is this: 77 | 78 | Control.kr(0) == 0; 79 | 80 | which is false! It's like apples and oranges, comparing a UGen to an Integer. These will never be equal in the eyes of the language. So the result is, and always will be, sig = PinkNoise. If we dumpUGens on the SynthDef, we can see there is no SinOsc present. 81 | 82 | ( 83 | SynthDef.new(\if, { 84 | arg num=0; 85 | var sig; 86 | if( 87 | num == 0, 88 | { sig = SinOsc.ar(500) }, 89 | { sig = PinkNoise.ar(1) } 90 | ); 91 | Out.ar(0, sig * 0.2!2); 92 | }).dumpUGens; 93 | ) 94 | 95 | So, from the server's perspective, it actually just looks like this: 96 | 97 | ( 98 | SynthDef.new(\if, { 99 | arg num=0; 100 | var sig; 101 | sig = PinkNoise.ar(1); 102 | Out.ar(0, sig * 0.2!2); 103 | }).add; 104 | ) 105 | 106 | Now, the version with less than 1: 107 | 108 | ( 109 | SynthDef.new(\if, { 110 | arg num=0; 111 | var sig; 112 | if( 113 | num < 1, 114 | { sig = SinOsc.ar(500) }, 115 | { sig = PinkNoise.ar(1) } 116 | ); 117 | Out.ar(0, sig * 0.2!2); 118 | }).add; 119 | ) 120 | 121 | What the language sees is this: 122 | 123 | Control.kr(0) < 1; 124 | 125 | which returns a BinaryOpUGen, another type of UGen created behind the scenes. Most other binary operations produce the same result, the equality check is actually one of the few exceptions. 126 | 127 | Control.kr(0) + 1; 128 | Control.kr(0) - 1; 129 | Control.kr(0) == 1; 130 | 131 | A BinaryOpUGen is non boolean - it's neither true nor false, so the language doesn't know what to do, and we get an error. 132 | 133 | So, is it possible to use conditional logic in a SynthDef? Yes. But — we have to think about it a little differently. An easy solution is the Select UGen, basically the closest thing we have to a UGen version of if. We provide a number, and the integer part is used as an index into a UGen array. 134 | 135 | ( 136 | SynthDef.new(\if, { 137 | arg num=0; 138 | var sig; 139 | sig = Select.ar( 140 | num, 141 | [ 142 | SinOsc.ar(500), 143 | PinkNoise.ar(1) 144 | ] 145 | ); 146 | Out.ar(0, sig * 0.2!2); 147 | }).add; 148 | ) 149 | 150 | x = Synth(\if, [\num, 0]); 151 | x.set(\num, 1); 152 | x.set(\num, 0); 153 | 154 | Related to this is SelectX which crossfades between adjacent signals when the index is a non-integer, and as a throwback to mini tutorial 3, we'll put a varlag on the index: 155 | 156 | ( 157 | SynthDef.new(\if, { 158 | arg num=0; 159 | var sig; 160 | sig = SelectX.ar( 161 | num.varlag(2), 162 | [ 163 | SinOsc.ar(500), 164 | PinkNoise.ar(1) 165 | ] 166 | ); 167 | Out.ar(0, sig * 0.2!2); 168 | }).add; 169 | ) 170 | 171 | x = Synth(\if, [\num, 0]); 172 | x.set(\num, 1); 173 | 174 | Select has other applications, too — here's it's just looking up values in a table to create an arpeggiator, combining topics from mini tutorials 5 and 6: 175 | 176 | ( 177 | SynthDef.new(\if, { 178 | var notes, index, freq, sig; 179 | notes = (0,7..42) +.x [0,4]; 180 | index = LFSaw.kr(1,1).range(0, notes.size.postln); 181 | freq = Select.kr(index, notes); 182 | freq = (freq + 50).midicps; 183 | sig = SinOsc.ar(freq); 184 | Out.ar(0, sig * 0.2!2); 185 | }).add; 186 | ) 187 | 188 | Synth(\if); 189 | 190 | The downside of Select is that all UGens in the array are continuously running, even when not selected, so computational efficiency is a consideration. But, forget about Select for a moment — conditional expressions do have meaning on the server, but the results are represented using 1 and 0 instead of true and false. Here, the amplitude of pink noise randomly moves between 0.02 and 0.5. If it's greater than 0.1, isLoud has a value of 1, otherwise, 0. 191 | 192 | ( 193 | { 194 | var sig, amp, isLoud; 195 | sig = PinkNoise.ar(1!2); 196 | amp = LFDNoise3.kr(4).exprange(0.02,0.5); 197 | isLoud = amp > 0.1; 198 | isLoud.poll; 199 | sig = sig * amp; 200 | }.play; 201 | ) 202 | 203 | And this is really useful, if we think in terms of mathematical signal manipulation. For example, it could be used as a kind of gate, to turn something on or off depending on the condition. Here, we mix in an impulse generator which turns on whenever the amplitude of the noise is below threshold. 204 | 205 | ( 206 | { 207 | var sig, amp, isLoud, imp; 208 | sig = PinkNoise.ar(1!2); 209 | amp = LFDNoise3.kr(4).exprange(0.02,0.5); 210 | isLoud = amp > 0.1; 211 | isLoud.poll; 212 | imp = Impulse.ar([19,20], mul:0.25); 213 | imp = imp * (1 - isLoud).lag(0.1); 214 | sig = sig * amp; 215 | sig = sig + imp; 216 | }.play; 217 | ) 218 | 219 | Lots of other possibilities, these conditionals can do some really sophisticated things with signal logic — the hardest part, I think, is training yourself *not* to think in conventional terms of if-then-else, and instead think in terms of 1s and 0s, and mathematically weave these numbers into your algorithms to get the results you want. 220 | 221 | So, that's it for this tutorial, shoutout and big thanks as always to my awesome patrons, I hugely appreciate the ongoing support, thank you so, so much. And to everyone, hope this helps lift the fog around using if inside of a SynthDef, thanks for watching. -------------------------------------------------------------------------------- /SCmini06_script.scd: -------------------------------------------------------------------------------- 1 | s.boot 2 | 3 | It's pretty easy to make random bleeps and bloops in SC, just take a noise generator, plug it into an oscillator. 4 | 5 | ( 6 | { 7 | var sig, freq; 8 | freq = LFDNoise0.kr(7).exprange(110, 880); 9 | sig = VarSaw.ar(freq, mul:0.2!2); 10 | }.play; 11 | ) 12 | 13 | Easy, yes. Musically appealing and useful?...ehh, not so much. Instead, we can think in terms of MIDI note numbers, and convert to frequency with midicps: 14 | 15 | ( 16 | { 17 | var sig, pch, freq; 18 | pch = LFDNoise0.kr(7).range(45, 81); 19 | freq = pch.midicps; 20 | sig = VarSaw.ar(freq, mul:0.2!2); 21 | }.play; 22 | ) 23 | 24 | This noise generator spits out floats, which we can confirm by polling it: 25 | 26 | ( 27 | { 28 | var sig, pch, freq; 29 | pch = LFDNoise0.kr(7).range(45, 81); 30 | pch.poll(7); 31 | freq = pch.midicps; 32 | sig = VarSaw.ar(freq, mul:0.2!2); 33 | }.play; 34 | ) 35 | 36 | so, basically identical to the first example. If we want actual...notes from a piano keyboard, we can round these pitch values to the nearest integer: 37 | 38 | ( 39 | { 40 | var sig, pch, freq; 41 | pch = LFDNoise0.kr(7).range(45, 81).round(1); 42 | pch.poll(7); 43 | freq = pch.midicps; 44 | sig = VarSaw.ar(freq, mul:0.2!2); 45 | }.play; 46 | ) 47 | 48 | It's more musical but still pretty random, we can round to a different number to do intervallic stuff, like, a value of 2 picks notes from a whole-tone scale...7 picks from a bunch of stacked fifths... 49 | 50 | ( 51 | { 52 | var sig, pch, freq; 53 | pch = LFDNoise0.kr(7).range(45, 81).round(7); 54 | pch.poll(7); 55 | freq = pch.midicps; 56 | sig = VarSaw.ar(freq, mul:0.2!2); 57 | }.play; 58 | ) 59 | 60 | very retro video game flavor...ok, but the obvious question here is -- is it possible to round (or musically "quantize") these pitches to the nearest scale degree in some scale? The answer is yes, and the first step is to load a collection of scale degrees into a buffer, let's do the minor pentatonic scale, so that would look like this: 61 | 62 | Scale.minorPentatonic.degrees; 63 | 64 | ~scale0 = Buffer.loadCollection(s, Scale.minorPentatonic.degrees); 65 | 66 | And let's plot this buffer just to really understand what's going on here 67 | 68 | ~scale0.plot; 69 | 70 | These connecting lines are misleading, so I'm gonna hit the 'm' key on my keyboard once to plot the data as points, stretch it vertically, and we can see this buffer contains 5 values, which are 0, 3, 5, 7, and 10. 71 | 72 | And now, we're going use a UGen called Index, which retrieves buffer values, using the integer part of its input signal as an index. So, new variable called index, and gonna reuse this noise generator, but give it a range between 0 and 1, which is gonna represent octaves, and then scale it by the number of frames in the buffer. Index.kr uses this signal to look up scale degrees in the buffer. If we stop here, the values produced are gonna be 0, 3, 5, 7, or 10, which are really low for MIDI note numbers, so we should add a value, which effectively determines the starting note of the scale. 73 | 74 | We'll poll the index, so as this plays, watch the integer part of the numbers in the post window, and you'll notice that they correspond to the scale degrees that we hear: 75 | 76 | ( 77 | { 78 | var sig, index, pch, freq; 79 | index = LFDNoise0.kr(7).range(0,1); 80 | index = index * BufFrames.kr(~scale0); 81 | index.poll(7); 82 | pch = Index.kr(~scale0, index) + 48; 83 | freq = pch.midicps; 84 | sig = VarSaw.ar(freq, mul:0.2!2); 85 | }.play; 86 | ) 87 | 88 | It's a good start, but doesn't give us a lot of flexibility. If we increase the range of the indices, y'know, thinking we're gonna expand out to four octaves or whatever, Index isn't really designed to work this way. Instead, it just clips the index within the buffer frame range, so in this case we're just going to hear that highest scale degree like 75-80% percent of the time: 89 | 90 | ( 91 | { 92 | var sig, index, pch, freq; 93 | index = LFDNoise0.kr(7).range(0,4); 94 | index = index * BufFrames.kr(~scale0); 95 | index.poll(7); 96 | pch = Index.kr(~scale0, index) + 48; 97 | freq = pch.midicps; 98 | sig = VarSaw.ar(freq, mul:0.2!2); 99 | }.play; 100 | ) 101 | 102 | An better option, I think, is a UGen called DegreeToKey which is similar, but instead of clipping index values, it wraps them within the frame range, and as it does so, shifts the octave accordingly. So, here's a four octave range, also dropping the starting note one octave lower... 103 | 104 | ( 105 | { 106 | var sig, index, pch, freq; 107 | index = LFDNoise0.kr(7).range(0,4); 108 | index = index * BufFrames.kr(~scale0); 109 | index.poll(7); 110 | pch = DegreeToKey.kr(~scale0, index) + 36; 111 | freq = pch.midicps; 112 | sig = VarSaw.ar(freq, mul:0.2!2); 113 | }.play; 114 | ) 115 | 116 | And just to emphasize what's going on here, let's replace the index with with LFSaw, to sweep across these four octaves, and I guess we don't really need to poll the values anymore: 117 | 118 | ( 119 | { 120 | var sig, index, pch, freq; 121 | index = LFSaw.kr(0.8, 1).range(0,4); 122 | index = index * BufFrames.kr(~scale0); 123 | pch = DegreeToKey.kr(~scale0, index) + 36; 124 | freq = pch.midicps; 125 | sig = VarSaw.ar(freq, mul:0.2!2); 126 | }.play; 127 | ) 128 | 129 | And what's really cool about this is that these scales are stored in buffers, which means we can swap 'em out in real-time. To make this a little cleaner I'm gonna paste in a SynthDef version of this code, which has an argument for the scale buffer, and side-note, perfect example of why we should use kr for buffer information instead of ir, because if we switch to a scale with a different number of degrees, kr will track this change, ir will not. 130 | 131 | ( 132 | SynthDef(\d2k, { 133 | arg buf; 134 | var sig, index, pch, freq; 135 | index = LFSaw.kr(0.75, 1).range(0,4); 136 | index = index * BufFrames.kr(buf); 137 | pch = DegreeToKey.kr(buf, index) + 36; 138 | freq = pch.midicps; 139 | sig = VarSaw.ar(freq, mul:0.2!2); 140 | Out.ar(0, sig); 141 | }).add; 142 | ) 143 | 144 | Here's a couple of different scales to play with: 145 | ( 146 | ~scale1 = Buffer.loadCollection(s, [0,4,6,7,10]); 147 | ~scale2 = Buffer.loadCollection(s, [0,3,7,9,10]); 148 | ) 149 | 150 | And try 'em out: 151 | 152 | x = Synth(\d2k, [\buf, ~scale0]); 153 | x.set(\buf, ~scale1); 154 | x.set(\buf, ~scale2); 155 | x.free; 156 | 157 | And finally, here's a fancy version I made ahead of time, pause and study at your leisure, it's got a randomly changing index, multichannel expansion to create multiple voices, a detuning effect on the pitch, an argument for the starting note so we can easily transpose, frequency very slightly lagged so there's a micro-glissando whenever the pitch changes, one additional octave at the top, an envelope mostly for fading-out, and a delay/reverb combo at the end. 158 | 159 | ( 160 | SynthDef(\d2k, { 161 | arg buf, inote=36, gate=1; 162 | var sig, index, pch, freq; 163 | index = LFDNoise3.kr(1!4).range(0,5); 164 | index = index * BufFrames.kr(buf); 165 | pch = DegreeToKey.kr(buf, index) + inote; 166 | pch = pch + LFNoise1.kr(1!4).bipolar(0.12); 167 | freq = pch.midicps.lag(0.02); 168 | sig = VarSaw.ar(freq, mul:0.2); 169 | sig = Splay.ar(sig, 0.75); 170 | sig = sig * EnvGen.kr( 171 | Env([0,1,0,0],[0.2,4,7],[0,-2,0],1), 172 | gate, doneAction:2 173 | ); 174 | sig = sig.blend( 175 | CombN.ar(sig, 0.25, 0.25, 2), 176 | 0.5 177 | ); 178 | sig = sig.blend( 179 | LPF.ar(GVerb.ar(sig.sum, 299, 5), 2000), 180 | 0.4 181 | ); 182 | Out.ar(0, sig); 183 | }).add; 184 | ) 185 | 186 | and it sounds like this: 187 | 188 | x = Synth(\d2k, [\buf, ~scale0, \inote, 36]); 189 | x.set(\buf, ~scale1, \inote, 32); 190 | x.set(\buf, ~scale2, \inote, 30); 191 | x.set(\buf, ~scale1, \inote, 39); 192 | x.set(\buf, ~scale0, \inote, 38); 193 | x.set(\gate, 0); 194 | 195 | DegreeToKey, great UGen for scale quantization, a gateway to all sorts of scale- ande mode-based pitch work. Shoutout and big thanks to my Patrons, for the ongoing support, very much appreciated. And to everyone, if you're enjoying this mini series, please like and subscribe, and I hope you'll consider becoming a Patron yourself. Hope this helps, thanks for watching. 196 | 197 | Patreon.thanks; -------------------------------------------------------------------------------- /SCmini02_script.scd: -------------------------------------------------------------------------------- 1 | s.boot; 2 | 3 | It's pretty common to encounter error messages in SuperCollider, and it can be really frustrating if you don't have strategies for dealing with them. So let's have a look at some of the most common types. Throughout this video, when you see an error, I encourage you to pause and figure it out yourself before moving on — it's good practice. 4 | 5 | Let's begin with some code that actually works just fine. But one of the most common problems is not the code itself, but the way it's evaluated. In SuperCollider there are two keystrokes for evaluating code: shift-enter evaluates a single statement based on where the cursor is, and the other keystroke, command-enter on macOS, and control-enter on windows and linux, will evaluate a multi-line chunk enclosed in parentheses, like this here. So, command-enter works just fine, the whole chunk flashes orange, and it's all good. But if I use shift-enter, then only a single line flashes, and SuperCollider will return all sorts of weird and confusing things, depending on where the cursor happens to be. So when you see an error, first thing to do is make sure you used the correct keystroke. 6 | 7 | ( 8 | x = { 9 | arg rate=9; 10 | var sig, note, freq, amp; 11 | freq = LFNoise0.kr(rate).range(35,85); 12 | freq = freq + LFNoise0.kr(rate!2).bipolar(0.5); 13 | amp = VarSaw.kr(rate,1,0.01).exprange(0.01,1); 14 | sig = SinOsc.ar(freq.midicps) * amp; 15 | sig = sig * 0.2; 16 | }.play; 17 | ) 18 | 19 | Ok, moving on — this code doesn't work. It says, Class not defined. 20 | 21 | ( 22 | x = { 23 | var sig; 24 | sig = Pulse.ar([100,100.5], 0.5, 0.03); 25 | sig = sig + SinOsc.ar([150,150.6], 0, 0.15); 26 | sig = sig + BrowNoise.ar(0.03!2); 27 | sig = sig * SinOsc.kr(0.3, 3pi/2).exprange(0.1,1); 28 | }.play; 29 | ) 30 | 31 | When I see any error message, the first thing I like to do is clear the post window, and run it again. This guarantees there's no extra junk in the post window. 32 | 33 | This error is pretty easy to deal with. Classes are these things here, they always start with a captial letter, and this message usually means we misspelled a class name. This line number here, line 6, is *relative* to the chunk you just ran. So to sync up the line numbers, highlight the code you just ran, notice the line numbers change. On line 6, the class is BrownNoise, and it's missing a lowercase n. 34 | 35 | ( 36 | x = { 37 | var sig; 38 | sig = Pulse.ar([100,100.5], 0.5, 0.03); 39 | sig = sig + SinOsc.ar([150,150.6], 0, 0.15); 40 | sig = sig + BrownNoise.ar(0.03!2); 41 | sig = sig * SinOsc.kr(0.3, 3pi/2).exprange(0.1,1); 42 | }.play; 43 | ) 44 | 45 | Here's another example, producing a quite large error message. 46 | 47 | ( 48 | x = { 49 | var sig; 50 | sig = Pulse.ar([100,100.5], 0.5, 0.03); 51 | sig = sig + SinOsc.ar([150,150.6], 0, 0.15); 52 | sig = sig + BrownNoise.ar(0.03!2); 53 | sig = sig * SinOsc.kr(0.3, 3pi/2).exprnage(0.1); 54 | }.play; 55 | ) 56 | 57 | Step 1, don't freak out! It looks kinda scary, but you know what, don't even look at it, just stay calm and read the summary at the bottom, which says: message "exprnage" not understood, receiver: a SinOsc. 58 | 59 | In SuperCollider, code expression often takes the form something-dot-something, for example, 3.cubed. The thing on the left is the receiver, and the thing on the right is the message, or method. 60 | 61 | This error tells us an instance of SinOsc is receiving an undefined method, which probably means, like the previous case, we misspelled something. And sure enough, we did. This is supposed to be exprange, change it, problem solved. 62 | 63 | ( 64 | x = { 65 | var sig; 66 | sig = Pulse.ar([100,100.5], 0.5, 0.03); 67 | sig = sig + SinOsc.ar([150,150.6], 0, 0.15); 68 | sig = sig + BrownNoise.ar(0.03!2); 69 | sig = sig * SinOsc.kr(0.3, 3pi/2).exprange(0.1); 70 | }.play; 71 | ) 72 | 73 | Here's another that says "Variable 'amp' not defined." 74 | 75 | ( 76 | x = { 77 | arg rate=9; 78 | var sig, note, freq; 79 | freq = LFNoise0.kr(rate).range(35,85); 80 | freq = freq + LFNoise0.kr(rate!2).bipolar(0.5); 81 | amp = VarSaw.kr(rate,1,0.01).exprange(0.01,1); 82 | sig = SinOsc.ar(freq.midicps) * amp; 83 | sig = sig * 0.2; 84 | }.play; 85 | ) 86 | 87 | Lines 7 and 8 is where SuperCollider got confused. The rule is: local variables must be declared before they can be used. Unless it's a single lowercase character, like x, which is a special case, you can't just pull a name out of thin air and get away with it — it's just not allowed. 88 | 89 | num = 5; 90 | 91 | The usual solution here is to declare the variable using a var statement at the top of the relevant section of code. 92 | 93 | ( 94 | var num; 95 | num = 5; 96 | ) 97 | 98 | In this example, we're using a thing called 'amp' but we forgot to declare it. Include it in the declaration, and we're good to go. 99 | 100 | ( 101 | x = { 102 | arg rate=9; 103 | var sig, note, freq, amp; 104 | freq = LFNoise0.kr(rate).range(35,85); 105 | freq = freq + LFNoise0.kr(rate!2).bipolar(0.5); 106 | amp = VarSaw.kr(rate,1,0.01).exprange(0.01,1); 107 | sig = SinOsc.ar(freq.midicps) * amp; 108 | sig = sig * 0.2; 109 | }.play; 110 | ) 111 | 112 | Here's another that pops up a lot, this one's a little bit trickier. Binary operator "multiply" failed. Receiver nil. 113 | 114 | ( 115 | x = { 116 | arg rate=9; 117 | var sig, note, freq, amp; 118 | freq = LFNoise0.kr(rate).range(35,85); 119 | freq = freq + LFNoise0.kr(rate!2).bipolar(0.5); 120 | sig = SinOsc.ar(freq.midicps) * amp; 121 | amp = VarSaw.kr(rate,1,0.01).exprange(0.01,1); 122 | sig = sig * 0.2; 123 | }.play; 124 | ) 125 | 126 | nil is a value that represents uninitialized data. If you declare a variable but don't set it equal to something, then it equals nil. This error tells us SuperCollider is trying to do nil times something, and that's an undefined operation. 127 | 128 | nil * 2; 129 | 130 | There's no line number, so tracking down the issue involves some backtracking and proofreading. But you should be able to see that the problem is here — sig is a sine wave times amp...but amp isn't given a proper value until the next line, so we need to swap these two statements. Keep in mind this error might look a little different, like a different binary operator, maybe you're trying to access an item in an array that doesn't exist yet. 131 | 132 | nil + 2; 133 | 134 | nil.at(5); 135 | 136 | The main clue is "receiver: nil" -- and that tells you something somewhere hasn't been initialized. 137 | 138 | Last one, very common, sometimes kind of tricky. Syntax error, unexpected blah blah blah blah blah. 139 | 140 | ( 141 | x = { 142 | arg rate=9; 143 | var sig, note, freq, amp; 144 | freq = LFNoise0.kr(rate).range(35,85) 145 | freq = freq + LFNoise0.kr(rate!2).bipolar(0.5); 146 | amp = VarSaw.kr(rate,1,0.01).exprange(0.01,1); 147 | sig = SinOsc.ar(freq.midicps) * amp; 148 | sig = sig * 0.2; 149 | }.play; 150 | ) 151 | 152 | This means there's a syntactical issue somewhere, in other words, you violated SuperCollider's basic rules of grammar, like a comma instead of a period, a left bracket that doesn't have a matching right bracket, etc. In addition to the line number, these carets point to the spot where SuperCollider got lost, and most of time, these carets don't point to the actual problem, instead, they point to something a handful of characters after the problem. So, line 6, here's the spot, if we make our way backwards a bit, sure enough, we forgot a semicolon at the end of the previous line. 153 | 154 | ( 155 | x = { 156 | arg rate=9; 157 | var sig, note, freq, amp; 158 | freq = LFNoise0.kr(rate).range(35,85); 159 | freq = freq + LFNoise0.kr(rate!2).bipolar(0.5); 160 | amp = VarSaw.kr(rate,1,0.01).exprange(0.01,1); 161 | sig = SinOsc.ar(freq.midicps) * amp; 162 | sig = sig * 0.2; 163 | }.play; 164 | ) 165 | 166 | The semicolon is the expression terminator. Without it, SuperCollider has no idea where one line ends and the next begins. The human equivalent is writing code with no return characters, which, is technically valid, but completely unreadable. 167 | 168 | (x = {arg rate=9;var sig, note, freq, amp;freq = LFNoise0.kr(rate).range(35,85);freq = freq + LFNoise0.kr(rate!2).bipolar(0.5);amp = VarSaw.kr(rate,1,0.01).exprange(0.01,1);sig = SinOsc.ar(freq.midicps) * amp;sig = sig * 0.2;}.play;) 169 | 170 | So, these are some of the most common errors, why they show up, and techniques for how to fix them. That's gonna be it for this tutorial, I wanna give a shoutout and very special thanks to my supporters on patreon, much love, really appreciate your generosity, and to everyone, hope this helps, and thanks for watching. 171 | -------------------------------------------------------------------------------- /SCmini12_script.scd: -------------------------------------------------------------------------------- 1 | s.boot; 2 | 3 | A vocoder is a signal processing algorithm that takes two sounds, a carrier and a modulator, it analyzes the spectrum of the modulator, usually someone's voice, and transplants that spectrum onto the carrier, which is usually some synthesized waveform. It's a popular choice for creating a "robot voice" effect, and featured in songs by kraftwerk, daft punk, imogen heap, beastie boys, and many others. 4 | 5 | It is surprisingly easy to build your own vocoder in SuperCollider, so let's do it. 6 | 7 | ( 8 | SynthDef(\vocoder, { 9 | 10 | // create modulator & carrier 11 | 12 | // track spectrum of modulator 13 | 14 | // apply spectrum to carrier 15 | 16 | }).play; 17 | ) 18 | 19 | For the modulator, I'm gonna use this recording of my voice: 20 | 21 | b = Buffer.read(s, "/Users/eli/Documents/Illinois/Teaching/AY 2023-2024/Fall 2023/MUS 499C/Lecture Code Files/Week 5 Lecture Code 2023-09-21/brain.aiff"); 22 | 23 | b.play; 24 | 25 | So PlayBuf takes care of that. For the carrier, the wider the spectrum, the better the results are gonna be, so I'm gonna pass white noise through a comb filter to create a resonator effect, which has a broad spectrum, but also a sense of pitch at the inverse of the delay time, which'll be 50 Hz by default. Comb filters can get pretty loud, especially with long decay times, so let's dial back the amplitude and take a listen. 26 | 27 | ( 28 | SynthDef(\vocoder, { 29 | 30 | var mod, car; 31 | 32 | // create modulator & carrier 33 | mod = PlayBuf.ar(1, b, BufRateScale.ir(b), loop: 1); 34 | car = CombL.ar(WhiteNoise.ar(1), 1/20, 1/\freq.kr(50), 3); 35 | 36 | // track spectrum of modulator 37 | 38 | // apply spectrum to carrier 39 | 40 | Out.ar(0, car * 0.02!2); 41 | }).play; 42 | ) 43 | 44 | Next, we're gonna pass the modulator through a bank of bandpass filters, and measure the amplitude of each band. So, BPF, with mod as the input. Then, an array of center frequencies. Our range of pitch sensation is about 10 octaves, and a lot of graphic EQs split the octave into three bands, so 10 times 3 is 30 filters, and that seems like a good model. A target range of 20 to 20000 is fine, but you'll still get get good results if you shrink this range a bit, which also feels a little safer to me, though I might just be paranoid. 45 | 46 | For reciprocal quality, I'm gonna do 1/q, and then the square root of q for mul, I find does a pretty good job of keeping the overall amplitude consistent across a wide range of q values. 47 | 48 | If you're curious, we can listen to the filtered modulator, using Splay to mix 30 channels down to stereo: 49 | 50 | ( 51 | SynthDef(\vocoder, { 52 | 53 | var mod, car, bpfmod, num = 30, 54 | bpfhz = (1..num).linexp(1, num, 25, 16000), 55 | q = \q.kr(20); 56 | 57 | // create modulator & carrier 58 | mod = PlayBuf.ar(1, b, BufRateScale.ir(b), loop: 1); 59 | car = CombL.ar(WhiteNoise.ar(1), 1/20, 1/\freq.kr(50), 3); 60 | 61 | // track spectrum of modulator 62 | bpfmod = BPF.ar(mod, bpfhz, 1/q, q.sqrt); 63 | 64 | // apply spectrum to carrier 65 | 66 | Out.ar(0, Splay.ar(bpfmod)); 67 | }).play; 68 | ) 69 | 70 | Though, keep in mind, at the end of the day, it's the carrier we're gonna be listening to, not the modulator, I'm just monitoring the modulator because it's kind of interesting. So, next, we're gonna measure the output amplitude of each filter using Amplitude.kr, and the output is 30 channels of real-time amplitude following, one for each band. In addition to monitoring the filtered modulator, let's also route the amplitude tracking to a bank of control busses, and let's view those on the scope to see what we're working with. 71 | 72 | ( 73 | SynthDef(\vocoder, { 74 | 75 | var mod, car, bpfmod, num = 30, track, 76 | bpfhz = (1..num).linexp(1, num, 25, 16000), 77 | q = \q.kr(20); 78 | 79 | // create modulator & carrier 80 | mod = PlayBuf.ar(1, b, BufRateScale.ir(b), loop: 1); 81 | car = CombL.ar(WhiteNoise.ar(1), 1/20, 1/\freq.kr(50), 3); 82 | 83 | // track spectrum of modulator 84 | bpfmod = BPF.ar(mod, bpfhz, 1/q, q.sqrt); 85 | track = Amplitude.kr(bpfmod); 86 | 87 | // apply spectrum to carrier 88 | 89 | Out.kr(0, track); 90 | Out.ar(0, Splay.ar(bpfmod)); 91 | }).play; 92 | ) 93 | 94 | Ok, cool, we are actually almost done. Last thing is to apply this spectral profile to the carrier, so we pass the carrier through an identical set of filters, multiply by track, and output the result, again mixing down to stereo. 95 | 96 | ( 97 | SynthDef(\vocoder, { 98 | 99 | var mod, car, bpfmod, num = 30, track, bpfcar, 100 | bpfhz = (1..num).linexp(1, num, 25, 16000), 101 | q = \q.kr(20); 102 | 103 | // create modulator & carrier 104 | mod = PlayBuf.ar(1, b, BufRateScale.ir(b), loop: 1); 105 | car = CombL.ar(WhiteNoise.ar(1), 1/20, 1/\freq.kr(50), 3); 106 | 107 | // track spectrum of modulator 108 | bpfmod = BPF.ar(mod, bpfhz, 1/q, q.sqrt); 109 | track = Amplitude.kr(bpfmod); 110 | 111 | // apply spectrum to carrier 112 | bpfcar = BPF.ar(car, bpfhz, 1/q, q.sqrt) * track; 113 | 114 | Out.ar(0, Splay.ar(bpfcar)); 115 | }).play; 116 | ) 117 | 118 | So — it works! But there's a couple things we can do to improve the results. The sound is kind of gritty and crunchy, that's because the amplitude tracking signal is jittery as-is, but we can smooth it out by lagging with a small time interval. It also sounds a little quiet relative to the modulator, so I'm gonna upscale these values a bit. 119 | 120 | ( 121 | SynthDef(\vocoder, { 122 | 123 | var mod, car, bpfmod, num = 30, track, bpfcar, 124 | bpfhz = (1..num).linexp(1, num, 25, 16000), 125 | q = \q.kr(20); 126 | 127 | // create modulator & carrier 128 | mod = PlayBuf.ar(1, b, BufRateScale.ir(b), loop: 1); 129 | car = CombL.ar(WhiteNoise.ar(1), 1/20, 1/\freq.kr(50), 3); 130 | 131 | // track spectrum of modulator 132 | bpfmod = BPF.ar(mod, bpfhz, 1/q, q.sqrt); 133 | track = Amplitude.kr(bpfmod).lag(0.03) * 2; 134 | 135 | // apply spectrum to carrier 136 | bpfcar = BPF.ar(car, bpfhz, 1/q, q.sqrt) * track; 137 | 138 | Out.ar(0, Splay.ar(bpfcar)); 139 | }).play; 140 | ) 141 | 142 | And if we Splay as-is, the low frequency bands end up on the left, high frequency on the right, sounds kind of unbalanced, so we can scramble the array, and set 'spread' closer to zero, which narrows the stereo image. 143 | 144 | ( 145 | SynthDef(\vocoder, { 146 | 147 | var mod, car, bpfmod, num = 30, track, bpfcar, 148 | bpfhz = (1..num).linexp(1, num, 25, 16000), 149 | q = \q.kr(20); 150 | 151 | // create modulator & carrier 152 | mod = PlayBuf.ar(1, b, BufRateScale.ir(b), loop: 1); 153 | car = CombL.ar(WhiteNoise.ar(1), 1/20, 1/\freq.kr(50), 3); 154 | 155 | // track spectrum of modulator 156 | bpfmod = BPF.ar(mod, bpfhz, 1/q, q.sqrt); 157 | track = Amplitude.kr(bpfmod).lag3(0.03) * 2; 158 | 159 | // apply spectrum to carrier 160 | bpfcar = BPF.ar(car, bpfhz, 1/q, q.sqrt) * track; 161 | 162 | Out.ar(0, Splay.ar(bpfcar.scramble, spread: 0.1)); 163 | }).play; 164 | ) 165 | 166 | 'freq' is an argument, so it's controllable in real-time, for example, let's give this Synth a name, and then prep a set message down below with some randomness: 167 | 168 | ( 169 | x = SynthDef(\vocoder, { 170 | 171 | var mod, car, bpfmod, num = 30, track, bpfcar, 172 | bpfhz = (1..num).linexp(1, num, 25, 16000), 173 | q = \q.kr(20); 174 | 175 | // create modulator & carrier 176 | mod = PlayBuf.ar(1, b, BufRateScale.ir(b), loop: 1); 177 | car = CombL.ar(WhiteNoise.ar(1), 1/20, 1/\freq.kr(50), 3); 178 | 179 | // track spectrum of modulator 180 | bpfmod = BPF.ar(mod, bpfhz, 1/q, q.sqrt); 181 | track = Amplitude.kr(bpfmod).lag3(0.03) * 2; 182 | 183 | // apply spectrum to carrier 184 | bpfcar = BPF.ar(car, bpfhz, 1/q, q.sqrt) * track; 185 | 186 | Out.ar(0, Splay.ar(bpfcar.scramble, spread: 0.1)); 187 | }).play; 188 | ) 189 | 190 | x.set(\freq, rrand(30, 50).midicps); 191 | 192 | That's a pretty lazy example of controlling pitch, but you could plug in a MIDI controller, and use that to modulate the frequency instead, and on that note you could also hook up a microphone, use SoundIn instead of PlayBuf, and you've got yourself a real-time robot voice auto-tuner. Don't forget you can also use whatever you want for the carrier, but keep in mind, the wider the spectrum, the better the results tend to be. 193 | 194 | Patreon.thanks; 195 | 196 | That's it for this tutorial, shoutout and thanks to my Patrons, thank you all so much for the support, I really appreciate it. Hope you have some fun with this vocoder, if you enjoyed this video please give it a like, leave a comment down below, and subscribe if you're new. Thanks for watching, see you next time. -------------------------------------------------------------------------------- /SCmini10_script.scd: -------------------------------------------------------------------------------- 1 | SuperCollider has this diverse library of patterns for expressing all kinds of sequences, common examples include Pseq, Prand, Pwhite, many others. Now, by design, patterns are language-side, and although it is often useful to have this separation from the audio server, it does mean that we can't plug them directly in a SynthDef, as you can see here! 2 | 3 | s.boot; 4 | 5 | ( 6 | SynthDef(\d, { 7 | var freq, sig; 8 | freq = Pseq([200, 300, 400], inf); 9 | sig = Saw.ar(freq, mul: 0.1 ! 2); 10 | Out.ar(0, sig); 11 | }).play; 12 | ) 13 | 14 | However, there's a category of Demand UGens that produce pattern-like behavior, most actually have nearly the same name as their pattern counterparts. These demand classes can be incorporated into a UGen function and applied directly to audio and control signals. They're kinda like sequencer modules on an analog synth, so they're a good choice for arpeggiators, rhythm generators, and just about anything sequential in nature. 15 | 16 | So, here's a basic filtered sawtooth instrument. 17 | 18 | ( 19 | SynthDef(\d, { 20 | var freq, sig; 21 | freq = 50.midicps; 22 | freq = freq * { Rand(-0.1, 0.1).midiratio }.dup(4); 23 | sig = Saw.ar(freq); 24 | sig = Splay.ar(sig); 25 | sig = MoogFF.ar(sig, 1500); 26 | Out.ar(0, sig); 27 | }).play; 28 | ) 29 | 30 | For this tutorial I actually want to think in terms of musical scale, so I'm gonna load the dorian mode into a buffer, which is just the integers 0 2 3 5 7 9 10. 31 | 32 | b = Buffer.loadCollection(s, Scale.dorian); 33 | 34 | b.plot.plotMode_(\plines); 35 | 36 | And use DegreeToKey, from mini tutorial 6, basically just retrieves buffer values by index, allowing us to use consecutive integers to traverse the scale. 37 | 38 | ( 39 | SynthDef(\d, { 40 | var freq, sig; 41 | freq = (DegreeToKey.ar(b, 0) + 50).midicps; 42 | freq = freq * { Rand(-0.1, 0.1).midiratio }.dup(4); 43 | sig = Saw.ar(freq); 44 | sig = Splay.ar(sig); 45 | sig = MoogFF.ar(sig, 1500); 46 | Out.ar(0, sig); 47 | }).play; 48 | ) 49 | 50 | And then here's a simple application of Demand UGens. We start with an impulse generator as a basic timing signal. Demand receives those triggers, it's also got a reset parameter which I'm gonna temporarily ignore, and then some demand-based pattern, like Dseq, for example, will step through its values in order. 51 | 52 | ( 53 | SynthDef(\d, { 54 | var freq, sig, trig, deg; 55 | trig = Impulse.ar(4); 56 | deg = Demand.ar(trig, 0, Dseq([ 0, 2, -1, -3 ], inf)); 57 | freq = (DegreeToKey.ar(b, deg) + 50).midicps; 58 | freq = freq * { Rand(-0.1, 0.1).midiratio }.dup(4); 59 | sig = Saw.ar(freq); 60 | sig = Splay.ar(sig); 61 | sig = MoogFF.ar(sig, 1500); 62 | Out.ar(0, sig); 63 | }).play; 64 | ) 65 | 66 | MouseX and MouseY to control impulse rate and cutoff frequency, for a little interactive fun and also to help understand what's going on a little bit more clearly. 67 | 68 | ( 69 | SynthDef(\d, { 70 | var freq, sig, trig, deg; 71 | trig = Impulse.ar(MouseX.kr(2, 150, 1)); 72 | deg = Demand.ar(trig, 0, Dseq([ 0, 2, -1, -3 ], inf)); 73 | freq = (DegreeToKey.ar(b, deg) + 50).midicps; 74 | freq = freq * { Rand(-0.1, 0.1).midiratio }.dup(4); 75 | sig = Saw.ar(freq); 76 | sig = Splay.ar(sig); 77 | sig = MoogFF.ar(sig, MouseY.kr(100, 8000, 1)); 78 | Out.ar(0, sig); 79 | }).play; 80 | ) 81 | 82 | Duty is similar to Demand but interprets its first argument as a duration instead of expecting a trigger signal. 83 | 84 | ( 85 | SynthDef(\d, { 86 | var freq, sig, deg; 87 | deg = Duty.ar( 88 | 1/4, 89 | 0, 90 | Dseq([ 0, 2, -1, -3 ], inf) 91 | ); 92 | freq = (DegreeToKey.ar(b, deg) + 50).midicps; 93 | freq = freq * { Rand(-0.1, 0.1).midiratio }.dup(4); 94 | sig = Saw.ar(freq); 95 | sig = Splay.ar(sig); 96 | sig = MoogFF.ar(sig, 1500); 97 | Out.ar(0, sig); 98 | }).play; 99 | ) 100 | 101 | That's the same thing we heard a moment ago, but the duration doesn't have to be static, in fact it can be any signal, like an LFO or even another demand-style pattern, like Drand. 102 | 103 | ( 104 | SynthDef(\d, { 105 | var freq, sig, deg; 106 | deg = Duty.ar( 107 | Drand([ 1/4, 1/8 ], inf), 108 | 0, 109 | Dseq([ 0, 2, -1, -3 ], inf) 110 | ); 111 | freq = (DegreeToKey.ar(b, deg) + 50).midicps; 112 | freq = freq * { Rand(-0.1, 0.1).midiratio }.dup(4); 113 | sig = Saw.ar(freq); 114 | sig = Splay.ar(sig); 115 | sig = MoogFF.ar(sig, 1500); 116 | Out.ar(0, sig); 117 | }).play; 118 | ) 119 | 120 | The reset argument can be used to restart a sequence at any point. So, for example, if the duration is 1/8 and we want a repeating sequence of 11 notes, we can use an impulse generator with a frequency of 8/11 because frequency and period are inversely related. 121 | 122 | ( 123 | SynthDef(\d, { 124 | var freq, sig, deg; 125 | deg = Duty.ar( 126 | 1/8, 127 | Impulse.ar(8/11), 128 | Dseq([ 0, 2, -1, -3 ], inf) 129 | ); 130 | freq = (DegreeToKey.ar(b, deg) + 50).midicps; 131 | freq = freq * { Rand(-0.1, 0.1).midiratio }.dup(4); 132 | sig = Saw.ar(freq); 133 | sig = Splay.ar(sig); 134 | sig = MoogFF.ar(sig, 1500); 135 | Out.ar(0, sig); 136 | }).play; 137 | ) 138 | 139 | Just like patterns, Demand UGens can be nested inside of each other for more complex results, so here's a composite rhythmic sequence, and a little bit of randomness on the second pitch value. 140 | 141 | ( 142 | SynthDef(\d, { 143 | var freq, sig, deg; 144 | deg = Duty.ar( 145 | Dseq([ 146 | Dseq([ 1/4, 1/8 ], 4), 147 | Dseq([ 1/8 ], 4) 148 | ], inf), 149 | 0, 150 | Dseq([ 0, Drand([ 2, 3, 4 ], 1), -1, -3 ], inf) 151 | ); 152 | freq = (DegreeToKey.ar(b, deg) + 50).midicps; 153 | freq = freq * { Rand(-0.1, 0.1).midiratio }.dup(4); 154 | sig = Saw.ar(freq); 155 | sig = Splay.ar(sig); 156 | sig = MoogFF.ar(sig, 1500); 157 | Out.ar(0, sig); 158 | }).play; 159 | ) 160 | 161 | For amplitude sequencing, we can use TDuty to trigger an envelope according to some unique rhythm. TDuty is very similar to Duty but outputs its values as triggers, instead of sample-and-hold behavior, as you can see on these two plots. 162 | 163 | ( 164 | { 165 | Duty.ar( // change to TDuty 166 | 0.0005, 167 | 0, 168 | Dseq([1, 7, 3], inf) 169 | ); 170 | }.plot; 171 | ) 172 | 173 | So, a little syncopated rhythm for these triggers, and since we're just gating an envelope, we don't need some complicated value sequence — just any positive number is fine. 174 | 175 | ( 176 | SynthDef(\d, { 177 | var freq, sig, trig, deg; 178 | trig = TDuty.ar( 179 | Dseq([ 0.75, 0.75, 0.5 ], inf), 180 | 0, 181 | 1 182 | ); 183 | deg = Duty.ar( 184 | 1/8, 185 | 0, 186 | Dseq([ 0, 2, -1, -3 ], inf) 187 | ); 188 | freq = (DegreeToKey.ar(b, deg) + 50).midicps; 189 | freq = freq * { Rand(-0.1, 0.1).midiratio }.dup(4); 190 | sig = Saw.ar(freq); 191 | sig = Splay.ar(sig); 192 | sig = MoogFF.ar(sig, 1500); 193 | sig = sig * Env.perc(0.005, 0.5).ar(gate: trig); 194 | Out.ar(0, sig); 195 | }).play; 196 | ) 197 | 198 | A very analog synth thing to do is modulate the cutoff frequency with another envelope controlled by the same trigger signal. 199 | 200 | ( 201 | SynthDef(\d, { 202 | var freq, sig, trig, deg; 203 | trig = TDuty.ar( 204 | Dseq([ 0.75, 0.75, 0.5 ], inf), 205 | 0, 206 | 1 207 | ); 208 | deg = Duty.ar( 209 | 1/8, 210 | 0, 211 | Dseq([ 0, 2, -1, -3 ], inf) 212 | ); 213 | freq = (DegreeToKey.ar(b, deg) + 50).midicps; 214 | freq = freq * { Rand(-0.1, 0.1).midiratio }.dup(4); 215 | sig = Saw.ar(freq); 216 | sig = Splay.ar(sig); 217 | sig = MoogFF.ar( 218 | sig, 219 | Env( 220 | [100, 1500, 100], 221 | [0.05, 0.75], 222 | -4 223 | ).ar(gate: trig) 224 | ); 225 | sig = sig * Env.perc(0.005, 0.5).ar(gate: trig); 226 | Out.ar(0, sig); 227 | }).play; 228 | ) 229 | 230 | If we want a chord progression, all we have to do is create some array representing a stack of scale degrees, and add it to the base degree value, taking advantage of multichannel expansion. I'm also gonna slow things down, because 8 chord changes per second seems a little fast. 231 | 232 | ( 233 | SynthDef(\d, { 234 | var freq, sig, trig, deg; 235 | trig = TDuty.ar( 236 | Dseq([ 0.75, 0.75, 0.5 ], inf), 237 | 0, 238 | 1 239 | ); 240 | deg = Duty.ar( 241 | 2, 242 | 0, 243 | Dseq([ 0, 2, -1, -3 ], inf) 244 | ); 245 | freq = (DegreeToKey.ar(b, deg + [ 0, 4, 6, 7, 8, 9 ]) + 50).midicps; 246 | freq = freq * ({ Rand(-0.1, 0.1).midiratio} ! 4); 247 | sig = Saw.ar(freq); 248 | sig = Splay.ar(sig); 249 | sig = MoogFF.ar( 250 | sig, 251 | Env( 252 | [100, 1500, 100], 253 | [0.05, 0.75], 254 | -4 255 | ).ar(gate: trig) 256 | ); 257 | sig = sig * Env.perc(0.005, 0.5).ar(gate: trig); 258 | Out.ar(0, sig); 259 | }).play; 260 | ) 261 | 262 | Dconst is worth highlighting here, I think, it outputs values according to its second input until the sum of those values would exceed the threshold represented by the first value, at which point it truncates the last value to fit. So, in this specific case we'll get 0.75, five times in a row, which adds up to 3.75, and the sixth value is shortened to 0.25. We're gonna wrap this in a Dseq so that the whole process repeats. And then, some finishing touches, an LFO for the envelope release, another LFO modulating cutoff frequency, and we'll throw some delay and reverb on the end. 263 | 264 | ( 265 | SynthDef(\d, { 266 | var freq, sig, trig, deg; 267 | trig = TDuty.ar( 268 | Dseq([ 269 | Dconst(4, 0.75), 270 | ], inf), 271 | 0, 272 | 1 273 | ); 274 | deg = Duty.ar( 275 | 2, 276 | 0, 277 | Dseq([ 0, 2, -1, -3 ], inf) 278 | ); 279 | freq = (DegreeToKey.ar(b, [0, 4, 6, 7, 8, 9] + deg) + 50).midicps; 280 | freq = freq * ({ Rand(-0.1, 0.1).midiratio} ! 6); 281 | sig = Saw.ar(freq); 282 | sig = Splay.ar(sig); 283 | sig = MoogFF.ar( 284 | sig, 285 | Env( 286 | [100, LFTri.kr(1/16, 3).exprange(200, 2500), 100], 287 | [0.05, 0.75], 288 | -4 289 | ).ar(gate: trig) 290 | ); 291 | sig = sig * Env.perc(0.005, LFTri.kr(1/16, 3).exprange(0.15, 4)).ar(gate: trig); 292 | sig = sig + CombN.ar(sig, 0.5, 0.5, 4, -12.dbamp); 293 | sig = sig.blend(LPF.ar(GVerb.ar(sig, 200, 5).sum, 1000, 0.33)); 294 | Out.ar(0, sig); 295 | }).play; 296 | ) 297 | 298 | So, nice little collection of UGens, and pretty convenient, I think, being able to put pattern-style logic straight into a SynthDef. Hope this gives you some fun ideas to explore, shoutout to my supporters on Patreon, thank you all for the support, I really appreciate it. If you enjoyed this video, please like and subscribe, thanks for watching, see you next time. -------------------------------------------------------------------------------- /SCmini11_script.scd: -------------------------------------------------------------------------------- 1 | Clock dividers and step sequencers are two examples of tools that interact with trigger signals and allow us to do interesting things with timing and rhythm. In SuperCollider, these tools are called PulseDivider and Stepper, so let's take a look at how they work and what they can sound like. So, here, I've got 15 pulses per second, fed to PulseDivider, which also takes a division value. PulseDivider counts pulses, and when count equals div, it outputs a pulse of its own and resets the count to zero. These signals generate single-sample impulses, which generally don't show up nicely on a plot, but we can use Trig to extend them and make them more visible. 2 | 3 | ( 4 | { 5 | var clock, pdiv; 6 | clock = Impulse.ar(15); 7 | pdiv = PulseDivider.ar(clock, div: 2); 8 | Trig.ar([clock, pdiv], SampleDur.ir * 50); 9 | }.plot(0.5, bounds: Rect(150, 300, 1600, 500), minval: -0.1, maxval: 1.1); 10 | ) 11 | 12 | As you can see, PulseDivider gives one pulse for every two received. Multichannel expansion lets us simultaneously view division by 2, 3, 4, you get the idea. 13 | 14 | ( 15 | { 16 | var clock, pdiv; 17 | clock = Impulse.ar(15); 18 | pdiv = PulseDivider.ar(clock, div: [2, 3, 4]); 19 | Trig.ar([clock, pdiv].flat, SampleDur.ir * 50); 20 | }.plot(0.5, bounds: Rect(150, 100, 1600, 800), minval: -0.1, maxval: 1.1); 21 | ) 22 | 23 | The start parameter offsets the initial count, it's essentially a phase control, here's division by three with start values 0, 1, and 2. If start equals div minus one, as it does here at the bottom, PulseDivider will generate a pulse right at the beginning. 24 | 25 | ( 26 | { 27 | var clock, pdiv; 28 | clock = Impulse.ar(15); 29 | pdiv = PulseDivider.ar(clock, div: 3, start: [0, 1, 2]); 30 | Trig.ar([clock, pdiv].flat, SampleDur.ir * 50); 31 | }.plot(0.5, bounds: Rect(150, 100, 1400, 800), minval: -0.1, maxval: 1.1); 32 | ) 33 | 34 | PulseDivider is a good option for polyrhythms, like 3 against 4: 35 | 36 | ( 37 | { 38 | var clock, pdiv, sig, div = [3, 4]; 39 | clock = Impulse.ar(15); 40 | pdiv = PulseDivider.ar(clock, div: div, start: div - 1); 41 | sig = SinOsc.ar([500, 900]) * Env.perc(0.002, 0.1).ar(gate: pdiv) * 0.1; 42 | }.play(fadeTime: 0); 43 | ) 44 | 45 | Now, even though the array is 3 comma 4, we hear four pulses on the left and three on the right. It might seem like it should be the other way around, but these div values represent duration per pulse, in other words, a smaller number means more frequent pulses. So the rhythm we actually hear is least common multiple divided by div: 46 | 47 | 12 / [3, 4]; 48 | 49 | This means, for example, if we want to hear 3 against 4 against 5, the div array needs to be 20, 15, 12: 50 | 51 | 60 / [3, 4, 5]; 52 | 53 | ( 54 | { 55 | var clock, pdiv, sig, div = [20, 15, 12]; 56 | clock = Impulse.ar(60); 57 | pdiv = PulseDivider.ar(clock, div: div, start: div - 1); 58 | sig = SinOsc.ar([500, 900, 1300]) * Env.perc(0.002, 0.1).ar(gate: pdiv) * 0.1; 59 | Splay.ar(sig); 60 | }.play(fadeTime: 0); 61 | ) 62 | 63 | My advice, though, don't get distracted by the math, instead, just throw numbers at it until it sounds good. For example, here, div is the integers 2 through 20, used to divide pulses and also interpreted as scale degrees to generate pitch information. 64 | 65 | ( 66 | { 67 | var clock, pdiv, notes, sig, div = (2..20); 68 | clock = Impulse.ar(15); 69 | pdiv = PulseDivider.ar(clock, div: div, start: div - 1); 70 | notes = DegreeToKey.kr( 71 | LocalBuf.newFrom(Scale.lydian.degrees), 72 | div - 2 73 | ) + 64; 74 | sig = SinOsc.ar(notes.midicps) * Env.perc(0.002, 0.1).ar(gate: pdiv) * 0.1; 75 | Splay.ar(sig); 76 | }.play(fadeTime: 0); 77 | ) 78 | 79 | Here's a sampling example, where PulseDivider triggers and multichannel expands PlayBuf, and start positions are derived by mapping div values across the length of the buffer: 80 | 81 | b = Buffer.read(s, Platform.resourceDir ++ "/sounds/a11wlk01-44_1.aiff"); 82 | 83 | ( 84 | { 85 | var clock, pdiv, sig, div = (2..20); 86 | clock = Impulse.ar(15); 87 | pdiv = PulseDivider.ar(clock, div: div, start: div - 1); 88 | sig = PlayBuf.ar(1, b, BufRateScale.ir(b), 89 | trigger: pdiv, 90 | startPos: div.linlin(2, 21, 0, BufFrames.ir(b)), 91 | ); 92 | sig = sig * Env.perc(0.002, 0.1).ar(gate: pdiv) * 0.5; 93 | Splay.ar(sig); 94 | }.play(fadeTime: 0); 95 | ) 96 | 97 | Definitely consider swapping this sample for a drum loop, or something else inherently rhythmic, it can be a lot of fun to chop up something like that, but for now let's move on to Stepper, which also counts pulses, but outputs the count value itself, which repeatedly goes from min to max, in this case 0 to 15, also, it can be a good idea to provide a one-time reset trigger which forces Stepper to start at its min value, otherwise the first clock pulse makes it start one value higher. We'll poll to visualize the step count, and sonify each 16th note with white noise. 98 | 99 | ( 100 | { 101 | var metro, clock, step; 102 | clock = Impulse.ar(10); 103 | step = Stepper.ar(clock, reset: Impulse.ar(0), min: 0, max: 15).poll(clock); 104 | metro = WhiteNoise.ar(0.03 ! 2) * Env.perc(0, 0.01).ar(gate: clock); 105 | }.play(fadeTime: 0); 106 | ) 107 | 108 | To put a tone on each downbeat, we can gate the tone envelope with step < 1, which of course, is only true at the beginning of each cycle. This works because on the server, a conditional check is 1 if true, 0 if false. 109 | 110 | ( 111 | { 112 | var blip, metro, clock, step; 113 | clock = Impulse.ar(10); 114 | step = Stepper.ar(clock, reset: Impulse.ar(0), min: 0, max: 15).poll(clock); 115 | metro = WhiteNoise.ar(0.03 ! 2) * Env.perc(0, 0.01).ar(gate: clock); 116 | blip = SinOsc.ar(5000 ! 2) * Env.perc(0, 0.05).ar(gate: step < 1) * 0.3; 117 | blip + metro; 118 | }.play(fadeTime: 0); 119 | ) 120 | 121 | If we modulo step count by half the cycle size, we get a blip on every half note: 122 | 123 | ( 124 | { 125 | var blip, metro, clock, step; 126 | clock = Impulse.ar(10); 127 | step = Stepper.ar(clock, reset: Impulse.ar(0), min: 0, max: 15).poll(clock); 128 | metro = WhiteNoise.ar(0.03 ! 2) * Env.perc(0, 0.01).ar(gate: clock); 129 | blip = SinOsc.ar(5000 ! 2) * Env.perc(0, 0.05).ar(gate: step % 8 < 1) * 0.3; 130 | blip + metro; 131 | }.play(fadeTime: 0); 132 | ) 133 | 134 | Subdividing again puts a blip on every quarter note: 135 | 136 | ( 137 | { 138 | var blip, metro, clock, step; 139 | clock = Impulse.ar(10); 140 | step = Stepper.ar(clock, reset: Impulse.ar(0), min: 0, max: 15).poll(clock); 141 | metro = WhiteNoise.ar(0.03 ! 2) * Env.perc(0, 0.01).ar(gate: clock); 142 | blip = SinOsc.ar(5000 ! 2) * Env.perc(0, 0.05).ar(gate: step % 4 < 1) * 0.3; 143 | blip + metro; 144 | }.play(fadeTime: 0); 145 | ) 146 | 147 | And suppose we want four blips, one on each of the first four 16th notes of the cycle, it might seem correct to gate with step < 4, 148 | 149 | ( 150 | { 151 | var blip, metro, clock, step; 152 | clock = Impulse.ar(10); 153 | step = Stepper.ar(clock, reset: Impulse.ar(0), min: 0, max: 15).poll(clock); 154 | metro = WhiteNoise.ar(0.03 ! 2) * Env.perc(0, 0.01).ar(gate: clock); 155 | blip = SinOsc.ar(5000 ! 2) * Env.perc(0, 0.05).ar(gate: step < 4) * 0.3; 156 | blip + metro; 157 | }.play(fadeTime: 0); 158 | ) 159 | 160 | but this doesn't work because step < 4 is consistently true for the first four steps, so we just get one large trigger at the start of the cycle. 161 | 162 | ( 163 | { 164 | var blip, metro, clock, step; 165 | clock = Impulse.ar(10); 166 | step = Stepper.ar(clock, reset: Impulse.ar(0), min: 0, max: 15).poll(clock); 167 | metro = WhiteNoise.ar(0.03 ! 2) * Env.perc(0, 0.01).ar(gate: clock); 168 | blip = SinOsc.ar(5000 ! 2) * Env.perc(0, 0.05).ar(gate: step < 4) * 0.3; 169 | [Trig.ar(clock, SampleDur.ir * 100), step/8, step < 4]; 170 | }.plot(1, bounds: Rect(150, 100, 1400, 800), minval: -0.1, maxval: 1.1); 171 | ) 172 | 173 | The correct approach is to multiply the conditional by the clock, and that gives us the appropriate gate signal: 174 | 175 | ( 176 | { 177 | var blip, metro, clock, step; 178 | clock = Impulse.ar(10); 179 | step = Stepper.ar(clock, reset: Impulse.ar(0), min: 0, max: 15).poll(clock); 180 | metro = WhiteNoise.ar(0.03 ! 2) * Env.perc(0, 0.01).ar(gate: clock); 181 | blip = SinOsc.ar(5000 ! 2) * Env.perc(0, 0.05).ar(gate: step < 4) * 0.3; 182 | [Trig.ar(clock, SampleDur.ir * 100), step/8, step < 4 * Trig.ar(clock, SampleDur.ir * 100)]; 183 | }.plot(1, bounds: Rect(150, 100, 1400, 800), minval: -0.1, maxval: 1.1); 184 | ) 185 | 186 | ( 187 | { 188 | var blip, metro, clock, step; 189 | clock = Impulse.ar(10); 190 | step = Stepper.ar(clock, reset: Impulse.ar(0), min: 0, max: 15).poll(clock); 191 | metro = WhiteNoise.ar(0.03 ! 2) * Env.perc(0, 0.01).ar(gate: clock); 192 | blip = SinOsc.ar(5000 ! 2) * Env.perc(0, 0.05).ar(gate: step < 4 * clock) * 0.3; 193 | blip + metro; 194 | }.play(fadeTime: 0); 195 | ) 196 | 197 | To shift this cluster of notes to a later beat, we can subtract a value from step, and modulo by total steps to wrap back to the correct range: 198 | 199 | ( 200 | { 201 | var blip, metro, clock, step; 202 | clock = Impulse.ar(10); 203 | step = Stepper.ar(clock, reset: Impulse.ar(0), min: 0, max: 15).poll(clock); 204 | metro = WhiteNoise.ar(0.03 ! 2) * Env.perc(0, 0.01).ar(gate: clock); 205 | blip = SinOsc.ar(5000 ! 2) * Env.perc(0, 0.05).ar(gate: step - 4 % 16 < 4 * clock) * 0.3; 206 | blip + metro; 207 | }.play(fadeTime: 0); 208 | ) 209 | 210 | Modulo values that don't evenly divide the cycle create polyrhythms, so here's a new layer, step minus 2 mod 3 will be less than one on beats 2, 5, 8, 11, and 14, so, sort of a clave-style rhythm: 211 | 212 | ( 213 | { 214 | var clave, blip, metro, clock, step; 215 | clock = Impulse.ar(10); 216 | step = Stepper.ar(clock, reset: Impulse.ar(0), min: 0, max: 15).poll(clock); 217 | metro = WhiteNoise.ar(0.03 ! 2) * Env.perc(0, 0.01).ar(gate: clock); 218 | blip = SinOsc.ar(5000 ! 2) * Env.perc(0, 0.05).ar(gate: step - 4 % 16 < 4 * clock) * 0.3; 219 | clave = SinOsc.ar([2300, 2450]) * Env.perc(0, 0.1).ar(gate: step - 2 % 3 < 1 * clock) * 0.2; 220 | clave + blip + metro; 221 | }.play(fadeTime: 0); 222 | ) 223 | 224 | I'll paste in a synthesized kick, 225 | 226 | ( 227 | { 228 | var kick, clave, blip, metro, clock, step; 229 | clock = Impulse.ar(10); 230 | step = Stepper.ar(clock, reset: Impulse.ar(0), min: 0, max: 15).poll(clock); 231 | metro = WhiteNoise.ar(0.03 ! 2) * Env.perc(0, 0.01).ar(gate: clock); 232 | blip = SinOsc.ar(5000 ! 2) * Env.perc(0, 0.05).ar(gate: step - 4 % 16 < 4 * clock) * 0.3; 233 | clave = SinOsc.ar([2300, 2450]) * Env.perc(0, 0.1).ar(gate: step - 2 % 3 < 1 * clock) * 0.2; 234 | kick = SinOsc.ar( 235 | freq: Env([40, 350, 40], [0.0001, 0.1], -6).ar(gate: step % 4 < 1 * clock), 236 | mul: Env([0, 0.25, 0], [0.001, 0.6], -6).ar(gate: step % 4 < 1 * clock), 237 | ); 238 | clave + blip + metro + kick; 239 | }.play(fadeTime: 0); 240 | ) 241 | 242 | and we've got ourselves a nice little trance beat. Finally, here's an example that combines PulseDivider and Stepper, basically merging previous examples and adding some varations, pause and study if you like, it sounds like this: 243 | 244 | b = Buffer.read(s, Platform.resourceDir ++ "/sounds/a11wlk01-44_1.aiff"); 245 | 246 | ( 247 | x = { 248 | var pb, sig, kick, clave, blip, metro, notes, clock, pdiv, step, div = (4..16); 249 | clock = Impulse.ar(\n.kr(8)); 250 | pdiv = PulseDivider.ar(clock, div); 251 | step = Stepper.ar(clock, reset: Impulse.ar(0), min: 0, max: 15); 252 | metro = WhiteNoise.ar(0.03 ! 2) * Env.perc(0, 0.01).ar(gate: clock); 253 | blip = SinOsc.ar(5000 ! 2) * Env.perc(0, 0.05).ar(gate: step - 4 % 16 < 4 * clock) * 0.06; 254 | clave = SinOsc.ar([2300, 2450]) * Env.perc(0, 0.1).ar(gate: step - 2 % 3 < 1 * clock) * 0.08; 255 | kick = SinOsc.ar( 256 | freq: Env([40, 350, 40], [0.0001, 0.1], -6).ar(gate: step % 4 < 1 * clock), 257 | mul: Env([0, 0.25, 0], [0.001, 0.6], -6).ar(gate: step % 4 < 1 * clock), 258 | ); 259 | notes = DegreeToKey.kr(LocalBuf.newFrom([0, 3, 5, 7, 10]), div - 2) + 45; 260 | sig = Pulse.ar(notes.scramble.midicps) * Env.perc(0.001, 0.3).ar(gate: pdiv); 261 | sig = Splay.ar( 262 | MoogFF.ar(sig, Env.perc(curve: -12).ar(gate: pdiv).linexp(0, 1, 100, 3000), 2.5), 263 | spread: 0.75 264 | ); 265 | pb = Splay.ar( 266 | PlayBuf.ar(1, b, 267 | rate: BufRateScale.ir(b) * (8 - step).midiratio, 268 | trigger: pdiv, 269 | startPos: div/20 * BufFrames.ir(b) 270 | ) * Env.perc(0.002, 0.1).ar(gate: pdiv), 271 | spread: 0.75 272 | ); 273 | sig = [pb * 2, sig, clave, blip, metro, kick].sum; 274 | sig = sig.blend(LPF.ar(GVerb.ar(sig.sum, 300, 4), 1500), 0.015); 275 | }.play(fadeTime: 0); 276 | ) 277 | 278 | x.set(\n, 4); 279 | 280 | x.set(\n, 2); 281 | 282 | x.set(\n, 0); 283 | 284 | x.free; 285 | 286 | Patreon.thanks; 287 | 288 | Many more ideas left unexplored, I hope this gives you some fun ideas to play with, shoutout to my Patrons, huge thanks to all of you, for the support, I really appreciate it. If you're new and you enjoyed this video please like and subscribe, and I hope you'll consider becoming a Patron yourself, thanks for watching, see you next time. --------------------------------------------------------------------------------