Segnali di Controllo
Nelle tecniche illustrate in altri Paragrafi abbiamo trasformato i valori numerici provenienti dall'Interprete in segnali audio o di controllo (che vivono nel Server) e li abbiamo eventualmente modificati attraverso una qualche tecnica di smoothing.
( s.boot; s.plotTree; ) ( a = {arg val=0; [val, // senza smoothing val.lag(2)] // smoothing }.scope; ) a.set(\val,rand(1.0));
Risulta evidente che possiamo utilizzare direttamente un qualsiasi tipo di segnale per modificare dinamicamente qualsiasi parametro di un'altro segnale, a patto che il primo produca valori compresi in un range corretto per quel parametro specifico come tra 0.0 e 1.0 per l'ampiezza, 20 e 20.000 per la frequenza, 0 e 127 per il Midi, etc.
Possiamo classificare le diverse tipologie di segnale in base a tre proprietà:
- rata: il numero di valori letti, elaborati o generati nel tempo.
- range: il valore minimo e il valore massimo del segnale (ampiezze istantanee).
- morfologia: le caratteristiche della forma d'onda.
Rate
In tutti i software per l'audio digitale ci sono due tipi di segnali:
segnali audio: contraddistinti in SuperCollider dal metodo .ar (audio rate) generano in output una sequenza di numeri pari alla sample rate (ad esempio se la sample rate è di 44.100 Hz dall'output della UGen escono 44.100 numeri al secondo e, se presente un input ne entrano altrettanti).
I segnali scritti in uscita da SuperCollider (quelli che poi saranno convertiti in segnale analogico da inviare agli altoparlanti devono essere obbligatoriamente di questo tipo.
segnali di controllo: specificati col metodo .kr generano un numero di valori per secondo pari a sample_rate / block_size che, nel caso di default corrisponde a 44100 / 64 = 689.0625 samples. Risulta evidente come la definizione dei segnali a control rate sia minore di quella dei segnali ad audio rate e che possiamo utilizzarli in tutti i casi in cui un segnale controlla un'altro segnale senza uscire direttamente dagli altoparlanti allo scopo di risparmiare CPU.
Entrambi sono generati o elaborati da UGens e la maggior parte di queste può "lavorare" sia con uno che con l'altro tipo di segnale rispondendo semplicemente a uno dei due metodi dedicati (.ar o .kr). Nell'immagine sottostante lo stesso segnale ad audio e a control rate:
Possiamo convertire un segnale da una tipologia all'altra con due UGens dedicate: A2K.kr() e K2A.ar()
{[WhiteNoise.kr, A2K.kr(WhiteNoise.ar)]}.scope; // Audio --> controllo (downsampling) {[WhiteNoise.ar, K2A.ar(WhiteNoise.kr)]}.scope; // Controllo --> audio (upsampling)
Il primo (A2K.ar()) lo possiamo utilizzare quando abbiamo necessità di sottocampionare un segnale audio in ingresso per utilizzarlo come controllo di altri segnali o come vedremo più avanti come test per il trigger di eventi mentre il secondo (K2A.ar()) può tornare utile quando utilizziamo controller hardware esterni a bassa definizione (come un'interfaccia midi) per controllare qualsiasi parametro di una sintesi che necessita invece un'alta definizione.
Prestiamo attenzione che queste UGens non effettuano alcuna interpolazione, cambiano solamente il numero di campioni al secondo tra segnale in entrata e segnale in uscita.
Range
A priori tutti i segnali possono essere compresi in qualsiasi ambito numerico (range) tra +/- ∞ in quanto le UGen
trattano numeri non suoni.
Questa affermazione però vale solamente per i segnali di controllo (anche quelli ad audio rate) e non per i
segnali audio scritti sull'output di SuperCollider che devono essere obbligatoriamente compresi tra +/- 1.0.
Molte UGens hanno questo ambito di default.
I segnali di questo tipo vengono definiti bipolari in quanto trattano numeri sia positivi che negativi non necessariamente compresi tra +/- 1:
Viveversa i segnali compresi in un ambito con un solo segno (sia esso positivo o negativo) sono chiamati unipolari.
I segnali unipolari dovrebbero essere utilizzati solo come segnali di controllo, in quanto se un segnale audio in uscita è unipolare significa che ha un DC offset:
Riscalaggio
Possiamo modificare il range del segnale in uscita da una UGen in diversi modi:
Attraverso gli argomenti mul e add - quasi tutte le UGen hanno questi due argomenti che corrispondono a un fattore di moltiplicazione (costante o variabile)
( {[ SinOsc.ar(10), // tra +/- 1 SinOsc.ar(10, mul:5), // tra +/- 5 (costante) Line.ar(0,5,1), // rampa variabile SinOsc.ar(10, mul:Line.ar(0,5,1)) // tra +/- 5 (variabile) ]}.plot(1,minval:-10,maxval:10); )
e un fattore di addizione dei valori in uscita dalla UGen:
( {[ SinOsc.ar(10), // tra +/- 1 SinOsc.ar(10, mul:5,add:5), // tra 0 e 5 (costante) Line.ar(0,5,1), // rampa variabile SinOsc.ar(10, add:Line.ar(0,5,1)) // tra 0 e 10 (variabile) ]}.plot(1,minval:-10,maxval:10); )
Attraverso l'utilizzo combinato di questi due fattori possiamo convertire linearmente qualsiasi ambito in uscita da una Ugen in qualsiasi altro.
( a = {arg mult = 100, addi = 1000; // Valori tra 900 e 1100 var sigK = LFNoise0.kr(10,mult,addi); // 10 note al secondo SinOsc.ar(sigK, mul:0.2); // Riscala l'ampiezza }.play ) a.set(\mult,800); // Valori tra 500 e 1500 a.free;
Se l'output è compreso tra +/- 1 abbiamo a disposizione alcune UGens dedicate:
- .range(min,max). A accetta come argomenti le soglie del nuovo ambito.
( {var a = LFNoise0.ar(10); [a, a.range(-2,10)] }.plot(1,minval:-10,maxval:10); ) {SinOsc.ar(SinOsc.ar(0.3).range(440, 6600), 0, 0.5) * 0.1 }.play;
- .exprange(min,max). Identico al precedente ma il riscalaggio è realizzato seguendo una curva esponenziale.
Ha una limitazione: il range-out deve essere dello stesso segno e non avere 0 come limite inferiore o
superiore.
( {var a = LFNoise0.ar(10); [a, a.exprange(0.001,10)] }.plot(1,minval:-10,maxval:10); ) {SinOsc.ar(SinOsc.ar(0.3).exprange(440, 6600), 0, 0.5) * 0.1 }.play;
- .curverange(min,max,curva). Identico al precedente con un terzo argomento che specifica la curva (1 = lineare).
( {var a = LFNoise0.ar(10); [a, a.curverange(0.001,10,-4)] }.plot(1,minval:-10,maxval:10); ) {SinOsc.ar(SinOsc.ar(0.3).curverange(440, 6600, -4), 0, 0.5) * 0.1 }.play;
- .unipolar. Rende unipolare(tra 0 e 1) un segnale bipolare.
( {var a = LFNoise0.ar(10); [a, a.unipolar] }.plot(1,minval:-1,maxval:1); ) {SinOsc.ar(440, 0, SinOsc.kr(1).unipolar)}.play;
- .bipolar(max). Rende bipolare un segnale unipolare. L'argomento specifica
l'ampiezza di picco del segnale.
( {var a = LFPulse.ar(10); // segnale unipolare [a, a.bipolar(0.2)] }.plot(1,minval:-1,maxval:1); )
- .range(min,max). A accetta come argomenti le soglie del nuovo ambito.
Se l'output è compreso tra un minimo e un massimo qualsiasi abbiamo le stesse tecniche ma in generale dobbiamo specificare come primi due argomenti il range in ingresso:
- .linlin(minIn,maxIn, minOut,maxOut).
( {var a = Line.ar(-8,7,1); [a, a.linlin(-8,7,-2,2)] }.plot(1,minval:-10,maxval:10); )
- .linexp(minIn,maxIn, minOut,maxOut) - Il range-out deve essere dello stesso segno e non avere 0 come limite
inferiore o superiore.
( {var a = Line.ar(-8,7,1); [a, a.linexp(-8,7,2,50)] }.plot(1,minval:-50,maxval:50); )
- .explin(minIn,maxIn, minOut,maxOut) - Il range-in deve essere dello stesso segno e non avere 0 come limite
inferiore o superiore.
( {var a = XLine.ar(0.01,7,1); [a, a.explin(0.01,7,-20,50)] }.plot(1,minval:-50,maxval:50); )
- .expexp(minIn,maxIn, minOut,maxOut) - Sia range-in che range-out deve essere dello stesso segno e non avere 0
come limite inferiore o superiore.
( {var a = XLine.ar(0.01,7,1); [a, a.expexp(0.01,7,-50,-20),a.expexp(0.01,7,-1,-50)] }.plot(1,minval:-50,maxval:50); )
- .lincurve(minIn,maxIn,minOut,maxOut,curva) - Il range-in e range-out possono essere di segno diverso e anche 0.
( {var a = Line.ar(0,7,1); [a, a.lincurve(0,7,-50,20,-9)] }.plot(1,minval:-50,maxval:50); )
- .curvelin(minIn,maxIn,minOut,maxOut,curva) - Il range-in e range-out possono essere di segno diverso e anche 0.
( {var a = XLine.ar(0.001,7,1); [a, a.curvelin(0.001,7,-50,20,4)] }.plot(1,minval:-50,maxval:50); )
- .linlin(minIn,maxIn, minOut,maxOut).
Tipologie
Attraverso la combinazione di segnali in algoritmi di sintesi ed elaborazione del suono, possiamo generare un'infinità di timbri con caratteristiche morfologiche estremamete differenti. I segnali che li descrivono però (siano essi audio o di controllo) possono assumerne solo poche e le principali sono quattro:
- Segnali impulsivi o Clocking (di 1 sample)
- Segnali discreti o a bassa frequenza (LF UGens)
- Segnali uniformi: senza discontinuità eccessive tra due o più campioni contigui (o con continua discontinuità).
( {[ Impulse.kr(100), LFNoise0.kr(10), BrownNoise.kr(1).lag, WhiteNoise.kr(1) ]}.plot(0.5) )
Richiamando l'Help file delle diverse UGens presenti nel codice precedente possiamo cominciare autonomamente una piccola esplorazione delle specifiche carattteristiche.
Ogni segnale compreso in ognuna di queste categorie a sua volta può inoltre essere:
- periodico
- a-periodico
( {[ Impulse.kr(100), Dust.kr(100), LFSaw.kr(10), LFNoise0.kr(10), SinOsc.kr(10), WhiteNoise.kr(1) ]}.plot(1) )
Impulsivi
I segnali impulsivi sono quelli in cui il valore di un singolo campione si discosta da quello dei campioni limitrofi
...0 0 0 0 1 0 0 0 0 1...
In SuperCollider sono generati da Clocking UGens e possono essere sia regolari:
...1 0 0 0 1 0 0 0 1 0 0 0 1 0 0 0 1 0 0 0 1...
che irregolari:
...1 0 1 0 0 1 0 0 1 0 0 0 0 0 0 1 0 1...
Sono spesso utilizzati come trigger di eventi in molte tecniche di elaborazione del segnale.
Periodici
In Supercollider possiamo generare un treno di impulsi periodici attraverso la UGen Impulse.ar() che genera una sequenza di clicks isocroni e ha come argomento il numero di clicks per secondo (bps):
( s.boot; s.meter(2,2); s.plotTree; {Impulse.ar(MouseX.kr(1,1000))}.scope; // da ritmo a treno di impulsi )
Essendo un segnale unipolare possiamo utilizzarlo come moltiplicatore d'ampiezza di un altro segnale bipolare:
( {var trig1, trig2; trig1 = Impulse.kr(5); trig2 = Impulse.kr(3); WhiteNoise.ar(trig1) + SinOsc.ar(800,0,trig2)}.scope )
In questo caso siccome i valori generati da questa Ugen sono esclusivamente 0 e 1 se volessimo modificare l'ampiezza randomicamente, dovremmo moltiplicarla per un generatore di rumore bianco reso unipolare oppure in modo deterministico per una rampa o inviluppo:
( {var trig,noise,ramp; trig = Impulse.kr(10); noise = WhiteNoise.kr.unipolar; ramp = Line.kr(0,1,8); SinOsc.ar(1200,0,trig*noise) + WhiteNoise.ar(trig*ramp)}.scope )
Con le dovute modifiche al range possiamo applicarlo a qualsiasi parametro di un algoritmo di sintesi:
( {var trig = Impulse.kr(12), amp = WhiteNoise.kr.unipolar, imp = trig*amp, // tra 0 e 1 fmin = 800, // frequenza minima freq = fmin+(imp*fmin*3); // valori casuali nell'ambito di 3 ottave freq.poll(10); // visualizza SinOsc.ar(freq,0,imp)}.scope )
Un semplice esempio di sound design che riassume quanto appena esposto:
( {var trig1,trig2,trig3,trig4,noise,ramp,imp,fmin,freq; trig1 = Impulse.kr(5); trig2 = Impulse.kr(3); trig3 = Impulse.kr(10); trig4 = Impulse.kr(12); noise = WhiteNoise.kr.unipolar; ramp = Line.kr(0,1,8); imp = trig4*noise; fmin = 800; freq = fmin+(imp*fmin*3); WhiteNoise.ar(trig1) + SinOsc.ar(800,0,trig2) + SinOsc.ar(1200,0,trig3*noise) + WhiteNoise.ar(trig3*ramp) + SinOsc.ar(freq,0,imp) + SinOsc.ar(100,0,Impulse.ar(220))}.scope )
Aperiodici
In Supercollider possiamo generare un treno di impulsi aperiodici attraverso la UGen Dust2.ar() che, a differenza di
Impulse.ar() genera una sequenza di clicks con tempi delta e ampiezze randomiche. Il primo argomento specifica la
media di impulsi per secondo.
Dust.ar() genera un segnale unipolare mentre Dust2.ar() bipolare.
{Dust.ar(MouseX.kr(1,100))}.scope; {Dust2.ar(MouseX.kr(1,100))}.scope; ( {var trig1,trig2,trig3,trig4,noise,ramp,imp,fmin,freq; trig1 = Dust.kr(5); trig2 = Dust.kr(3); trig3 = Dust.kr(10); trig4 = Dust.kr(12); noise = WhiteNoise.kr.unipolar; ramp = Line.kr(0,1,8); imp = trig4*noise; fmin = 800; freq = fmin+(imp*fmin*3); WhiteNoise.ar(trig1) + SinOsc.ar(800,0,trig2) + SinOsc.ar(1200,0,trig3*noise) + WhiteNoise.ar(trig3*ramp) + SinOsc.ar(freq,0,imp) + Dust2.kr(250,0.4)}.scope )
Decay.ar()
Possiamo trasformare i segnali impulsivi in segnali continui utilizzando due diverse UGens: Decay.ar() e Decay2.ar().
Decay.ar() genera una rampa di decadimento esponenziale. Possiamo specificarne il tempo in secondi come argomento mentre l'attacco è immediato (discontinuità del segnale).
Decay2.ar() genera sia una rampa di attacco che di decadimento esponenziali. Possiamo specificarne il tempo in secondi come argomenti.
( {var trig = Impulse.ar(5), // trigger env = Decay.ar(trig,0.1); // inviluppo con decay WhiteNoise.ar(env) // segnale }.scope ) ( {var trig = Impulse.ar(3), // trigger env = Decay.ar(trig,0.5); // inviluppo con decay SinOsc.ar(800,0,env) // segnale }.scope )
A seconda del parametro sul quale mappiamo il segnale impulsivo (ampiezza, frequenza o altro) e/o a seconda della morfologia del segnale sul quale agisce possiamo sceglierne uno o l'altro. Nell'esempio precedente Decay.ar() funziona come moltiplicatore d'ampiezza del rumore bianco ma genera clicks quando lo utilizziamo con una sinusoide. In questo caso se non vogliamo udire questo artefatto dobbiamo utilizzare Decay2.ar().
Un altra questione da sottolineare consiste nel fatto che Impulse.ar() e Dust.ar() accettano valori temporali in bps mentre Decay.ar() e Decay2.ar() li accettano in secondi. E' consigliabile unificare l'unità di misura temporale effettuando le dovute conversioni:
- Tutto in bps:
( SynthDef(\dec , {arg bps=4; var trig,env; trig = Impulse.ar(bps); // bps env = Decay.ar(trig,1/bps); // secondi (1/bps oppure bps.reciprocal) Out.ar(0, WhiteNoise.ar*env) }).add; {a = Synth(\dec)}.defer(0.2) ) a.set(\bps,rrand(1,8));
- Tutto in secondi:
( SynthDef(\dec , {arg sec=4; var trig,env; trig = Impulse.ar(1/sec); // secondi env = Decay.ar(trig,sec); // bps (1/secondi oppure secondi.reciprocal) Out.ar(0, WhiteNoise.ar*env) }).add; {a = Synth(\dec)}.defer(0.2) ) a.set(\sec,rrand(0.01,0.6));
Per quanto riguarda Decay2.ar() dobbiamo calcolare il tempo del decadimento sottraendo alla durata il tempo dell'attacco. E' dunque consigliabile specificare tutto direttamente in secondi.
Facciamo attenzione che con questa UGen possiamo addolcire o indurire l'attacco con tempi assoluti al di sotto di 0.1 secondi ma non modificare l'inviluppo.
( SynthDef(\dec2 , {arg sec=0.2,atk=0.01; var bps,dec,trig,env; bps = sec.reciprocal; dec = 1-atk * sec; trig = Impulse.ar(bps); env = Decay2.ar(trig,atk,dec); Out.ar(0, SinOsc.ar*env) }).add; {a = Synth(\dec2)}.defer(0.2) ) a.set(\atk,rrand(0.008,0.01).postln); a.set(\sec,rrand(0.05,0.8));
Il prossimo esempio illustra come si possa utilizzare lo stesso segnale per controllare simultaneamente due o più parametri differenti semplicemente riscalandolo in ambiti diversi:
{Dust2.kr(5)}.plot(1); // tra -1 e +1 {Decay2.ar(Dust2.ar(5),0.005,0.3)}.plot(1); // applica un inviluppo {Decay2.ar(Dust2.ar(5),0.005,0.3)*200+1000}.plot(1); // tra 800 e 1200 ( SynthDef(\sync, {var trig,env,sig; trig = Dust2.ar(10); env = Decay2.ar(trig,0.005,0.3); sig = SinOsc.ar(env*500+4000,0,env); Out.ar(0,sig) }).add; {a = Synth(\sync)}.defer(0.2); )
Discreti
I segnali discreti sono quelli in cui il valore dell'ampiezza istantanea cambia ogni n campioni:
...[0.5 0.5 0.5 0.5] [-0.2 -0.2 -0.2 -0.2] [0.8 0.8 0.8 0.8] [-0.7 -0.7 -0.7 -0.7]...
La maggior parte dei segnali discreti può essere generata da UGens dedicate che hanno come prefisso LF che è l'acronimo di Low Frequency. Quasi tutti sono bipolari, compresi tra +/-1 e possono essere:
Segnali periodici
( {[LFCub.kr(5), LFGauss.kr(1/5), LFPar.kr(5), LFPulse.kr(5), LFSaw.kr(5), LFTri.kr(5)]}.plot(1) )
La caratteristica principale di questi segnali periodici è che non sono limitati in banda di frequenza e possono dunque generare aliasing o foldover. Nell'immagine seguente possiamo osservare le stesse forme d'onda nella versione LF (ottimizzata pre il controllo di altri segnali) e nella versione con filtro anti aliasing (ottimizzata per i segnali in uscita da SuperCollider):
{LFCub.ar(MouseY.kr(20000,200),0,0.1)}.scope; // con aliasing {SinOsc.ar(MouseY.kr(20000,200),0,0.1)}.scope; // senza aliasing
Segnali aperiodici
( {[LFNoise0.kr(50), LFNoise1.kr(50), LFNoise2.kr(50), LFClipNoise.kr(100), ]}.plot(1); )
I segnali di questo tipo sono caratterizzati dal fatto che il loro primo argomento è la frequenza espressa in Hz, ovvero il numero di valori pseudocasuali generato in un secondo (ricordiamo che i generatori di rumore tradizionali non hanno per loro natura il parametro frequenza, ma solo ampiezza). Prestiamo attenzione a non incappare in un'incomprensione: questi segnali non generano solo il numero di valori indicato come frequenza, ma quello indicato dal metodo invocato (.ar o .kr). Il parametro frequenza indica solo ogni quanto cambiano.
Smoothing
Possiamo ottenere segnali continui da segnali discreti in due modi differenti:
UGens dedicate. In questo caso non si tratta di una conversione morfologica vera e propria ma di generare direttamente un segnale continuo con le stesse caratteristiche di quello discreto:
( SynthDef(\fnoise, {arg trig = 10; var freq,amp,sig; freq = LFNoise0.kr(trig); // Discreto amp = LFNoise2.kr(trig); // Continuo sig = SinOsc.ar(freq.range(500,900)) * amp.unipolar; freq.scope; amp.scope; sig.scope; Out.ar(0,sig) }).add; {a = Synth(\fnoise)}.defer(0.2); ) a.set(\trig,rrand(2,10).postln);
Smoothing. Possiamo utilizzare diverse modalità di smoothing ovvero di arrotondamento del segnale. La tecnica principale in SuperCollider può essere realzzata con la UGen Lag.ar() che come argomenti accetta: il segnale da arrotondare e un tempo di lag in secondi (il tempo che il segnale in ingresso impiega per diminuire di 60 dB):
( {var imp = LFNoise0.kr(10); [imp, Lag.kr(imp,0.01), Lag.kr(imp,0.1), Lag.kr(imp,0.8), Lag.kr(imp,1.2) ]}.plot(1,minval:-1,maxval:1) ) ( SynthDef(\laggy, {arg lagT = 0.2; var freq,amp,sig; freq = Lag.kr(LFNoise0.kr(5),lagT); sig = SinOsc.ar(freq.range(500,900)); freq.scope; Out.ar(0,sig) }).add; {a = Synth(\laggy)}.defer(0.2); ) a.set(\lagT,[0.01,0.1,0.8,1.2].choose.postln);
Ci sono inoltre altre UGens della stessa famiglia che realizzano altri tipi di interpolazioni: Lag2.kr(), Lag3.kr() e VarLag.kr().
( {var ksig = LFNoise0.ar(50); [ksig, Lag.ar(ksig), Lag2.ar(ksig), // come 2 Lag in cascata (curva esponenziale) Lag3.ar(ksig), // come 3 Lag in cascata (curva cubica) VarLag.ar(ksig,0.01,0,\lin)] }.plot(1) )
Tutte possono anche essere specificate come metodi:
( {var ksig = LFNoise0.ar(10); [ksig, ksig.lag(0.2), ksig.lag2(0.2), ksig.lag3(0.2), ksig.varlag(0.05,0,\lin)] }.plot(0.5, minval:-1, maxval:1) )
Fra queste la UGen più duttile è VarLag.kr() perchè possiamo specificare la curva di interpolazione che in Lag.kr() è sempre esponenziale:
( {var ksig = LFNoise0.ar(10); [ksig, VarLag.ar(ksig,0.1,5,\step), // ritarda di 1 step VarLag.ar(ksig,0.1,5,\lin), VarLag.ar(ksig,0.1,5,\exp), VarLag.ar(ksig,0.1,5,\sin), VarLag.ar(ksig,0.1,5,\wel), VarLag.ar(ksig.unipolar,0.1,5,\sqr), // unipolare VarLag.ar(ksig.unipolar,0.1,5,\cub)] // unipolare }.plot(1) )
Per effettuare l'operazione opposta allo smoothing, ovvero rimuovere l'arrotondamento di un segnale, possiamo utilizzare la tecnica del Sample and hold attraverso la UGen Latch.kr().
( {var sig = LFNoise2.ar(133), nsamp = 100, // numero di samples (Hz) samp = Impulse.ar(nsamp), out = Latch.ar(sig, samp); [sig,out] }.plot(0.1) )
Ovviamente tutti questi segnali opportunamente riscalati possono essere mappati su qualsiasi parametro di un algoritmo di sintesi o di elaborazione del suono.
Changed.kr()
Per ottenere un segnale impulsivo da uno discreto possiamo utilizzare la UGen Changed.kr() che genera un 1 ogni volta che cambia il valore di un segnale in ingresso:
( SynthDef(\chan, {arg rate=5; var ksig,trig,env,sig; ksig = LFNoise0.ar(rate).scope; trig = Changed.ar(ksig); env = Decay2.ar(trig,0.01,rate.reciprocal-0.01); sig = SinOsc.ar(ksig.range(80,86).midicps); Out.ar(0,sig*env*ksig.unipolar) }).add; {a = Synth(\chan)}.defer(0.2) ) a.set(\rate,rrand(2,10).postln);
Il segnale impulsivo ottenuto ha tutte le peculiarità trattate nel paragrafo dedicato.
Uniformi
Questa tipologia rappresenta la maggior parte dei segnali audio, siano essi suoni campionati (segnali provenienti da microfoni o memorizzati in audio files), siano essi suoni sintetizzati direttamente in SuperCollider, siano essi suoni derivati dall'elaborazione di altri segnali. Tipicamente sono bipolari con valori compresi tra +/-1 e lavorano ad audio rate.
Anch'essi debitamente riscalati e/o sottocampionati e/o mofrfologicamente convertiti possono essere utilizzati per controllare uno o più parametri di altri segnali. Vediamo le principali tecniche che possiamo utilizzare.
Audio
Possono essere segnali in tempo reale oppure memorizzati su supporti sotto forma di sound file o buffer.
b = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav"); b.plot(\audio,800@200);
Periodici
Possono essere generati da UGens dedicate. In seguito alcune forme d'onda classiche. Notiamo come a differenza dei loro stretti parenti a bassa frequenza (LFUgens) queste siano bandlimited (si capisce visivamente dalla "rugosità" della forma d'onda) per evitare aliasing.
( {[SinOsc.kr(10), Pulse.kr(10), Saw.kr(10), Blip.kr(10), ]}.plot )
Aperiodici
Diverse tipologie di rumore generate da UGens dedicate.
( {[WhiteNoise.ar(1), PinkNoise.ar(1), BrownNoise.ar(1), GrayNoise.ar(1), ClipNoise.ar(1)]}.plot )
Triggers
Possiamo utilizzare segnali di qualsiasi tipologia per generare triggers ovvero discontinuità tra due valori limitrofi di ampiezza istantanea che possono far partire e/o arresatare un evento sonoro. E' prassi che un valore maggiore di 0 faccia partire un evento mentre un valore minore o uguale a 0 lo arresti.
Esistono due principali tipi di trigger:
- Impulsivo - un campione maggiore di 0 seguito da più zeri (...1 0 0 0 1 0 0 0 1 0 0 0 1 0 0 0...)
- Pulsivo - un alternarsi di più valori fissi maggiori di 0 con più zeri (...1 1 1 1 0 0 0 0 1 1 1 1 0 0 0 0...)
Entrambi sono unipolari e possono subire tutte le alterazioni morfologiche dei segnali impulsivi (Decay.ar()) e di quelli discreti (tecniche di smooothing). Nell'immagine seguente un riassunto delle principali morfologie che può assumere un trigger ottenuto da un segnale:
( {var sigk = SinOsc.ar(3), // segnale di controllo trig0 = Impulse.kr(3), // segnale impulsvo env = trig0.lag*10, // segnale impulsvo 'smoothed' trig1 = Trig1.kr(sigk,0.2), // con fase sostegno env0 = trig1.lag; // con fase sostegno 'smoothed' [sigk,trig0,env,trig1,env0] }.plot(2) )
In questi casi i valori sono compresi tra 0 e 1 ma possono essere riscalati in qualsiasi range invocando il metodo .range(min,max).
Modalità di generazione
Un trigger sotto forma di segnale può essere generato in quattro modi:
- Discreto - lo possiamo stabilire a piacere attraverso un'azione manuale su un device esterno
(Midi, Osc,
tastiera del computer,
mouse o altro).
( SynthDef(\discre , {var ksig,trig,sah,sig; ksig = SinOsc.ar(0.2).scope; // Segnale di controllo trig = MouseButton.kr; // Trigger del campionamento sah = Latch.kr(ksig,trig).scope; // Sample and hold sig = PinkNoise.ar * sah.unipolar; // Riscalato sull'ampiezza Out.ar(0,sig) }).add; {Synth(\discre)}.defer(0.2); )
- Delta regolari - attraverso una sequenza di impulsi regolari generata dalla UGen Impulse.kr() (trig singolo)
oppure dalla UGen LFPulse.kr() (doppio trig):
( SynthDef(\discre , {arg rate=10; var ksig,trig,sah,env,freq,sig; ksig = WhiteNoise.ar(1).scope; trig = Impulse.kr(rate).scope; sah = Latch.kr(ksig,trig).scope; freq = sah.range(1200,2000); env = Decay.kr(trig,rate.reciprocal).scope; sig = SinOsc.ar(freq) * env; Out.ar(0,sig) }).add; {a = Synth(\discre)}.defer(0.2); ) a.set(\rate,rrand(1,20).postln);
- Delta irregolari - attraverso una sequenza di impulsi irregolari generati dalla UGen Dust.kr():
( SynthDef(\discre , {arg rate=5; var ksig,trig,sah,env,freq,sig; ksig = WhiteNoise.ar(1).scope; trig = Dust.kr(rate).scope; sah = Latch.kr(ksig,trig).scope; freq = sah.range(2000,3000); env = Decay.kr(trig,rate.reciprocal).scope; sig = SinOsc.ar(freq) * env; Out.ar(0,sig) }).add; {a = Synth(\discre)}.defer(0.2); )
- Soglie nel Server - attraverso il monitoraggio dei valori di ampiezza di un segnale bipolare di
qualsiasi tipologia esso sia. Il trigger (passaggio da 0 a 1) viene generato nell'istante in cui il segnale supera un
valore soglia. In questo caso la regolarità o irregolarità dei delta dipenderanno dalla morfologia del segnale monitorato:
( SynthDef(\sah, {arg nsamp=10; var ksig,samp,freq,sig; ksig = SinOsc.ar(13.3); samp = Impulse.ar(nsamp); freq = Latch.ar(ksig, samp).range(600,1100); sig = SinOsc.ar(freq); Out.ar(0,sig) }).add; {a = Synth(\sah)}.defer(0.2); ) a.set(\nsamp,rrand(2,13)); // cambia pattern
Esistono numerose possibilità e variazioni di questo concetto che possiamo sfruttare nelle situazioni di controllo più disparate.
Trig.ar() e Trig1.ar()
Le UGens Trig.ar() e Trig1.ar() generano e mantengono un valore maggiore di 0 ogni qualvolta si verifica una transizione da polarità negativa a positiva (e non viceversa) di un qualsiasi segnale specificato come primo argomento:
( {var sig = LFNoise0.ar(15), trig = Trig.kr(sig), trig1 = Trig1.kr(sig); [sig, trig, trig1] }.plot(1) )
Notiamo come la differenza tra le due UGens consista nel fatto che Trig.ar() campiona e mantiene il valore del campione successivo alla transizione mentre Trig1.ar() assume sempre 1.
Il secondo argomento (dur:) corrisponde a un parametro chiamato zona morta (death zone) che specifica il tempo di sostegno ovvero il tempo che il segnale rimarrà al valore generato dal trigger. Tutti gli eventi che accadono all'interno di questa zona (lasso temporale) non sono presi in considerazione.
( {var sig = SinOsc.ar(10), trig1 = Trig1.kr(sig,0.005), trig2 = Trig1.kr(sig,0.09), trig3 = Trig1.kr(sig,0.29); [sig, trig1,trig2,trig3] }.plot(1) )
Di default queste due UGens generano il trigger solo nel momento in cui le ampiezze del segnale monitorato passano da valori negativi a positivi. Se però questo segnale è discreto ci sono altre due possibilità di generare triggers:
- Ogni volta che il valore aumenta. Ad ogni campione il valore corrente è sottratto al valore del precedente.
Se il valore corrente è maggiore del precedente il risultato sarà un numero positivo, se minore negativo.
L'alternarsi delle polarità genera il trigger.
( {var dur = 0.01, sig = LFNoise0.ar(10), // .ar perchè c'è Delay1 trig = Trig1.ar(sig-Delay1.ar(sig), dur); // Delay1 ritarda di 1 campione [sig, trig] }.plot(1); {var dur = 0.01, sig = LFNoise0.ar(10), trig = Trig1.ar(sig-Delay1.ar(sig), dur), // Delay1 ritarda di 1 campione env = Env.asr(0.01,1,1, -4), envi = EnvGen.kr(env,trig); SinOsc.ar(900!2)*envi }.play )
- Ad ogni cambio di valore. Identico al precedente ma con valori assoluti.
( {var dur = 0.01, sig = LFNoise0.ar(5), trig = Trig1.ar(abs(sig-Delay1.ar(sig)), dur); // abs = valore assoluto (no -0) [sig, trig] }.plot(1); {var dur = 0.01, sig = LFNoise0.ar(5), trig = Trig1.ar(abs(sig-Delay1.ar(sig)), dur), env = Env.asr(0.01,1,1, -4), envi = EnvGen.kr(env,trig); SinOsc.ar(900!2)*envi }.play )
Nel caso in cui le ampiezze del segnale da monitorare non siano comprese in un range di valori tra +/- 1.0 dobbiamo necessariamente riscalarle in questo con il metodo .linlin().
{var sig = Line.ar(-10,10,1); [sig,sig.linlin(-10,10,-1,1)]}.plot(1,minval:-10,maxval:10)
Soglia semplice
Possiamo sostituire il test per la generazione del trigger da: ogni volta che i valori del segnale passano da negativi a positivi con ogni volta che i valori superano un valore-soglia stabilito.
Utilizzando le UGens Trig.ar() e Trig1.ar() il valore 0 ritorna automaticamente dopo un tempo dato specificato come secondo argomento. Nel caso delle soglie invece abbiamo a disposizione tre altre possibilità:- se il segnale è sopra la soglia = 1, se è sotto la soglia = 0.
- se il segnale è sotto la soglia = 1, se è sopra la soglia = 0 (inverso).
- ogni volta che passa la soglia = 1, il campione successivo = 0.
In quesi casi il parametro temporale non è dunque stabilito da noi ma dall'andamento del segnale monitorato.
In SuperColider possiamo utilizzare le seguenti abbreviazione sintattiche:
- segnale di controllo > soglia
- segnale di controllo < soglia
- Changed.ar( segnale di controllo > soglia )
{(MouseX.kr(0, 10) > 5).scope * SinOsc.ar}.play; // Moltiplica {EnvGen.kr(Env.perc,MouseX.kr(0, 10) > 5) * SinOsc.ar}.scope; // Env no sust solo su {EnvGen.kr(Env.perc,Changed.kr(MouseX.kr(0, 10) > 5)) * SinOsc.ar}.scope; // Env no sust su e giù {EnvGen.kr(Env.asr,MouseX.kr(0, 10) > 5) * SinOsc.ar}.scope; // Env sust
In questo caso il valore della soglia può essere qualsiasi, non necessariamente compreso tra +/-1.
Soglia doppia
Un'altra tecnica (chiamata Schmidt trigger) consiste nello stabilire una doppia soglia: quando il segnale di controllo supera un determinato valore (soglia superiore) genera un 1, quando scende al di sotto di un secondo valore (soglia inferiore) genera uno 0.
Schmidt.kr(MouseX.kr(0, 10), 2, 6)}.scope; // argomenti: sig, min, max
Anche in questo caso i valori delle due soglie possono essere qualsiasi, non necessariamente compresi tra +/-1.
InRange.kr()
Un'altra possibilità è data dal monitorare se i valori di un segnale siano o meno all'interno di un range. Per farlo possiamo utilizzare la UGen InRange.kr() che accetta gli stessi argomenti di Schmidt.kr(). Se il segnale è compreso nei limiti genera 1 altrimenti 0.
{InRange.kr(MouseX.kr(0, 1), 0.2, 0.8)}.scope; // argomenti: sig, min, max
Ulteriori UGens
A questo link possiamo trovare ulteriori Ugens dedicate al controllo dei triggers
- Ogni volta che il valore aumenta. Ad ogni campione il valore corrente è sottratto al valore del precedente.
Se il valore corrente è maggiore del precedente il risultato sarà un numero positivo, se minore negativo.
L'alternarsi delle polarità genera il trigger.
- Soglie nell'Interprete - nelle tecniche appena illustrate il test utilizzato per la
generazione del trigger avviene nel Server. Da questo vengono inviati all'Interprete esclusivamente i valori del trigger, non
quelli del segnale che lo ha generato.
Esiste anche un'altra tecnica che ci permette di monitorare i valori di un segnale nell'Interprete e se in questo caso avessimo bisogno di ottenere un trigger converrebbe effettuare il test direttamente in quell'ambiente. La scelta nell'utilizzo di una o dell'altra dipenderà dalle situazioni musicali intrinseche di ogni diverso brano.Monitoraggio Bus di controllo
Possiamo recuperare i valori istantanei di un segnale nell'Interprete scrivendolo su di un Bus di controllo e invocando su di esso il metodo .get.
( s.boot; s.meter(1,2); s.plotTree; ) ( c = Bus.control(s, 1); SynthDef(\randi, {arg out,rate; Out.kr(out,LFNoise0.ar(rate))}).add; // controllo SynthDef(\sine5, {arg freq=0.5; Out.ar(0, SinOsc.ar(freq)*0.3)}).add; // audio {y = Synth(\randi,[\out,c,\rate,10]); x = Synth(\sine5,[\freq,0.5.linlin(-1,1,1000,1500)]) // frequenze tra +/-1... }.defer(0.2); ) c.get; // a ogni esecuzione campiona il segnale scritto sul Bus // N.B. Se volessimo utilizzare il valore nel client possiamo richiamarlo come ar- // gomento e assegnarlo a una variabile ( c.get( {arg val; val.round(0.01); // stampa il valore f = val; // assegno alla variabile 'f' il valore f.postln; x.set(\freq,f.linlin(-1,1,1000,1500)) }) ) // Ovviamente possiamo automatizzare il campionamento con Routine, Task e Clock ( b = 92; t = TempoClock(b/60); t.sched(0,{c.get({arg val; x.set(\freq, val.linlin(-1,1,1000,1500)); }); 0.3}) ) t.clear; // distrugge lo scheduler c.free; // libera l'assegnazione del Bus alla variabile 'c' c.get; // non funziona più... x.free; // distrugge l'istanza di Synth y.free;
Possiamo applicare le stesse tecniche utilizzate per generare trigger con segnali audio anche con i valori ottenuti dal monitoraggio.
Soglia semplice
Questa tecnica è conosciuta col nome di threshold detection. Ci sono diverse possibilità.
- Maggiore o minore (trigger a due stati): genera uno se i valori sono maggiori della soglia, zero se minori.
( SynthDef(\sine9, {arg freq,amp=0; Out.ar(0, SinOsc.ar(freq)*amp)}).add; c = Bus.control(s, 1); // Bus di controllo { y = Synth(\randi,[\out,c,\rate,10]); // Segnale da monitorare x = Synth(\sine9); // Segnale 'che suona' }.defer(0.2); r = Routine.new({ // Routine che cambia inf.do({ // freq e amp continuamente x.set(\freq,rrand(1000,1300), \amp, rand(1.0)); 0.1.wait; }) }); w = 0.5; u = Routine.new({var soglia=0; // definisco una soglia inf.do({ c.get({arg val; if(val > soglia, {r.reset.play; "play".postln}, // sopra la soglia {r.stop;x.set(\amp,0);"stop".postln}); // sotto la soglia }); w.wait }) }).reset.play; )
- Da sotto a sopra (trigger a uno stato): genera uno ogni volta che il valore passa la soglia verso l'alto. Oltre
al confronto con il valore-soglia dobbiamo effettuare quello tra due valori consecutivi nel tempo:
( c = Bus.control(s, 1); // Bus di controllo y = Synth(\randi,[\out,c,\rate,100]); // Segnale da monitorare x = Synth(\sine9,[\freq,1000,\amp,0.2]); // Segnale 'che suona' r = Routine.new({ // Routine che cambia inf.do({ // freq e amp continuamente x.set(\freq,rrand(1000,1300), \amp, rand(1.0)); 0.1.wait; }) }); w = 3; u = Routine.new({var soglia=0, ora=0, prec=0, dur; inf.do({ c.get({arg val; ora = val}); // Recupera il valore dal Bus if((ora > soglia) && (prec < soglia), {r.reset.play; // Vero dur = (ora+1)*2.0; // Setta la durata dur.round(0.01).postln; // Stampa SystemClock.sched(dur, // Stop dopo una durata {r.stop}, nil) }, {"ten".postln}); // Falso prec = ora; // Aggiorna il valore w.wait }) }).reset.play; )
- Da sopra a sotto (trigger a uno stato): basta invertire > e < .
( c = Bus.control(s, 1); // Bus di controllo y = Synth(\randi,[\out,c,\rate,100]); // Segnale da monitorare x = Synth(\sine9,[\freq,1000,\amp,0.2]); // Segnale 'che suona' r = Routine.new({ // Routine che cambia inf.do({ // freq e amp continuamente x.set(\freq,rrand(1000,1300), \amp, rand(1.0)); 0.1.wait; }) }); u = Routine.new({var soglia=0, ora=0, prec=0, dur=3; inf.do({ c.get({arg val;ora = val}); if((ora < soglia) && (prec > soglia), {r.reset.play; // Vero dur = (ora+1)*2.0; // Setta la durata dur.round(0.01).postln; // Stampa SystemClock.sched(dur, // Stop {r.stop}, nil) }, {"ten".postln}); // Falso prec = ora; dur.wait // la durata determina }) // il tempo delta }).reset.play; )
- Ogni passaggio (trigger a due stati): in questo caso sostituiamo il condizionale if() con case.
( c = Bus.control(s, 1); // Bus di controllo y = Synth(\randi,[\out,c,\rate,100]); // Segnale da monitorare x = Synth(\sine9,[\freq,1000,\amp,0.2]); // Segnale 'che suona' r = Routine.new({ // Routine che cambia inf.do({ // freq e amp continuamente x.set(\freq,rrand(1000,1300), \amp, rand(1.0)); 0.1.wait; }) }); w = 0.5; u = Routine.new({var soglia=0, ora=0, prec=0, dur=3; inf.do({ c.get({arg val;ora = val}); case {(ora > soglia) && (prec < soglia)} {r.reset.play; "Play".postln} {(ora < soglia) && (prec > soglia)} {r.stop; "Stop".postln}; prec = ora; w.wait; }) }).reset.play; ) r.stop;u.stop;x.free;
Soglia doppia
Possiamo definire due o più soglie differenti come abbiamo visto con lo Schmidt trigger. Questa tecnica si chiama anche debouncing.
( var dt = 0.8, soglia_start = 0.2, // due soglie soglia_stop = - 0.4; ~ascolta = Task.new({var prec = 0, ora = 0, dur; inf.do{~bus.get({arg val; ora = val}); //---------------------------------------------------- Condizionale // se 'ora' > soglia_start AND 'prec' < soglia_start = Play // se 'ora' < soglia_stop AND 'prec' > soglia_stop = Stop case {(ora > soglia_start) && (prec < soglia_start)} {~voce1.play(~tempo); "Play".postln} {(ora < soglia_stop) && (prec > soglia_stop)} {~voce1.stop; "Stop".postln;}; prec = ora; dt.wait} }) ) ~ascolta.play; ( ~ascolta.stop; ~voce1.stop; // ...potrebbe rimanere su "play"... ) ( c = Bus.control(s, 1); // Bus di controllo y = Synth(\randi,[\out,c,\rate,100]); // Segnale da monitorare x = Synth(\sine9,[\freq,1000,\amp,0.2]); // Segnale 'che suona' r = Routine.new({ // Routine che cambia inf.do({ // freq e amp continuamente x.set(\freq,rrand(1000,1300), \amp, rand(1.0)); 0.1.wait; }) }); w = 0.5; u = Routine.new({var soglia_start = 0.2, soglia_stop = 0.4, ora=0, prec=0, dur=3; inf.do({c.get({arg val; ora = val}); case {(ora > soglia_start) && (prec < soglia_start)} {r.reset.play; "Play".postln; ora.round(0.01).postln} {(ora < soglia_stop) && (prec > soglia_stop)} {r.stop; "Stop".postln; ora.round(0.01).postln}; prec = ora; w.wait; }) }).reset.play; ) r.stop;u.stop;x.free;
Aumento valore
Trigger a uno stato (una sola condizione determina l'evento). Viene generato ogni volta che il valore corrente è maggiore del precedente. Nell'esempio seguente il trigger controlla un inviuppo senza fase di sostegno che necessita solo di un comando gate:1.
( SynthDef(\sine6, {arg t_gate=0; var sig, env; sig = SinOsc.ar(1200, mul:0.3); env = EnvGen.kr(Env.perc(0.01,1),t_gate); Out.ar(0, sig*env);} ).add; ) ( c = Bus.control(s, 1); // Bus di controllo y = Synth(\randi,[\out,c,\rate,10]); // Segnale da monitorare x = Synth(\sine6); // Segnale 'che suona' w = 1; // tempo di sottocampionamento r = Routine.new({var prec=0, ora=0, dur; inf.do({c.get({arg val;ora = val}); if(ora > prec, // test {x.set(\t_gate,1); // vero (ora.round(0.01).asString ++" > " ++ prec.round(0.01).asString).postln} ); prec = ora; // riassegnazione w.wait }) }).reset.play; )
Trigger a due stati. L'esempio seguente invece ha un inviluppo che richiede specificatamente un comando di gate 0 che viene triggerato ogni volta che si verifica la condizione opposta, ovvero il valore corrente (ora) è inferiore del precedente.
( SynthDef(\sine7, {arg gate=0; // tolta la 't_' da gate var sig, env; sig = SinOsc.ar(1200!2); env = EnvGen.kr(Env.adsr(0.01,0.3,0.2,0.6),gate); Out.ar(0, sig*env);} ).add; ) ( c = Bus.control(s, 1); // Bus di controllo y = Synth(\randi,[\out,c,\rate,10]); // Segnale da monitorare x = Synth(\sine7); // Segnale 'che suona' w = 1; r = Routine.new({var prec=0, ora=0, dur; inf.do({c.get({arg val;ora = val}); if(ora > prec, {x.set(\gate,1); // vero (gate 1) (ora.round(0.01).asString ++ " > " ++ prec.round(0.01).asString).postln}, {x.set(\gate,0)}; // falso (gate 0) ); prec = ora; w.wait }) }).reset.play; ) r.stop;
Diminuzione valore
Trigger a uno stato. Semplicemente la condizione opposta alla precedente, basta invertire il segno condizionale o il contenuto delle funzioni legate a vero e falso.
( c = Bus.control(s, 1); // Bus di controllo y = Synth(\randi,[\out,c,\rate,10]); // Segnale da monitorare x = Synth(\sine7); // Segnale 'che suona' w = 1; r = Routine.new({var prec=0, ora=0, dur; inf.do({c.get({arg val;ora = val}); if(ora < prec, // cambio segno {x.set(\gate,1); (ora.round(0.01).asString++" < "++prec.round(0.01).asString).postln}, {x.set(\gate,0)}); prec = ora; w.wait }) }).reset.play; ) r.stop;
- Maggiore o minore (trigger a due stati): genera uno se i valori sono maggiori della soglia, zero se minori.
Principali utilizzi
In seguito le pricipali tecniche che utilizzano triggers.
Sottocampionamento
In alcuni casi potremmo voler sottocampionare i valori di un segnale per poterli mappare debitamente riscalati e/o "arrotondati" o meno su un qualche parametro di altri segnali.
( s.boot; s.meter(2,2); s.plotTree; SynthDef(\sotto, {var rate,ksig,trig,vals,sig; rate = Rand(2,5); // numero di triggers per secondo ksig = WhiteNoise.kr; // segnale da sottocampionare trig = Impulse.kr(rate); // generatore di trigger vals = ksig*trig; // sottocampionamento sig = Saw.ar(VarLag.kr(vals,rate.reciprocal).range(900,2500)); // mappato su freqs Out.ar(0,sig*Decay.kr(vals.abs,rate.reciprocal)) // mappato su amps }).add; {Synth(\sotto)}.defer(0.2) )
Ramp.ar()
Possiamo ottenere rampe generate attraverso un'interpolazione lineare tra i valori campionati con la UGen Ramp.ar()
( SynthDef(\rate, {var rate,ksig,glis,sig; rate = Rand(2,5); ksig = WhiteNoise.kr; glis = Ramp.kr(ksig,rate); sig = Pulse.ar(glis.range(900,2500)); // mappato su freqs Out.ar(0,sig*glis.unipolar*0.3) // mappato su amps }).add; {Synth(\rate)}.defer(0.2) )
Sample and hold
Possiamo trasformare un segnale appartenente ad una qualsiasi tipologia in un segnale discreto attraverso la tecnica del campiona e mantieni che deriva sia dalle tecniche proprie dell'audio analogico che dal procedimento del campionamento. Fondamentalmente si tratta di campionare e mantenere uno o più valori tra le ampiezze istantanee di un segnale. In SuperCollider la UGen da utilizzare è Latch.ar() che accetta due argomenti:
- un segnale da sottocampionare.
- un segnale che genera il trigger per il campionamento (e mantenimento del valore) a ogni transizione da negativo a positivo.
Le diverse modalità di generazione dei triggers sono illustrate in un paragrafo dedicato.
Prestiamo attenzione che se utilizziamo questa tecnica per sottocampionare segnali impulsivi i due segnali devono coincidere oppure devono essere perfettamente sincronizzati. Stesso segnale (usando Impulse avremmo una serie di 1...)
( SynthDef(\sah1, {var ksig,amp,sig; ksig = Dust.kr(10); amp = Latch.ar(ksig, ksig).scope; sig = SinOsc.ar(amp.range(600,1100)); Out.ar(0,sig*amp.lag(0.2)) }).add; {a = Synth(\sah1)}.defer(0.2); )
Step sequencing
Un'altra tecnica per ottenere segnali discreti da qualsiasi tipologia di segnale è lo step sequencing e in SuperCollider possiamo realizzarla con la Ugen Stepper.ar(). Questo oggetto incrementa un contatore ad ogni trigger, che passa da un valore minimo ad un valore massimo con un passo dato. I suoi argomenti sono:
Stepper.kr(trig, (reset), min, max, step)
- trig: un segnale che genera un trigger.
- min: il valore minimo del contatore.
- max: il valore massimo del contatore.
- step: il passo tra i singoli trigger.
Utilizziamo come esempio Impulse.ar() come trigger per generare una scala da 1 a 10 con passo (step) di 1:
( {var trig = Impulse.ar(10,-0.001), // un segnale min = 1, // valore minimo max = 10, // valore massimo passo = 1; // passo Stepper.ar(trig, 0,min,max,passo)}.plot(2,minval:0,maxval:10); )
Come possiamo notare dalla rappresentazione grafica una volta raggiunto il valore massimo ritorna al valore minimo (wrap around).
Come facilmente intuibile basterà mappare i valori al range proprio del parametro che vogliamo controllare con questa tecnica. Nell'esempio seguente variamo il pitch di un Synth per realizzare degli arpeggi di vario tipo con le frequenze dei parziali.
( SynthDef(\step, {arg fond = 200, rate = 8, step = 1; var trig,seq,sig,env; trig = Impulse.kr(rate); seq = Stepper.kr(trig,0,1,10,step); (seq*0.1).scope; sig = SinOsc.ar(seq * fond,0,0.1); env = Decay2.kr(trig,0.01,rate.reciprocal-0.01); // ...inviluppo d'ampiezza Sync Out.ar(0,sig*env) }).add; {a = Synth(\step)}.defer(0.2); ) a.set(\step,rrand(2,20).postln); a.set(\step,rrand(2,14),\rate,rrand(2,14));
Stepper.ar() può essere usato in combinazione con Select.ar() per leggere gli indici di un Array e generare qualsiasi tipo di sequenza numerica. In questo caso i valori generati da Stepper.ar() sono da considerarsi come indici di lettura degli items:
( SynthDef(\step2, {arg seq = #[1523.0,1311.0,1392.0,1523.0,1196.0,1294.0,1311.0,1262.0 ]; var rate,trig,count,freq,sig,env; rate = MouseX.kr(1,20); // dinamica... trig = Impulse.kr(rate,0.1); // trigger count = Stepper.kr(trig,0,0,seq.size,1); // counter (indici da 0 a size) freq = Select.kr(count,seq); // lettore Array tramite indici sig = SinOsc.ar(freq); env = Decay2.kr(trig,0.01,rate.reciprocal-0.01); Out.ar(0,sig*env) }).add; {a = Synth(\step2)}.defer(0.2); ) a.set(\seq, 8.collect({rrand(92,100)}).postln.midicps)
Playback (grani)
Possiamo utilizzare un trigger per far partire il playback di un sound file oppure di una porzione di esso.
( b = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav"); SynthDef(\grani, {var trig,pos,sig,env; trig = Impulse.kr(MouseY.kr(0.5, 50, 1)); pos = MouseX.kr(0, BufFrames.kr(b)); sig = PlayBuf.ar(1, b, BufRateScale.kr(b), trig, pos, 1); // Playback env = EnvGen.kr(Env.perc(0.01,0.2),trig); // Inviluppo d'ampiezza Out.ar(0,sig*env) }).add; {a = Synth(\grani)}.defer(0.2); )
Dobbiamo utilizzare lo stesso trigger (sincronizzare) per far partire un qualsiasi tipo di inviluppo d'ampiezza della stessa durata del playback per evitare discontinuità nel segnale.
Inviluppi
Possiamo mappare i valori di un trigger sul parametro gate di un inviluppo. A seconda della sua tipologia dobbiamo però utilizzare tecniche differenti.
Inviluppi senza sostegno
Per quanto riguarda questo tipo di inviluppi abbiamo bisogno di un solo campione con valore maggiore di 0 (gate:n) che faccia partire l'inviluppo seguito da un altro campione con valore 0 (gate:0) che permetterà di farlo ripartire successivamente.
( {var ksig,sig,env; ksig = Impulse.kr(1); sig = SinOsc.ar; env = EnvGen.kr(Env.triangle,ksig); [sig,ksig,sig*Decay.kr(ksig),env,sig*env] }.plot(3) )
Nelle modalità di generazione del trigger che prevedono una fase di sostegno come l'utilizzo di devices esterni o delle UGens Trig.ar() e Trig1.ar() il valore 0 servirà solamente a resettare l'inviluppo mentre se utilizziamo generatori d'impulsi ci troviamo a dover afforntare la stessa problematica osservata nel paragrafo su Decay.ar(): Impulse.ar(), Dust.ar() e Changed.ar() accettano valori temporali in bps mentre i diversi tipi di Env li accettano in secondi. E' dunque consigliabile uniformare l'unità di misura temporale effettuando le dovute conversioni:
// Tutto in bps ( SynthDef(\bps, {arg bps=4; var ksig,sig,bpf,env; ksig = Impulse.kr(bps); sig = SinOsc.ar(1234); bpf = Env.new([0,1,0.3,0],[0.01,0.2,3],\cub); env = EnvGen.kr(bpf.duration_(bps.reciprocal), // durata env riscalata in secondi ksig,doneAction:0); Out.ar(0,sig*env) } ).add; {a = Synth(\bps)}.defer(0.1); ) a.set(\bps,rrand(0.5,10).postln); // Tutto in secondi ( SynthDef(\sec, {arg dur=1; var ksig,sig,bpf,env; ksig = Dust.kr(dur.reciprocal); // durata env riscalata in bps sig = SinOsc.ar(1234); bpf = Env.new([0,1,0.3,0],[0.01,0.2,3],\cub); env = EnvGen.kr(bpf.duration_(dur),ksig,doneAction:0); Out.ar(0,sig*env) } ).add; {a = Synth(\sec)}.defer(0.1); ) a.set(\dur,rrand(0.05,1).postln);
Inviluppi con sostegno
Per quanto riguarda questo tipo di inviluppi alcune modalità di generazione del trigger come l'utilizzo
di devices esterni che prevedono già una fase di sostegno non danno alcun problema e possiamo mappare direttamente
i valori sul parametro gate.
Per tutte le altre dovremo obbligatoriamente utilizzare le UGens Trig.ar() e
Trig1.ar() oppure le diverse tecniche relative al superamento di una o più
soglie. Osserviamo come sia possibile utilizzare queste due UGens sia con inviluppi con fase di sostegno che senza.
( SynthDef(\nosust, {arg dur=0.125; var sigk = SinOsc.kr(5), // Sine 5 hz trig = Trig1.kr(sigk,dur), // trig ogni transizione env = Env.perc(0.1,1,1,-4).duration_(dur), // inviluppo percussivo envi = EnvGen.kr(env,trig).scope; // L'intensità Out.ar(0,SinOsc.ar(600)*envi) }).add; {a = Synth(\nosust)}.defer(0.2); ) a.set(\dur, 1); a.set(\dur, 0.5); a.set(\dur, 0.25); a.set(\dur, 0.125); a.set(\dur, 0.075); //=============================== ( SynthDef(\sust, {arg dur=0.125; var sigk = SinOsc.kr(5), // Sine 5 hz trig = Trig1.kr(sigk,dur), // trig ogni transizione env = Env.asr(0.01,1, 0.2, -4), // inviluppo con release Node envi = EnvGen.kr(env,trig).scope; // L'intensità Out.ar(0,SinOsc.ar(600)*envi) }).add; {a = Synth(\sust)}.defer(0.2); ) a.set(\dur, 1); a.set(\dur, 0.5); a.set(\dur, 0.25); a.set(\dur, 0.01);
Trigger all'Interprete
Possiamo inviare i valori di un trigger dal Server all'Interprete per poi utilizzarli come vogliamo, tipicamente per la visualizzazione su di una GUI oppure per controllare partenza e arresto di processi di sequencing o un counter.
Per farlo è però necessario filtrare la ridondanza di dati in quanto i valori 1 e 0 del trigger sono inviati a una rata di (sotto)campionamento.
( var ora, prec = 0; // inizializza la variabile SynthDef(\trigInt, {var x,trig; x = MouseX.kr(-1,1); // Tra -1 e 1 trig = Trig1.ar(x,1); // Durata 1 secondo SendReply.kr(Impulse.kr(50), // rata di campionamento '/trig', // indirizzo o nome trig); // segnale da campionare } ).add; {a = Synth(\trigInt)}.defer(0.1); // ------------------------------ OSC OSCFunc.new({arg msg; ora = msg[3]; // ------------------------------ Filtro case {(ora == 1 ).and(ora != prec)}{"noteOn".postln; prec = ora} // se == 1 e diverso dal precedente {(ora == 0 ).and(ora != prec)}{"noteOff".postln; prec = ora}; // se == 0 e diverso dal precedente // ------------------------------ }, '/trig', // indirizzo o nome s.addr); // eventuale NetAddr )
ReSync UGens
Alcuni oscillatori come SyncSaw.ar() possono essere re-triggerati (resync), ovvero la fase viene resettata forzatamente. Questo produce un effetto simile alla pulse width modulation. Gli argomenti di SyncSaw sono: SyncSaw.ar(sincFreq, sawFreq ) dove il primo indica la frequenza della fondamentale, il secondo la frequenza dell'onda a dente di sega che effetua il resync. La seconda deve essere sempre maggiore della prima.
s.scope(1); ( {[SyncSaw.ar(800, 1200), Impulse.ar(400)]}.plot // solo per visualizzare la rata di sync ) {SyncSaw.ar(100, Line.kr(100, 800, 12), 0.1) }.play; // Armonici... {SyncSaw.ar(MouseX.kr(100,400), MouseY.kr(400,800), 0.1)}.play;
In generale gli oscillatori che usano un resync sono di due tipi:
- hard sync: reset immediato dell'oscillatore slave (dipendente, può generare aliasing)
- soft sync: reset al periodo successivo
Attraverso la tecnica del resync possiamo utilizzare un inviluppo compreso tra +/- 1.0 come forma d'onda (wavetable) e un segnale di trigger per controllarne la frequenza. Nell'esempio seguente se muoviamo il mouse:
- sull'asse delle Y variamo la lunghezza di un ciclo di saw (timbro)
- sull'asse delle X variamo la frequenza del resync (pitch)
( {var samptosec = [0,128,256,128]/SampleRate.ir, // da sample a secondi twind = MouseY.kr(0,1)*samptosec, // interazione col mouse func = Env([0,0,1,-1,0], twind), // Inviluppo = 1 ciclo di onda a dente di sega trig = Impulse.ar(MouseX.kr(10,300,'exponential')!2); // Sync trigger EnvGen.ar(func,trig) }.play )