SuperCollider

De Pontão Nós Digitais

Tutorial de Introdução à SC

Instalando e configurando

Atualmente SC possui pacotes para a versão 3.4. É recomendado que você comece por usar a versão para Ubuntu/Debian e depois se aventure na versão instável disponível no repositório Git (para tal, veja o Anexo A no final desse tutorial).

Primeiro adicione o PPA seguindo as instruções em https://launchpad.net/~supercollider/+archive/ppa:

   sudo add-apt-repository ppa:supercollider/ppa
   sudo aptitude update

e depois:

   sudo aptitude install supercollider supercollider-doc supercollider-quarks supercollider-gedit

Agora abra o Gedit e vá em Editar -> Preferências -> Plugins e habilite o SCED (SuperCollider EDitor). Assim você poderá usar o Gedit para controlar o SuperCollider, enviando para ele comandos na linguagem do SuperCollider.

Para começar a editar código em SC, vá em Ferramentas e depois Supercollider Mode. Pronto, o Supercollider estará executando em um painel do Gedit. É no painel de cima que começaremos a escrever o código.

Antes de começar a programar... entendendo a arquitetura de SuperCollider

SC é baseado em uma arquitetura cliente-servidor. O servidor é o responsável por acessar o dispositivo de áudio e fazer todo o trabalho de processamento de áudio, e é chamado de scsynth. Como cliente podemos usar qualquer aplicativo que suporte o protocolo OSC. O SC vem com um cliente padrão, chamado sclang. É com ele que iremos programar o scsynth, usando uma linguagem específica, a linguagem SuperCollider.

Portanto, temos o seguinte cenário:

   scsynth  <----OSC----> sclang
      ^---.
          `------OSC----> qualquer programa que suporte OSC (Python, Processing, PD, ...)

A Linguagem SuperCollider

Antes de tudo, é necessário sempre iniciar o servidor de SC (SC Server). É importante que durante toda a leitura desse tutorial você vá copiando os trechos de código e colando no seu editor. Comece copiando e colando o código abaixo:

// Iniciando o servidor
s = Server.local;
s.boot;

Para avaliar esses comandos, digite CTRL + E enquanto seu cursor estiver na linha da expressão. Grave esse comando, pois usaremos ele para executar (avaliar) qualquer comando em SC.

Com isso fizemos o SC Lang conectar no SC Server e inicializamos ele. É importante lembrar que você terá que iniciar o servidor JACK de áudio. Para isso, recomendo instalar o qjackctl (sudo aptitude install qjackctl), executá-lo e clicar em Start.

A sintaxe de SC será familiar para quem já programou em C, Java e Smalltalk. SC Lang é uma linguagem orientada a objetos, fortemente baseada em Smalltalk e com algumas noções de Scheme.

Para não perder o costume, vamos a um "Hello World" em SC:

"Hello, world!".postln;

Note que o que estamos fazendo é chamando a função (ou método) postln do objeto "Hello, world!", uma string. Esses detalhes ficarão claros a seguir.

Vamos tirar um primeiro som de SC, apenas para testar. Novamente, copie o código abaixo no seu Gedit e aperte CTRL+E:

{[SinOsc.ar(660, 0, 0.2), SinOsc.ar(662, 0, 0.2)]}.play(s);

Para para de tocar todos os sons, aperte ESC.

Esse é o básico do uso de SC: iniciar o servidor, avaliar as expressões (comandos) com CTRL+E e parar todos os sons com ESC.

Objetos

Tudo em SC são objetos. Todo objeto tem um valor. Números, strings, até mesmo funções (blocos de código) são tratados como objetos e possuem seus respectivos valores.

Tente avaliar cada comando abaixo e veja o que é retornado (note que avaliar é exatamente o que o termo sugere: saber o valor de algo):

2.value;
(2.5).value;
{ "foo".postln; }.value;
"foo".value;

Funções

f = { "foo bar".postln; };
f;
f.value;

// Uma função anônima sendo avaliada:
{ "foo bar".post; }.value;

// Avaliar uma função, é, como o nome sugere, saber seu valor

// Funções podem ter argumentos

f = { arg foo;
      foo.value * 2;
};

f.value(3);

// Podemos definir argumentos de modo alternativo, usando | var |

f = { | a, b |
      a + b;
};

f.value(2, 3);

// Keywords
g = { arg a, b, c;
      a * b + c;
};

g.value(b: 4, a: 3, c: 2);

// Valores de argumentos padrão
h = { arg foo, bar = 1;
      foo / bar;
};

h.value(2);
h.value(2, 1);

// Variáveis dentro de funções

f = { arg foo;
      var bar = 2;
      foo + bar;
};

f.value(2);

Variáveis padrões

// Perceba que todas as variáveis que usamos até agora (f, g, h, s, ...)
// não foram previamente declaradas por nós (usando var). Porquê?!
// Quando SC é inicializado, ele declara as variáveis de a-z para testes.
// Existem variáveis definidas também por motivo especial, como o caso de s,
// que aponta sempre para o servidor local de scsynth.

// Tente avaliar:
a;
b;
c;
s;
foo; // somente essa não é declarada ainda...

Blocos de código

// Para facilitar a avaliação (sem ter que ficar selecionando blocos de código)
// é uma prática comum em SC delimitar blocos que iremos avaliar durante
// a execução do programa com parênteses

// É muito mais prático avaliar isso:

( // <- sempre deixe o cursor no início do bloco para avaliar
f = { | a, b |
      (a * b) / 10;
};
)

f.value(2,3)

// ... do que isso:

f = { |a, b|
      (a * b) / 10;
}

Arrays

a = [2, 3.3, "foo"];
a[0];
a[1] = 3.4;
a[2];
a.at(2);
a.choose;

Sintetizando Áudio

{ SinOsc.ar(222, 0, 0.5); }.play;

(
{  var ampOsc;
   ampOsc = SinOsc.kr(0.5, 1.5pi, 0.5, 0.5);
   SinOsc.ar(440, 0, ampOsc);
}.play;
)

// Podemos usar arrays para especificar múltiplos canais de áudio

{ [SinOsc.ar(440, 0, 0.2), SinOsc.ar(441, 0, 0.2)] }.play;

// ou...

{ SinOsc.ar([440, 442], 0, 0.2) }.play;

// Alguns UGens se aproveitam disso

{ Pan2.ar(PinkNoise.ar(0.2), SinOsc.kr(0.5)) }.play;

// Podemos também somar ondas (mixing)

{ SinOsc.ar(440, 0, 0.3) + SinOsc.ar(441, 0, 0.3) }.play;

// o que é equivalente a mixá-as, ou usar Mix:

{ Mix([SinOsc.ar(440, 0, 0.3), SinOsc.ar(441, 0, 0.3)]) }.play;

// É interessante plotar as ondas para ter uma ideia de sua forma

{ Mix([SinOsc.ar(60, 0, 0.2), Saw.ar(80, 0.2)]) }.plot;

// Usando MouseX

(

        { var input, kernel;
                
        input=AudioIn.ar(1);    
        kernel= Mix.ar(LFSaw.ar([300,500,800,1000]*MouseX.kr(1.0,2.0),0,1.0));

        //must have power of two framesize
        Out.ar(0,Convolution.ar(input,kernel, 1024, 0.5));
         }.play;

)

(
{ var freq;

freq = [[660, 880], [440, 660], 1320, 880].choose;

SinOsc.ar(freq, 0, 0.2) * SinOsc.ar(20, 0, 0.2); 

}.plot;

)

{ Pan2.ar(PinkNoise.ar(0.2), MouseX.kr(-1.0, 1.0)) }.play;
a = SynthDef(\sinetest, {arg out = 1, freq = 440; Out.ar(out, SinOsc.ar(freq))}) ;
z = SynthDefAutogui(\sinetest, scopeOn:false) ; 

Classes que abstraem o server (scsynth)

{ SinOsc.ar(440, 0, 0.5) }.play;

SynthDef("meuSinOsc", { Out.ar(0, SinOsc.ar(440, 0, 0.5)) }).play;

SynthDef("meuSinOsc", { Out.ar(0, SinOsc.ar(440, 0, 0.5)) }).send(s);
x = Synth("meuSinOsc");
y = Synth("meuSinOsc");
x.free;
y.free;

(
SynthDef.new("meuSinOscStereo", { var outArray;
      outArray = [SinOsc.ar(440, 0, 0.5), SinOsc.ar(440, 0, 0.5)];
      Out.ar(0, outArray);
}).play;
)

// Quando trabalhamos com SynthDef, é importante definir o segundo argumento,
// o 'grafo de UGens' como sendo ele todo composto por UGens
// Veja que até a geração de números aleatórios é dada por um UGen: Rand

SynthDef("meuSinOscRandomico", { Out.ar(0, SinOsc.ar(Rand(440, 660), 0, 0.5)) }).send;
x = Synth("meuSinOscRandomico")
x.free

// Outro exemplo de SynthDef, agora com argumentos (muito mais flexível!)

(
SynthDef("sin-param", { | freq=440, out=0 |
    Out.ar(out, SinOsc.ar(freq, 0, 0.5))
}).send;
)

x = Synth("sin-param"); // pega os argumentos padrões
x = Synth("sin-param", ["freq", 660, "out", 0]); // especificando parâmetros
x.free

// Podemos também usar o método set() para alterar argumentos
// Muito interessante para livecoding!

x.set("freq", 1000);
x.set("out", 1)
x.set("out", 0)
x.set("freq", 660);

Símbolos versus Strings

// Podemos usar símbolos no lugar de strings

// Repare no símbolo usado: 'meu-sin' ao invés da string "meu-sin"

(
SynthDef('meu-sin', { Out.ar(0, SinOsc.ar(440, 0, 0.5)) }).send;
)

// ou... (repare no símbolo usado: \meuSin)

(
SynthDef(\meuSin, { Out.ar(0, SinOsc.ar(440, 0, 0.5)) }).send;
)

// Símbolos portanto são representados como 'meu simbolo' ou \meuSimbolo

Busses

// Pense em um bus como uma forma de acessar o IO de áudio e rotear
// o áudio entre bus e synths

// Bus de saída
Out.ar(0, SinOsc.ar(440, 0, 0.5));
// Bus de entrada
In.ar(0, 1);

// Bus genérico
Bus.control(s, 2) // cria dois busses de controle
Bus.audio(s)      // cria um bus de áudio (1 é padrão)

// Exemplo mais completo de uso de Busses

(
// the arg direct will control the proportion of direct to processed signal
SynthDef("tutorial-DecayPink", { arg outBus = 0, effectBus, direct = 0.5;
    var source;
    // Decaying pulses of PinkNoise. We'll add reverb later.
    source = Decay2.ar(Impulse.ar(1, 0.25), 0.01, 0.2, PinkNoise.ar);
    // this will be our main output
    Out.ar(outBus, source * direct);
    // this will be our effects output
    Out.ar(effectBus, source * (1 - direct));
}).send(s);

SynthDef("tutorial-DecaySin", { arg outBus = 0, effectBus, direct = 0.5;
    var source;
    // Decaying pulses of a modulating Sine wave. We'll add reverb later.
    source = Decay2.ar(Impulse.ar(0.3, 0.25), 0.3, 1, SinOsc.ar(SinOsc.kr(0.2, 0, 110, 440)));
    // this will be our main output
    Out.ar(outBus, source * direct);
    // this will be our effects output
    Out.ar(effectBus, source * (1 - direct));
}).send(s);

SynthDef("tutorial-Reverb", { arg outBus = 0, inBus;
    var input;
    input = In.ar(inBus, 1);
    // a low rent reverb
    // aNumber.do will evaluate it's function argument a corresponding number of times
    // {}.dup(n) will evaluate the function n times, and return an Array of the results
    // The default for n is 2, so this makes a stereo reverb
    16.do({ input = AllpassC.ar(input, 0.04, { Rand(0.001,0.04) }.dup, 3)});
    Out.ar(outBus, input);
}).send(s);

b = Bus.audio(s,1); // this will be our effects bus
)

(
x = Synth.new("tutorial-Reverb", [\inBus, b]);
y = Synth.before(x, "tutorial-DecayPink", [\effectBus, b]);
z = Synth.before(x, "tutorial-DecaySin", [\effectBus, b, \outBus, 1]);
)

// Change the balance of wet to dry

y.set(\direct, 1); // only direct PinkNoise
z.set(\direct, 1); // only direct Sine wave
y.set(\direct, 0); // only reverberated PinkNoise
z.set(\direct, 0); // only reverberated Sine wave
x.free; y.free; z.free; b.free;

Groups

Buffers

// Criamos um buffer de 100 frames e 2 canais
b = Buffer.alloc(s, 100, 2);
// Liberamos memória
b.free;

// Podemos ler um arquivo para um Buffer
b = Buffer.read(s, "/home/vilson/Samples/drums1.wav");

// E tocá-lo
(
x = SynthDef("player", { | out=0, bufnum |
        Out.ar(out,
               PlayBuf.ar(1, bufnum, BufRateScale.kr(bufnum))
        )
}).play(s, [\bufnum, b]);
)
x.free; b.free;

// ou simplesmente:
b.play;
b.play(true); // toca em loop
b.free;
// e também visualizá-lo:
b.plot;

// Podemos usar buffer com qualquer tipo de dado (útil para waveshapping):

b = Buffer.alloc(s, 512, 1);

b.cheby([1,0,1,1,0,1]);

(
x = play({ Shaper.ar(b, SinOsc.ar(300, 0, Line.kr(0,1,6)), 0.5) });
)

x.free; b.free; 

Agendando eventos

// A música só acontece no tempo. Em SC, controlamos o "quando" agendando
// coisas em relógios.

// Existem 2 relógios importantes em SC: TempoClock, SystemClock
// TempoClock é mais interessante para eventos musicais (respeitam compasso)
// SystemClock é mais interessante para eventos mundanos (em segundos)

// Agendamos para mostrar Hello daqui a 5 segundos
SystemClock.sched(5, { "Hello".postln })
// Quantos segundos até agora?
SystemClock.seconds;
// Quantas batidas até agora? (do TempoClock padrão, podem haver quantos TempoClocks desejarmos)
TempoClock.default.beats;
// Podemos usar schedAbs() para especificar quando exatamente (absoluto) queremos que algo seja executado
(
TempoClock.default.tempo = 2;
var timeNow = TempoClock.default.beats;
TempoClock.default.schedAbs(timeNow + 5, { "foo".postln; });
)

//
// Agengando eventos com Routines e Tasks
//

Sequenciando com Patterns

p = Pseq([60, 72, 71, 67, 69, 71, 72, 60, 69, 67], 1);
r = p.asStream;
while { (m = r.next).notNil } { m.postln };

(
var midi, dur;
midi = Pseq([60, 72, 71, 67, 69, 71, 72, 60, 69, 67], 1).asStream;
dur = Pseq([2, 2, 1, 0.5, 0.5, 1, 1, 2, 2, 3], 1).asStream;

SynthDef(\smooth, { |freq = 440, sustain = 1, amp = 0.5|
    var sig;
    sig = SinOsc.ar(freq, 0, amp) * EnvGen.kr(Env.linen(0.05, sustain, 0.1), doneAction: 2);
    Out.ar(0, sig ! 2)
}).send(s);

r = Task({
    var delta;
    while {
        delta = dur.next;
        delta.notNil
    } {
        Synth(\smooth, [freq: midi.next.midicps, sustain: delta]);
        delta.yield;
    }
}).play(quant: TempoClock.default.beats + 1.0);
)

(

SynthDef(\bass, { |freq = 440, gate = 1, amp = 0.5, slideTime = 0.17, ffreq = 1100, width = 0.15,

detune = 1.005, preamp = 4|

var sig,

env = Env.adsr(0.01, 0.3, 0.4, 0.1);

freq = Lag.kr(freq, slideTime);

sig = Mix(VarSaw.ar([freq, freq * detune], 0, width, preamp)).distort * amp

* EnvGen.kr(env, gate, doneAction: 2);

sig = LPF.ar(sig, ffreq);

Out.ar(0, sig ! 2)

}).add;


TempoClock.default.tempo = 132/60;


p = Pxrand([

Pbind(

\instrument, \bass,

\midinote, 36,

\dur, Pseq([0.75, 0.25, 0.25, 0.25, 0.5], 1),

\legato, Pseq([0.9, 0.3, 0.3, 0.3, 0.3], 1),

\amp, 0.5, \detune, 1.005

),

Pmono(\bass,

\midinote, Pseq([36, 48, 36], 1),

\dur, Pseq([0.25, 0.25, 0.5], 1),

\amp, 0.5, \detune, 1.005

),

Pmono(\bass,

\midinote, Pseq([36, 42, 41, 33], 1),

\dur, Pseq([0.25, 0.25, 0.25, 0.75], 1),

\amp, 0.5, \detune, 1.005

),

Pmono(\bass,

\midinote, Pseq([36, 39, 36, 42], 1),

\dur, Pseq([0.25, 0.5, 0.25, 0.5], 1),

\amp, 0.5, \detune, 1.005

)

], inf).play(quant: 1);

)

(

SynthDef(\kik, { |preamp = 1, amp = 1|

var freq = EnvGen.kr(Env([400, 66], [0.08], -3)),

sig = SinOsc.ar(freq, 0.5pi, preamp).distort * amp

* EnvGen.kr(Env([0, 1, 0.8, 0], [0.01, 0.1, 0.2]), doneAction: 2);

Out.ar(0, sig ! 2);

}).add;


// before you play:

// what do you anticipate '\delta, 1' will do?

k = Pbind(\instrument, \kik, \delta, 1, \preamp, 4.5, \amp, 0.32).play(quant: 1);

)


p.stop;

k.stop;

Anexo A: Instalando a partir do GIT

Se quiser as últimas atualizações é recomendável usar a versão de desenvolvimento:

   git clone --recursive git://supercollider.git.sourceforge.net/gitroot/supercollider/supercollider
   cd supercollider
   mkdir build
   cd build
   cmake ..
   make
   sudo make install 

E mantenha-se atualizado:

   git pull
   git submodule update

Anexo B: Usando controlador MIDI

MIDIClient.init;
MIDIClient.sources.at(3);
MIDIClient.destinations;
MIDIIn.connect(0, MIDIClient.sources.at(3));
MIDIIn.noteOn = { |port, chan, note, vel| [port, chan, note, vel].postln }; 

(
SynthDef("sik-goo", { arg freq=440,formfreq=100,gate=0.0,bwfreq=800;
        var x;
        x = Formant.ar(
                SinOsc.kr(0.2, 0, 1, freq),
                formfreq,
                bwfreq
        );
        x = EnvGen.kr(Env.adsr, gate,Latch.kr(gate,gate)) * x;
        Out.ar(0, x);
}).send(s);
)

x = Synth("sik-goo");

//set the action:
(
MIDIIn.noteOn = {arg src, chan, num, vel;
        x.set(\freq, num.midicps / 4.0);
        x.set(\gate, vel / 200 );
        x.set(\formfreq, vel / 127 * 1000);
};
MIDIIn.noteOff = { arg src,chan,num,vel;
        x.set(\gate, 0.0);
};
MIDIIn.bend = { arg src,chan,val;
        //(val * 0.048828125).postln;
        x.set(\bwfreq, val * 0.048828125 );
};
)

Anexo C: Synths de exemplo

Alguns synths interessantes para estudo.


// 1

(

{Pluck.ar(WhiteNoise.ar(0.1), Impulse.kr(1), 440.reciprocal, 440.reciprocal, 10, 

coef:MouseX.kr(-0.999, 0.999))

}.play(s)

)

// 2

{o=0; 15.do{|i| o=o+LFTri.ar(138.59*(1.0595**(3*i)), 0, LFPulse.kr((51+i)/60, 0, 0.05, 0.1))}; o;}.play;

// 3

fork{{var t=0.2,x=t.rand;play{PMOsc.ar(100+400.rand,400+400.rand,1+4.0.rand,0,0.2)*EnvGen.kr(Env.linen(0,x,0,1),1,1,0,1,2)};(t-x).wait}!200};

// 4

(

{var n;

n=34;


Resonz.ar(
    
Mix.arFill(n,{

var freq, numcps;


freq= rrand(50,560.3);

numcps= rrand(2,20);

Pan2.ar(Gendy1.ar(6.rand,6.rand,1.0.rand,1.0.rand,freq ,freq, 1.0.rand, 1.0.rand, numcps, SinOsc.kr(exprand(0.02,0.2), 0, numcps/2, numcps/2), 0.5/(n.sqrt)), 1.0.rand2)

})

,MouseX.kr(100,2000), MouseY.kr(0.01,1.0))

;

}.play

)


(

 

{

var freq,time, ex, delay, filter, local;

 

freq= 440;

time= freq.reciprocal;


ex= WhiteNoise.ar(EnvGen.kr(Env([1.0,1.0,0.0,0.0], [time,0,100])));


freq= SinOsc.ar(6, 0, 10, freq);

time= freq.reciprocal;  


local= LocalIn.ar(1);


filter= LPZ1.ar(ex+local); //apply filter


//maximum delay time is 440-10

delay= DelayN.ar(filter, 430.reciprocal, time-ControlDur.ir);  


LocalOut.ar(delay*0.99); 


Out.ar(0, Pan2.ar(filter,0.0))

}.play


)

Referências