Você está na página 1de 328

EZRanger MelFilter Link XFader MethodQuote CtkProtoNotes NumChannels SOS EZSlider MultiTap AutoDocTestClas JSpeech Gradient LRUNumberAllocator SoundFileFormats

Pin Grap Model JavaObj UI BeatSched JStethoscope GeneralHIDSlot PatchIn Tempo Gui SCIBTo Sample JPen GeneralHIDDevi TaskPro ProxyMixer GuidoTimeSig SCIBViewP GeoGraphSynth JSCWindow Exception Unix NodeMapSetting GraphView Insets E ModalDialog En LocalQuarks SwingGUI Document Abstr PageLayout No UGenInstr Bu ImageView Impulsar FreqShift PitchCollection CtkNoteObject MIDIClient Mono PatchOut Client Pen Module Crucial Collapse QuarkDepe OSCResponderQueue FlowLayout Runner WiiMoteGUI Quarks InstrAt TaskProxyE ControlName JMouseBase Au XSession ResponderArrayWiiRemoteGU GeoAudio MultiChanRecorder D Updater WiiCalibrationInfo Manuscrip Color SwingDialog Warp Chronometer XFaderN ParserGUI Point ModalFreqUGe CocoaDialog MixRecordProxyMixer Do HIDDeviceService TDuty_old MXHIDSlot QuarkSVNRepository RingNumberAllocato NotificationRegistration Spee PowerO Nil Help Quark PatternCondu Position Player88 Stethoscope OSCresponderNode Process PhilologusGuiTable SCWindowGuidoVo FormantChaO SoundIn UGenHelper SCNSObjectAb Notificat Env B UniqueID SlotInspector Ar SCIBPanelWindow WiiNunchukGUI HIDDevice Quar MagnitudePriorityQueue MIDIEvent Inspect Class GeoClock DOMNode RawPointerLayout NB JSoundFi Praa AbstractConstrain ClassBrowser Instr XFader4NetAd Graph DOMImplementation Condition TChoos Philologus SkipJack Rect RelativeToPa AbstractConsole StackNumberAllocator Spec ControlPrototypes KeyCodeRes MIDIIn TestDep VolumeGui GetStringDialog GraphParser S PlatformScheduler AutoCompMetho Quant HarmonicsDocParser SCVie GUI BroadcastServer Date GuidoTime KeyCodeRe MIDIEndPoint NodeProxyEdit SOGui SoundFileViewProgressWindow HIDDeviceElement CocoaGUI SystemSynthDefs Spla File GraphBuilder CmdPeriod Glyph ClassHelper AbstractPlayControl SelectXFocus SimpleContro IndexL Pitc JKeyState StartUp XInFeedback Insp SpeechChannel Monitor Sheet Messa GeneralHID Actant WiiMote SelectButtonSet ServerOptions Histor PhilologusGuiScore SCIBAreaSelection JSpeechChan Def Splay SerialPort SynthDef PitchClass CtkTimer FreqScope Staff MIDIRespo Painter BusSynthDefs Font MXHIDAbsInfo Praat SimpleKD UpdateListener GlyphWriter EventStreamClea andrea valle Page TempoBus MIDIOut KeyMapper SOScore J GuidoArtInBus InterpreterSCIBDrag MIDIClockOut Keyboarder Finalize RunnerMixer Semaphore ContiguousBlockAlloca AGProcessor CmdStringTopicHe Boolean Vol SoundFileEZNumber GuidoMark HIDInfo JPeakMeterMa SelectX EnvGate NodeIDAllocator Sc InterplEnv HiliteGradient Editor HistoryGuiSymbol IODesc MultiPageLayout SynthDesc PMOsc MLIDbrowser Post Grapher Frame Collection EnvironmentRed MyClass JFreq Co Bus GeoGrapher LilyPond FlowVar UnicodeResponder GuidoScor Midi2FreqUGen Helper Scribe ToolboxClientFunc Node InspManagerSwi EventTyp MXHID ProxyMoni AutoClassHelp OSCService ContiguousBlock GetFileDialog CtkObj FunctionDef JFont SynthDescLib XIn AudioIn PowerOfTwoBlock PathName RunnerGUI NodeCon WiiMoteIRObject JKnob ClockSOScoreNodeBoxGraphGenerator PrettyState OSC

Obj

Abstrac

Questopera stata rilasciata sotto la licenza Creative Commons AttribuzioneNon commerciale-Condividi allo stesso modo 2.5 Italia. Per leggere una copia della licenza visita il sito web http://creativecommons.org/licenses/by-ncsa/2.5/it/ o spedisci una lettera a Creative Commons, 171 Second Street, Suite 300, San Francisco, California, 94105, USA.

Questo lavoro utilizza molti software open source SuperCollider: ci di cui si parla (http://supercollider.sourceforge.net/) Praat: analisi e visualizzazione dei segnali audio (http://www.praat.org/) Python: generazione del codice TEX colorato a partire da quello in SuperCollider (http://www.python.org/) TEX: tipograa automatica (http://www.ctan.org/) ConTEXt: preparazione globale del documento basata su TEX (http://www.pragma-ade.nl/) PyX: visualizzazione dei segnali audio (cap. 5) (http://pyx.sourceforge.net/) GraphViz: generazione dei gra che rappresentano le relazioni di ereditariet tra classi in SuperCollider, diagrammi di usso per le unit audio (http://www.graphviz.org/) TeXShop: tutto lediting basato su TEX (http://www.uoregon.edu/~koch/texshop/) NodeBox: immagine di copertina (http://nodebox.net/)

Sito: http://www.cirma.unito.it/andrea/sc.html Sussuri e grida: andrea.valle@unito.it

Andrea Valle

tSCIRMA

the SuperCollider Italian Manual at CIRMA

( DRAFT: 9 aprile 2008)

Sommario
1 Introduzione 1.1 Ci di cui si parla: SuperCollider 1.2 Obiettivi 1.3 Fonti 1.4 Convenzioni grache 2 SuperCollider grado 0 2.1 Che cos SuperCollider 2.2 Alcuni buoni motivi (a scelta) per usare SC 2.3 Disponibilit rispetto ai sistemi operativi 2.4 Dove reperire SuperCollider 2.5 Interfacce grache 2.6 Informazioni di base sullambiente di sviluppo 2.7 Salvare ed aprire documenti, help le compresi 3 Object Oriented Programming in SC: fondamenti 3.1 Minima objectalia 3.2 Oggetti in SC 3.3 Metodi e messaggi 3.4 I metodi di tipo post e dump 3.5 Numeri 4 Sintassi: elementi fondamentali 4.1 (Parentesi) 4.2 // Commenti (/*di diverso tipo*/) 4.3 Stringhe 4.4 Variabili 4.5 Simboli 4.6 Espressioni ; 4.7 Errori 4.8 {Funzioni} 8 8 9 9 10 12 12 15 16 17 18 20 23 25 25 28 34 41 44 48 48 49 49 50 54 55 57 58

4.9 4.10 4.10.1 4.10.2 4.10.3 4.10.4 4.10.5 4.10.6 4.11 4.12 4.12.1 4.12.2 4.12.3

Classi, messaggi/metodi e keyword Esempio Il pattern GUI Il codice Introduzione Creazione degli elementi GUI Interazione Per nire Controlli di usso Ancora un esempio GUI La funzione di generazione dei valori colore Modularizzare la GUI Controllo delle azioni

65 67 67 70 71 72 73 75 75 80 81 82 86 95 97 101 111 112 124 127 132 143 144 144 148 148 151 151 152

5 Sintesi, I: fondamenti 5.1 Poche centinaia di parole dacustica 5.2 Algoritmi di sintesi 5.3 Nota sul metodo play 5.4 Altri segnali e altri algoritmi 5.5 Funzione valore assoluto e funzione quadratica 5.6 Ancora sullelaborazione di segnali 5.7 Segnali di controllo 5.8 Conclusioni 6 Larchitettura e il server 6.1 Larchitettura 6.2 Esempi 6.2.1 SwingOSC 6.2.2 Graphista! 6.3 Il client sclang 6.4 Un impianto chimico per la produzione di liquidi e un server audio in tempo reale

6.5 6.5.1 6.5.2 6.5.3 6.5.4

Appetizer: un esempio di sintesi e controllo in tempo reale Una synthDef UGen e UGen-Graph Un synth GUI e controller

163 165 167 172 174 177 177 183 192 209 215 226 226 236 236 239 248 259 264 271 281 282 284 290 293 300 300 304 308

7 Controlli e canali 7.1 Inviluppi 7.2 Generalizzazione degli inviluppi 7.3 Sinusoidi & sinusoidi 7.4 Segnali pseudo-casuali 7.5 Espansione e riduzione multicanale 8 Sintesi, II: tecniche di generazione del segnale audio 8.1 Oscillatori e tabelle 8.2 Campionamento 8.2.1 Campionamento semplice 8.2.2 Resampling e interpolazione 8.3 Sintesi additiva 8.4 Sintesi granulare 8.5 Sintesi sottrattiva 8.6 Analisi e risintesi 8.7 Modulazione 8.7.1 Modulazione ad anello e dampiezza 8.7.2 Modulazione ad anello come tecnica di elaborazione 8.7.3 Modulazione di frequenza 8.7.4 C:M ratio 9 Suono organizzato: (minimal) scheduling in SC 9.1 Server side: attraverso le UGen 9.2 Server side: le UGen Demand 9.3 Lang side: Routines

9.4 9.5 9.6 9.7

Orologi Sintetizzatori/eventi Routine/Task Micro/macro

310 314 319 322

1 Introduzione

1.1 Ci di cui si parla: SuperCollider

SuperCollider (SC) un pacchetto software per la sintesi e il controllo dellaudio in tempo reale 1. Attualmente rappresenta lo stato dellarte nellambito della programmazione audio: non c altro software disponibile che sia insieme cos potente, efciente, essibile. Lunico punto a sfavore di SC che non intuitivo: richiede competenze relative alla programmazione, alla sintesi del segnale audio, alla composizione musicale (nel senso pi lato, aperto e sperimentale del termine). Ci si potrebbe chiedere daltra parte in che senso un violino, una chitarra elettrica, una batteria presentino uninterfaccia utente intuitiva: non c forse bisogno di un duro lavoro per cavare fuori una nota da un violino o per tenere un tempo suonando una batteria? Il disagio che il neota prova con SC dipende per forse da un altro fatto: SC preciso ed efciente ma puntiglioso ed ha un forte
1

Si vedr pi avanti che cosa indica specicamente pacchetto software

1.18

senso della responsabilit. Ci che spetta al programmatore, spetta a lui solo: SC non lo aiuter in caso di sue mancanze. Per chiudere il parallelo, SC come uno Stradivari per il violinista: dipende da questultimo farlo suonare come quello sa fare. Non come un carillon in cui basta caricare la molla, poich tutta la competenza relativa alla generazione e al controllo del suono in fondo gi ascritta al meccanismo. Anche se a dire il vero, come si vedr, SC pu tramutare un carillon in uno Stradivari.

1.2 Obiettivi

Lobiettivo di quanto segue duplice: 1. introdurre alcuni aspetti fondamentali relativi a quellambito che viene usualmente indicato, con una denizione a volte imprecisa e a volte impropria, computer music 2. fornire una breve panoramica ed insieme unintroduzione a SuperCollider in italiano. Lipotesi di partenza (e indipendentemente dai risultati) quella di far interagire i due ambiti, attraverso una reciproca specicazione

1.3 Fonti

1.39

Il materiale presentato , in qualche misura, originale. La parte relativa alla sintesi riprende alcune parti di Audio e multimedia 2 e vi include parti di materiali provenienti dai corsi degli anni precedenti. Trattandosi di materiale introduttivo, chiaro che il testo si afda saldamente al suo intertesto costituito dai molti testi analoghi, pi complessi e pi completi, che lo precedono. Questo vale a maggior ragione per la parte dedicata a SuperCollider. Non una traduzione in senso stretto di scritti gi esistenti: tra laltro, attualmente, non esiste un libro di riferimento per SuperCollider 3. E tuttavia chiaro che il testo scritto ttamente intessuto di prestiti provenienti dagli help le, dai tutorial di James McCartney, Mark Polishook, Scott Wilson, dal testo di David Cottle, dalle osservazioni preziosi fornite dalla SuperCollider mailing list, dalle informazioni accessibili attraverso il SuperCollider wiki. La lettura di queste fonti non in nessun modo resa superua dal testo seguente, il quale ha invece semplicemente un ruolo propedeutico rispetto alle stesse, perch quantomeno evita al lettore italiano la difcolt supplementare della lingua straniera.

1.4 Convenzioni grache

Il manuale prevede tre distinzioni:

2 3

Lombardo, V. e Valle, A., Milano, Apogeo 2008 (3 ed.). in corso di pubblicazione (luscita prevista per la ne del 2008) il progetto di un SuperCollider Book, sullesempio del Csound Book, per gli stessi tipi della MIT press.

1.410

1. testo: in carattere nero normale, senza particolarit, esattamente come quanto scritto qui 2. codice: scritto in carattere typewriter, utilizza lo schema colori della versione MacOSX per la sintassi, riquadrato in blu e le righe sono numerate. Al di sotto di ogni esempio presente un marcatore interattivo. Esso permette di accedere al le sorgente dellesempio che incluso nel pdf, e di aprirlo direttamente con lapplicazione SuperCollider. Per garantire la funzione bene usare Acrobat Reader (che gratuito e multipiattaforma). Alla prima apertura Reader richiede di impostare il suo comportamento nei confronti dellallegato
1 2 // ad esempio "a caso".postln ;

3. post-window: scritto in nero; con carattere typewriter, e riporta una parte di sessione con linterprete SuperCollider. riquadrato in arancio e le righe sono numerate.
1 "cos".postln

1.411

2 SuperCollider grado 0

2.1 Che cos SuperCollider

SuperCollider (SC) un pacchetto software per la sintesi e il controllo dellaudio in tempo reale. La denizione di pacchetto software tuttavia si rivela piuttosto vaga. Per arrivare ad una denizione pi analiticamente ed esauriente, meglio partire dalla denizione di SC che appare sulla homepage di Sourceforge: SuperCollider is an environment and programming language for real time audio synthesis and algorithmic composition. It provides an interpreted object-oriented language which functions as a network client to a state of the art, realtime sound synthesis server (http://supercollider.sourceforge.net/) Pi analiticamente:

2.112

1. an environment: SC unapplicazione che prevede pi componenti separate. Di qui lutilizzo del termine ambiente. 2. and: SC anche unaltra cosa del tutto diversa. 3. a programming language: SC infatti anche un linguaggio di programmazione. Come si dice in seguito, appartiene alla famiglia dei linguaggi orientato agli oggetti, ed , tra laltro, tipologicamente vicino a Smalltalk. Il codice del linguaggio SC, per essere operativo (per fare qualcosa), deve essere interpretato da un interprete. Un interprete un programma che capisce il linguaggio e agisce di conseguenza. SC anche linterprete del linguaggio SC. 4. for realtime sound synthesis: SC ottimizzato per la sintesi del segnale audio in tempo reale. Questo lo rende ideale per unutilizzo strumentale (performance live) cos come per la realizzazioni di installazioni/eventi. senzaltro possibile utilizzare SC non in tempo reale per generare materiale audio, ma in un certo senso meno immediato che non utilizzarlo in tempo reale. 5. and algorithmic composition: uno dei punti di forza di SC sta nel fatto che permette due approcci complementari e opposti alla sintesi audio. Da un lato, permette di svolgere operazioni di basso livello sul segnale audio. Dallaltro, permette al compositore di esprimersi ad alto livello, cio non in termini di campioni audio, ma di strutture che rappresentino oggetti per la composizione musicale (ad esempio: scale, pattern ritmici, etc.). In questo senso, si rivela ideale per la composizione algoritmica, ovvero per un approccio alla composizione musicale basato sullutilizzo di procedure formalizzate. In SC questo tipo di operazioni pu essere svolto interattivamente ed in tempo reale.

2.113

6. [the] language [. . .] functions as a network client to a [. . .] server: lapplicazione che interpreta il linguaggio SC anche un cliente che comunica, attraverso una rete, con un server, un fornitore di servizi. 7. a state of the art: attualmente SC rappresenta lo stato dellarte nellambito della programmazione audio: non c altro software disponibile che sia insieme cos potente, efciente, essibile (e ormai anche portabile). 8. sound synthesis server: SC un fornitore di servizi, in particolare di servizi audio. La locuzione pu sembrare misteriosa. Si traduce cos: SuperCollider genera audio in tempo reale su richiesta. In questo senso, SC fornisce audio su richiesta: chi richiede audio a SC un suo cliente (client). Riassumendo: quando si parla di SC si possono indicare (generando una certa confusione) cinque cose diverse. Queste cose sono: 1. 2. 3. 4. 5. un server ( un fornitore di servizi) audio un linguaggio di programmazione per laudio linterprete ( il programma interprete) per il linguaggio linterprete in quanto cliente del server audio il programma ( lapplicazione complessiva) che comprende tutte le componenti 1-4

La situazione schematizzata in 2.1. Lapplicazione SC prevede due parti: una il server audio (denominato scsynth), laltra linterprete per il linguaggio (denominato sclang) che, oltre a interpretare il linguaggio SuperCollider, svolge il ruolo di client rispetto a scsynth. Pu sembrare complicato. In effetti lo . Installare SC vuol dire perci installare unapplicazione complessiva che comprende un server audio e un interprete del linguaggio/client del primo. Si vedr in seguito meglio che cosa indicano i termini: per

2.114

SuperCollider application
Client 3 Interprete 1 Linguaggio 2 4 Server

sclang

scsynth

Fig. 2.1

Struttura di SC.

ora si tenga a mente che esistono due programmi distinti, e che quando si installa SC si ottengono due programmi al costo di 1 (il costo si calcola cos: 2 0 = 0. Come recita un madrigale di Cipriano de Rore, mia benigna fortuna).

2.2 Alcuni buoni motivi (a scelta) per usare SC

Alcune applicazioni di SC, in ordine sparso:

costruire un proprio sistema per fare musica live, interfaccia graca compresa http://www.dewdrop-world.net/sc3/index.php fare musica dance (nel senso pi vago del termine) http://crucial-systems.com/SuperCollider

2.215

allestire un dj set http://www.durftal.com/music/cylob/bf/ fare composizione elettroacustica (nel senso pi vago del termine) http://www.bridgerecords.com/pages/catalog/9210.htm sonicare dati http://www.sonification.de/ controllare un complesso sistema di altoparlanti (> 170) dal vivo http://www.woutersnoei.nl/ ricostruire in audio binaurale il Pome lectronique (ovvero: la diffusione di 3 tracce in movimento su 350 altoparlanti) http://www.edu.vrmmp.it/vep/ integrare audio e video dal vivo http://www.klippav.org/ praticare live coding http://www.toplap.org/

Una ricerca su YouTube permette agevolmente di vedere allopera SuperCollider in situazioni molto diverse. Alcuni video sono qui raccolti qui: http://supercollider.sourceforge.net/videos

2.3 Disponibilit rispetto ai sistemi operativi

2.316

SuperCollider stato originariamente sviluppato da James McCartney su piattaforma Macintosh. In particolare la versione 2 era fortemente integrata con il sistema operativo Mac OS9. SuperCollider 3 (che insieme molto simile e molto diverso da SC 2) stato sviluppato per il sistema operativo Mac OSX ed ora un software open source, sviluppato da una consistente comunit di programmatori a partire dal lavoro di James McCartney. La comunit di sviluppatori ha cos effettuato il porting anche per le piattaforme Windows e Linux 4. Queste ultime due versioni di SC differiscono principalmente per difetto rispetto alla versione per OSX, nel senso che alcune funzionalit presenti in questultima non sono state portate nelle altre due. Tuttavia, nellarco della seconda met del 2007 stato compiuto un grande sforzo dalla comunit si sviluppatori, in vista delluscita (prossima) del SuperCollider Book. La versione attuale (3.2), che quella di riferimento per il libro, ha apportato notevoli migliorie al software, oltre ad un notevole incremento della documentazione ed un aumentata compatibilit tra le piattaforme. Si pu dire che le differenze tra le piattaforme sia ormai limitate agli ambienti di sviluppo (necessariamente dipendenti dal sistema operativo) 5.

2.4 Dove reperire SuperCollider

SuperCollider pu essere scaricato da Sourceforge, in particolare allindirizzo http://supercollider.sourceforge.net/downloads


4 5

In particolare, Stefan Kersten il principale autore della versione Linux, mentre la versione Windows stata implementata da Christopher Frauenberger. I riferimenti, scarsi, a piattaforme speciche in questo testo si riferiscono alle versioni MacOSX e Windows, non a Linux.

2.417

Nel caso si utilizzino i le binari, linstallazione in s un processo banale, poich prevede un le .dmg su Mac 6 e un le .exe per Windows 7. In generale, il sito ufciale presso Sourceforge http://sourceforge.net/projects/supercollider costituisce il punto di riferimento della comunit di utilizzatori e sviluppatori, ed utile farvi riferimento. In particolare, vale la pena menzionare la mailing list degli utilizzatori, attraverso la quale possibile confrontarsi rapidamente con i migliori programmatori di SC. http://www.create.ucsb.edu/mailman/listinfo/sc-users Gli archivi della mailing list sono poi una risorsa preziosa, in cui molto spesso gi disponbile una risposta ai propri quesiti. Linterfaccia web via Nabble forse di pi agevole consultazione: http://www.nabble.com/SupercolliderUser-f535.html Inne, il sito di James McCartney ha un valore eminentemente storico: http://www.audiosynth.com/

2.5 Interfacce grache

Esistono versioni compilate anche per versioni ormai superate del sistema operativo (10.2) cos come per processori pi datati (G3). Per ottenerle si pu consultare il sito e la mailing list. La versione per Windows prende il nome di PsyCollider. Il nome deriva da Py + SCollider (con metatesi), poich limplementazione utilizza il linguaggio Python. A differenza di quanto avveniva inizialmente, lutente nale non tenuto n conoscere n ad avere Python installato sulla propria macchina per potre eseguire Psycollider.exe. Python incluso nei le binari ed utilizzato internamente da Psycollider.

2.518

La principale differenza tra le versioni per i diversi sistemi operativi concerne le classi per lo sviluppo di interfacce grache (in SC possibile programmare la GUI cos come si programma la generazione dellaudio), rispetto alle quali ogni piattaforma ha le sue specicit. importante sottolineare che SC permette di programmare gli elementi della GUI facendo astrazione dal pacchetto graco prescelto. In sostanza in questo modo possibile costruire pulsanti, cursori, nestre riferendosi genericamente ad essi e selezionando un gestore graco che si occuper di costruirle. Si pensi alle skin in cui una stessa funzione gestita secondo modalit grache diverse. In SC si seleziona un gestore graco tra quelli disponibili e questi costruir loggetto prescelto. A dopo il dettaglio in termini di programmazione. I gestori graci non sono molti. Linterfaccia graca nativa su MacOSX Cocoa, mentre su Linux il primo gestore graco stato SCUM. Recentemente si reso disponibile il server graco SwingOSC http://www.sciss.de/swingOSC/ . SwingOSC un server graco del tutto autonomo rispetto a SC (pu essere utilizzato anche da altre applicazioni), ma, ed quanto qui interessa, pu essere utilizzato da dentro SC (in altre parole lutente pu praticamente dimenticarsi dellesistenza del meccanismo client/server). SwingOSC scritto in Java e per funzionare richiede che sia installata il Java Runtime Environment. Per controllare lo stato della propria macchina e installare la versione pi recente di Java si pu visitare il sito dedicato: http://java.com/en/download/installed.jsp SwingOSC permette la costruzione di interfacce grache di qualit superiore rispetto a quelle originariamente sviluppate per MacOSX (almeno, a parere di chi scrive, anche se probabilmente meno efcienti computazionalmente). In pi, SwingOSC, essendo basata su Java, completamente multipiattaforma, essendo disponibile per Mac, Windows, Linux. La versione per Windows (detta PsyCollider) integra

2.519

in forma predenita SwingOSC, che lunica interfaccia graca disponibile. Se si guarda la Post Window durante lavviamente di PsyCollider si potr notare come il server SwingOSC venga avviato automaticamente (booting java -jar SwingOSC. . . etc). Gli esempi del testo verranno scritti in forma astratta rispetto al gestore graco, ma chi scrive utilizza sempre (pur essendo su Mac) SwingOSC.

2.6 Informazioni di base sullambiente di sviluppo

SC funziona in tempo reale ed in forma interattiva attraverso uninterfaccia testuale: tutte le comunicazioni tra utente e programma avvengono attraverso testo. Si osservino le gure 2.2 e 2.3. Eseguendo il programma si apre appunto un editor di testo. In particolare si apre la Post Window, la nestra che visualizza i messaggi che SC indirizza allutente (2.2 e 2.3, 1). In Windows, la nestra SC Log stampa gli stessi messaggi di un terminal aperto in parallelo (2.3, 4 allinterno per dellapplicazione PsyCollider, 2.3, 5). Entrando in esecuzione, SC effettua alcuni passaggi di inizializzazione (di cui si discuter pi avanti), il cui risultato viene scritto sulla Post Window. Nel caso di PsyCollider, viene immediatamente avviato il server audio (si vede bene sul terminal, 2.3, booting 57110, 6): si notino i messaggi relativi al rilevamento della scheda audio (Device options:, Booting with:). Lavviamento del server audio avviene invece per mano dellutente

2.620

in OSX, attraverso due interfacce grache gemelle: in particolare, sono disponibili due server distinti, local e internal 8 (2.2, 4 e 5). In Windows viene altres avviato il server graco SwingOSC (2.3, 7, booting java -jar SwingOSC. . . etc). possibile utilizzare la Post Window per scrivere il proprio codice (almeno in fase di test) ma sempre meglio creare una nuova nestra (2.2 e 2.3, 2). A questo punto, lutente immette del codice e chiede allinterprete di valutarlo. Ad esempio, il codice "Hello World".postln richiede di stampare sullo schermo la stringa "Hello World". Si noti la sintassi colorata che distingue tra la stringa e il metodo .postln; 9. Se linterpretazione va a buon ne, SC risponder con un certo comportamento che dipende dal codice immesso: in questo caso, stampando sulla Post Window la stringa "Hello World" (2.2 e 2.3, 3). Altrimenti segnaler lerrore attraverso la Post Window. Leditor di testo (sia in OSX che in Windows) prevede un insieme di caratteristiche tipiche di ogni editor, ed inutile soffermarvicisi. Altre funzioni sono utili ad un utente pi avanzato, ed esulano da quanto qui previsto. Alcune indicazioni minimali:

MacOSX

Valutazione del codice: selezionare il codice e premere Enter (non Return). Se il codice consiste in una sola riga, sufciente posizionare il cursore in un punto della riga e premere enter. Arresto dellaudio: Apple + .

8 9

Si veda dopo. Gli schemi colori per le due piattaforme sono differenti (chiss perch). Qui si segue quella originale di OSX

2.621

3 4

Fig. 2.2
4

SC in Mac OSX.
5

Fig. 2.3

SC in Windows (PsyCollider).

Aiuto: Apple + ?. Se la combinazione premuta contestualmente alla selezione di codice SC, si apre il le di aiuto correlato (laddove esistente). Pulizia della Post Window: Apple + Shift + k
Windows

2.622

Valutazione del codice: selezionare il codice e premere Ctrl + Return (invio). Se il codice consiste in una sola riga, sufciente posizionare il cursore in un punto della riga e premere Ctrl + Return (invio). Arresto dellaudio: Alt + . Aiuto: F1. Se la combinazione premuta contestualmente alla selezione di codice SC, si apre il le di aiuto correlato (laddove evistente). Pulizia della Post Window: Alt + P

2.7 Salvare ed aprire documenti, help le compresi

Il contenuto della nestra pu essere salvato su le e riaperto in unaltra sessione di lavoro, per essere modicato o semplicemente per essere rieseguito. Una caratteristica importante di SC che gli help les contengono codice valido che pu essere valutato interattivamente. Quando si apre un le di help allora possibile sperimentare con gli esempi contenuti per capire esattamente il funzionamento descrittovi. Rispetto al formato dei les predenito MacOSX e Windows prevedono alcune differenze:

MacOSX: il formato le prescelto in origine una versione speciale del formato di testo RTF. In MacOSX gli help le sono in formato HTML, ma vengono convertiti automaticamente in RTF, e sono documenti validi per SC: in altre parole, possibile valutare il codice che vi contenuto. Lutilizzo del formato RTF consente

2.723

di avere formattazione avanzata nei documenti di SC (font, color, dimensione, etc.). Il formato per nativo soltanto per Mac, e in caso di utilizzo su unaltra piattaforma, la formattazione deve essere eliminata (il le deve essere convertito in testo semplice). In ogni caso perfettamente possibile e consigliabile (salvo specici interessi tipograci) utilizzare per i propri le il formato ASCII (cio: testo semplice), in particolare scegliendo lestensione scd (SuperCollider Document), per evitare ambiguit.

Windows: il formato le soltanto ASCII (si pu aprirlo con Notepad/Blocconote), e spetta allutente scegliere lestensione (evidentemente, .scd una scelta sensata). Quando si vuole eseguire il codice contenuto nei le di help in HTML necessario convertire il formato HTML in codice selezionando HTML To Code Window dal menu File (Ctrl+T). Si apre una nestra in cui il formato HTML viene convertito in ASCII (la nuova nestra una nestra di codice, la sintassi colorata): a questo punto, possibile valutarlo.

2.724

3 Object Oriented Programming in SC: fondamenti

3.1 Minima objectalia

Nella programmazione orientata agli oggetti si assume che lutente, per programmare il comportamento di un calcolatore, manipoli entit dotate di propriet e di capacit. Il termine, volutamente generico, per indicare queste entit oggetti, mentre tipicamente le propriet sono pensate come attributi degli oggetti stessi e le capacit come metodi che gli oggetti possono adottare per compiere delle operazioni. Per poter essere riconosciute dal linguaggio le entit devono appartenere ad un insieme nito di tipi: un oggetto del tipo A, laltro del tipo B e cos via. I tipi vengono chiamati classi in OOP. Un oggetto dunque una particolare istanza di una classe: la classe pu essere pensata come il tipo astratto, ma anche come lo stampo da cui si fabbricano le istanza. Da un unico conio (la classe) si stampa un un numero indenito di monete uguali (gli oggetti). nella classe

3.125

che si deniscono i metodi di cui tutti gli oggetti di quel tipo saranno dotati. Una classe descrive anche il modo in cui creare un oggetto a partire dalla classe. Le classi sono organizzate gerarchicamente: ogni classe pu derivare da unaltra classe e ogni classe pu avere delle classi derivate. Questo principio prende il nome di ereditariet. Ad esempio un conio una ulteriore specicazione di un pi generico stampo: lo stampo la sopraclasse del conio, e il conio una sottoclasse dello stampo. Un sigillo (per la ceralacca) un altro stampo, ma di un tipo completamente diverso dal conio: il sigillo una sottoclasse dello stampo, da cui eredita alcuni aspetti che condivide con il conio (la capacit di impressione), ma da cui si differenza per altri (prevede unimpugnatura manuale, mentre il conio viene battuto a martello). Lereditariet va pensata in termini genetici: i caratteri del padre sono presenti (come patrimonio genetico, appunto) nei gli, ma, a scanso di equivoci, si noti che qui ereditariet intesa in termini sistematici, non evolutivi.La relazione di ereditariet prende infatti a modello le tassonomie naturalistiche. Ad esempio, il grafo seguente illustra la posizione tassomica dellippopotamo. Lippopotamo appartiene al subordine dei Suina (ad esempio, intuibilmente, i maiali), con i quali condivide alcuni tratti, che differenziano entrambi dai Ruminantia (ad esempio, le mucche), pur essendo Ruminantia e Suina entrambi Actiodactyla (e distinguendosi entrambi dai Perissodactyla, ad esempio i cavalli). Se le classi sono astratte (ad esempio, la species dellippopotamo), gli oggetti (gli ippopotami da classicare) sono concreti. Tornando alla programmazione, nel paradigma object-oriented il mondo si presenta al programmatore come un insieme di oggetti che si prestano, sotto determinate condizioni, ad essere manipolati. In particolare, per manipolare un oggetto per chiedergli di fare qualcosa

3.126

Fig. 3.1

Tassonomia dellippopotamo.

necessario inviargli un messaggio. Un oggetto, per poter rispondere al messaggio, deve conoscere un metodo. In sostanza, pu rispondere ad una richiesta (messaggio) attraverso una competenza (metodo). Loggetto che riceve il messaggio il ricevente di quel messaggio e vi pu rispondere se implementa un metodo corrispondente. Riassumendo:

oggetto e metodo concernono la denizione delloggetto dallinterno messaggio e ricevente concernono la comunicazione con loggetto dallesterno

Linsieme dei messaggi a cui un oggetto pu rispondere prende il nome di interfaccia: ed uninterfaccia in senso proprio, perch ci che loggetto rende disponibile allutente per linterazione, dove lutente pu essere anche un altro oggetto. Nella maggior parte dei

3.127

linguaggi ad oggetti, la sintassi tipica per passare un messaggio ad un oggetto utilizza il punto (.) e prende la forma oggetto.messaggio. La relazione tra oggetto e messaggio non va pensata come una descrizione alla terza persona (loggetto fa una certa cosa), ma piuttosto come una coppia vocativo/imperativo: oggetto, fai qualcosa!. Ad esempio, conio.imprimi, o anche: ippopotamo.nuota.

3.2 Oggetti in SC

SuperCollider (qui intendendo: sclang) un linguaggio orientato agli oggetti. Lo per di pi in termini molto puri, poich ha come suo modello storico, e come parente tipologico assai prossimo, il linguaggio Smalltalk. In Smalltalk, come in SC, letteralmente ogni entit possibile un oggetto. Questa radicalit pu essere spiazzante inzialmente, ma un punto di forza poich garantisce che tutte (proprio tutte) le entit potranno essere controllate dallutente secondo un unico principio: tutte avranno attributi e metodi, a tutte sar possibile inviare dei messaggi poich presenteranno allutente una certa interfaccia. Si prenda il caso delle strutture dati: SC possiede una grande ricchezza di strutture dati, cio di classi che funzionano da contenitori di altri oggetti, ognuna dotati di particolari capacit e specializzata per certi tipi di oggetti. Ad esempio un array un contenitore ordinato di oggetti. Si scriva Array. Se si richiama la colorazione della sintassi, si nota come venga applicato il blu, colore che contraddistingue le classi (oltre che le parole riservate var, arg, this e alcune altre). SC sa che Array una classe perch la prima lettera maiuscola: tutto ci che inizia con la maiuscola per SC indica una classe. Se si esegue il codice, SC restuituisce (per ora si intenda: stampa sullo schermo) la

3.228

classe stessa. Se si richiama lhelp le, si nota come immediatamente venga indicata la sopraclasse, Superclass: ArrayedCollection. Lhelp le fornisce alcune indicazioni sui metodi disponibili per gli oggetti di tipo Array. Il codice:
z = Array.new;

costruisce un nuovo array vuoto attraverso il messaggio new invocato sulla classe Array. Tipicamente un messaggio viene inviato ad unistanza particolare e non ad una classe. Ma prima di poterlo fare, necessario avere unistanza a cui inviare un messaggio. Al messaggio new rispondono allora tutte le classi restituendo una loro istanza. Il metodo new il costruttore della classe: il metodo cio che istanzia un oggetto a partire dalla classe. Possono esserci molti metodi costruttori, che restituiscono un oggetto secondo modalit speciche: sono tutto metodi della classe perch, invocati sulla classe, restituiscono un oggetto. Uno di essi il messaggio newClear, che prevede anche una parte tra parentesi tonde:
z = Array.newClear(12);

Nellesempio le parentesi contengono una lista di argomenti (uno solo, in questo caso), che specicano ulteriormente il messaggio newClear, ovvero oggetto, fai qualcosa (cos)! 10. In particolare newClear(12) prevede un argomento (12) che indica che larray dovr contenere al massimo 12 posti. possibile indicare esplicitamente largomento: oggetto, fai qualcosa (nel modo: cos)! . Ogni argomento ha un nome specico, la sua keyword: nel caso di newClear indexedSize, che indica il numero di posti contenuti nel nuovo array. Il codice:
z = Array.newClear(indexedSize:12);
10

Tornando a prima, conio.imprimi(forte) o ippopotamo.nuota(veloce)

3.229

identico al precedente ma esplicita la keyword dellargomento. Inne, z = indica che larray verr assegnato alla variabile z. Si noti che la lettera utilizzata minuscola: se si scrivesse Z = SC intepreterebbe Z come una classe (inesistente) 11 e solleverebbe un errore. Adesso z rappresenta un array vuoto di capienza 112: unistanza della classe Array. Si pu chiedere a z di comunicare la classe a cui appartiene invocando il metodo class:
1 2 z.class Array

Il metodo class (1) restituisce la classe di z: Array (2). Traducendo in italiano, la frase equivalente potrebbe essere: z, dichiara la tua classe!. Quando si usano degli array molto spesso si insoddisfatti dei metodi elencati nellhelp le: sembra che manchino molti metodi intuitivamente utili. molto difcile che sia veramente cos: molto spesso il metodo cercato c, ma denito nella sopraclasse ed ereditato dalle classi gli. A partire da Array si pu navigare la struttura degli help le risalendo a ArrayedCollection, SequenceableCollection, Collection: sono tutte sopraclassi (di tipo sempre pi astratto) che deniscono metodi che le sottoclassi possono ereditare. Se si prosegue si arriva a Object. superclass: nil Object is the root class of all other classes. All objects are indirect instances of class Object.

11

La Z verrebbe colorata di blu.

3.230

Tutti le classi in SC ereditano da Object. Ad esempio, il metodo class che stato chiamato su z nellesempio precedente denito a livello di Object ed ereditato, lungo lalbero delle relazioni di ereditariet, da Array, cos che unistanza di questultima classe (z) vi possa rispondere. Al di l della navigazione nella struttura degli help les, SC mette a disposizione dellutente molti metodi per ispezionare la struttura interna del codice: la capacit che tipicamente viene denita introspezione. Ad esempio i metodi dumpClassSubtree e dumpSubclassList stampano su schermo rispettivamente una rappresentazione gerarchica delle sottoclassi della classe su cui invocato il metodo e una lista in ordine alfabetico. Le due rappresentazioni sono equivalenti. Nella prima sono pi chiari i rapporti di parentela tra le classi attraverso la struttura ad albero, nella seconda invece possibile percorrere -per ognuna delle sottoclassi della classe - la struttura dellalbero lungo i rami ascendenti no a Object. Si prenda la classe Collection, una classe molto generale di cui Array una sottoclasse, e si inviino i messaggi dumpClassSubtree e dumpSubclassList. Quanto segue quanto appare sulla Post Window nei due casi.

3.231

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 43 45 46

Collection.dumpClassSubtree; Collection [ Array2D Range Interval MultiLevelIdentityDictionary [ LibraryBase [ Archive Library ] ] Set [ Dictionary [ IdentityDictionary [ Environment [ Event ] ] ] IdentitySet ] Bag [ IdentityBag ] Pair TwoWayIdentityDictionary [ ObjectTable ] SequenceableCollection [ Order LinkedList List [ SortedList ] ArrayedCollection [ RawArray [ DoubleArray FloatArray [ Wavetable Signal ] [] ] Collection

3.232

1 2 3 4 5 6 7 8 9 10 11 12 14 16 17

Collection.dumpSubclassList Archive : LibraryBase : MultiLevelIdentityDictionary : Collection : Object Array : ArrayedCollection : SequenceableCollection : Collection : Object Array2D : Collection : Object ArrayedCollection : SequenceableCollection : Collection : Object Bag : Collection : Object Collection : Object Dictionary : Set : Collection : Object DoubleArray : RawArray : ArrayedCollection : SequenceableCollection : Collection : Object Environment : IdentityDictionary : Dictionary : Set : Collection : Object Event : Environment : IdentityDictionary : Dictionary : Set : Collection : Object FloatArray : Raw [] 36 classes listed. Collection

Con Collection.dumpClassSubtree si vede la posizione di Array in relazione ai suoi vicini (47). sullo stesso livello di RawArray (36), entrambi sono sottoclassi di ArrayedCollection (34). Questultima classe appartiene alla famiglia delle SequenceableCollection (28). Il metodo Collection.dumpSubclassList lista le classi in ordine alfabetico: agevole trovare Array (2) per poi seguire i rami dellalbero (lungo la stessa riga) no a Object. La gura 3.2 una visualizzazione tramite un grafo ad albero della struttura delle classi di Collection, ottenuta elaborando automaticamente loutput di Collection.dumpSubclassList. Lesempio tratto dallhelp le Internal-Snooping, che dedicato allintrospezione in SC. Anche lhelp le Class particolarmente interessante al proposito.

3.233

Event

Environment

Archive

Library

IdentityDictionary

LibraryBase

Dictionary

IdentitySet

IdentityBag

ObjectTable

Array2D

Range

Interval

MultiLevelIdentityDictionary

Set

Bag

Pair

TwoWayIdentityDictionary

Collection

Object

Fig. 3.2 Grafo ad albero delle sottoclassi di Collection, a partire da Object. Lultima riga che SC stampa in entrambi i caso loggetto Collection (39), che ci che i metodi ritornano. Il perch verr discusso a breve. Se si sostituisce a Collection la classe Object, si ottiene tutta la struttura delle classi di SC. La gura 3.3 riporta una rappresentazione radiale della struttura di tutte delle classi di SC, ottenuta elaborando il risultato di Object.dumpSubclassList 12. Si noti come un punto di addensamento sia rappresentato da UGen, la sopraclasse diretta di tutte le classi che generano segnali in SC. una classe comprensibilmente molto numerosa.

3.3 Metodi e messaggi

SC scritto per la maggior parte in SC (con leccezione di un nucleo di primitive scritte in linguaggio C per ragioni di efcienza): il codice stesso del programma trasparente al programmatore, nel senso che scritto in un linguaggio comprensibile per chi parla SC. Questo
12

Propriamente si tratta delle classi installate sulla macchina di scrive.

3.334

KrNumberEditorGui Midi2FreqGui ModalFreqGui InstrSpawnerGui SFPGui PlayerAmpGui PlayerEffectGui StreamKrDurGui DualSeriesEfxGui PopUpEditor StaticIntegerSpec ScalarPatchIn ClassGui BooleanEditorGui NumberEditorGui PopUpEditorGui IntegerEditor BooleanEditor AnyIrNumberEditor TempoBusClock IsNotInKrNumberEditor CountLimit JClassInspector JFunctionDefInspector JMethodInspector JStringInspector RootNode ControlPatchIn UpdatingScalarPatchOut AudioPatchOut Sample ProxySpace TrigSpec NoLagControlSpec HasItemSpec SampleSpec EnvSpec BufferProxySpec MultiTrackAudioSpec SineWarp CosineWarp StaticSpec StreamSpec PlayerSpec ArraySpec

SC2DTabletSlider SCKnob

BeatClockPlayerGui InterfaceGui KrPlayerGui PatchGui PlayerBinopGui PlayerEfxFuncGui PlayerMixerGui PlayerPoolGui PlayerUnopGui AbstractSFPGui SimpleTriggerGui HasSubjectGui XFaderPlayerGui ClassNameLabel InspectorLink MethodLabel SelectorLabel Tile CXObjectInspector EnvEditorGui MethodGui PlayButton ModelImplementsGuiBody XPlayPathButton EditorGui SampleGui ServerErrorGui ServerGui TempoGui SCEnvelopeEdit AbstractPlayerGui ActionButton ToggleButton PlayPathButton ObjectGui

SCDragBoth ArgNameLabel CXLabel VariableNameLabel

SC2DSlider SCRangeSlider SCSlider SCTopView FlowView SCHLayoutView SCVLayoutView SCDragSink SCDragSource SCTextField CXAbstractLabel

EnvEditor SynthConsole SaveConsole TempoClock IsEven SystemClock AppClock IsIn IsNil NumberEditor SharedBus SeenBefore NotNilConstraint Not Every XorIsOdd JFrameInspector JObjectInspector CXMenu SCViewAdapter StartRow

EnvirDocument Synth Group AudioPatchIn ScalarPatchOut ControlPatchOut PrettyEcho PrettyEat AbstractSample ArrayBuffer TabFileReader CSVFileReader LazyEnvir

SCEnvelopeView SCButton SCListView SCPopUpMenu SCSliderBase SCFreqScope SCSoundFileView SCButtonAdapter SCMovieView SCMultiSliderView SCControlView

SCCompositeView SCLayoutView SCDragView SCNumberBox SCStaticText Lines Polygon ZigZag

SCScope SCTabletView SCTextView SCUserView SCContainerView SCStaticTextBase InterfaceDef PointArray

SCViewHolder

ModalDialog PowerOfTwoAllocator PageLayout ClockGUI SheetCXBundle Bus AbstractConsole SoundFileFormats Editor NodeIDAllocator MultiPageLayout Scheduler JKnob AbstractConstraint PowerOfTwoBlock LRUNumberAllocator StackNumberAllocator Impulsar JavaObject UI ContiguousBlockAllocator RingNumberAllocator ContiguousBlock Enveloper2 Mono NotificationCenter Updater XFader4 XFaderN NotificationRegistration GetFileDialog NumChannels XFader GetStringDialog Document JSlotInspector CmdPeriod SelectButtonSet StartUp JInspector Node HIDDevice OSCService HIDDeviceElement Def Do HIDDeviceService InBus SwingOptions XIn TestCase XInFeedback InspManager Monitor Insp PatchIn GeoGraphSynth GeoGrapher PatchOut DebugFrame Post MethodQuote Message PrettyState Phrase SoundFile IODesc SynthDesc SynthDescLib BufferProxy FileReader TempoBus EnvironmentRedirect Unix NodeControl ModalFreqUGen Midi2FreqUGen JFont JSCWindow Buffer NodeMap

ProxyNodeMap ControlSpec TempoSpec ScalarSpec ObjectSpec AudioSpec LinearWarp FaderWarp ExponentialWarp DbFaderWarp CurveWarp

StringInspector MethodInspector FunctionDefInspector ClassInspector OSCpathDispatcher

Spec

ObjectInspector FrameInspector OSCpathResponder OSCMultiResponder

JSCVLayoutView JSCHLayoutView JSCKnob JSC2DTabletSlider

Float Integer Event

EZNumber EZSlider FreqScope SoundFileViewProgressWindow HiliteGradient Gradient

Object

Complex Polar SimpleNumber Environment Archive Library IdentityDictionary DoubleArray Int16Array Int32Array Int8Array String SymbolArray FloatArray IdentityBag LibraryBase SortedList Array RawArray Dictionary IdentitySet ObjectTable LinkedList Order List ArrayedCollection Server SwingOSC Association Char Number False True

SCView

Signal Wavetable

Main Method Array2D Bag Interval MultiLevelIdentityDictionary Pair Range Set TwoWayIdentityDictionary

UGenInstr InstrAt Instr Point ServerOptions EnvGate GraphBuilder Font MIDIOut MIDIIn MIDIEvent MIDIClient MIDIEndPoint TestDependant SimpleController Model RawPointer Magnitude Boolean Nil Symbol Finalizer Interpreter Process Frame FunctionDef

JSCTextField JSCNumberBox LocalClient Warp ResponderClientFunc JSCDragSource JSCDragSink Splay JSCLayoutView SplayZ JFreqScope JSCDragBoth Module JSCTopView Peep Mix JSCPlugContainerView Stethoscope JMouseY JSCCompositeView JMouseX JSCSlider Inspector JMouseButton JSCRangeSlider SlotInspector Tempo JSC2DSlider ProxySynthDef JSCTextEditBase OSCresponder InstrSynthDef OSCresponderNode JSCStaticText CocoaDialog Cocoa MLIDbrowser KDRMaskTester JSCDragView JEZSlider JEZNumber Client ClientFunc JSCSoundFileView Pen JSCContainerView JSCFreqScope ControlPrototypes Crucial Position ControlName JSCSliderBase JSCUserView TChoose JSCTextView JMouseBase JSCPopUpMenu JSCListView PriorityQueue JStethoscope JSCButton JSCStaticTextBase SynthDef KeyCodeResponder KeyCodeResponderStack SimpleKDRUnit JSCScope SC2compat JSCControlView JSCView JSCPlugView JSCMultiSliderView BundleNetAddr OSCSched AutoCompClassSearch

SequenceableCollection Collection

GBinaryOp GBinaryOpX GBindF GClump GCollect GKeep GKeepUntil GKeepWhile GRepeat GReset GSelect GStutter GUnaryOp Fdef Ndef RecNodeProxy SharedNodeProxy

GBind GCat GCyc GFib GFunc GGeom GInf GIter GLace GSeries GZip GenFilter FuncProxy RefCopy NodeProxy Pbind PbindProxy Pbinop Pbrown Pchain Pdict Penvir Pevent Pfunc Pfuncn Pgeom Phid Pindex Plazy Pmono Pnaryop Ppatmod Pprob Prout Pseries Pstep Pstep2add Pstep3add PstepNfunc Pswitch Ptime Punop Pwhite

BinaryOpFunction Function

Gen AbstractFunction NAryOpFunction Ref BusPlug AbstractPlayer HasPatchIns Patch InstrSpawner

MixedBundle BendResponder AutoCompClassBrowser NoteOffResponder JSoundFileViewProgressWindow NetAddr BeatSched PingPong FunctionPlayer OSCBundle TouchResponder FlowLayout NoteOnResponder LinkedListNode CCResponder AutoCompMethodBrowser SystemSynthDefs BusSynthDefs PMOsc MultiTap AudioIn Public MIDIResponder ResponderArray ClassBrowser Speech DotViewer EnvirDispatch JPen UniqueID Help Color SynthDefControl Rect PatternControl Condition FlowVar ProxyNodeMapSetting CXSynthPlayerControl BroadcastServer SynthControl PathName StreamControl Harmonics AnnotatedDebugNodeWatcher Scale CXPlayerControl NodeMapSetting NodeWatcher DebugNodeWatcher AbstractPlayControl SwingDialog BasicNodeWatcher Score UnicodeResponder SubclassResponsibilityError AbstractNodeWatcher ShouldNotImplementError OSCResponderQueue JClassBrowser PrimitiveFailedError PatternConductor OutOfContextReturnError FormantTable NotYetImplementedError MethodError MustBeBooleanError Error Exception ImmutableError DoesNotUnderstandError BinaryOpFailureError InstrGateSpawner ScurryableInstrGateSpawner AbstractSFP SFP KrPlayer TempoPlayer Midi2Freq BeatClockPlayer BusDriver SynthlessPlayer SimpleTrigger Silence PlayerUnop MIDIPlayer AbstractPlayerProxy MultiplePlayers PlayerSocket PlayerBinop ModalFreq HasSubject Interface PlayerMixer MonoAudioIn AudioInPlayer MultiTrackPlayer AbstractSinglePlayerEffect AbstractPlayerEffect Date SCWindow Env VSFP StreamKrDur PlayerInputProxy ObjectNotFound MIDIHoldsNotes CCPlayer PlayerPool PlayerEffectSocket PlayerAmp EnvelopedPlayer ZeroCrossing Stream2Trig MIDIGatePlayer MIDIFreqPlayer

PlazyEnvirN

PlazyEnvir Proutine Pseg PstepNadd Pswitch1 Pavaroh Pbindf Pbus Pclump Pconst PdegreeToKey Pdrop PfadeIn Pfin Pfindur Pflatten Pflow Pfx Pgroup Plag Pn Pplayer Prewrite Prorate Pseed Pset Psetpre Pstretch Pstutter Psync Ptrace

Pattern MultiOutUGen FilterPattern

XY Control TGrains Silent AbstractIn

TrigControl LagControl SharedIn LocalIn LagIn InTrig InFeedback In Rotate2 PanB2 PanB PanAz Pan4 Pan2 DecodeB2 BiPanB2 Balance2 LinPan2

PfadeOut

Panner AbstractOut XLine XFade Index InRange WhiteNoise Vibrato VarSaw VOsc3 VOsc BasicOpUGen PlayBuf Pitch ImageWarp DiskIn Demand DC BufRd XOut SharedOut Out LocalOut XFade2 LinXFade2 WrapIndex Shaper Wrap Schmidt Fold Clip PinkNoise NoahNoise GrayNoise ClipNoise BrownNoise

Thunk UnaryOpFunction AbstractOpPlug ListPattern PatternProxy Stream

Padd Pmul Psetp Paddpre Pmulpre Pstretchp PdurStutter Paddp Pmulp

FuncFilterPattern Pwrap Pdfsm Pfsm Prand Pseq Pshuf Pslide Ppar Ptuple Pwalk Pwrand Pxrand Pdefn TaskProxy

Pcollect Pfset Preject Pselect Pwhile

Pgtpar

Pdef

Pbindef

BinaryOpPlug UnaryOpPlug BinaryOpStream BinaryOpXStream CleanupStream EmbedOnce FuncStream GenStream NAryOpStream Filter OneShotStream StreamClutch PauseStream UnaryOpUGen BinaryOpUGen Thread UnaryOpStream TwoPole Place IOStream Slope Slew Ppatlace SOS Pser A2K AmpComp Ringz Amplitude Trig1 Resonz Ball Blip Trapezoid ToggleFF Pgpar Lag Timer Ptpar BufCombN Tap UGen TWindex RLPF TWChoose OnePole BufDelayN TRand TPulse MidEQ TIRand Median TExpRand LeakDC BufInfoUGenBase Duty TBall SyncSaw EventStreamPlayer LPZ2 TwoZero Sweep BufWr EventPatternProxy Task APF COsc Routine CoinGate LPZ1 Tdef LPF Pretty CombN InfoUGenBase Integrator Formlet CollStream Compander FOS CompanderD DetectSilence Convolution UnixFILE Convolution2 Stepper Decay2 Crackle DegreeToKey Decay Ramp Delay1 AmpCompA BPF DelayN Lag3 DemandEnvGen Trig Lag2 Dgeom ChaosGen TDelay Dbrown DiskOut RHPF Done BufAllpassC Dseries OneZero Dswitch1 BufAllpassL Dust Dust2 BufAllpassN Dwhite Spring BufCombC DynKlank SinOscFB EnvGen SinOsc BufCombL ExpRand FFT PulseCount BufDelayC FSinOsc Formant SendTrig Free Select HPZ2 BufDelayL FreeSelf ScopeOut FreeSelfWhenDone Saw ScoreStreamPlayer BufChannels Gendy1 RunningSum BRZ2 Gendy2 RecordBuf BufDur Gendy3 Hasher RandSeed RandID IFFT Rand BPZ2 BufFrames IRand PulseDivider TDuty Impulse Pulse FuncStreamAsRoutine BufRateScale InRect PitchShift HPZ1 JScopeOut K2A PeakFollower Phasor BufSampleRate KeyState PauseSelfWhenDone Klang Klank PauseSelf Pause HPF BufSamples PV_RectComb2 PV_RandWipe PV_RectComb LFNoise0 PV_RandComb SubsampleOffset PV_MagSquared LimitedWriteStream LFPulse PV_PhaseShift SampleRate LFSaw LastValue LatchLine PV_MagMul SampleDur PrettyPrintStream Latoocarfian LinExp Logistic PV_ConformalMap PV_BinShift AllpassC LinRand MouseX PV_Diffuser LinLin MostChange Linen MulAdd RadiansPerSample ListDUGen OutputProxyPV_MagSmear PV_HainsworthFoote PV_BrickWall PV_JensenAndersen Pipe AllpassL MantissaMask MouseButton PV_MagAbove Normalizer PV_MagFreeze NRand PV_BinScramble PSinGrain Osc OscN PV_BinWipe NumRunningSynths AllpassN NumOutputBuses File CombC NumInputBuses CombL NumControlBuses NumBuffers BRF NumAudioBuses ControlRate StandardN QuadN Delay2 DelayC LorenzL DelayL LinCongN LatoocarfianN Dibrown HenonN GbmanN FBSineN CuspN Diwhite SetResetFF Peak ZArchive LFClipNoise LFDClipNoise LFDNoise0 LFDNoise1 LFDNoise3 LFNoise1 LFNoise2 LFCub LFPar LFTri Gate PV_PhaseShift90 PV_PhaseShift270 PV_MagNoise PV_Min PV_Mul PV_CopyPhase PV_Max PV_Add PV_MagClip PV_MagShift PV_LocalMax PV_MagBelow

ReplaceOut OffsetOut

Drand MouseY Dseq Dxrand Limiter Dser LeastChange

StandardL QuadL QuadC LinCongL LinCongC LatoocarfianL LatoocarfianC HenonL HenonC GbmanL FBSineL FBSineC CuspL RunningMin RunningMax

Fig. 3.3

Grafo radiale della struttura delle classi di SC, a partire da Object.

ovviamente molto diverso da capire esattamente cosa viene detto attraverso il linguaggio: ma in ogni caso sbirciando i sorgenti si possono recuperare molte informazioni interessanti. questo fatto che permette a SC un grande potere di introspezione. Dopo aver selezionato Array attraverso il menu Lang possibile accedere alla denizione della classe Array attraverso Open Class Def.

3.335

1 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 21 23 24 25 26 27 28 29 30 31 32 33 35 36

Array[slot] : ArrayedCollection { *with { arg args; // return an array of the arguments given // cool! the interpreter does it for me.. ^args } reverse { _ArrayReverse ^this.primitiveFailed } scramble { _ArrayScramble ^this.primitiveFailed } mirror { _ArrayMirror ^this.primitiveFailed } // etc sputter { arg probability=0.25, maxlen = 100; var i=0; var list = Array.new; var size = this.size; probability = 1.0 - probability; while { (i < size) and: { list.size < maxlen }}{ list = list.add(this[i]); if (probability.coin) { i = i + 1; } }; ^list } // etc }

3.336

Senza addentrarsi nel merito si noti come la prima riga (1) denisca la classe Array come una sottoclasse di ArrayedCollection(Array[slot] : ArrayedCollection ). Fa seguito (da 3 in poi) la lista dei metodi implementati per la classe (width, reverse, scramble, ognuno chiuso tra una coppia di graffe). Un modo agevole per ottenere una lista dei metodi implementati consiste nello sfruttare il potere di introspezione di SC. SC fornisce molti metodi per conoscere informazioni relativi al suo stato interno. I metodi dumpInterface, dumpFullInterface, dumpMethodList visualizzano sulla Post Window informazioni sui methodi implementati per linterfaccia di una classe.

dumpInterface: stampa tutti i metodi deniti per la classe dumpFullInterface: come prima, ma include anche i metodi ereditati dalle sopraclassi della classe dumpMethodList: come il precedente, ma con i metodi in ordine alfabetico e lindicazione della classe da cui sono ereditati.

Ad esempio quanto segue riporta il risultato della valutazione di Array.dumpInterface. Le liste fornite dagli altri due metodi proposti sono decisamente pi lunghe.

3.337

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41

Array.dumpInterface reverse ( ) scramble ( ) mirror ( ) mirror1 ( ) mirror2 ( ) stutter ( n ) rotate ( n ) pyramid ( patternType ) pyramidg ( patternType ) sputter ( probability, maxlen ) lace ( length ) permute ( nthPermutation ) allTuples ( maxTuples ) wrapExtend ( length ) foldExtend ( length ) clipExtend ( length ) slide ( windowLength, stepSize ) containsSeqColl ( ) flop ( ) multiChannelExpand ( ) envirPairs ( ) shift ( n ) source ( ) asUGenInput ( ) isValidUGenInput ( ) numChannels ( ) poll ( interval, label ) envAt ( time ) atIdentityHash ( argKey ) atIdentityHashInPairs ( argKey ) asSpec ( ) fork ( join, clock, quant, stackSize ) madd ( mul, add ) asRawOSC ( ) printOn ( stream ) storeOn ( stream ) prUnarchive ( slotArray ) jscope ( name, bufsize, zoom ) scope ( name, bufsize, zoom ) Array

3.338

A partire da un metodo possibile risalire alle classi che lo implementano: necessario selezionarlo e scegliere dal menu Lang la voce Implementations of: SC apre una nestra dove sono listati le classi che rispondono al metodo e la lista delle keyword. Tornando a z, un metodo pi agevole per creare un array consiste semplicemente nello scrivere larray in questione tra parentesi quadre (secondo una notazione molto diffusa). Ad esempio, z = [1,2,3,4] assegna a z larray [1,2,3,4] (1). Come si visto nella denizione di classe, uno dei metodi che la classe Array prevede reverse: intuibilmente, il metodo prende larray e ne inverte lordine. Nel momento in cui si passa il messaggio reverse a z che ne diventa il ricevente (5), z cerca il metodo reverse tra quelli che sono deniti nella sua classe, e si comporta conseguentemente. Nel caso in questione, come loperazione di inversione venga svolta da SC non interessante in questa sede (ne molto interessante di per s): in ogni caso, se si vede la denizione del metodo (supra, 8-11), si nota come il metodo chiami una riga misteriosa, _ArrayReverse (supra, 9): loperazione di inversione allora realizzata da una primitiva di SC, scritta in linguaggio C e non in SC. Al contrario il metodo sputter scritto completamente in SC. I metodi restituiscono delle entit come risultato delle operazioni svolte: queste entit sono oggetti a tutti gli effetti. Ad esempio, z.reverse restituisce un nuovo array, invertito rispetto a z (7). Poich un oggetto diverso, se si intende riutilizzarlo, necessario assegnarlo ad una variabile, la stessa z ad esempio (12). Lultimo oggetto restituito viene stampato da SC sullo schermo come risultato del processo di intepretazione.

3.339

1 3 5 7 9 10 12 13 14 15 17 18 20 21 23 24

z = [1,2,3,4] [ 1, 2, 3, 4 ] z.reverse [ 4, 3, 2, 1 ] z [ 1, 2, 3, 4 ] z = z.reverse [ 4, 3, 2, 1 ] z [ 4, 3, 2, 1 ] z.mirror [ 4, 3, 2, 1, 2, 3, 4 ] z [ 4, 3, 2, 1 ] z.reverse.mirror.mirror [ 1, 2, 3, 4, 3, 2, 1, 2, 3, 4, 3, 2, 1 ]

In una nuova sessione con linterprete viene valutato il codice z = [1,2,3,4] (1): SC restituisce larray [ 1, 2, 3, 4 ] (2) e lo assegna a z. Nella riga successiva z.reverse (5) chiede a z di svolgere le operazioni previste da reverse: il risultato [ 4, 3, 2, 1 ], che non assegnato ad alcuna variabile (7). Se infatti si chiama z, si ottiene [ 1, 2, 3, 4 ] (10). Per assegnare a z il risultato della computazione effettuata da reverse necessario riassegnare a z il valore calcolato dal metodo attraverso z = z.reverse (12). Chiamando z (14) si ottiene il suo valore: questa volta larray z invertito (15). Il metodo mirror genera un nuovo array a partire da quello a cui il messaggio passato, simmetrico rispetto al centro (palindromo): z.mirror (17) restituisce [ 4, 3, 2, 1, 2, 3, 4 ] (18),

3.340

senza assegnarlo a z, come prima (20-21). Lultima riga (23) di codice mette in luce un aspetto importante di SC: il cosiddetto concatenamento dei messaggi (message chaining). Sul risultato di z.reverse viene calcolato mirror, su questo secondo risultato viene calcolato mirror di nuovo. Si consideri il risultato stampato sulla Post Window (24). Questi sono i passaggi a partire da z = [ 4, 3, 2, 1 ] (valore iniziale pi tre messaggi): [4,3,2,1] [1,2,3,4] [1,2,3,4,3,2,1][1,2,3,4,3,2,1,2,3,4,3,2,1] Sebbene permetta di scrivere codice in forma estremamente economica, il concatenamento dei messaggi va usato con cautela perch rischia di rendere di difcile lettura il codice.

3.4 I metodi di tipo post e dump

Si detto che tutti i metodi restituiscono un oggetto. A scanso di equivoci va ricordato il comportamento dei metodi che permettono di ottenere informazioni attraverso la Post Window. Esempi gi visti sono dumpClassSubtree e dumpSubclassList, e dumInterface, dumpFullInterface, dumpMethodList. Il metodo pi usato per ottenere generiche informazioni su quanto sta avvenendo postln, che stampa una stringa di informazione relativa alloggetto su cui chiamato. Ad esempio si consideri questo codice:
1 2 3 Array.postln Array Array

3.441

Viene chiamato il metodo postln su Array (1). SC esegue il codice il quale prevede di stampare informazioni su Array: in questo caso, essendo Array una classe, viene semplicemente stampato il nome della classe, Array appunto (2). Inne SC stampa sempre sullo schermo uninformazione sullultimo oggetto su cui stato invocato un metodo: in sostanza SC chiama sullultimo oggetto proprio postln. E infatti si ottiene di nuovo Array (3). Se in questo caso lutilit di postln virtualmente nulla, si consideri invece questo caso:
1 3 5 7 8 9 10 12 14 15 16 17 18 z = [ 4, 3, 2, 1 ] [ 4, 3, 2, 1 ] z.postln.reverse.postln.mirror.postln.mirror [ [ [ [ 4, 1, 1, 1, 3, 2, 2, 2, 2, 3, 3, 3, 1 ] 4 ] 4, 3, 2, 1 ] 4, 3, 2, 1, 2, 3, 4, 3, 2, 1 ]

z.postln.reverse.postln.mirror.postln.mirror.postln [ [ [ [ [ 4, 1, 1, 1, 1, 3, 2, 2, 2, 2, 2, 3, 3, 3, 3, 1 ] 4 ] 4, 3, 2, 1 ] 4, 3, 2, 1, 2, 3, 4, 3, 2, 1 ] 4, 3, 2, 1, 2, 3, 4, 3, 2, 1 ]

Come nel caso precedente, a z assegnato larray [ 4, 3, 2, 1 ]. Viene chiamato il concatenamento di metodi .reverse.mirror.mirror, senonch dopo ogni metodo concatenato anche un messaggio postln (5). In sostanza in questo caso postln permette di vedere sullo schermo il risultato intermedio resituito da ognuno dei metodi invocati. Si noti come sia inutile (in questo caso) concatenare di nuovo postln dopo

3.442

lultimo mirror (come avviene in 12), visto che per denizione SC stampa il risultato dellultima computazione (ci che mirror restituisce). Ci si potrebbe aspettare che, poich postln serve a stampare sullo schermo, ci che questo metodo restituisce sia un oggetto di tipo stringa, un insieme di caratteri. Non cos: postln

stampa una stringa sullo schermo restituisce loggetto su cui chiamato il metodo

Questo comportamento assolutamente utile in fase di correzione degli errori, perch permette di concatenare messaggi di stampa per vericare i comportamenti dei metodi invocati, ma senza per questo interferire con il processo. Se infatti il metodo postln restituiscce una stringa, allora in z.postln.reverse il messaggio reverse sarebbe ricevuto da un oggetto di tipo stringa e non da un oggetto array, come nellesempio seguente.
1 3 "[ 1, 2, 3, 4 ]".reverse ] 4 ,3 ,2 ,1 [

Questo vale per tutti metodi di introspezione. Si osservino gli esempi di Collection.dumpClassSubtree, Collection.dumpSubclassList, Array.dumpInterface . In tutti e tre i casi lultima riga stampa loggetto che il metodo restituisce: si noti come venga restituita la classe, secondo quanto stampato nellultima riga delle rispettiva schermate (Collection, Collection, Array). Questo tipo di comportamento vale tipicamente per tutti i metodi di stampa e di introspezione. Sono molti: ad esempio varianti di postln sono post, postc, postcln.

3.543

3.5 Numeri

Linterprete di SC pu essere utilizzato come calcolatore. Ad esempio, si cosideri questa sessione interattiva:
1 2 4 5 7 8 10 11 2.3*2 4.6 4/3 1.3333333333333 4**3 64 4+2*3 18

Due cose sono di rilievo. La prima concerne lordine di esecuzione. Se si osserva 4+2*3 si nota come non ci sia gerarchia tra gli operatori: ad esempio, la moltiplicazione non viene valutata prima delladdizione. Nella riga prima viene valutato 4+2, quindi *3 che viene riferito al risultato delloperazione precendente (4 + 2 = 6 3 = 18). Il secondo aspetto che dovrebbe stupire il lettore che la sintassi utilizzata contraddice il presupposto per cui in SC ogni cosa un oggetto dotato di uninterfaccia, per cui ogni operazione dovrebbe seguire il modello generale oggetto.metodo. Qui in effetti SC fa uneccezione, almeno per le quattro operazioni, che possono essere scritte in modo pi intuitivo nella consueta forma funzionale. Ci non toglie che i numeri (interi, a virgola mobile, etc.) siano oggetti a tutti gli effetti. Se si chiama il metodo

3.544

class su un intero, ad esempio 5 (1), si ottiene la classe a cui appartiene in quanto istanza: Integer.
1 2 5.class Integer

Si pu allora chiamare su Integer il metodo superclasses che restituisce un array (attenzione, questa volta il metodo restituisce un array) contenente tutte le sopraclassi no a Object.
1 3 Integer.superclasses [ class SimpleNumber, class Number, class Magnitude, class Object ]

Intuibilmente, a parte Object, che sopraclasse di tutto, Magnitude la classe che pi in generale si occupa di grandezze (tra cui i numeri).
1 3 Magnitude.allSubclasses [ class Association, class Number, class Char, class Polar, class Complex, class SimpleNumber, class Float, class Integer ]

Con Magnitude.dumpClassSubtree si accede ad una rappresentazione ad albero delle sottoclassi di Magnitude: tutte le classi che si occupano di grandezze: Integer -i numeri interi- vicino a Float -i numeri a virgola mobile-, poich sono due sottoclassi di SimpleNumber. Qustultima classe fa parte del pi vasto insieme delle sottoclassi di Number, i numeri in generale, compresi quelli polari e quelli complessi (Polar, Complex, che qui non interessano).

3.545

1 2 3 4 5 6 7 8 9 10 11 12 13 14

Magnitude.dumpClassSubtree Magnitude [ Association Number [ Polar Complex SimpleNumber [ Float Integer ] ] Char ] Magnitude

In quanto oggetto, possible passare ai numeri, il 3 ad esempio, il messaggio postln, il quale stampa il numero e restituisce il numero stesso.
1 2 3 5 6 7 8 3.postln 3 3 3.postln * 4.postln 3 4 12

Per numerose operazioni matematiche disponibile una doppia notazione, funzionale e ad oggetti. Ad esempio sqrt(2) (10) chiede di eseguire la radice quadrata di 2: in notazione ad oggetti si tratta di chiamare su 2 il metodo sqrt (14), che restituisce il risultato delloperazione radice quadrata applicata alloggetto su cui chiamato. Analogamente, lelevamento a potenza pu essere scritto funzionalmente come 4**3

3.546

(18), oppure come 4.pow(3) (22): si chiama il metodo pow con argomento 3 sulloggetto 4. Ovvero, oggetto 4, levati a potenza con esponente 3.
1 3 5 7 9 11 13 15 sqrt(2) 1.4142135623731 2.sqrt 1.4142135623731 4**3 64 4.pow(3) 64

3.547

4 Sintassi: elementi fondamentali

4.1 (Parentesi)

Negli esempi di codice SC si troveranno spesso le parentesi tonde, (), utilizzate come delimitatori. Le parentesi tonde non sono esplicitamente previste a tal ne nella sintassi di SC. Tuttavia una convenzione che la loro presenza indichi un pezzo di codice che deve essere valutato tutto insieme (ovvero, selezione di tutte le linee e valutazione). In Mac OSX il doppio-click dopo una parentesi tonda di apertura permette la selezione di tutto il blocco di codice: le parentesi agevolano perci di molto linterazione dellutente con linterprete. Nellesempio di pagina 71 le parentesi alle righe 1 e 20 indicano il che il codice 2-19 un blocco unico. In generale la presenza delle parentesi per convenzione unindicazione non allinterprete SC quanto allutente (magari a se stessi il giorno dopo che si scritto il codice).

4.248

4.2 // Commenti (/*di diverso tipo*/)

La colorazione della sintassi nellesempio in discussione evidenzia in rosso i commenti (comments), le parti di codice che linterprete non tiene in considerazione e che si rivelano molto utili per rendere il codice leggibile no allautodocumentazione. In SC i commenti sono di due tipi: a. // indica un commento che occupa una riga o la parte terminale di essa; b. la coppia /* */, che delimita un commento multilinea: tutto ci che incluso tra i due elementi ignorato dallinterprete.

4.3 Stringhe

Una sequenza di caratteri delimitata da doppi apici una stringa. Le stringhe possono occupare pi linee. La classe String una sottoclasse di RawArray: le strighe sono cio sequenze di oggetti a cui possibile accedere.

4.349

1 2 4 5 7 9 11 12

t = "stringa" stringa t[0] s t[1] t t.size 7

Cos, t[0] chiede il primo elemento dellarray "stringa", ovvero s, e cos via. nella classe String che vengono deniti i metodi post e afni. Quando si invia ad un oggetto il messagio di post, SC tipicamente chiede alloggetto una sua rappresentazione in stringa, e sulla stringa richiama il metodo post. Ad esempio il metodo di concatenazione di stringhe ++ vale anche se gli oggetti concatenati alla prima stringa non sono stringhe: ++ chiede internamente a tutti gli oggetti una loro rappresentazione come stringa: . . .++ 5 equivale cio a . . .++ (5.asString).
1 3 "stringa"++5 stringa5

4.4 Variabili

4.450

In SC necessario dichiarare le variabili che si intendono utilizzare. I nomi di variabili devono iniziare con un carattere alfabetico minuscolo e possono contenere caratteri alfanumerici (caratteri maiuscoli, numeri). La dichiarazione delle variabili richiede la parola riservata var (che dunque non pu essere usata come nome di variabile). possibile assegnare un valore ad una variabile mentre la si dichiara.
1 2 3 5 var prima, seconda; var terza = 3; var quarta; nil

Nellesempio si noti che linteprete resituisce nil. Nel valutare una serie di espressioni, linterprete resituisce sempre il valore dellultima: in questo caso quello della variabile quarta, a cui non stato ancora assegnato alcun valore, come indicato da nil. La dichiarazione pu anche occupare pi righe, purch iniziali e consecutive. Le lettere a-z sono gi riservate da SC per le variabili dambiente: in altre parole si possono utilizzare (ad esempio in fase di testing) senza bisogno di dichiararle. Nellesempio (pagina 52, 1) lutilizzo della variabile a in a = [1,2,3] ammissibile senza dichiarazione: lespressione valutata e alla variabile viene assegnato larray (SC restituisce larray, 2). Nella valutazione dellespressione seguente

4.451

1 2 4 5 6 7 8 9 10

a = [1,2,3] [ 1, 2, 3 ] array = [1,2,3] ERROR: Variable 'array' not defined. in file 'selected text' line 1 char 17 : array = [1,2,3] nil

linterprete solleva un errore, poich riconoscendo unassegnazione di valore a una variabile, rileva che la variabile in question (array) non stata dichiarata ( ERROR: Variable 'array' not defined.). Il problema si risolve dichiarando la variabile:
1 2 3 4 5 ( var array ; array = [1,2,3] ) [ 1, 2, 3 ]

Si noti luso delle parentesi tonde ad indicare che le riga di codice vanno valutate tutte insieme. Lesistenza della variabile vale soltanto per il momento in cui viene valutato il codice. In altre parole, se si esegue il seguente codice:
1 array.postln

Si ottiene di nuovo:

4.452

1 2 3 4 5 6

ERROR: Variable 'array' not defined. in file 'selected text' line 1 char 5: array.postln nil

Durante una sessione interattiva pu essere desiderabile mantenere lesistenza di variabili per poterle riutillizare in un secondo momento. A tal ne, si possono utilizzare le variabili dambiente 13. Come si visto, i caratteri alfabetici sono per convenzione interna assegnabili senza dichiarazione. In pi, ogni variabile il cui primo carattere sia una variabile dambiente. Una volta dichiarata in questo modo
1 ~array = [1,2,3]

la variabile array persistente per tutta la sessione 14.


1 3 ~array [ 1, 2, 3 ]

13

14

Una nota pi avanzata. Le variabile sono dambiente perch sono persistenti nellintera sessione interattiva di lavoro con SC. Sebbene possa sembrare, non sono intese come variabili globali (che sono molto pericolose per la programmazione, semplicemente perch sono sempre accessibili, e dunque scarsamente controllabili). Servono piuttosto per poter lavorare interattivamente con SC, per conversare per cos dire con SC, ma non per scrivere codice in forma strutturata. Si discuter pi avanti il problema dello scoping, cio dellambito di validit delle variabili.

4.553

4.5 Simboli

Un simbolo un nome che rappresenta qualcosa. Pu essere pensato come un identicativo assoluto. cio un nome che rappresenta univocamente un oggetto. Si scrive tra apici semplici, o, nel caso la sequenza di caratteri non preveda spazi al suo interno, preceduto da un \
1 3 5 7 9 11 a = \symbol symbol b = 'sono un simbolo' sono un simbolo [a.class, b.class].postln [ class Symbol, class Symbol ]

Una stringa una sequenza di caratteri. Ad esempio, qui a e b sono due stringhe.
1 3 5 7 a = "symbol" symbol b = "symbol" symbol

4.554

Le due stringhe sono equivalenti (detto approssimativamente: hanno lo stesso contenuto).


1 2 a == b true // == chiede: sono equivalenti? // risposta: s

ma non sono lo stesso oggetto.


1 2 a === b false // === chiede invece: sono lo stesso oggetto? // no

Invece, la relazione di identit vera nel caso dei simboli:


1 3 5 7 9 10 12 14 a = \symbol symbol b = 'symbol' symbol a == b true a === b true // lo stesso oggetto? // s // scrittura equivalente

4.6 Espressioni ;

4.655

Unespressione in SC un enunciato nito e autonomo del linguaggio: una frase conclusa, si potrebbe dire. Le espressioni in SC sono delimitate dal ;. Ogni blocco di codice chiuso da un ; dunque unespressione di SC. Quando si valuta il codice SC nellinterprete, se il codice composto di una riga sola possibile omettere il ;.
1 3 5 7 a = [1,2,3] [ 1, 2, 3 ] a = [1,2,3] ; [ 1, 2, 3 ]

In generale, meglio prendere labitudine di mettere il ; anche quando si esperimenta interattivamente con linterprete riga per riga. Quando si valutano pi linee di codice la presenza del ; lunica informazione disponibile allinterprete per sapere dove nisce unespressione e ne inizia unaltra. Le due espressioni seguenti sono uguali, poich la capo non rilevante per SC (questo permette di utilizzare la capo per migliorare la leggibilit del codice). Si noti come in questo caso lassenza del punto in virgola nella seconda versione non crei problemi. In assenza del ; SC considera unespressione tutto ci che selezionato: in questo caso il codice selezionato effettivamente ununica espressione sensata, e dunque linteprete non segnala errori.

4.656

1 2 3 5 6 7 8 9

( a = [1,2,3] ) ( a = [

1, 2, 3 ]

Lordine delle espressioni lordine in cui SC esegue le istruzioni fornite.

4.7 Errori

Si riconsideri lesempio precedente:


1 2 3 4 5 6 7 array = [1,2,3] ERROR: Variable 'array' not defined. in file 'selected text' line 1 char 17 : array = [1,2,3] nil

Lesempio illustra il modo in cui SC segnale gli errori. SC molto scale: un inteprete decisamente poco caritatevole. Questo richiede una particolare attenzione ai principianti, che rischiano di impiegare un tempo interessante prima di riuscire a costruire unespressione corretta. In pi, la segnalazione degli errori piuttosto laconica in SC: se nel caso precedente decisamente chiara, in altri pu esserlo meno. In particolare

4.757

pu essere poco agevole individuare dov che si trova lerrore. Di solito la parte di codice segnalata da SC mentre riporta lerrore il punto immediatamente successivo a dove si vericato lerrore. Nel caso in esame, ci che manca una dichiarazione di variabile prima di array = [1,2,3].

4.8

{Funzioni}

Le funzioni sono uno degli elementi meno intuitivi da comprendere per chi non arrivi da un background informatico. Si consideri la denizione fornita dallhelp le: A Function is an expression which denes operations to be performed when it is sent the value message. La denizione precisa ed esaustiva. Una funzione : 1. unespressione 2. che denisce operazioni 3. che vengono effettuate soltanto nel momento in cui la funzione riceve il messaggio value. Una funzione perci un oggetto: implementa un metodo value con cui risponde al messaggio value (si provi Function.dumpInterface). Una funzione pu essere pensata come un oggetto (sico) capace di un fare certe cose. Nel momento in cui la si dichiara si dice a SC di costruire loggetto, non di farlo funzionare. A quel punto loggetto c: si tratta poi di metterlo in funzione quando serve attraverso il messaggio value. Le denizioni delle funzioni sono racchiuse tra parentesi graffe {}.

4.858

1 3 5 6

f = { 5 } ; a Function f.value ; 5

La funzione f un oggetto che butta fuori a richiesta il valore 5. La denizione stocca loggetto funzione il cui comportamento viene attivato a richiesta attraverso il messaggio value (5, 6).
1 3 5 7 9 11 g = { arg input; input*2 } ; a Function g.value(2) ; 4 g.value(134) ; 268

Un uso pi interessante delle funzioni prevede luso di argomenti: gli argomenti possono essere pensati come gli input delloggetto. Gli argomenti vengono deniti attraverso la parola riservata arg cui fanno seguito i nomi degli argomenti separati da una , e delimitati da un ;. Ad esempio la funzione g denita come { arg input; input*2 } (8): g accetta un argomento e restituisce il risultato delloperazione sullargomento. In particolare g restituisce il doppio del valore input che gli viene dato in entrata. La funzione g come un frullatore: si mette luovo input in entrata e il frullatore restituisce il risultato di frullatore.sbatti(uovo) in uscita.

4.859

1 3 5 7

h = { arg a, b; (a.pow(2)+b.pow(2)).sqrt } ; a Function c = h.value(4,3) ; 5

Inne la funzione h = { arg a, b; (a.pow(2)+b.pow(2)).sqrt } (20) un oggetto-modulo di calcolo che implementa il teorema di Pitagora: accetta in entrata i cateti a e b e restituisce lipotenusa c, secondo due la relazione c = a2 + b2 (24, 26). Si noti tra laltro come la radice quadrata sia un messaggio inviato allintero risultante dal calcolo della parentesi. In una seconda versione (qui di seguito) la denizione non cambia in sostanza ma permette di denire ulteriori aspetti. Al codice fa seguito una sessione della Post window in cui la funzione denita viene utilizzata.
1 2 3 4 5 6 7 ( h = { // calcola l'ipotenusa a partire dai cateti arg cat1, cat2 ; var hypo ; hypo = (cat1.pow(2)+cat2.pow(2)).sqrt ; "hypo: "++hypo } ; )

4.860

1 3 5 7 8

h.value(4,3) ; hypo: 5 h.value(4,3).postln.class hypo: 5 String

Si notino alcuni passaggi:

i commenti funzionano come al solito allinterno delle funzioni (2); i nomi degli argomenti seguono i criteri deniti per le variabili (3); a seguito degli argomenti possibile aggiungere una dichiarazione di variabili (4). Nel corpo della funzione, specie se complessa, pu essere utile avere a disposizione dei nomi di variabili. In questo caso, hypo un nome signicativo che permette di rendere pi leggibile lultima riga, in cui vi si fa riferimento ("hypo: "++hypo). Per le variabili valgono le osservazioni gi riportate. una funzione restituisce un unico valore (sia esso un numero, una stringa, un oggetto, un array, etc): il valore dellultima espressione denita nel corpo della funzione. Lultima espressione allora loutput della funzione. In particolare la funzione restituisce una stringa composta da "hypo: " a cui concatenato attraverso ++ il contenuto della variabile hypo. Ci che la funzione restituisce in questo caso una stringa (cfr. 5-8 della Post window).

Questultimo punto ha conseguenze di rilievo. Se si ridenisce h -nella sua prima versione- secondo quanto proposto dallesempio seguente, se ne altera radicalmente il funzionamento. Laggiunta dellespressione a

4.861

in coda alla denizione fa s che la funzone h restituisca in uscita a (cio il primo argomento in entrata (1)).
1 3 5 7 9 11 h = { arg a, b; (a.pow(2)+b.pow(2)).sqrt ; a } ; a Function h.value(4,3) ; 4 h.value(3,4) 3

In denitiva una funzione ha tre parti, tutte e tre opzionali, ma in ordine vincolante: 1. una dichiarazione degli argomenti (input) 2. una dichiarazione delle variabili (funzionamento interno) 3. un insieme di espressioni (funzionamento interno e output) Una funzione che ha solo la dichiarazione degli argomenti un oggetto che accetta entit in entrata, ma non fa nulla. La funzione i accetta a in entrata, ma alla dichiarazione degli argomenti non fanno seguito espressioni: la funzione non fa nulla e resituisce nil. Al suo caso limite, possibile anche una funzione che non ha nessuno dei tre componenti: ad esempio La funzione l (7) restituisce sempre e solo nil.

4.862

1 2 4 5 7 9 11 13

i = {arg a;} a Function i.value(3) nil l = {} a Function l.value nil

La situazione pu essere schematizzata come in gura 4.1, dove le funzioni sono rappresentate come moduli, che possono essere dotati di entrate ed uscite. Il testo in grassetto nellultima riga rappresenta il codice SC, il testo in corpo normale i possibili input e output.

arg a, b 5

arg a, b ... a+b

nil {}

nil { arg a, b; }

5 {5}

a+b { arg a, b; ... a+b }

Fig. 4.1

Funzioni.

Lintroduzione delle funzioni permette di affrontare il problema dellambito di visibilit delle variabili (scope). Nellesempio seguente seguente,

4.863

func.value restituisce 8 perch la variabile val, essendo dichiarata fuori della funzione func, vale anche al suo interno.
1 2 3 4 5 ( var val = 4 ; var func = { val*2 } ; func.value ; )

Qui di seguito invece func restituisce sempre 8, ma ci dipende dalla dichiarazione di val allinterno di func.
1 2 3 4 5 ( var val = 4 ; var func = { arg val = 4 ; val*2 } ; func.value ; )

Tant che lesempio seguente solleva un errore perch a val (dichiarata dentro func non assegnato valore.
1 2 3 4 5 ( var val = 4 ; var func = { arg val ; val*2 } ; func.value ; )

In sostanza, lambito di visibilit delle variabili procede dallesterno allinterno. Una variabile visibile ntanto che non viene lo stesso nome non dichiarato pi internamente.

4.964

4.9 Classi, messaggi/metodi e keyword

Si gi visto come in SC le classi siano indicate da una sequenza di caratteri che inizia con una maiuscola. Si osservi lesempio seguente di interazione con la Post Window:
1 2 3 4 5 6 7 9 10 11 12 13 14 15 superCollider ERROR: Variable 'superCollider' not defined. in file 'selected text' line 1 char 13 : superCollider nil SuperCollider ERROR: Class not defined. in file 'selected text' line 1 char 13 : SuperCollider nil

Come gi ricordato, un messaggio viene inviato a una classe a ad un oggetto con il .: rispettivamente per il tramite delle sintassi Classe.metodo e oggetto.metodo. I metodi possono in fondo essere pensati come funzioni denite per una classe o un oggetto: quando si invoca un metodo tramite un messaggio come se si mandasse un messaggio value a una funzione. In pi, anche i metodi possono avere argomenti che costituiscono i parametri di entrata. SC tipicamente prevede opportuni valori di default nel caso in cui il metodo richieda degli argomenti, cos che

4.965

in molti casi non necessario specicare gli argomenti. Luso delle keyword utile perch consente di selezionare di quale argomento si vuole specicare il valore, lasciando agli altri i valori predeniti. Laddove non si usino le keyword, lunico criterio disponibile per SC per poter attribuire un valore ad un argomento lordine in cui il valore si presenta nella lista degli argomenti. Ad esempio il metod plot per gli oggetti ArrayedCollection prevede gli argomenti
plot(name, bounds, discrete, numChannels, minval, maxval, parent, labels)

Il metodo crea una nestra e vi disegna sotto forma di spezzata un oggetto di tipo ArrayCollection. Largomento name denisce il titolo della nestra. Cos, la nestra creata da [1,2,3,1,2,3].plot("test") ha come titolo test. Il metodo consente anche di denire il numero dei canali numChannels. Se il numero superiore a 1, plot assume che i primi n campioni siano i campioni numero 1 dei canali 1 . . . n. Ad esempio, se i canali sono due, allora i primi due campioni sono i campioni numero 1 dei canali 1 e 2, e cos via: plot disegna una nestra per canale 15. Se si volesse specicare che numChannels deve essere pari a 2, senza keyword sarebbe necessario specicare anche gli argomenti precedenti. Ad esempio:
[1,4,3,2].plot("test", Rect(200 , 140, 705, 410), false, 2)

Assai pi agevolmente possibile scrivere:


[1,4,3,2].plot(numChannels:2)

15

Lutilit del metodo sta nel fatto che i segnali audio multicananle, rappresentabili attraverso un array, sono memorizzati in questa forma. Se il segnale stereo, plot con numChannels:2 disegna la forma donda dei segnali sui due canali.

4.1066

Inne, luso delle keyword in generale leggermente pi costoso da un punto di vista computazionale ma rende il codice molto pi leggibile.

4.10 Esempio

Un esempio di codice relativo alla creazione di un semplice elemento graco permette di introdurre i fondamenti della sintassi di SC. Il codice presentato si propone di creare un elemento graco a manopola, la cui rotazione controlli in forma parametrica il colore dello sfondo.

4.10.1 Il pattern GUI

La generazione di elementi GUI (Graphical User Interface) richiede di costruire oggetti GUI, cio di istanziare oggetti a partire da classi specializzate. Intuibilmente, ogni gestore graco denisce le sue classi. Ad esempio, se si vuole creare una nestra usando il gestore di Mac Cocoa, necessario istanziare un oggetto dalla classe SCWindow. Se invece (come avviene necessariamente su Windows) si vuole creare una nestra analoga usando SwingOSC bisogna invece utilizzare la classe JSCWindow. Intuibilmente, questa soluzione vincola la programmazione della GUI al gestore graco. SwingOSC denisce le sua classi a partire da Cocoa, che stato il primo gestore graco: come si vede nellesempio, aggiunge una J alle classi denite in Cocoa. Cos, una strategia di cerca e sostituisci permette di adattare il codice per usare uno dei due gestori. Sebbene praticabile, la soluzione non soddifacente:

4.1067

cerca e sostituisci lavora sulle stringhe e potrebbe introdurre errori non previsti ne risultano due copie del codice non vale per altri eventuali gestori graci

Tuttavia, si potrebbe anche osservare come tutti i gestori graci sostanzialmente offrano funzionalit simili: nestre, pulsanti, cursori, etichette, campi per limmissione di testo, e cos via. Si possono allora denire astrattamente le funzionalit che un gestore deve/pu offrire, per poi chiedere al gestore attualmente in uso (o disponibile) di realizzarle. Nellesempio precedente, ci che interessa una nestra,indipendentemente dla gestore graco. La classe GUI implementa questo comportamento, ed un esempio interessante di programmazione ad oggetti. Per costruire una nestra si potrebbe scrivere utilizzando la classe di Cocoa
1 w = SCWindow.new

Oppure SwingOSC:
1 w = JSCWindow.new

Invece, bene scrivere:


1 w = GUI.window.new

Che cosa succede nella riga precedente? La classe GUI si occupa, per cos dire, di inoltrare i messaggi successivi al gestore graco prescelto. Dunque, GUI riceve un messaggio window, e seleziona la classe opportuna presso il gestore graco attuale. A questa classe viene inviato il messaggio new. Si noti la differenza:

4.1068

1 3 5 7

w = GUI.window JSCWindow w = GUI.window.new a JSCWindow

Nel primo caso (1), SC risponde restituendo la classe che implementa la funzione astratta window (nestra) presso il gestore attuale, cio JSCWindow (3). Nel secondo caso (5), avviene la stessa cosa, ma attraverso il concatenamento dei messaggi, alla classe cos ottenuta si invia il messaggio new, che istanzia un oggetto attuale (7). Poich il gestore predenito su Mac Cocoa, in quel caso si avrebbe tipicamente:
1 3 5 7 w = GUI.window SCWindow w = GUI.window.new a SCWindow

Attraverso luso della classe GUI, pu anche cambiare il gestore graco, ma ci non richiede di cambiare il codice, che astrae la funzione (fare una nestra) rispetto allimplementazione (con SwingOSC, con con Cocoa, e cos via). Nel caso in cui si abbiano pi gestori disponibili, possibile selezionare il gestore attuale attraverso un messaggio alla classe GUI.

4.1069

1 2 4 5

GUI.cocoa CocoaGUI GUI.swing CocoaGUI

// seleziona Cocoa (Mac)

//selezione SwingOSC (Mac, Win, Linux)

Gli esempi successivi utilizzano sempre il metodo descritto.

4.10.2 Il codice

4.1070

1 2 4 6 7 9 10 11 13 14 15 17 18 19 20 21 22 23 24 25 27 28 29

( /* accoppiamento di view e controller */ var window, knob, screen ; // dichiarazione delle variabili usate // una finestra contenitore window = GUI.window.new("Knob", Rect(300,300,100, 100)) ; // una manopola nella finestra, range: [0,1] knob = GUI.knob.new(window, Rect(30, 30, 50, 50)) ; knob.value = 0.5 ; // una finestra-sfondo screen = GUI.compositeView.new(window,Rect(0,0, 150,150)); screen.background = Color.black; // azione associata a knob knob.action_({ arg v; var red, blue, green ; red = v.value ; green = red*0.5 ; blue = 0.25+(red*0.75) ; ["red, green, blue", red, green, blue].postln screen.background = Color(red, green, blue); }); // non dimenticarmi window.front ; )

4.10.3 Introduzione

4.1071

1: il blocco di codice racchiuso tra parentesi tonde (1 e 31); 3: un commento multilinea utilizzato a mo di titolo (3) e molti altri commenti che forniscono alcune informazioni sulle diverse parti del codice (ad esempio, 7). 5: a parte i commenti, il codice inizia con la dichiarazione delle tre variabili utilizzate;

4.10.4 Creazione degli elementi GUI

7-9: la prima cosa da fare creare una nestra contenitore, cio un oggetto di riferimento per tutti gli altri elementi graci che verranno creati in seguito. un approccio tipico nei sistemi di creazione di GUI. Alla variabile window viene assegnato un oggetto GUI.window, generato attraverso il metodo costruttore new. A new vengono passati due argomenti: una stringa che indica il titolo visualizzato dalla nestra ("Knob" e un oggetto di Rect, cio un rettangolo di 100x100 pixel, il cui angolo superiore sinistro nel pixel (300,300). 10-12: la costruzione di un elemento graco a manopola segue un procedimento analogo a quanto avvenuto per la nestra-contenitore. Alla variabile knob viene assegnato un oggetto GUI.knob (11). Il costruttore sembra uguale a quello di GUI.window: senonch questa volta necessario specicare a quale nestra-contenitore vada riferito loggetto GUI.knob: la nestra-contenitore window, e il rettangolo che segue prende come punto di riferimento non lo schermo, ma la nestra window. Dunque un rettangolo 50x50, la cui origine nel pixel (30,30) della nestra window. Il punto di partenza della manopola 0.5 (12). Per default lescursione di valori di un oggetto GUI.knob

4.1072

varia tra 0.0 e 1.0: dunque attraverso lattributo knob.value = 0.5 si indica la met.

14-16: si tratta ora di costruire una nestra da spalmare sul tutto lo sfondo di window, a cui far cambiare colore attraverso knob. La nestra screen un oggetto di tipo GUI.compositeView (14). Si noti che pi estesa della nestra-contenitore ( grande 150x150 pixel, a partire dal punto (0,0) di window): soltanto un trucco per essere sicuri che occupi tutto lo sfondo visibile (si provi a ridurre le dimensioni). Allattributo della nestra screen viene assegnato un colore attraverso un oggetto Color. Anche i colori sono oggetti in SC e la classe Color prevede alcuni attributi predeniti, che permettono di avere a disposizione i nomi pi comuni dei colori: ad esempio il nero (Color.black).

4.10.5 Interazione

18-26: dal punto di vista del design del codice screen rappresenta un display i cui attributi dipendono da knob, che opera come controller: si pu pensare a screen come ad un dispositivo di output che dipenda dallinput di knob. Alloggetto knob possibile associare un azione: previsto per denizione che lazione venga portata a termine tutte le volte che lutente cambia il valore di knob, cio muove la manopola. Una funzione rappresenta opportunamente questo tipo di situazione, poich come si visto un oggetto che denisce un comportamento richiamabile di volta in volta e parametrizzato da un argomento. Il metodo knob.action_ chiede di attribure a knob lazione seguente, descritta attraverso una funzione: la funzione tutto quanto compreso tra parentesi graffe, 19-26. Quanto

4.1073

avviene che, dietro le quinte, quando si muove la manopola alla funzione viene spedito un messaggio value. Il messaggio value chiede di calcolare la funzione per il valore della manopola, che linput della funzione: dietro le quinte cio viene inviato alla funzione il messaggio value(knob.value). Nella funzione linput descritto dallargomento v (19): in altre parole, la funzione risponde alla domanda cosa bisogna fare quando arriva un certo valore v dalla manopola knob.

20-25: il comportamento previsto richiede di cambiare il colore di sfondo di screen. Vengono dichiarate tre variabili (red, green, blue) (20). Esse identicano i tre componenti RGB del colore di screen, che SC denisce nellintervallo [0, 1]. A red viene assegnato il valore in entrata di v (21). A green e blue due valori proporzionali (ma in modo diverso) a v, in modo da denire un cambiamento continuo in funzione di v nelle tre componenti cromatiche. Quindi si dice di stampare su schermo un array composto da una stringa e dei tre valori (24): in questo caso SC stampa gli elementi dellarray sulla stessa riga opportunamente formattati. La stampa su schermo permette di capire come vengono calcolati i valori (ed utile senzaltro in fase di debugging). Inne, allattributo background di screen viene assegnato un oggetto Color, a cui sono passate le tre componenti. Il costruttore di Color, new accetta cio le tre componenti RGB in escursione [0,1] come denizione del colore da generare. Dov new? In generale tutti i metodi costruttori possono essere omessi per semplicare il codice: quando SC vede una classe seguita da una coppia di parentesi contenenti dati assume che si sia invocato Class.new(argomenti) . una delle molte abbreviazioni possibili in SC. Dunque, Color(red, green, blue) equivalente in tutto e per tutto a Color.new(red, green, blue).

4.1074

4.10.6 Per nire

29: importante. Tutti i sistemi GUI distinguono tra creazione e visualizzazione. Un conto creare gli oggetti GUI, un conto dire che debbano essere visibili: questa distinzione permette di fare apparire/sparire elementi GUI sullo schermo senza necessariamente costruire e distruggere nuovi oggetti. Il metodo front rende window e gli elementi che da essa dipendono visibili: in caso domissione tutto funziona uguale, ma nulla visualizzato sullo schermo.

4.11 Controlli di usso

In SC il usso delle informazioni segue lordine di lettura delle espressioni. I controlli di usso sono quei costrutti sintattici che possono modicare questordine. Ad esempio, un ciclo for ripete le istruzioni annidata per un certo numero di volte, e quindi procede sequenzialmente da l in avanti, un condizionale if valuta una condizione rispetto alla quale il usso di informazioni si biforca (se vero / se falso). I controlli di usso sono illustrati nellhelp le Control structures, da cui sono tratti (con una piccola modica) i tre esempi (rispettivamente if, while e for).

4.1175

1 2 3 4 5 7 8 9 10 12 14

( var a = 1, z; z = if (a < 5, { 100 },{ 200 }); z.postln; ) ( i = 0; while ( { i < 5 }, { i = i + 1; [i, "boing"].postln }); ) for (3, 7, { arg i; i.postln }); forBy (0, 8, 2, { arg i; i.postln });

Nel primo caso illustrato luso di if. La sintassi :


if ( condizione da valutare, se falso

funzione se vero

funzione

})

In altre parole la valutazione della condizione d origine a una biforcazione a seconda che il risultato sia true oppure false. Passando allesempio, la variabile a (che dichiarata anche se potrebbe non esserlo) vale 1. La condizione a < 5. Se la condizione vera, viene eseguita la funzione { 100 }, che restituisce 100, se falsa viene eseguita la funzione { 200 }, che restituisce 200. Poich la condizione vera, viene restituito il valore 100, che viene assegnato a z: z vale 100. Come noto, la traduzione in italiano di while (in computer science) nch:
while ({ condizione vera

}, {

funzione da eseguire

Nellesempio , i vale 0. Finch i inferiore a 5, viene chiamata la funzione seguente. La funzione incrementa i (altrimenti non si usicrebbe mai

4.1176

dal ciclo) ed esegue una stampa di un array che contiene i e la stringa "boing". Inne, il caso del ciclo for, che itera una funzione.
for (partenza, arrivo,

funzione

Nellesempio La funzione viene ripetuta cinque volte (3, . . . 7). Il valore viene passato alla funzione come suo argomento in modo che sia accessibile: la funzione infatti stampa i per ogni chiamata (3, . . . 7). Si noti che il fatto che largomento si chiami i del tutto arbitrario. Ovvero:
1 2 3 4 5 6 7 9 11 12 13 14 15 16 for (3, 7, { arg i; i.postln }); 3 4 5 6 7 3 for (3, 7, { arg index; index.postln }); 3 4 5 6 7 3

Listruzione ForBy richiede un terzo parametro che specica il passo:


forBy (partenza, arrivo, passo,

funzione

Lesempio una variazione del precedente che stampa lescursione [0, 8] ogni 2. Esistono altre strutture di controllo. Qui vale la pena di introdurre do, che itera sugli elementi di una collezione. Si pu scrivere cos:

4.1177

do ( collezione, funzione )

ma molto pi tipicamente la si scrive come un metodo denito sulla collezione. Ovvero:


collezione.do({ funzione

})

lesempio tratto dallhelp le Control-structures, con alcune piccole modiche.


1 3 5 [ 101, 33, "abc", Array ].do({ arg item, i; [i, item].postln; }); 5.do({ arg item; ("item"+item.asString).postln }); "you".do({ arg item; item.postln });

Se si valuta la prima riga si ottiene nella Post Window:


1 2 3 4 5 [ [ [ [ [ 0, 1, 2, 3, 1, 101] 33 ] abc ] class Array ] 2, abc, class Array ]

A scanso dequivoci, lultima riga semplicemente restituisce larray di partenza. Alla funzione vengono passati lelemento su cui sta effettuando literazione (item) e un contatore (i). Meglio ribadire: i nomi item e i sono totalmente arbitrari. il loro posto che ne specica la semantica. Ovvero:

4.1178

1 2 3 4 5 6

[ 101, 33, "abc", Array ].do({ arg moby, dick; [dick, moby].postln; }); [ 0, 101 ] [ 1, 33 ] [ 2, abc ] [ 3, class Array ] [ 101, 33, abc, class Array ]

La funzione stampa un array che contiene il contatore i (colonna di sinistra delle prime quattro righe) e lelemento item (colonna di destra). Il metodo do denito anche sugli interi (n volte.valuta la funzione). Il funzionamento illustrato nel secondo esempio. Se lo si esegue si ottiene:
1 3 4 5 6 7 8 5.do({ arg item; ("item"+item.asString).postln }); item item item item item 5 0 1 2 3 4

Lultima riga lintero su cui chiamato il metodo. La funzione stampa una stringa costituita dal concatenamento di "item" e della rappresentazione sotto forma di stringa restituta dal metodo asString chiamato sul numero intero item (0, . . . 4). Poich la maggior parte dei cicli for iterano a partire da 0 e con passo 1, molto spesso si trovano scritti in SC attraverso do. La sintassi di do (oggetto.metodo) pi OOP. Inne, lultimo esempio dimostra semplicemente che ogni stringa una collezione i cui elementi sono i singoli caratteri alfanumerici che compongo la stringa.

4.1279

4.12 Ancora un esempio GUI

Lesempio seguente presenta il codice di creazione di una GUI, utilizzando SwingOSC. Come nel caso precedente, lesempio doppiamente eccentrico: non utilizza in nessun modo laudio ed completamente inutile (. . .). In ogni caso, il controllo di elementi GUI particolarmente interessante per dimostrare alcuni aspetti della sintassi, e la visualizzazione del processo di aiuto alla comprensione. La widget risultante si compone di un certo numero di manopole. Ogni manopola controlla il colore dei quattro riquadri alla sua destra. Il valore della manopola viene passato ad una funzione che calcola il valore delle componenti rossa, verde e blu. I primi tre riquadri visualizzano soltanto le singole componenti (rossa, verde e blu), il quarto il colore complessivo risultante dal contributo di tutte e tre. Quando si muove la manopola, si ottengono due comportamenti diversi:

no ala met della sua escursione, la manopola attivata controlla le altre manopole (in sostanza, le manopole sono sincronizzate). oltre la met, ogni movimento della manopola innesca, in ognuna delle altre, la scelta di un valore casuale nellescursione possibile, a partire dal quale vengono calcolati i colori dei riquadri (secondo la stessa funzione)

Il processo illustrato in Figura 4.2, dove si rappresenta il caso in cui le manopole siano quattro e la manopola con cui interagisce lutente sia la numero 1 (la seconda dallalto). Poich oltre la meta, ad ogni scatto muove casualmente le altre tre (0, 1, 3).

4.1280

0 random 1

Red

Green

Blue

All

Fig. 4.2

Una GUI widget.

4.12.1 La funzione di generazione dei valori colore

Prima di tutto, si pu denire la funzione che mappa il valore della manopola su una tripla di colori.
1 2 3 4 5 6 7 colorFunction = { arg val ; var red, green, blue ; val = val.max(0).min(1) ; red = val ; green = (val*0.7)+0.3 ; blue = 1.0-(val*0.7) ; [red, green, blue]} ;

La funzione accetta un valore in entrata. I colori sono deniti in SC nellintervallo [0, 1]. La funzione assume perci genericamente che largomento val sia compreso in quellintervallo. Se cos non fosse, la riga 3 provvede a troncare ogni numero minore di 0 a 0 e ogni numero maggiore di 1 a 1.

4.1281

Si noti che si sarebbe potuto usare un doppio condizionale (if (val < 0 . . . ; if (val > 1 . . . ;) ma serebbe stato pi complesso da leggere e meno efciente computazionalmente. Le righe 4-6 deniscono una triplice correlazione tra val e le variabili che rappresentano le componenti rgb 16. Poich val compreso in [0, 1], lo sono anche i valori delle tre variabili. Inne (7), la funzione restituisce un array che contiene i tre valori. La funzione riceve dunque in entrata il valore selezionato dalla manopola (che tra laltro compreso nellintervallo [0, 1]) e restituisce una tripla i cui valori possono essere utilizzati per regolare il colore dei riquadri. Siccome deve essere richiamata pi volte, la funzione viene assegnata alla variabile colorFunction, che deve essere dichiarata.

0: se val negativo, max(0) restituisce 0, altrimenti val. Analogamente si comporta min(1): se val minore a 1 viene restituio val, altrimenti 1.

Il metodo max(0) restituisce il massimo tra il numero sui cui invocato e

4.12.2 Modularizzare la GUI

Si tratta ora di generare gli elementi GUI, che sono di due tipi: manopole (dalla classe JKnob e riquadri (qui si impiegano istanze di GUI.compositeView, di cui lunico attributo di rilievo il colore dello sfondo).
1 window = GUI.window.new("Some knobs", Rect(300,300, step*8, step*n)) ;

Il primo elemento semplicemente la nestra contenitore alla quale tutti gli elementi fanno riferimento (1), assegnata alla variabile window. Se

16

Il mapping qui non di rilievo: quello proposto del tutto empirico.

4.1282

si osserva la Figura 4.2 si nota come tutti gli elementi (manopole e riquadri) siano contenuti in un quadrato. In sostanza si trarra di costruire una griglia basata su un simile quadrato, assumendo perci come modulo il lato dello stesso. Si potrebbe blindare il lato denendolo in pixel ogni volta (ad esempio, 50 pixel). Poich si tratta di un modulo, un approccio pi essibile consiste nel denire una variabile step a cui viene assegnato un valore (ad esempio, 50), e nel richiamarla ogni qualvolta sia necessario. Un ragionamento analogo vale per il numero delle manopole, che non deve necessariamente essere 4, ma pu essere arbitrario: dunque si pu assegnarlo ad una variabile n. Quale dovr essere la dimensione di window? Laltezza dipende dal numero delle manopole, ognuna delle quali alta step: dunque, n step. Ogni gruppo manopola/riquadri occupa una riga: quattro riquadri a cui si pu aggiungere uno spazio equivalente per la manopola, per un totale di 8 step. Dunque il rettangolo occupato dalla nestra ha il suo vertice in alto a sinistra in (300, 300), largo step*8 ed alto step*n. La programmazione di uninterfaccia graca pu essere piuttosto noiosa e richiedere molte righe di codice. Se si dovessero gestire direttamente tutti gli elementi GUI previsti bisognerebbe allocare (e modicare) 5 n variabili (manopola+quattro riquadri per n). Un approccio pi intelligente (e molto usato in SC) consiste nel raccogliere gli elementi in un array. Questo permette di generare gli elementi stessi sfruttando i metodi di costruzione della classe . Il problema di richiamare ogni oggetto (manca infatti una variabile) pu essere risolto attraverso lindice dellarray, che di fatto identica in termini univoci loggetto. Una simile tecnica permette di generare gli elementi GUI anche in assenza di un numero predenito. Pi concretamente, si consideri la riga 1. Larray screenArrRed contiene tutti gli n elementi di tipo GUI.compositeView che rappresentano la componente rossa. Attraverso il metodo fill viene costruito un array che contiene riquadri (viene riempito di istanze

4.1283

GUI.compositeView generate da new: ogni riquadro un quadrato di dimensioni step step. Laspetto pi interessante il posizionamento: ogni riquadro ha infatti una ascissa ssa (step*4, inizia dalla seconda met della nestra window), mentre lordinata varia in funzione dellargomento i, che lindice progressivo 0 . . . n 1. La riga disegna insomma la prima colonna di riquadri, che sono memorizzati nellarray con gli indici 0 . . . n 1. Analogamente avviene per le altre tre colonne, i cui elementi di ognuna sono contenuti in un array dedicato. Inne, larray delle manopole. Il ragionamento del tutto analogo, ma con una aggiunta decorativa: il posizionamento orizzontale varia pseudocasualmente nellescursione [0, step 3]. Se step = 50px allora lascissa dellangolo in alto a sinistra pu valere al massimo 150px. Se vi si aggiunge il lato (= step) del quadrato si copre la met della larghezza della nestra window (200px).

4.1284

1 2 3 5 6 7 9 10 11 13 14 15 17 18 19

screenArrRed = Array.fill(n, { arg i ; GUI.compositeView.new(window,Rect(step*4, step*i, step,step)); }) ; screenArrGreen = Array.fill(n, { arg i ; GUI.compositeView.new(window,Rect(step*5, step*i, step, step)); }) ; screenArrBlue = Array.fill(n, { arg i ; GUI.compositeView.new(window,Rect(step*6, step*i, step, step)); }) ; screenArrRGB = Array.fill(n, { arg i ; GUI.compositeView.new(window,Rect(step*7,step*i, step,step)); }) ; knobArr = Array.fill(n, { arg i ; GUI.knob.new(window, Rect(rrand(0,step*3), step*i, step, step)) ; }) ;

La situazione schematizzata in Figura 4.3, dove sono evidenziati i cinque array: ognnuno rappresenta una colonna). Allo stesso indice corrispondono poi gli elementi comuni correlati alla manopola. Dunque, la dichiarazione delle variabili inne la seguente:
1 2 3 var colorFunction ; var window, step = 50, n = 4 ; var knobArr, screenArrRed, screenArrGreen, screenArrBlue, screenArrRGB ;

4.1285

0 1 2 3 4 5 6 7 8 9
knobArr screenArrRed screenArrGreen screenArrBlue screenArrRGB

Fig. 4.3

Struttura della GUI widget.

4.12.3 Controllo delle azioni

Si tratta ora di collegare ad ogni manopola lazione opportuna. In particolare lazione deve prevedere due parti:

4.1286

1. il calcolo della funzione colore per il valore della manopola e laggiornamento dei colori dei riquadri; 2. laggiornamento delle altre manopole e dei rispettivi riquadri, in funzione della soglia 0.5. In primo luogo, va osservato come la stessa azione debba essere associata a tutte le manopole. Poich le manopole sono raccolte in un array, che una sottoclasse di Collection possibile utilizzare il metodo do e ciclare sugli elementi di knobArr (cio: su tutti gli oggetti JKnob). bene ribadire il punto: in un array lordine degli elementi vincolante e dunque attraverso lindice dellelemento possibile identicare univocamente ogni elemento. La funzione contenuta in do ha due argomenti: il primo lelemento, il secondo lindice dellelemento (ovvero un numero progressivo a partire da 0) - nel codice indicati da knob e da index. Dunque, il valore di partenza di ogni manopola viene impostata a 0.5 (knob.value = 0.5). Nella denizione del metodo action_ largomento (qui k) indica listanza della manopola: dunque, k.value accede al valore selezionato dallutente. Nella denizione sono presenti due parti.
1 2 3 4 6 7 8 9 10 ).postln ; rgb red green blue = = = = colorFunction.value(k.value) ; rgb[0] ; rgb[1] ; rgb[2] ;

screenArrRed[index].background = Color(red,0,0) ; screenArrGreen[index].background = Color(0,green,0) ; screenArrBlue[index].background = Color(0,0,blue) ; screenArrRGB[index].background = Color(red,green,blue) ; ("operating on knob: "+index +" with: "+rgb.round(0.001)

4.1287

Nella prima viene descritto il punto 1: con il calcolo della funzione colore a partire dal valore assunto dalla manopola vengono assegnate le tre componenti alle variabili red, green, blue. Si ricorder che index lindice dellelemento manopola. Allo stesso indice corrispondono per costruzione negli array dei riquadri i rispettivi elementi. Cio, ad esempio lindice 3 seleziona nellarray delle manopole knobArr quella correlata ai riquadri con indice 3 nei quattro array screenArrRed, screenArrGreen, screenArrBlue, screenArr A questo punto, per ognuno dei riquadri viene denito il valore opportuno di colore per la proprit background (ununica componte per i primi tre, tutte per lultimo). Lultima riga stampa sulla Post Window alcuni dati che permettono di controllare il risultato del processo 17.
1 2 indexArr = Array.series(n) ; indexArr.removeAt(index) ;

Lidea che, mentre si ruota la manopola, le altre manopole girino anchesse e aggiornino i riquadri. A parte la manopola attivamente controllata dallutente, tutte le altre manopole devono eseguire il comportamento di aggiornamento di nuovo il caso di denire un ciclo, come quello in cui lazione contenuta, ma denito su tutte le manopole a parte quella attiva. Larray indexArr contiene una serie numerica 0 . . . n 1, ovvero tutti gli indici degli array (1). Da questo array necessario eliminare lindice relativo alla manopola che che sta controllando il processo: si tratta dellargomento index. Il metodo removeAt rimuove lelemento specicato, stringe la collezione su cui invocato, ma restituisce lelemento stesso: quindi in questo caso non si effettua la
17

una funzionalit molto utile in fase di debugging. Si tenga presenta che pu essere computazionalmente pi costosa di quanto immaginato.

4.1288

riassegnazione dellarray (indexArr = indexArr.removeAt(index) sarebbe uguale a indexArr = index). A questo punto, sui restanti n 1 elementi di tuti gli array necessario denire lazione di aggiornamento.
1 2 3 4 5 6 7 8 9 10 11 12 14 15 16 18 19 20 21 22 23 24 26 indexArr.do({ arg index ; var iknob, ired, igreen, iblue, itotal ; var rgb ; var chosenValue ; iknob = knobArr[index] ; ired = screenArrRed[index] ; igreen= screenArrGreen[index] ; iblue = screenArrBlue[index] ; itotal = screenArrRGB[index] ; if ( knob.value < 0.5, { chosenValue = knob.value ; }, { chosenValue = 1.0.rand ; } ); iknob.value = chosenValue ; rgb = colorFunction.value(chosenValue) ; ired.background = Color(rgb[0], 0, 0) ; igreen.background = Color(0, rgb[1], 0) ; iblue.background = Color(0, 0, rgb[2]) ; itotal.background = Color(rgb[0], rgb[1], rgb[2]) ; ("knob no. "+index.asString+": "+rgb.round(0.001)).postln; });

In primo luogo qui va notato come index sia il nome di un argomento e dunque non indichi pi lindice dellelemento esterno: una questione di ambito di validit delle variabili. Il ciclo denito per ogni index in indexArray. Le righe 5-9 attribuiscono a variabili gli elementi numero

4.1289

index degli array di manopole e riquadri: in sostanza viene elaborato il gruppo (la la) index. Il condizionale valuta il valore della manopola attiva knob, il cui valore, essendo denito fuori del ciclo, accessibile dentro il ciclo. Se knob.value inferiore a 0.5, allora il valore prescelto (chosenValue) quello di knob, altrimenti un valore scelto a caso nellintervallo [0, 1]. Il valore prescelto viene attribuito alla manopola index passato a colorFunction: i valori rgb cos ottenuti permettono di denire la propriet background dei riquadri index Inne, di nuovo una stampa su schermo dei dati calcolati (26).

4.1290

1 3 4 5 6 7 9 10 11

( /* The useless GUI: demonstrating some syntax stuff first part */ var colorFunction ; var window, step = 50, n = 4 ; var knobArr, screenArrRed, screenArrGreen, screenArrBlue, screenArrRGB ; colorFunction = { arg val ; var red, green, blue ; val = val.max(0).min(1) ; red = val ; green = (val*0.7)+0.3 ; blue = 1.0-(val*0.7) ; [red, green, blue]} ; window = GUI.window.new("Some knobs", Rect(300,300, step*8, step*n)) ; screenArrRed = Array.fill(n, { arg i ; GUI.compositeView.new(window,Rect(step*4, step*i, step,step)); }) ; screenArrGreen = Array.fill(n, { arg i ; GUI.compositeView.new(window,Rect(step*5, step*i, step, step)); }) ; screenArrBlue = Array.fill(n, { arg i ; GUI.compositeView.new(window,Rect(step*6, step*i, step, step)); }) ; screenArrRGB = Array.fill(n, { arg i ; GUI.compositeView.new(window,Rect(step*7,step*i, step,step)); }) ; knobArr = Array.fill(n, { arg i ; GUI.knob.new(window, Rect(rrand(0,step*3), step*i, step, step)) ; }) ; 4.1291

13 14 15 16 17 18 19 21 23 24 25 27 28 29 31 32 33 35 36 37 39 40 41

4.1292

1 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44

/* The useless GUI: continue */ knobArr.do( { arg knob, index; knob.value = 0.5 ; knob.action_({ arg k; var indexArr ; var rgb, red, blue, green ; rgb = colorFunction.value(k.value) ; red = rgb[0] ; green = rgb[1] ; blue = rgb[2] ; screenArrRed[index].background = Color(red,0,0) ; screenArrGreen[index].background = Color(0,green,0) ; screenArrBlue[index].background = Color(0,0,blue) ; screenArrRGB[index].background = Color(red,green,blue) ; ("operating on knob: "+index +" with: "+rgb.round(0.001) ).postln ; indexArr = Array.series(n) ; indexArr.removeAt(index) ; indexArr.do({ arg index ; var iknob, ired, igreen, iblue, itotal ; var rgb ; var chosenValue ; iknob = knobArr[index] ; ired = screenArrRed[index] ; igreen= screenArrGreen[index] ; iblue = screenArrBlue[index] ; itotal = screenArrRGB[index] ; if ( knob.value < 0.5, { chosenValue = knob.value ; }, { chosenValue = 1.0.rand ; } ); iknob.value = chosenValue ; rgb = colorFunction.value(chosenValue) ; ired.background = Color(rgb[0], 0, 0) ; igreen.background = Color(0, rgb[1], 0) ; iblue.background = Color(0, 0, rgb[2]) ; itotal.background = Color(rgb[0], rgb[1], rgb[2]) ; ("knob no. "+index.asString+": "+rgb.round(0.001)).postln; }); }) ; 4.1293 }) ; window.front ; )

4.1294

5 Sintesi, I: fondamenti

Un suono una variazione continua della pressione atmosferica percepibile dallorecchio umano. In quanto vibrazione, dipende dalleccitazione di corpi del mondo sico (una chitarra suonata con un plettro, una tenda agitata dal vento, un tavolo battuto con le nocche). Un suono pu essere registrato in forma di segnale analogico -cio rappresentato in forma continua- o digitale -in forma numerica. Si osservino le due gure dedicate alla catena dellaudio analogico e digitale (Figura 5.1).
es. piatto
Onda di pressione Lettura Onda di pressione

DAC
Conversione

trasduzione elettroacustica
Microfono

es. disco vinile


Supporto

Elaborazione analogica Microfono

trasduzione elettroacustica

es. hard disk


Supporto

Elaborazione analogica

Elaborazione analogica

Altoparlante

Elaborazione analogica

Altoparlante

trasduzione elettroacustica
Scrittura Onda di pressione Conversione

trasduzione elettroacustica DIGITALE


Onda di pressione

Master

ADC

Fig. 5.1

Catena dellaudio: analogico e digitale

Nel caso dellaudio digitale, il segnale, appunto digitalizzato, disponibile sotto forma di informazione numerica, e dunque pu essere rielaborato attraverso un calcolatore: si possono cio fare calcoli a partire dai

95

numeri che rappresentano il segnale. I passi principali della registrazione digitale sono i seguenti: 1. conversione anologico-digitale: il segnale analogico viene ltrato e convertito dal dominio analogico (in quanto variazione continua della tensione elettrica, ad esempio prodotta da un microfono) nel formato numerico attraverso lADC; 2. elaborazione: il segnale digitalizzato, che ha assunto la forma di una sequenza di numeri, pu essere modicato; 3. conversione digitale-analogica: per essere ascoltabile, la sequenza numerica che compone il segnale viene riconvertita in segnale analogico attraverso il DAC: effettuato il ltraggio, si origina di nuovo una variazione continua della tensione elettrica che pu, ad esempio, mettere in azione un altoparlante. Lassunto di partenza della computer music che il calcolatore possa essere impiegato per sintetizzare direttamente il suono. Il cuore della sintesi digitale sta nellescludere il passaggio 1, generando direttamente la sequenza numerica che poi verr convertita in segnale analogico. Questo non esclude affatto la possibilit di lavorare con campionamenti provenienti da fonti esterne, ma sottolinea piuttosto come laspetto fondamentale risieda nei metodi e nelle procedure di calcolo che presiedono alla sintesi. Se dunque sempre possibile per il compositore digitale lavorare sulla componente analogica (ad esempio registrando ed elaborando analogicamente il segnale), la componente pi caratterizzante della sua prassi sta nello sfruttare la natura numerica (e dunque calcolabile) del segnale digitale (Figura 5.2.

5.196

Onda di pressione

Onda di pressione

trasduzione elettroacustica
Microfono

es. disco vinile


Microfono Supporto

trasduzione elettroacustica

es. hard disk


Supporto

Elaborazione analogica

Elaborazione analogica

Scrittura

Conversione

DIGITALE

Master

ADC

Fig. 5.2

Composizione analogica e digitale: ambiti

5.1 Poche centinaia di parole dacustica

Un segnale una rappresentazione di un andamento temporale. Poich rappresenta una sequenza di compressioni/rarefazioni della pressione atmosferica, un segnale audio assume la forma di una oscillazione tra valori positivi e valori negativi. Se questoscillazione regolare nel tempo, il segnale periodico, altrimenti aperiodico: la maggior parte dei suoni si colloca in punto tra i due estremi, cio pi o meno periodica/aperiodica. Il segnale periodico pi elementare la sinusoide, corrispondente al suono di un diapason. Come si vede in Figura 5.3, collegando un pennino alla lamella del diapason se ne registra lescursione ed il tracciato che ne consegue appunto una sinusoide. La periodicit indica che il segnale si ripete nel tempo: la frequenza di un segnale il numero delle ripetizioni nellunit di tempo mentre la durata di un ciclo il periodo. Lescursione tra il massimo positivo e quello

5.197

Fig. 5.3

Vibrazione di un diapason e sinusoide

negativo lampiezza del segnale. Le frequenze udibili sono (approssimativamente) comprese tra i 16 e i 25.000Hz (Hz -Herz- indica numero dei cicli al secondo, anche cps). Il teorema di Fourier stabilisce che ogni segnale periodico pu essere rappresentato come una somma di sinusoidi di diversa ampiezza: come se un numero (teoricamente innito) di sinuosoidi di diverso volume suonassere tutte insieme. Se si rappresenta un segnale non nel tempo ma in frequenza si ottiene il suo spettro: a partire dal teorema di Fourier, si pu osservare come lo spettro di ogni segnale complesso (non sinusoidale) sia costituito di molte componenti di frequenza diversa. In un segnale periodico queste componenti (dette armoniche) sono multipli interi della frequenza fondamentale (che ne il massimo comun denominatore). Segnali di questo tipo sono ad esempio londa a dente di segna, londa quadra, londa triangolare, ed in generale la fase stazionaria di tutti i segnali ad altezza musicale ricononscibile (intonabile). In un segnale aperiodico le componenti possono essere distribuite in frequenza in maniera arbitraria. Quando si parla (molto vagamente) di rumore spesso (ma non sempre) si indicano segnali aperiodici. Un segnale digitale una rappresentazione numerica di un segnale analogico ed doppiamente discreto: rappresenta cio variazioni dampiezza discrete (quantizzazione) in istanti discreti di tempo (frequenza, o meglio tasso di campionamento). Pu essere cio pensato come una griglia che viene sovrapposta ad un segnale analogico (Figure 5.4 e5.5).

5.198

segnale analogico 1 1

campionamento

1 0 Time (s) quantizzazione 1 0.00625

1 0 Time (s) campionamento + quantizzazione 1 0.00625

1 0 Time (s) 0.00625

1 0 Time (s) 0.00625

Fig. 5.4 Digitalizzazione del segnale. Forma donda, campionamento, quantizzazione, campionamento e quantizzazione. Sebbene rappresentato nei software tipicamente come un curva continua, il segnale digitale dunque una sequenza ordinata di impulsi (si vedano diverse rappresentazioni possibili in Figura 5.5).
1 1 1 0.5 0.5 0.5

0.5

0.5

0.5

1 0 20 40 60 80 100

1 0 20 40 60 80 100

1 0 20 40 60 80 100

Fig. 5.5

Segnale digitale: rappresentazioni.

Un segnale descrivibile, nel caso discreto, attraverso una funzione matematica la funzione indica che per ogni istante discreto di tempo x il segnale ha il valore dampiezza y . Un segnale digitale una sequenza di caselle x0 , x1 , x2 , . . . a cui corrispondono valori dampiezza y0 , y1 , y2 , . . ..

y = f [x]

5.199

La struttura dati che rappresenta un segnale allora tipicamente un array, una sequenza di celle di memoria consecutive e omogenee, contenenti cio lo stesso tipo di dati (il tipo numerico prescelto per il segnale in questione). Cos ad esempio un array come [0, 0.5, 0, -0.25, 0, 0.1, -0.77, 0.35] descrive un segnale composto da 8 campioni (Figura 5.6), dove lindice (il numero dordine che etichetta progressivamente ognuno degli otto valori) rappresenta x inteso come istante di tempo, mentre il dato numerico associato rappresenta y , inteso come il valore dampiezza del segnale nellistante x:

0.5

x=0y=0 x=1y=
7

... x=
0.35
1

0.5

0.5

1 0 2 4 6 8

Fig. 5.6

[0, 0.5, 0, -0.25, 0, 0.1, -0.77, 0.35]

5.2100

5.2 Algoritmi di sintesi

Un algoritmo per la sintesi del suono una procedura formalizzata che ha come scopo la generazione della rappresentazione numerica di un segnale audio. Il linguaggio SC (sclang) permette di sperimentare algoritmi di sintesi del segnale in tempo differito senza scomodare -per ora- il server audio (scsynth). Ad esempio, la classe Array risponde al messaggio plot generando una nestra e disegnandovi la curva spezzata ottenuta congiungendo i valori contenuti nellarray. Il metodo plot implementato non solo nella classe Array ma in altre, ed estremamente utile per capire il comportamento dei segnali su cui si sta lavorando. Il codice [0, 0.5, 0, -0.25, 0, 0.1, -0.77, 0.35].plot("an array") genera lelemento graco 5.7.

Fig. 5.7

[0, 0.5, 0, -0.25, 0, 0.1, -0.77, 0.35]

5.2101

Poich il segnale audio previsto per i CD audio campionato a 44.100 Hz (attualmente lo standard audio pi diffuso), se si vuole generare un segnale mono della durata di 1 secondo a qualit CD, necessario costruire un array di 44.100 posti: il processo di sintesi del segnale consiste allora nel denire ed implementare un algoritmo che permetta di riempire ognuno di questi posti (letteralmente numerati) con un valore. Cos, se si vuole generare un segnale sinusoidale puro, il metodo pi semplice consiste nel calcolare lampiezza y per ogni campione x del segnale in accordo con la funzione del seno e nellassociare il valore y allindice x dellarray A, che rappresenta S . Una funzione periodica si denisce come segue:

y = f (2 x)
Un segnale sinusoidale descritto dalla formula:

y = a sin(2 k x)
Leffetto dei parametri a e k rappresentato in Figura 5.8, dove disegnato (in forma continua) un segnale (discreto) composto da 1000 campioni. Lalgoritmo di sintesi per un segnale sinusoidale, scritto in pseudo-codice (ovvero in un linguaggio inesistente ma essenziale), dunque il seguente:
1 2 3 Per ogni x in A: y = a*sin(k*x) A[x] = y

dove la riga 1 del ciclo calcola il valore y in funzione di due parametri a e k che controllano lampiezza e la frequenza della sinusoide, mentre la seconda riga assegna allindice x di A il valore y. SC permette agevolmente di implementare un simile algoritmo. Una classe utile a tal ne Signal,

5.2102

0.5

0.5

0.5

0.5

1 0 200 400 600 800 1000

1 0 200 400 600 800 1000

a = 1, k = 1/1000
1 1

a = 1, k = 2/1000

0.5

0.5

0.5

0.5

1 0 200 400 600 800 1000

1 0 200 400 600 800 1000

a = 0.5, k = 1/1000
Fig. 5.8

a = 0.5, k = 2/1000 Sinusoide e variazione dei parametri a e k .

che una sottoclasse di ArrayedCollection (la superclasse pi generale degli oggetti array-like) specializzata per la generazione di segnali. Nel codice, la riga 3 assegna a sig un array Signal di 44100 celle, ovvero un secondo di segnale audio mono a qualit CD. Le righe 4-7 sono occupate da un ciclo: sig.size restituisce la dimensione dellarray (44100). Per sig.size (44100) volte viene valutata la funzione nel ciclo do: poich x rappresenta lincremento lungo larray (che rappresenta il tempo, ovvero 0, 1, 2 . . . 44098, 44099), in funzione di x che viene calcolato il valore della funzione (f [x]). Il valore della frequenza del segnale desiderato indica il numero di cicli (2 ) al secondo: se la frequenza desiderata 440 Hz ( cicli al secondo) allora ci devono essere 440 2 cicli ogni secondo (2*pi*freq). Questo valore deve essere calcolato per tutti i

5.2103

posti dellarray (x/44100). Ottenuto il valore val, questo sar compreso (per denizione trigonometrica) nellescursione [1, 1] e dunque pu essere scalato per amp. La riga 6 assegna al posto x di sig il valore val. Si noti che per sicurezza in SC sempre meglio, quando si usano gli array, riassegnare alla variabile che contiene larray: sig = sig.put(x, val) indica che sig uguale a se stesso, ma con il posto x occupato da val. Il segnale viene quindi disegnato nellescursione dampiezza [1, 1] (8). Inne, possibile invocare il metodo play su un oggetto Signal: SC svolge tutte le operazioni necessarie per poter ascoltare il segnale (il come lo si vedr poi): largomento true specica che lesecuzione avviene in loop.
1 2 3 4 5 6 7 8 10 12 14 15 16 17 19 21 22 ( var sig, amp = 0.75, freq = 440, val ; sig = Signal.newClear(44100) ; sig.size.do({ arg x ; val = amp*sin(2*pi*freq*(x/44100)) ; sig = sig.put(x, val) ; }) ; sig.plot(minval:-1, maxval:1]) ; sig.play(true) ; ) ( var sig, amp = 0.75, freq = 440 ; sig = Signal.newClear(44100) ; sig.waveFill({ arg x ; amp*sin(x) }, 0, 2pi*freq) ; sig.plot(minval:-1, maxval:1]) ; sig.play(true) )

5.2104

La classe Signal prevede molte possibilit di elaborazione. Ad esempio lalgoritmo precedente in realt pi agevolmente implementabile utilizzando il metodo waveFill che permette di riempire il segnale con il risultato di una funzione ({ arg x ; amp*sin(x) }) che viene calcolato tra 0 e 2pi*440. In altre parole, la funzione viene calcolata per 440 volte (440 cicli, 2 ) a partire dallindice 0. Si noti come nel caso di costanti denite in SC il segno di moltiplicazione possa essere omesso: si pu scrivere 2pi invece che 2*pi.
1 2 3 5 6 ( var sig, amp = 0.75, freq = 440 ; var soundFile ; sig = Signal.newClear(44100) ; sig.waveFill({ arg x, i; amp*sin(x) }, 0, 2pi*440) ; // 1 cycle for 440 times soundFile = SoundFile.new ; soundFile.headerFormat_("AIFF").sampleFormat_("int16").numChannels_(1) ; soundFile.openWrite("sounds/signalTest.aiff") ; soundFile.writeData(sig) ; soundFile.close ; )

8 9 10 11 12 13

Il segnale ottenuto pu essere salvato su hard disk: Signal pu cos essere utilizzato per generare materiali audio utilizzabili in seguito. Sempre dal lato client (in sclang) disponible la classe SoundFile, che crea un le audio (8), di cui sono specicabili le propriet (9): il tipo ("AIFF"), la

5.2105

quantizzazione (16 bit, "int16"), il numero di canali (mono, 1) 18. importante specicare la quantizzazione perch SC internamente (e per default) lavora a 32 bit: un formato utile per la precisione interna ma piuttosto scomodo come formato di rilascio nale. Dopo aver create loggetto ti tipo le necessario specicare il percorso del le richiesto (10). A questo punto si possono scrivere sul le i dati contenuti nellarray sig (11). Ad operazioni concluse, il le deve essere chiuso (12). Anche nel caso in cui la funzione sia diversa, si tratta di una procedura di estrema semplicit concettuale. Ad esempio, possibile generare altri segnali periodici gi ricordati. Per denizione, un onda a dente di sega un segnale periodico che ha innite armoniche di frequenza f n, dove f la frequenza fondamentale e n = 2, 3, 4, . . ., e di ampiezza rispettivamente pari a 1/2, 3, 4 . . . (ovvero inversamente proporzionale al numero di armonica). Il metodo sineFill di Signal permette agevolmente di osservare questo comportamento. I suoi argomenti sono 1. la dimensione 2. un array che specica una serie di ampiezze 3. un array che specica una serie di fasi Ampiezze e fasi sono riferite alle armoniche del segnale sinusoidale. Ad esempio, un array dampiezze [0.4, 0.5, 0, 0.1] indica che verranno calcolate le prime 4 armoniche, dove f2 avr ampiezza 0.4, f3 0.5 e cos via. Si noti che per eliminare una componente armoniche sufciente passare un valore dampiezza 0 ( il caso di f4 . Il le di help propone il codice:
Signal.sineFill(1000, 1.0/[1,2,3,4,5,6])

Il codice genera un array di 1000 punti e lo riempie con una sinusoide e con le sue prime 5 armoniche, come si vede in Figura 5.9.
18

Si noti il concatenamento dei messaggi: ognuno dei metodi restituisce infatti loggetto stesso.

5.2106

0.5

0.5

1 0 200 400 600 800 1000

Fig. 5.9

Prime sei armoniche dispari.

La sintassi 1.0/[1,2,3,4,5,6] interessante. Se la si valuta, la Post Window restituisce:


1 2 1.0/[1,2,3,4,5,6] [ 1, 0.5, 0.33333333333333, 0.25, 0.2, 0.16666666666667 ]

Cio: un numero diviso un array restituisce un array in cui ogni ogni elemento pari al numero diviso allelemento di partenza. In altre parole come scrivere [1.0/1, 1.0/2, 1.0/3, 1.0/4, 1.0/5, 1.0/6]. Larray contiene dunque una serie di 6 ampiezze inversamente proporzionali al numero di armonica. Il segnale risultante approssima infatti unonda a dente di sega 19 (Il contributo delle 6 armoniche visibile nel numero delle gobbe che il segnale presenta). Nellesempio seguente lapprossimazione decisamente migliore. Il metodo series, denito per Array, crea un array ed ha come argomenti size -la dimensionestart e step: larray riempito da una serie di size interi successivi che, iniziando da start, proseguono incrementando di step. Dunque,

19

Qui non si considera la fase, ma il discorso analogo.

5.2107

larray contiene i valori 1, 2, 3, . . . 1000. Il segnale sig genera una sinusoide e i suoi primi 999 armonici superiori con valore inversamente proporzionale al numero darmonica.
1 2 3 4 5 7 9 ( var sig, arr ; arr = Array.series(size: 1000, start: 1, step: 1) ; sig = Signal.sineFill(1024, 1.0/arr) ; sig.plot ; sig.play(true) )

Unonda quadra pu essere generata nello stesso modo dellonda a dente sega, ma aggiungendo soltanto le armoniche dispari (n = 1, 3, 5 . . .). La stessa cosa: unonda a dente di sega in cui le armoniche pari hanno ampiezza nulla. Il codice riportato nellesempio seguente (anche Figura 5.10, a).

5.2108

1 2 3 4 5 6 7 8 10 11 13 15

( var sig, arr, arr1, arr2 ; arr1 = Array.series(size: 500, start: 1, step: 2) ; arr1 = 1.0/arr1 ; arr2 = Array.fill(500, 0) ; // arr2 = Array.fill(500, 0) ; arr = [arr1, arr2].flop.flat ; // arr = [arr1, arr2].lace(1000) ; sig = Signal.sineFill(1024, arr) ; sig.plot ; sig.play(true) )

Le ampiezze delle armoniche pari devono essere pari a 0, quelle dispari inversamente proporzionali al loro numero dordine. Larray arr1 larray delle ampiezze delle armoniche dispari. Si noti che step: 2, e che arr1 gi opportunamente scalato (4). Larray arr2 (5) creato con il metodo fill che riempie un array della dimensione voluta (qui 500) valutando per ogni posto la funzione. Siccome necessario un array costituito da zeri, la funzione deve semplicemente restituire 0. La riga 6 crea il nuovo array arr, ed pi interessante, poich fa uso dei metodi flop e flat. Si veda lesempio dalla Post Window seguente:

5.2109

1 3 5 7 9 11 13 15

a = Array.fill(10, 0) ; [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] b = Array.fill(10, 1) ; [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ] c = [a,b] ; [ [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ] ] c = c.flop ; [ [ 0, 1 ], [ 0, 1 ], [ 0, 1 ], [ 0, 1 ], [ 0, 1 ], [ 0, 1 ], [ 0, 1 ], [ 0, 1 ], [ 0, 1 ], [ 0, 1 ] ] c = c.flat ; [ 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1 ]

17 19

Dopo aver creato due array di 10 posti (1, 5), viene create un nuovo array c che contiene i due array a e b come elementi (9). Il metodo flop (13) interallaccia coppie di elementi dai due array (si veda 15). Il metodo flat appiattisce un array eliminando tutte le parantesi: si perde la struttura in sotto-array degli elementi. Rispetto alla riga 9 il risultato unalternanza di elementi da uno e dallaltro array (da a e da b). Nellesempio relativo allonda quadra il risultato della riga 6 unalternanza di elementi di arr1 e di zeri (provenienti da arr2). Come accade molto spesso, loperazione in realt praticabile in SC pi semplicemente attraverso un metodo dedicato, lace (7, commentata): lace(1000) restituisce un array di dimensione 1000 pescando alternativamente da arr1 e arr2. Il risultato disegnato in Figura 5.10, b.

5.2110

0.5

0.5

0.5

0.5

1 0 200 400 600 800 1000

1 0 200 400 600 800 1000

a Fig. 5.10

b Onda a dente di sega e onda quadra.

5.3 Nota sul metodo play

Il metodo play offre la possibilit di ascoltare il contenuto delloggetto Signal (come ci avvenga nel dettaglio lo si vedr poi). Ci si pu chiedere a quale frequenza. In tempo reale SC per default genera un segnale con un tasso di campionamento (sample rate, sr) pari a 44.100 campioni al secondo. Il contenuto dellarray, dopo essere stato messo in un buffer (una locazione di memoria temporanea) viene letto alla frequenza di 44.100 campioni al secondo. In altri termini, SC preleva un valore dal buffer ogni 1/44.100 secondi. Con il metodo play(true) lesecuzione in loop: una volta arrivato alla ne, SC riprende da capo. Dunque, se size la dimensione in punti dellarray, il periodo del segnale (quanto dura in secondi) size/sr , e la frequenza il suo inverso: 1/size/sr = sr/size. Se size = 1000, allora f = 44100/1000 = 44.1Hz Viceversa, se si intende ottenere un segnale la cui fondamentale sia f , la dimensione dellarray deve essere size = sr/f . un calcolo soltanto approssimativo perch size deve essere necessariamente intero. Si consideri di nuovo lesempio:

5.3111

1 2 3 4 6 8 9

( var sig, amp = 0.75, freq = 440 ; sig = Signal.newClear(44100) ; sig.waveFill({ arg x ; amp*sin(x) }, 0, 2pi*freq) ; sig.plot(minval:-1, maxval: 1) ; sig.play(true) )

Se dimensione dellarray 44.100 (come in molti esempi relativi a waveFill) allora il segnale viene letto una volta al secondo. Poich larray contiene un numero f req di cicli, freq indica effettivamente la frequenza del segnale.

5.4 Altri segnali e altri algoritmi

Unonda triangolare pu essere generata per approssimazione in maniera analoga allonda quadra (sempre attraverso componenti sinusoidali superiori di ampiezza opportuna). Un approccio diverso invece di tipo geometrico. Il periodo di unonda triangolare pu essere pensato come costituito di quattro segmenti: il primo nellintervallo [0.0, 1.0], il secondo in quello [1.0, 0.0], il terzo in quello [0.0, 1.0] e il quarto in quello [1.0, 0]. Nellesempio la variabile size rappresenta la dimensione degli array che contengono i quattro segmenti, mentre step lincremento del valore di ampiezza (ogni segmento copre unescursione di 1 campionata nellarray). Il primo segmento allora un array di tipo Signal riempito da un numero step di valori con incremento step: contiene valori da 0 a 1 step (8). Il secondo segmento segue il percorso

5.4112

contrario (il metodo reverse restituisce un array leggendo dallultimo al primo elemento larray sui cui chiamato). Prima, viene aggiunto uno step ad ognuno degli elementi dellarray: second contine valori da 1 a 0+ step. I segmenti successivi sono ottenuti generando due array third e fourth che sottraggono 1 rispettivamente a second e first, cio li traslano in basso (10, 11). Inne larray total ottenuto concatenando i quattro segmenti. Si noti che le operazioni di addizione (come le altre operazioni sugli array) restituiscono un array in cui ognuno degli elementi risulta dallapplicazione delloperazione sul rispettivo elemento dellarray di partenza. Ovvero:
1 2 [1, 2, 3, 4]*2 [ 2, 4, 6, 8 ]

1 2 4 5 7 8 9 10 11 12 14 15

( // Segment-generated triangle wave var first, second, third, fourth, total ; var size = 50 , step; step = 1.0/size ; first = Signal.series(size, 0, step) ; second = (first+step).reverse ; third = second-1 ; fourth = first-1 ; total = (first++second++third++fourth) ; total.plot )

5.4113

Il metodo di sintesi geometrico permette in effetti di ottenere risultati migliori di una somma di sinusoidi che pu soltanto ad approssimare londa triangolare, poich sarebbero necessarie innite componenti, gli spigoli risultanti sono sempre un po smussati (Figura 5.11). Gli approcci di questo tipo alla sintesi del segnale audio sono tipicamente chiamati sintesi diretta.

0.5

0.5

0.5

0.5

1 0 50 100 150 200

1 0 200 400 600 800 1000

a Fig. 5.11 Onda a triangolare e somma dei primi piezza pseudo-casuale.

20 armonici con am-

Tornando alla sintesi attraverso componenti sinusoidali, nellesempio seguente (disegnato in Figura 5.11, b) la variabile harmonics contiene il numero degli elementi dellarray arr, cio il numero delle armoniche. In questo caso, il valore dampiezza di ognuna delle componente generato dalla funzione {1.0.rand} ed un valore pseudo-casuale compreso tra [0.0, 1.0] (tra assenza e massimo valore normalizzato). Se si esegue il codice pi volte si noter che il suono cambia, poich lapporto delle componenti dipende dalla funzione del metodo fill. Se si incrementa o decrementa il valore di partials il segnale rispettivamente si arrichisce o si impoverisce di componenti elevate. La riga 7 permette di visualizzare larray delle ampiezze come una spezzata che unisce i valori discreti che lo compongono: con jplot2 si possono specicare i valori

5.4114

descursione massimi e minimi ([0, 1]). Si tratta, come si vedr, di una forma di sintesi additiva.
1 2 4 5 7 9 10 12 14 ( var sig, arr, harmonics = 20 ; arr = Array.fill(harmonics, {1.0.rand}); //arr = Array.fill(harmonics, 1.0.rand); sig = Signal.sineFill(1024, arr) ; arr.plot(minval:0, maxval:1) ; sig.plot ; sig.play(true) )

Si noti la differenza tra le righe 4 e 5. La riga 5 crea un array riempiendolo con il valore calcolato (una volta sola) da 1.0.rand, mentre necessario che ad essere invocata sia una funzione: una funzione un oggetto che, chiamato dal messaggio value (qui nascosto) risponde calcolando un valore. Dunque, per ogni posto di harmonics viene chiamata la funzione {1.0.rand} che calcola un nuovo valore. Ovvero:
1 3 5 7 Array.fill(3, 1.0.rand) [ 0.31042301654816, 0.31042301654816, 0.31042301654816 ] Array.fill(3, {1.0.rand}) [ 0.92207717895508, 0.91672122478485, 0.79339396953583 ]

5.4115

Lintroduzione dei numeri pseudo-casuali permette di avvicinare anche i segnali non periodici. Un rumore bianco un segnale il cui comportamento non predicibile se non in termini statistici. Questo comportamento si traduce in una distribuzione uniforme dellenergia su tutto lo spettro del segnale. Un rumore bianco pu essere descritto come una variazione totalmente aperiodica nel tempo: in altre parole, il valore di ogni campione del tutto indipendente da quelli precedenti e da quelli successivi 20. Dunque, il valore di un campione x indipendente dal valore del campione precedente x 1: x pu avere qualsiasi valore, sempre, evidentemente, allinterno dello spazio nito di rappresentazione dellampiezza. Intuibilmente, lalgoritmo di generazione molto semplice. In pseudo-codice:
1 2 3 Per ogni x in A: y = a*rand(-1, 1) A[x] = y

Nellesempio seguente, il metodo fill (6) valuta per ogni campione x una funzione che restituisce un valore casuale allinterno dellescursione normalizzata [1, 1] (rrand(-1.0,1.0)). Il codice specica una frequenza di campionamento (sr) e una durata in secondi (dur). Il risultato (5msec di rumore bianco) viene scalato per amp, e dunque, dato un valore di 0.75 per amp, si otterr unoscillazione (pseudo-)casuale nellescursione [0.75, 0.75]. Si noti come la funzione (nella denizione di partenza f [x]) in realt sia calcolata indipendente da x: essendo aperiodica, il suo comportamento non dipende dal tempo.

20

Si dice perci che ha autocorrelazione = 0.

5.4116

1 2 4 6 7 9 10

( /* 5 msec of white noise*/ var sig, amp = 0.75, dur = 0.005, sr = 44100 ; sig = Signal.fill(dur*sr, { amp*rrand(-1.0,1.0) }) ; sig.plot(minval:-1, maxval:1) ; sig.play(true) )

I software rappresentano tipicamente il segnale audio attraverso una curva continua che connette i valori dampiezza. Nel caso del rumore, questa rappresentazione continua -di per s non corretta rispetto alla natira digitale del segnale ma signicativa da un punto di vista acustico decisamente meno icastica di una rappresentazione discreta (Figura 5.12).
1 1

0.5

0.5

0.5

0.5

1 0 50 100 150 200

1 0 50 100 150 200

Fig. 5.12

Rumore bianco: curva continua e dispersione dei valori

unovviet ma bene ricordarsi che un segnale digitale appunto semplicemente una sequenza di valori, sulla quale possibile svolgere operazioni matematiche. Tutta lelaborazione digitale del segnale nasce da questassunto. I prossimi sono due esempi tratti da Puckette (?).

5.4117

La funzione seno oscilla periodicamente tra [1, 1]. Calcolando il suo valore assoluto si ribalta la parte negativa sul positivo: se si osserva la curva si nota come i due emicicli siano identici: ne consegue un segnale con frequenza doppia di quello di partenza. In generale lapplicazione della funzione del valore assoluto una tecnica rapida per far saltare dottava un segnale. Poich la curva occupa soltanto valori positivi (tra [0, 1]), possibile traslarla in modo da evitare offset: decrementando di 0.5 lescursione del segnale diventa [0.5, 0.5] ed simmetrica tra positivi e negativi (Figura 5.13).
1 1

0.5

0.5

0.5

0.5

1 0 200 400 600 800 1000

1 0 200 400 600 800 1000

Fig. 5.13

y = abs(sin(2 x)), y = abs(sin(2 x)) 0.5

Nel codice seguente il primo esempio utilizza il metodo waveFill e calcola il valore assoluto per ogni campione: una tecnica potenzialmente utilizzabile per il tempo reale. Tuttavia la classe Signal implementa molti metodi matematici. Cos, il metodo abs invocato su un oggetto Signal restituisce un altro oggetto Signal in cui ogni elemento il risultato del valore assoluto dellelemento del segnale di partenza ( come chiamare abs per ogni elemento). Lo stesso vale per la sottrazione -0.5 e per la moltiplicazione *amp: si applicano a tutti gli elementi dellarray.

5.4118

1 3 4 6 7 9 10 12 13 15 17 19

/* Absolute value on a sine */ ( var sig, amp = 0.75 ; sig = Signal.newClear(44100) ; sig.waveFill({ arg x, i; (sin(x).abs - 0.5)*amp }, 0, 2pi) ; sig.plot(minval:-1, maxval:1) ; ) ( var sig, amp = 0.75 ; sig = (Signal.sineFill(1024, [1]).abs-0.5)*amp ; sig.plot(minval:-1, maxval: 1) ; )

Nella funzione risultante dallappplicazione del valore assoluto la presenza di uno spigolo molto acuto introduce per molte componenti superiori. Lutilizzo dellelevamento al quadrato evita questo problema ottenendo un risultato analogo: infatti il quadrato dei valori negativi del segnale positivo, e si ottiene un ribaltamento della curva analogo a quanto avviene con il valore assoluto (Figura 5.14. Anche in questo caso viene indicata nel codice una doppia versione. Nella seconda viene utilizzato al posto di pow(2) il metodo (del tutto analogo) squared.

5.4119

0.5

0.5

0.5

0.5

1 0 200 400 600 800 1000

1 0 200 400 600 800 1000

Fig. 5.14
1 3 4 6 7 9 10 12 13 15 17 18

y = sin(2 x) , y = sin(2 x) 0.5

/* Squaring the sine */ ( var sig, amp = 0.75 ; sig = Signal.newClear(1024) ; sig.waveFill({ arg x, i; (sin(x).pow(2)-0.5)*amp }, 0, 2pi) ; sig.jplot2(range:[-1,1]) ; ) ( var sig, amp = 0.75 ; sig = (Signal.sineFill(1024, [1]).squared-0.5)*amp ; sig.plot(minval:-1, maxval: 1) ; )

Come ulteriore esempio di elaborazione del segnale si pu considerare loperazione di clippping: in caso di clipping tutti i valori superiori ad una certa soglia s (o inferiori al negativo della stessa) vengono riportati a s. Il clipping prende anche il nome di distorsione digitale perch

5.4120

quanto avviene quando il segnale digitalizzato ha unampiezza superiore a quella rappresentabile dalla quantizzazione. Il clipping una sorta di limiter (limita infatti lampiezza del segnale) radicale e pu essere usato come effetto di tipo distorcente. In SC denito il metodo clip2(t) che taglia un valore fuori dellescursione [t, t] a t. Si consideri lesempio seguente in cui t =3.
1 3 5 7 9 11 12 14 1.clip2(3) 1 -1.clip2(3) -1 4.clip2(3) 3 -4.clip2(3) -3

5.4121

1 2 4 6 7 8 9 10 11 12 13 14 15 16 18 19 21

( var sig, sig2, sig3, clipFunc ; sig = Signal.sineFill(512, [1]) ; clipFunc = { arg signal, threshold = 0.5 ; var clipSig = Signal.newClear(signal.size) ; signal.do({ arg item, index; var val ; val = if (item.abs < threshold, { item.abs }, { threshold}) ; val = val*item.sign ; clipSig.put(index, val) ; }) ; clipSig ; } ; sig2 = clipFunc.value( sig ) ; sig3 = clipFunc.value( sig, threshold: 0.75 ) ; //[sig, sig2, sig3].flop.flat.plot(minval:-1, maxval: 1, numChannels:3) ; sig.play(true) ; //sig2.play(true) ; //sig3.play(true) ; )

23 24 25 27

Come esercizio pu essere interessante implementare un modulo clipper. Si detto che una funzione esegue un comportamente quando riceve il messaggio value in funzione degli argomenti in entrata. Nellesempio appunto denita una funzione che implementa il clipping, clipFunc: gli argomenti in entrata prevedono un segnale (un oggetto Signal) e un valore di soglia (threshold). Ci che clipFunc restituisce un altro

5.4122

oggetto Signal. La riga 6 dichiara la variabile a cui viene assegnato il segnale e gli assegna subito un oggetto Signal di dimensione pari a quella del segnale in entrata, riempito di 0. Lidea quella di valutare ognuno degli elementi (che rappresentano campioni audio) di signal: si tratta cio di ciclare su signal (8). Nel ciclo la variabile val rappresenta il valore che dovr essere scritto nellarray clipSig. Il valore da assegnare a val dipende da una condizione. Se il campione valutato allinterno dellescursione [threshold, threshold] allora non ci sar clipping: val ha lo stesso valore del valore di signal in entrata. Se invece il valore in entrata cade allesterno dellintervallo, allora in uscita si avr il valore della soglia stessa. Il condizionale contenuto nelle righe 10-11. La valutaione avviene sul valore assoluto di item (item.abs). Avviene cio nellintervallo [0, 1]. In uscita si ha lo stesso valore assoluto di item oppure threshold. Se il valore in entrata negativo, verrebbe cos resitutito un valore positivo. Il segno del valore in entrata viene per recuperato con la riga 12 in cui il valore val viene moltiplicato per il segno di item: infatti, item.sign restituisce 1.
1 3 5 6 0.216.sign 1 -0.216.sign -1

Se item = -0.4 si ha:

item.abs = 0.4 inferiore a threshold = 0.5? S, allora val = item.abs = 0.4 item.sign = -1 (item = -0.4: negativo) val = 0.4 * -1 = -0.4

5.4123

Seguono due esempi, uno che sfrutta il valore predenito di threshold (0.5), laltro in cui threshold vale 0.75. Il clipping tende a squadrare la forma donda ed in effetti il segnale risultante tende ad assomigliare -anche allascolto- ad unonda quadra (Figura 5.15).
1 1 1

0.5

0.5

0.5

0.5

0.5

0.5

1 0 100 200 300 400 500

1 0 100 200 300 400 500

1 0 100 200 300 400 500

Fig. 5.15 Clipping: sinusoide, threshold = 0.5, threshold = 0.75.

5.5 Funzione valore assoluto e funzione quadratica

Lapproccio modulare dellesempio precedente permette di riscrivere i casi relativi al valore assoluto e al quadrato. In particolare si possono esplicitamente denire due funzioni di trasferimento:

Wabs : f (x) = x2 Wsquare : f (x) = |x|


Queste funzioni si comportano come veri moduli di elaborazione del segnale in entrata. Sono implementate in SC nellesempio seguente. Nel codice lunica cosa di rilievo leliminazione automatica delloffset. Il metodo peak restituisce lelemento di Signal che ha il valore assoluto pi alto (il massimo dampiezza del segnale). Assumendo che il segnale sia simmetrico rispetto allo 0, ogni nuovo valore risultante dallapplicazione della funzione viene traslato di peak/2. Se il segnale compreso in

5.5124

[0.7, 0.7] allora peak = 0.7: il segnale trasformato sar compreso in [0.0, 0.7] e viene traslato di 0.7/2 = 0.35, cos che la sua escursione in ampiezza risulti simmetricamente intorno allo 0 in [0.35, 0.35].

5.5125

1 2 4 6 8 9 10 11 12 13 14 15 16 17 19 20 21 22 23 24 25 26 27 28 30 32 34 36

( /* abs and quadratic functions */ var sig, sig2, sig3, absFunc, squareFunc ; sig = Signal.sineFill(512, [1]); absFunc = { arg signal ; var absSig = Signal.newClear(signal.size) ; var peak; signal.do({ arg item, index; var val ; val = item.abs ; absSig.put(index, val) ; }) ; absSig - (absSig.peak*0.5) ; } ; arg signal ; squareFunc = { var squareSig = Signal.newClear(signal.size) ; var peak ; signal.do({ arg item, index; var val ; val = item.squared ; squareSig.put(index, val) ; }) ; squareSig - (squareSig.peak*0.5) ; } ; sig2 = absFunc.value( sig ) ; sig3 = squareFunc.value( sig ) ; [sig, sig2, sig3].flop.flat.plot(minval:-1, maxval:1, numChannels:3) ; )

5.6126

5.6 Ancora sullelaborazione di segnali

La natura numerica del segnale permette di denire operazioni analoghe alle precedenti anche su materiale pre-esistente. La classe SoundFile permette non soltanto di scrivere sui le ma anche di accedere a le audio disponibili sullhard-disk. Il codice seguente riempie loggetto Signal sig con il contenuto del le audio sFile attraverso il metodo readData di sFile. Si noti come la dimensione di sig sia stata fatta dipendere dal numero di campioni di sFile attraverso lattributo numframes.
1 2 4 5 6 7 8 9 ( var sFile, sig ; sFile = SoundFile.new; sFile.openRead("sounds/a11wlk01-44_1.aiff"); sig = Signal.newClear(sFile.numFrames) ; sFile.readData(sig) ; sFile.close; )

Loperazione seguente sfrutta la natura di sequenza numerica del segnale audio per implementare una sorta di granulazione. In sostanza il segnale audio importato viene suddiviso in un numero numChunks di pezzi, ognuno dei quali comprende un numero step di campioni. Quindi i pezzi vengono mischiati a caso e rimontati. Nellimplementazione

5.6127

indices la sequenza degli indici dei pezzi del segnale ed una progressione a partire da 0. Questa progressione lineare (1, 2, 3, 4 . . .) viene mescolata attraverso il metodo scramble (cos da diventare ad esempio 9, 1, 3, 7 . . .). Quindi, attraverso il ciclo su ognuno degli indici, viene recuperato il pezzo corrispondente nel segnale originale e concatenato in sequenza. probabile che lo step non si un divisore intero del segnale in ingresso. La parte che avanza (tail) viene concatenata alla ne del nuovo segnale newSig.

5.6128

1 3 4 5 7 9 10 11 12 13 15 16 17 18 19 21 22 24 26 27 29 30 31 32 34 36 37 39 40

( /* Scrambling a signal */ var sFile, sig, newSig ; var numChunks, step, rest, indices ; var block, tail ; var period = 441; sFile = SoundFile.new; sFile.openRead("sounds/a11wlk01-44_1.aiff"); sig = Signal.newClear(sFile.numFrames) ; sFile.readData(sig) ; sFile.close; /* sig = Signal.newClear(44100) ; sig = Signal.sineFill(period, [1]) ; (44100/period).do(sig = sig.addAll(sig)) ; */ step = 50 ; // try: 5, 10, 50, 500 numChunks = (sig.size/step).asInt ; tail = (sig.size-(step*numChunks)) ; indices = Array.series(numChunks); indices = indices.scramble; newSig = Signal.new; indices.do({arg item; block = sig.copyRange(item*step, (item+1)*step-1) ; newSig = newSig.addAll(block) ; }) ; tail = sig.copyRange(sig.size-tail, sig.size) ; newSig = newSig.addAll(tail) ; newSig.play(true) ; )

5.6129

Questa forma di granulazione del segnale permette di introdurre inne una forma di sintesi per permutazione. Attraverso lo scrambling del segnal si produce un nuovo segnale che mantiene pi o meno in memoria il segnale originale. Minore lo step pi grande la ricombinazione del segnale. Se unoperazione simile viene svolta su un segnale sinusoidale si nota ancora pi chiaramente la proporzione tra diminuzione dello step e incremento della rumorosit. Lo si pu fare togliendo gli indicatori di commento nellesempio precedente. In quel caso viene prodotta una sinusoide di frequenza pari a 1/period. Si noti che la sintesi della sinusoide ottenuta concatenando cicli di durata pari a periods (in campioni). In questo caso la dimensione del segnale di 44.100 elementi: dunque se il periodo 441, vuol dire che ci staranno 44100/441 cicli dentro un secondo (assumendo la frequenza di campionamento pari a 44.110): la frequenza del segnale sar 44.100/441 = 100Hz . Nellesempio seguente viene implementata una forma di sintesi per permutazione. Il processo del tutto analogo a quanto avviene nellesempio precedente, con una differenza. Il segnale viene sempre suddiviso in blocchi di durata step. Al posto delloperazione di scramble viene invece implementata una permutazione di questo tipo: [ 0, 1, 2, 3, 4, 5 ] [ 1, 0, 3, 2, 5, 4 ] Essendo la permutazione del tutto periodica si ottiene un segnale dallo spettro molto ricco ma che presenta una periodicit che dipende sia dalla frequenza della sinusoide in entrata sia dalla dimensione dello step.

5.6130

1 2 4 5 6 7 9 10 11 13 14 15 17 18 19 21 22 23 24 25 27 28 30 31 32 33

( /* Distorsion via permutation on a sinusoid */ var var var var sig, sig2, newSig ; numChunks, step, rest, indices ; block, tail ; period = 441;

sig = Signal.new ; sig2 = Signal.sineFill(period, [1]) ; (44100/period).do(sig = sig.addAll(sig2)) ; step = 50 ; // try: 5, 50, 500 numChunks = (sig.size/step).asInt ; tail = (sig.size-(step*numChunks)) ; a = Array.series((numChunks/2).asInteger, 1,2) ; b = Array.series((numChunks/2).asInteger, 0,2) ; indices = [a,b].flop.flat ; newSig = Signal.new; indices.do({ arg item; block = sig.copyRange(item*step, (item+1)*step-1) ; newSig = newSig.addAll(block) ; }) ; tail = sig.copyRange(sig.size-tail, sig.size) ; newSig = newSig.addAll(tail) ; newSig.play(true) ; sig.plot ; newSig.plot )

La periodicit diventa evidente, oltre allorecchio, se si confronta il primo ciclo della sinusoide, della sinusoide scrambled e della sinusoide distorta per permutazione (Figura 5.16).

5.6131

0.5

0.5

0.5

0.5

0.5

0.5

1 0 100 200 300 400

1 0 100 200 300 400

1 0 100 200 300 400

Fig. 5.16 Sinusoide con un periodo di 441 campioni, scrambling e permutazione con un periodo di 25 campioni.

5.7 Segnali di controllo

Un segnale sinusoidale, come tutti i segnali perfettamente periodici, manca totalmente delle caratteristiche di dinamicit usualmente ascritte ad un suono naturale o, meglio, acusticamente interessante: un suono, come direbbe Pierre Schaeffer, senza forma temporale, omogeneo. Suoni di questo tipo, peraltro, arredano il paesaggio sonoro della modernit meccanica ed elettrica sotto forma di humming, buzzing -e cos via- prodotti dai ventilatori, dallimpedenza elettrica, dai motori. A parte questi casi, tipicamente la forma temporale di un suono prende la forma di un prolo dinamico, di una variazione della dinamica del suono che descritta acusticamente sotto forma di una curva di inviluppo, le cui fasi prendono usualmente il nome di attacco/decadimento/sostegno/rilascio: da cui lacronimo ADSR. Il modo pi semplice di rappresentare un simile inviluppo consiste nellutilizzare una spezzata (Figura 5.17). A guardare linviluppo si osserva agevolmente come si tratti di altra curva, e cio propriamente di un altro segnale, che si distingue dai segnali nora considerati per due caratteristiche importanti: 1. non periodico (si compone di un unico ciclo). Dunque, il periodo del segnale dinviluppo pari alla durata del segnale audio: se il segnale dura 2 secondi (ovvero il periodo dellinviluppo) allora

5.7132

0.8881

0.3456

0.8233

0 Time (s)

4.23184

0.3398

0 Time (s)

0.588944

Fig. 5.17 Descrizione del prolo dinamico attraverso una spezzata (inviluppo). linviluppo ha una frequenza 1/2 = 0.5Hz . Si noti come la frequenza non rientri nel dominio udibile; 2. unipolare: assumendo che il segnale audio sia nellescursione normalizzata [1, 1] (bipolarit), linviluppo compreso in [0, 1] (unipolarit). Passando dallanalisi del segnale alla sua sintesi, si tratter perci di riprodurre le propriet dellinviluppo (la sua forma) e di applicare questa forma al segnale audio.Linviluppo un tipico segnale di controllo: un segnale che modica -controlla- un segnale audio. Essendo un segnale, per rappresentare un inviluppo si pu utilizzare un array. Ad esempio, un tipico inviluppo ADSR potrebbe essere descritto dallarray di dieci punti di Figura 5.18. Detto interattivamente:
[0, 0.9, 0.4, 0.4, 0.4, 0.4, 0.3, 0.2, 0.1, 0].plot

Per incrementare o decrementare lampiezza di un segnale si pu (lo si visto abbondantemente) moltiplicare il segnale per una costante: ogni campione viene moltiplicato per la costante. Ad esempio, se la costante

5.7133

0.75

0.5

0.25

0 0 2 4 6 8 10

Fig. 5.18 [0, 0.9, 0.4, 0.4, 0.4, 0.4, 0.3, 0.2, 0.1, 0].

trebbe pensare ad una simile costante nei termini di un segnale: come un array, di dimensione pari a quella del segnale scalato, che contenga sempre lo stesso valore. Ogni valore dellarray da scalare viene moltiplicato per il rispettivo valore dellarray Amp. Lidea smette di essere una complicazione inutile nel momento in cui larray Amp non contiene pi sempre lo stesso valore, ma contiene invece valori variabili che rappresentano appunto un inviluppo dampiezza. Dunque, ogni campione del segnale audio (ogni elemento dellarray) viene moltiplicato per un valore incrementale del segnale dinviluppo ottenuto. La situazione rappresentata in Figura 5.19. Dovendo descrivere lo sviluppo del segnale audio in tutta la sua durata, la dimensione dellarray dinviluppo deve essere la stessa del segnale audio. Emerge qui un punto fondamentale, su cui si ritorner: tipicamente sufciente un numero molto minore di punti per rappresentare un inviluppo rispetto ad un segnale audio, ovvero -assumendo che il segnale audio duri un solo secondo a qualit CD- il rapporto tra i due pari a 10/44.100. Questo in effetti il tratto pertinente che identica un segnale di controllo. Passando allimplementazione in SC, per intanto si pu generare una

Amp = 0.5, lampiezza del segnale viene ridotta della met. Si po-

5.7134

0.75

0.5

0.5

0.25

0.5

0 0
1

10
1

200

400

600

800

1000

0.5

0.5

0.5

0.5

1 0 200 400 600 800 1000

1 0 200 400 600 800 1000

Fig. 5.19 Inviluppo, segnale audio risultante, segnale audio, segnale audio inviluppato. sinusoide di durata pari ad un secondo attraverso il solito metodo waveFill. La frequenza prescelta molto grave perch cos sono meglio visibili i cicli del segnale. Larray di partenza composto da dieci segmenti: poich questi devono essere ripartiti sulla durata del segnale audio, ognuno dei segmenti comprender 44100/10 campioni. Questo valore assegnato alla variabile step. Si tratta ora di generare gli array che costituiscono i quattro segmenti ADSR (gli array att, dec, sus, rel), per poi concatenarli in un array complessivo env. Poich devono essere moltiplicati per un oggetto Signal allora devono anchessi essere oggetti Signal. Il metodo series(size, start, step) crea una progressione lineare di size valori a partire da start con incremento step. Lattacco parte da 0 e arriva a 0.9, occupa un segmento, cio 4410 campioni. Il valore 0.9 deve cio essere raggiunto in 4410 punti: lincremento di ognuno sar perci di 0.9/4410. Dunque, 0.9/step. Allultimo punto

5.7135

si avr un valore pari a 0.9/step step = 0.9. Nel caso di dec si tratta di scendere a 0.5 nella durata di un altro segmento. In questo caso la progressione parte da 0.9 e lincremento negativo: bisogna scendere di 0.5 in uno step, dunque lincremento 0.5/step. Nel terzo caso (sus) il valore costante a 0.4 per una durata di 4 passi, dunque si pu pensare ad un incremento 0. Il caso di rel analogo a quello di dec. Il nuovo segnale di inviluppo env ottenuto concatenando i quattro segmenti ed impiegato come moltiplicatore di sig per ottenere il segnale inviluppato envSig.
1 3 4 5 7 8 9 11 12 13 14 16 17 19 20 21 /* inviluppi, I */ ( var sig, freq = 440, size = 44100, step ; var env, att, dec, sus, rel, envSig ; step = 44100/10 ; sig = Signal.newClear(size) ; sig.waveFill({ arg x, i; sin(x) }, 0, 2pi*50) ; att dec sus rel = = = = Signal.series(step, 0, 0.9/step) ; Signal.series(step, 0.9, -0.5/step) ; Signal.series(step*4, 0.4, 0) ; Signal.series(step*4, 0.4, -0.4/(step*4)) ;

env = att++dec++sus++rel ; envSig = sig * env ; [sig, env, envSig].flop.flat.plot(minval:-1, maxval: 1], numChannels: 3) ; envSig.play(true) )

5.7136

Il metodo precedente piuttosto laborioso. SC prevede una classe Env specializzata nel costruire inviluppi. Env assume che un inviluppo sia una spezzata che connette valori dampiezza nel tempo e fornisce diverse modalit di interpolazione per i valori intermedi. Si considerino i due array seguenti v e d:
1 2 v d = [0, 1, = [ 2, 0.3, 3, 1, 0.8, 4 0] ; ] ;

Una coppia simile tipicamente utilizzata per specicare un inviluppo:

v: indica i punti che compongono la spezzata (i picchi e le valli); d: indica la durata di ogni segmento che connette due punti.

Dunque, larray delle durate contiene sempre un valore di durata in meno di quello delle ampiezze. Infatti, t[0] (= 2) indica che per andare da v[0] (= 0) a v[1] (= 1) sono necessari 2 unit di tempo (in SC: secondi). Attraverso i due array v, d cos possibile descrivere un prolo (v) temporale (d). Nellesempio, resta tuttavia da specicare cosa succede per ogni campione compreso nei due secondi che intercorrono tra 0 e 1. Il modo in cui i campioni sono calcolati dipende dalla modalit din interpolazione. Nellesempio seguente e1, e2, e3 sono oggetti Env specicati dalla stessa coppia di array v, d, ma con differenti modalit di interpolazione (lineare, discreta, esponenziale).

5.7137

1 2 4 6 7 9 10 12 13 15 16

( /* Using Env*/ var v, d, e1, e2, e3 ; v d = [0, 1, = [ 2, 0.3, 3, 1, 0.8, 4 0] ; ] ;

e1 = Env.new(v, d,'linear').asSignal ; e2 = Env.new(v, d,'step').asSignal ; v = [0.0001, 1, 0.3, 0.8, 0] ; e3 = Env.new(v, d,'exponential').asSignal ; [e1, e2, e3].flop.flat.plot(numChannels:3) ; )

Si noti che nel caso di un inviluppo esponenziale il valore di partenza non pu essere pari a 0: il primo valore di v viene quindi ridenito con un valore prossimo allo zero. Il signicato dei parametri illustrato nella Figura 5.20 che commenta quanto disegnato dal metodo plot. La classe Env eredita direttamente da Object e dunque non un oggetto di tipo array. Tipicamente vine utilizzata come specicazione di inviluppo per il tempo reale (come si vedr). Quando per un oggetto Env riceve il messaggio asSignal Env restituisce un oggetto Signal che contiene un inviluppo campionato nel numero di punti che compongono il nuovo array. La classe Env permette allora di utilizzare anche in tempo differito una specicazione per gli inviluppi decisamente pi comoda. Inoltre, la classe prevede alcuni costruttori che restituiscono inviluppi particolarmente utili. Ad esempio:

5.7138

d:

0.5 0
'linear'

1 0.5 0

200

400

600

800

1000

'step'

200

400

600

800

1000

0.5 0 0
v: 0.0 'exponential'

200
1.0

400
0.3

600
0.8

800

1000
0.0

Fig. 5.20

Env: parametri.

triangle richiede due argomenti: il primo indica la durata, il secondo il valore di picco di un inviluppo triangolare (il picco cade cio a met della durata). perc: permette di denire un inviluppo percussivo (attacco + rilascio). Gli argomenti sono tempo dattacco, tempo di rilascio, valore di picco e valore di curvatura.

5.7139

1 3 4 5 7 8 10 11 13

/* inviluppi, I */ ( var sig, freq = 440, size = 1000 ; var envT, envP ; sig = Signal.newClear(size) ; sig.waveFill({ arg x, i; sin(x) }, 0, 2pi*50) ; envT = Env.triangle(1,1).asSignal(size); envP = Env.perc(0.05, 1, 1, -4).asSignal(size) ; [sig, envT, sig*envT, envP, sig*envP].flop.flat.plot(minval:-1, maxval: 1, numChannels: 5) ; )

15

Gli inviluppi envT e envP, e le loro applicazioni a sig (Figura 5.19, b), sono rappresentati in Figura 5.21. Lapplicazione di un segnale di inviluppo ovviamente possibile anche per un segnale audio di provenienza concreta. Nellesempio seguente al segnale sig, ottenuto importando il contento di sFile, viene applicato un segnale di inviluppo env: env ottenuto attraverso due array riempiti di numeri pseudo-casuali, v e d. Il primo oscilla nellintervallo [0.0, 1.0] ( un segnale unipolare). Per evitare offset nellampiezza, il primo e lultimo valore dellarray vengono posti a 0, ed aggiunti dopo. Larray d composto di un numero di elementi pari a quelli di v, secondo quanto richiesto dalla sintassi di Env. Gli intervalli di durata variano nellintervallo [0.0, 4.0].

5.7140

0.5

0.5

0.5

0.5

1 0 1 200 400 600 800 1000

1 0 1 200 400 600 800 1000

0.5

0.5

0.5

0.5

1 0 200 400 600 800 1000

1 0 200 400 600 800 1000

Fig. 5.21

Inviluppi con Env: envT, sig*envT, envP, sig*envP

5.7141

1 3 4 5 6 8 9 10 11 12 14 15 16 17 19 21 22 23

/* inviluppi, II */ ( var sig, freq = 440 ; var env, v, d, breakPoints = 10 ; var sFile ; sFile = SoundFile.new; sFile.openRead("sounds/a11wlk01-44_1.aiff"); sig = Signal.newClear(sFile.numFrames) ; sFile.readData(sig) ; sFile.close; v = Array.fill(breakPoints-2, { arg i ; 1.0.rand ; }) ; v = v.add(0) ; v = [0.001].addAll(v) ; v.size.postln; d = Array.fill(breakPoints-1, { arg i; 4.0.rand ;}) ; env = Env(v, d, 'lin').asSignal(sig.size) ; // Achtung! next line can be computationally expensive [sig, env, sig*env].flop.flat.jplot2(minval:-1, maxval: 1, numChannels: 3) ; )

Tre inviluppi sono disegnati in Figura 5.22: ad ogni valutazione del codice linviluppo assume infatto una forma diversa, a parte per i due estermi pari a 0.
1 1 1 0.75 0.75 0.75

0.5

0.5

0.5

0.25

0.25

0.25

0 0 20 40 60 80 100

0 0 20 40 60 80 100

0 0 20 40 60 80 100

Fig. 5.22

Inviluppi pseudo-casuali.

5.8142

5.8 Conclusioni

Lobiettivo di quanto visto nora era di introdurre il concetto di segnale digitale, attraverso alcune operazioni che si rendono tipicamente possibili grazie alla sua natura numerica. Si poi avuto modo di osservare come sia possibile modicare un segnale attraverso un altro segnale. In particolare, un segnale di controllo un segnale che richiede una risoluzione temporale molto minore del segnale e la cui frequenza di situa al di sotto delle frequenze udibilei (sub-audio range). Un segnale di controllo tipicamente modica un segnale audio. a questo punto opportuno riprendere ed espandere gli aspetti affrontati attraverso il modus operandi pi tipico di SC, il tempo reale.

5.8143

6 Larchitettura e il server

6.1 Larchitettura

Come si avuto modo di osservare, scaricando il programma SC ci si porta a casa due componenti, di principio autonomi, un server e un client. Il primo viene chiamato scsynth, il secondo sclang (SC-language). Il programma SC sfrutta cio unarchitettura client/server, separando due funzioni, una di richiesta e laltra di fornitura servizi, che comunicano attraverso una rete. In 6.1 descritta una generica architettura di rete: pi client comunicano via rete con un server scambiando messaggi. In SC il client e il server comunicano attraverso la rete attraverso messaggi scritti in un protocollo specico, piuttosto usato nellambito delle applicazioni multimediali (ad esempio, implementato in Max/MS, PD, EyesWeb, Processing, etc.), che si chiama OSC. http://www.cnmat.berkeley.edu/OpenSoundControl/ A scanso di equivoci, la rete di cui si parla denita a livello astratto. Ci vuol dire che client e server possono essere in esecuzione sulla stessa macchina: ci che avviene quando si manda in esecuzione lapplicazione SC.

6.1144

Rete

client 1
ms g

client 2

msg

Server

ms
client 3

Fig. 6.1 Architettura client/server generica. In altre parole, aprendo SC si mandano in esecuzione due programmi, scsynth e sclang. Il server un motore per la sintesi audio, di basso livello, potente, efciente, e non molto intelligente (non ha molta capacit di programmazione). Il cliente di questo server sclang: anche sclang in realt due cose, un linguaggio di programmazione e insieme linterprete di questo linguaggio. Linterprete ha due funzioni: 1. il client: in altre parole, linterfaccia che permette allutente di scrivere e spedire messaggi OSC al server. Per scrivere una lettera al server, necessario avere un foglio di carta e un postino che la consegni: sclang fa entrambe le cose. 2. linterprete del linguaggio: i messaggi OSC sono piuttosto macchinosi da scrivere, e condividono con il server la prospettiva di basso livello. Il linguaggio sclang invece un linguaggio di alto livello (tipo Smalltalk). Il codice sclang viene allora tradotto in messaggi OSC dallinterprete e questi vengono cos inviati al server. La poesia che

6.1145

lutente scrive in linguaggio sclang viene parafrasata in prosa OSC dallinterprete sclang per essere inviata al (prosaico) server. La situazione schematizzata in 6.2. La comunicazione tra lato client e lato server avviene attraverso messaggi OSC che il client (tipicamente) spedisce al server. Linterprete sclang spedisce messaggi OSC in due modi: 1. direttamente. In altre parole, sclang-inteprete un buon posto per lutente da dove parlare al server al livello di questultimo (da dove spedire messaggi OSC); 2. indirettamente. Il codice simil-Smalltalk dello sclang-linguaggio (ad un livello pi astratto) a disposizione dellutente viene tradotto dallinterprete automaticamente in messaggi OSC (a livello server) per il server ( il cosidetto language wrapping). Riassumendo, a partire dallapplicazione sclang-inteprete si pu scrivere in poesia afdandosi alla traduzione prosastica ad opera dello stesso inteprete (che traduttore e postino) o direttamente in prosa OSC (linteprete fa solo il postino). Ci si potrebbe chiedere perch complicarsi la vita con una simile architettura. I vantaggi sono i seguenti:

stabilit: se il client sperimenta un crash, il server continua a funzionare (ovvero: laudio non si ferma, ed un fatto importante per un concerto/installazione/performance) e viceversa. modularit: un conto la sintesi, un conto il controllo. Separare le due funzioni consente ad esempio di controllare scsynth anche da applicazioni che non siamo sclang: limportante che sappiano spedire i giusti messaggi al server. Il server democratico (tutti possono

6.1146

ottenere servizi audio) e burocratico allo stesso tempo (limportante rispettare il protocollo OSC).

controllo remoto: la rete di cui si parla pu essere sia interna al calcolatore, sia esterna. Quando ci si occupa esclusivamente di sintesi audio, tipicamente le due componenti lavorano sullo stesso calcolatore e usando lindirizzo locale. Ma client e server potrebbero benissimo trovarsi via rete, anche ai due estremi opposti del globo e comunicare via internet.

Gli svantaggi principali di una simile architettura sono due: 1. la circolazione dei messaggi introduce un piccolo ritardo (che pu essere di rilievo per vista la sensibilit temporale dellaudio); 2. in caso di alta densit temporale dei messaggi sulla rete, quetsultima pu essere sovraccaricata, e la gestione dei messaggi pu indurre un ritardo. Va altres notato che decisamente raro incorrere in simili problemi.

altri livelli pi astratti

livello messaggi OSC app. esterna

Client side
sclang func .play synth .play language wrapping s
.sendMsg

messaggi OSC

Server side
scsynth synthDefs

messaggi OSC

app. esterna

messaggi OSC

Fig. 6.2

Architettura client/server di SC.

6.1147

Ad esempio, in 6.2 si distingue tra due lati, client e server side. Sul lato server c scsynth. Su quello client ci possono essere un numero indenito di applicazioni capaci di parlare via OSC al server (livello dei messaggi OSC). Dalla discussione precedente risulta infatti chiaro che sclang soltanto uno dei possibili client di scsynth. Tuttavia, sclang, essendo pensato esplicitamente per lavorare con scsynth, ne in qualche modo il client privilegiato, ad esempio fornendo allutente un linguaggio di alto livello (altri livelli pi astratti) e traducendolo per lui in messaggi OSC.

6.2 Esempi

I due esempi seguenti illustrano lutilit dellarchitettura client/server di SuperCollider, che lo rendono particolarmente essibile nellinterazione con altri software. Si tratti di aspetti piuttosto complessi a livello introduttivo, ma che hanno il solo scopo di far intuire il funzionamento generale.

6.2.1 SwingOSC

Si gi osservato che SwingOSC un server graco in Java, che pu essere controllato dallinterno di sclang. In sostanza, vale un ragionamento analogo a quanto visto per la comunicazione con scsynth. SwingOSC un server e riceve messaggi via OSC da qualsiasi client che sia in grado di inviarglieli. esattamente lo stesso meccanismo visto in precedenza, tant che, analogamente a quanto visto per la relazione sclang/scsynth, possibile inviare direttamente da sclang messaggi OSC a SwingOSC.

6.2148

Sono allora state predisposte dallo sviluppatore alcune classi che permettono dallinterno di sclang un controllo di pi alto livello rispetto allinvio diretto di messaggi OSC. Si tratta delle classi gi incontrate in precedenza discutendo di interfacce grache. La situazione rafgurata in 6.3.
altri livelli pi astratti livello messaggi OSC

Client side
app. esterna sclang func .play synth .play s
.sendMsg

Server side
messaggi OSC

scsynth

messaggi OSC messaggi OSC

JSCWindow. new

g
.sendMsg

language wrapping

SwingOSC

app. esterna

messaggi OSC

Fig. 6.3 Architettura client/server: sclang, scsynth e SwingOSC. Ad esempio, il codice sclang GUI.window.new crea un elemento GUI attraverso SwingOSC, una semplice nestra. Sclang interpreta il codice e invia gli opportuni messaggi a SwingOSC per la creazione dellelemento graco. Il caso pi interessante per quando si crea un elemento che non soltanto una view ma anche un controller: ad esempio un elemento-manopola la cui escursione permmetta di controllare un parametro audio (ad esempio una manopola del volume). In fase di inizializzazione (6.4, 1) necessario:

6.2149

1. istanziare un dispositivo di sintesi sul scsynth 2. istanziare un elemento GUI su SwingOSC 3. stabilire la relazione tra variabile di controllo dellelemento GUI e variabile di controllo dellelemento audio In fase di esecuzione, il comportamento dellutente viene intercettato da SwingOSC (lutente muove la manopola che oggetto istanziato dal server SwingOSC), il quale comunica di ritorno a sclang il nuovo valore della manopola (6.4, 2). Sclang a sua volta utilizza il valore ottenuto per inviare a scsynth un messaggio che indichi di modicare quel parametro audio (6.4, 3).

1 scsynth 3 sclang 2 SwingOSC 1

Fig. 6.4 Creazione e controllo di un elemento GUI. Sebbene allutente nale, soprattutto in Windows, la discussione precedente possa sembrare troppo macchinosa, va ricordato che la consapevolezza della architettura lunica strada per capire esattamente il funzionamento di SC. In altre parole, in SC, anche quando non si vede in alcun modo, se c segnale audio c sempre un server audio che per funzionare ha bisogno di essere controllato via messaggi OSC.

6.2150

6.2.2 Graphista!

Un esempio abbastanza sosticato di sfruttamento della architettura di rete di SC fornito in Figura 6.5. Graphista! un programma con interfaccia graca per la composizione musicale algoritmica inizialmente interamente scritto nel linguaggio Python. Non pensato per il tempo reale, stato ampliato per funzionare anche in RT utilizzando SC come motore per la sintesi audio. La reimplementazione in SC dellinterfaccia graca, piuttosto complessa, sarebbe stata complicata. Poich esiste un modulo di Python che gestisce messaggi OSC, stato allora possibile connettere direttamente Graphista! a scsynth: lutente controlla la sintesi attraverso SC a partire dallinterfaccia graca in Python di Graphista! (6.5, a). In fase di implementazione, ci si per resi conto che la precisione di Python per il controllo della generazione di eventi in tempo reale piuttosto limitata. La soluzione consistita nel trattare anche sclang come un server (6.5, b). sclang infatti non solo invia, ma pu ricevere messaggi OSC. Dunque, Graphista! invia messaggi OSC a sclang: i messaggi contengono frammenti di codice sclang. Questultimo intepreta il codice e lo traduce in altri messaggi OSC per scsynth. In questa implementazione lutente attraverso Graphista! controlla indirettamente scsynth attraverso sclang (propriamente scsynth inaccessibile a Graphista!).

6.3 Il client sclang

Come si detto, sclang soltanto uno tra i possibili client di scsynth. Ma non un cliente qualunque.

6.3151

Client side
OSC messages init User event Graphista! event (Python) init

Server side
Client side
SC scsynth graph data structures
init OSC messages SC sclang init Graphista! event event variables update (Python)

Server side
OSC messages SC scsynth

audio rate
User

graph data structures synthDefs

audio rate

synthDefs

init

init

a. Prima ipotesi (progetto) Fig. 6.5

b. Seconda ipotesi (implementazione)

Graphista!: uso dellarchitettura di rete

1. In primo luogo, sclang linterprete di un linguaggio di alto livello che permette allutente quasi di dimenticarsi della struttura client/server e di molto di quello visto nora. 2. In secondo luogo, il privilegio maggiore di sclang sta nel fatto che permette la denizione e linvio al server delle synthDef. Le synthDef devono risiedere sul server, ma come si fa a mettercele? Sclang offre questa possiblit. Non impossibile farlo da altri client, ma molto pi complicato.

6.4 Un impianto chimico per la produzione di liquidi e un server audio in tempo reale

Dunque scsynth un motore di sintesi audio programmabile e controllabile in tempo reale. Non agevole di primo acchito riuscire a tener presente le relazioni tra tutti gli elementi pertinenti per il server audio scsynth. Conviene perci introdurre una quadro metaforico e pensare server come ad un impianto chimico per la sintesi di liquidi. Nella discussione seguente si prenda in considerazione la gura 6.6.

6.4152

Macchinario
contenitore contenitore

Impianto
Disposi tivo

Disposi tivo

Disposi tivo

tubo di collegamento tubo in entrata tubo in uscita Disposi tivo Disposi tivo Disposi tivo tubo in uscita

Macchinario

Disposi tivo

Disposi tivo

Macchinario

Fig. 6.6 Il server audio come impianto chimico per generazione di liquidi di sintesi. 1. Per sintetizzare un liquido necessaria un macchinario complesso 2. Un macchinario costituito di dispositivi specializzati in cui i liquidi subiscono trasformazioni. I dispositivi sono collegati attraverso tubi interni. 3. Un macchinario deve essere progettato predisponendo le relazioni tra dispositivi componenti. A partire da un progetto, pu essere costruito un numero indenito di macchinari identici. 4. Una volta costruito, il macchinario non pu essere modicato nella sua struttura interna 5. Ma un addetto pu controllarne il comportamento dallesterno attraverso leve e comandi, cos come monitorarne il funzionamento 6. Un impianto pu comprendere pi macchinari che lavorano in parallello

6.4153

7. Quando limpianto in funzione i liquidi scorrono lungo i tubi a velocit costante, senza mai potersi fermarsi. 8. I liquidi possono scorrere nei tubi a due velocit differenti (ma sempre costanti), in particolare a velocit di controllo e a velocit di sintesi 9. I liquidi possono per essere stoccati in quantit limitate dentro appositi contenitori, da cui possibile attingere quando serve. Questi liquidi di per s non scorrono, ma, attraverso dispositivi specializzati, possono essere riversati in un liquido in scorrimento. 10. Tipicamente (anche se non necessariamente) un macchinario prevede un dispositivo munito di un tubo che permette di far uscire il liquido allesterno. Altre volte pu avere anche dispositivo con un tubo in entrata da cui ricevere un liquido che proviene da altri macchinari 11. La circolazione dei liquidi tra limpianto e lesterno (lacqua dallacquedotto in entrata, il prodotto sintetizzato in uscita) oppure tra i diversi macchinari nellimpianto avviene attraverso tubi speciali. I primi sono tubi di entrata/uscita, i secondi di collegamento. Attraverso quesi ultimi, i liquidi possono cos circolare nellimpianto e sono a disposizione degli altri macchinari. Questi tubi permettono perci di connettere diversi macchinari 12. I tubi di collegamento disperdono il loro liquido (che non uscito attraverso i tubi di uscita dellimpianto) negli scarichi dellimpianto. I liquidi non inquinano e la loro dispersione non rilevante. possibile a questo punto riconsiderare i punti precedenti, sostituendo opportunamente i nomi di gura 6.6 con quelli di gura 6.7.

6.4154

buffer

buffer

Synth
UG

SERVER

UG

UG

bus In bus Out bus UG UG UG

Synth

Out bus

UG

UG

Synth

Fig. 6.7 Osservazione 1

Componenti del server audio.

Per sintetizzare un liquido necessaria un macchinario complesso Per sintetizzare un segnale audio necessario un macchinario software che in SC prende il nome di Synth: un synth appunto un sintetizzatore audio. Osservazione 2 Un macchinario costituito di dispositivi specializzati in cui i liquidi subiscono trasformazioni. I dispositivi sono collegati attraverso tubi interni Per generare segnale audio un synth richiede di specicare quali algoritmi di elaborazione/sintesi del segnale da utilizzare. In SC, seguendo la tradizione della famiglia di linguaggi Music N, gli algoritmi di elaborazione/sintesi sono implementati in UGen ( Unit Generator): una UGen semplicemente un dispositivo software che elabora o sintetizza segnale audio. Ad esempio SinOsc una UGen che genera segnali sinusoidali: per avere unidea sufciente eseguire questa riga di codice:

6.4155

synth. Un synth appunto un sintetizzatore, un dispositivo di sintesi costruito con componenti UGen. Le UGen possono avere pi entrate, ma hanno sempre soltanto unuscita. Una UGen pu ricevere in entrata unaltra UGen: questo processo si chiama patching, e pu essere tradotto (non letteralmente ma ad sensum) con innesto, cos come to patch ha un buon equivalente in innestare. Un insieme di UGen innestate tra di loro formano uno UGen-graph, un grafo di UGen, una struttura che rende conto delle relazioni tra UGen. Poich le UGen generano segnali lo UGen-graph descrive il usso dei segnali che dalle diverse sorgenti si raccolgono nel segnale. Lo UGengraph la cartina geograca di un ume che raccoglie contributi di diversi afuenti per poi terminare in mare. Osservazione 3 Un macchinario deve essere progettato predisponendo le relazioni tra dispositivi componenti. A partire da un progetto, pu essere costruito un numero indenito di macchinari identici Il client di SC chiede perci al server di costruire e di far funzionare un synth per lui. Per soddisfare la richiesta il server deve sapere quali pezzi (UGen) utilizzare e in quali relazioni combinarli (patching in uno UGen-graph). Poich probabile che allutente possano servire pi volte gli stessi dispositivi, SC prevede un passaggio supplementare. Lutente prima specica al server una denizione di un synth (synthDef), una sorta di progetto dettagliato di come deve essere fatto il synth desiderato, e quindi chiede al server di costruire un synth seguendo quel progetto. Una synthDef associa un nome n ad uno UGen-graph u, in modo che si possano creare synth di tipo n che generano segnali attraverso le relazioni tra UGen previste da u. Una volta create, le synthDef possono

{SinOsc.ar}.play. Le UGen costituiscono i componenti di base di un

6.4156

essere memorizzate in formato binario e restare perci sempre disponibili allutente. Lutente pu in altre parole crearsi una libreria di synthDef (intese come progetti o come stampi da cui creare synth) e, quando opportuno, chiedere al server di creare un synth a partire dalla synthDef. In sostanza per usare SC come motore di sintesi necessario compiere almeno passi: 1. denire una synthDef (denire il progetto del sintetizzatore) 2. istanziare un synth a partire da una synthDef (costruire il sintetizzatore) Osservazione 4 Una volta costruito, il macchinario non pu essere modicato nella sua struttura interna Una synthDef un diagramma, uno schema: oggetto statico. Una volta spedita al server, immutabile. Se prevede due entrate, quelle avr. Daltra parte, sempre possibile spedire al server una nuova synthDef che prevede le modiche desiderate. Osservazione 5 Ma un addetto pu controllarne il comportamento dallesterno attraverso leve e comandi, cos come monitorarne il funzionamento Il progetto di un sintetizzatore descritto in una synthDef attraverso lo UGenGraph. Lo UGen-Graph descritto sintatticamente attraverso una funzione (ed infatti racchiuso tra parentesi graffe). Come ogni funzione pu prevedere argomenti in entrata: questi argomenti sono appunto parametri per il calcolo, svolto dal corpo della funzione, del valore (lampiezza del segnale) che la funzione restituisce in uscita.

6.4157

Osservazione 6 Un impianto pu comprendere pi macchinari che lavorano in parallello Ogni macchinario dellesempio idraulico rappresenta un synth, ovvero, musicalmente parlando, uno strumento -o in fondo anche una voce. Il server audio pu gestire un numero indenito di sintetizzatori in parallelo. In altre parole, il numero massimo degli strumenti -o delle voci- che possono suonare contemporaneamente dipende esclusivamente dalle risorse hardware a disposizione. Osservazione 7 Quando limpianto in funzione i liquidi scorrono lungo i tubi a velocit costante, senza mai potersi fermarsi Si detto che il liquido rappresenta il segnale audio. La scelta del liquido dipende dal fatto che, poich a questo punto non si sta parlando solo di segnali, ma di segnali in tempo reale, i segnali sono certo sequenze di valori di ampiezza secondo quanto visto nora, ma in pi con il vincolo che tali campioni devono essere inesorbilmente calcolati ad un tasso uniforme nel tempo (tipicamente, ma non necessariamente, nel caso di segnali audio 44.100 volte in un secondo). Ad ogni istante di tempo, un nuovo campione deve essere calcolato nella sequenza: ogni synth effettua tutti i calcoli previsti da tutte le UGen che lo compongono e restituisce un valore. In altre parole, ad ogni istante deve essere attraversato tutto lo UGen-Graph, indipendentemente dalla sua complessit. Attenzione: se si considera lesempio idraulico, ci signica che se una goccia entra da un tubo in entrata nellistante x listante dopo (x + 1) deve essere gi attraversato tutto limpianto, ed essere in uscita. Ovvero: dentro

6.4158

ogni macchinario, lo scorrimento del liquido lungo i tubi che connettono i dispositivi letteralmente istantaneo. Osservazione 8 I dispositivi possono lavorare a due velocit differenti (ma sempre costanti), in particolare a velocit di controllo e a velocit di sintesi Dunque ad ogni istante una nuova goccia deve uscire da un macchinario dopo aver percorso tutto il complesso dei dispositivi. I dispositivi non necessariamente per aggiornano il loro comportamento ad ogni istante: possono modicare la loro azione soltanto una volta ogni n istanti. Un segnale di controllo tipicamente un segnale che cambia meno nel tempo di un segnale audio e che quindi pu essere calcolato ad una risoluzione pi bassa. Ad esempio, inutile calcolare per ogni campione audio il valore di un inviluppo dampiezza. Se infatti si calcola un valore dellinviluppo per il quale moltiplicare il segnale audio, e lo si mantiene costante per 10 campioni audio, per poi ricalcolarlo allundicesimo, di nuovo maneternlo costante per altri 10, e cos via, si ottiene un segnale che evidentemente pi scalettato in ampiezza ad unanalisi della forma donda, ma che di fatto non sensibilmente diverso da un segnale in cui linviluppo dampiezza sia stato calcolato per ogni campioni. In compenso sono state risparmiate notevoli risorse computazionali. Un segnale simile un segnale calcolato non a tasso audio (audio rate), ma a tasso di controllo (control rate). come si vedr, le UGen generano segnali nel momento in cui ricevano il messaggio .ar, o .kr: rispettivamente il segnale risultante sar aggiornato a tasso audio (audio rate) o a tasso di controllo ([k]ontrol rate). Si noti che si sta parlando di tasso di aggiornamento, e non di numero di campioni. SC genera un segnale audio in

6.4159

tempo reale per forza a tasso audio: ma alcuni segnali che intervengono nella sintesi sono aggiornati ad un tasso pi basso di controllo. Osservazione 9 I liquidi possono per essere stoccati in quantit limitate dentro appositi contenitori, da cui possibile attingere quando serve. Questi liquidi di per s non scorrono, ma, attraverso dispositivi specializzati, possono essere riversati in un liquido in scorrimento Un buffer una memoria temporanea che permette di conservare dei dati audio richiesti da certi algoritmi di sintesi. Ad esempio, si consideri la lettura di un le audio in funzione dellelaborazione del suo contenuto. Si potrebbe partire da un frammento di voce umana e costruire un sintetizzatore che intoni la voce sulle diverse altezze come rappresentate sui tasti di un pianoforte. Un simile sintetizzatore concettualmente prevede due UGen.

la prima elabora il frammento vocale rispetto allaltezza la seconda responsabile della comunicazione con la scheda audio

Per poter elaborare il frammento, questultimo deve essere disponibile alla prima UGen tutte le volte che questa ne ha bisogno: si pensi ad eseguire una melodia a partire da quellunico frammento trasformando opportunamente laltezza lungo la sequenza temporale delle note che compongono la melodia. Il contenuto del le audio deve allora essere letto dallhard disk e viene conservato in memoria temporanea. In SC un buffer appunto una simile memoria che il server allocata su richiesta. Il segnale audio contenuto nel buffer di per s statico: e tuttavia vi

6.4160

pu essere una UGen che legge il contenuto del buffer in tempo reale e lo invia alla scheda audio. Osservazione 10 Tipicamente (anche se non necessariamente) un macchinario prevede un dispositivo munito di un tubo che permette di far uscire il liquido allesterno. Altre volte pu avere anche dispositivo con un tubo in entrata da cui ricevere un liquido che proviene da altri macchinari Il segnale numerico sintetizzato deve essere inviato alla scheda audio in modo tale che questultima lo converta in segnale elettrico e lo invii agli altoparlanti. Per questo compito esistono UGen specializzate, che prevedono entrate ma non uscite: infatti il segnale che entra non pi disponibile per la ulteriore elaborazione in SC, ma viene inviato alla scheda audio. Tipico esempio la UGen Out. Evidentemente UGen di questo tipo occupano lultimo posto nello UGen-Graph che rappresenta un synth. Se si omette una UGen di uscita il segnale viene calcolato secondo quanto previsto dallae altre UGen nello UGen-Graph ma non inviato alla scheda audio (fatica mentale e computazionale sprecata). Si supponga poi di collegare un dispositivo di entrata alla scheda audio, ad esempio un microfono. Una UGen specializzata pu rendere disponibile al synth tale segnale, in modo che possa essere elaborato (ad esempio subire un qualche tipo di distorsione). A tal proposito SC prevede la UGen AudioIn. Osservazione 11 La circolazione dei liquidi tra limpianto e lesterno (lacqua dallacquedotto in entrata, il prodotto sintetizzato in uscita) oppure tra i diversi macchinari nellimpianto avviene attraverso tubi speciali. I primi sono tubi di entrata/uscita, i secondi di collegamento. Attraverso quesi ultimi, i

6.4161

liquidi possono cos circolare nellimpianto e sono a disposizione degli altri macchinari. Questi tubi permettono perci di connettere diversi macchinari Si gi osservato come il server preveda una comunicazione con la scheda audio, in entrata (dal microfono) e in uscita (agli altoparlanti). Questi canali di comunicazione prendono il nome di bus, secondo un termine che deriva dalla tecnologia dei mixer 21. In effetti, rispetto a canale (che pure il termine audio pi vicino) il termine tubo pu essere meno fuorviante oltre mnemotecnicamente efcace. Il sistema dei bus non deve essere pensato come un sistema di tubi che connettono staticamente i macchinari, ma come un sistema di tubi disponibili a cui i macchinari si raccordano. Ad esempio, tutti i synth che intendono elaborare un segnale che provenga dallesterno possono raccordarsi al bus che deputato alla lettura dellentrata della scheda audio (al tubo che immette un liquido dallesterno). Tutti i synth, per inviano i segnali in uscita alla scheda audio, si raccordano ai bus che gestiscono la comunicazione con questultima: i segnali sui bus in uscita semplicemente si sommano. I bus nora citati sono specializzati per segnali audio (audio busses), ma esistono anche bus specicamente dedicati ai segnali di controllo control busses. I bus sono indicati attraverso un numero progressivo, un indice a partire da 0. Per i bus di controllo, la numerazione progressiva e non ci sono aspetti particolari da tener presenti. Per i bus audio, invece necessario ricordare che essi gestiscono la comunicazione con la scheda audio. In particolare i primi bus audio (0, . . . , n) sono riservati alle uscite della scheda audio, i secondi alle entrate (n + 1, . . . , o) (i tubi di entrata/uscita), seguono i bus ad uso interno (o + 1, . . .) (i tubi di collegamento. Dunque, lindicizzazione dipende dalla propria scheda audio. La congurazione standard con uscita stereo e ingresso microfono prevede i bus 0, 1 per i due canali stereo (i due altoparlanti) e il bus 2 per il
21

Il termine non sta per bus = mezzo di trasporto. Ed infatto pensare ad un bus audio come a un autobus fuorviante.

6.4162

microfono. Dal 3 in avanti, i bus possono essere utilizzati internamente. Per farci cosa? Per connettere diversi synth. Ad esempio, un synth instrada il segnale in uscita sul bus 4 da dove altri synth possono prenderlo. Ovvero, idraulicamente un macchinario si raccorda ad un tubo in un punto e vi immette del liquido: pi avanti lungo il tubo un secondo macchinario si raccorda e preleva il liquido immesso. Osservazione 12 I tubi di collegamento disperdono il loro liquido (che non uscito attraverso i tubi di uscita dellimpianto) negli scarichi dellimpianto. I liquidi non inquinano e la loro dispersione non rilevante Una volta immessi su un bus, i segnali sono disponbili. Se non si scrive su un bus connesso alla scheda audio semplicemente non si ha un risultato percepibile. In altre parole, inviare un segnale su un bus non richiede di sapere cosa altri potranno fare di quel segnale, e neppure se mai qualche altro synth lo utilizzer. Che cosa succede al segnale sul bus irrilevanet. Questo assicura una comunicazione possibile tra synth, ma senza che questa diventi obbligatoria o prevista in anticipo.

6.5 Appetizer: un esempio di sintesi e controllo in tempo reale

Lesempio seguente serve per dare unidea degli insieme degli elementi n qui citati. Senza scendere troppo nel dettaglio (scarso), ci si pu quantomeno fare unidea generale di come funziona il tutto.

6.5163

1 3 4 5 6 7 8 10 11 12 14 16 17 18 20 21 23 24 26 27 28 30 31 32 33 34 35 36 38

s = Server.local.boot ;

// first boot the server

( // audio SynthDef.new("sineMe", { arg out = 0, amp = 0.25, kfreq = 5 ; Out.ar(out, SinOsc.ar(kfreq*50, mul: LFPulse.kr(kfreq, 0.25))*amp); }).send(s); ) ( // four vars var aSynth, window, knob1, knob2, button; aSynth = Synth.new("sineMe"); // the synth // GUI: creation window = GUI.window.new("Knob", Rect(300,300,240,100)); window.front; knob1 = JKnob.new(window, Rect(30, 30, 50, 50)); knob1.value = 0.25; knob2 = JKnob.new(window, Rect(90, 30, 50, 50)); knob2.value = 0.3; button = GUI.button.new(window, Rect(150, 30, 50, 50)) ; button.states = [ // array of states [ "stop", Color.black ], ["start", Color.red]] ; // GUI: controlling audio knob1.action_({arg v; aSynth.set("amp", v.value); }); knob2.action_({arg v; aSynth.set("kfreq", v.value*15); }); button.action = ({ arg button; var val = button.value.postln; if (val == 1, { aSynth.run(false) }, { aSynth.run }) }); )

6.5164

6.5.1 Una synthDef

In primo luogo, necessario costruire lo strumento che generi il segnale. Un esempio minimale di synthDef quello riportato di seguito.
1 2 3 SynthDef.new("bee", { Out.ar(0, SinOsc.ar)} ).send(s);

SynthDef: loggetto che interessa .new( ): new il metodo costruttore, che costruisce effettivamente la synthDef (restituisce loggetto synthDef). Il metodo new prevede un certo numero di argomenti. Qui ne vengono specicati due, per gli altri opportuno lasciare quelli predeniti. "bee": il primo argomento una stringa che rappresenta il nome della synthDef: il nome verr associato allo UGen-graph. I synth generati a partire da questa synthDef saranno dei bee, cio dei synth del tipo bee. Qui bee una stringa, ma potrebbe anche essere un simbolo (\bee). { Out.ar(0, SinOsc.ar)}: lo UGen-graph racchiuso tra parentesi graffe. Tutto ci che tra graffe in SC una funzione. Dunque, lo UGen-graph descritto da una funzione. Lo UGen-graph costituito da due UGen, Out e SinOsc: questo fatto reso esplicito dal messaggio .ar che le due UGen ricevono. In generale ci che risponde al messaggio .ar o .kr una UGen. E perch una UGen generi

6.5165

in tempo reale un segnale deve ricevere i messaggi .ar o .kr. Perch una funzione per descrivere le relazioni tra UGen? Si ricordi che una funzione un oggetto che restituisce un valore ogni qualvolta glielo si chiede. Un synth allora descritto da una funzione perch ad ogni istante che passa gli viene chiesto di restituire un valore di ampiezza, il valore del campione audio. Ad ogni istante viene calcolato il valore della funzione descritta dalla UGen-Graph.
Out bus: 0

SERVER
SinOsc Out

Synth: "bee"

Fig. 6.8

Schema di una synthdef minimale.

In Figura 6.8 rappresentato, secondo le convenzioni precedenti, un synth costruito a partire dalla synthDef bee. Come si vede il segnale risultante viene raccordato (inviato) sul bus 0. In maniera pi consueta lo UGen-Graph pu essere descritto in forma di diagramma di usso dal grafo di 6.9 22. Out la UGen che si occupa di inviare alla scheda audio il segnale generato: senza Out non c comunicazione con la scheda audio, quindi non c suono udibile. Out prevede due argomenti. Il primo lindice del bus su cui inviare il segnale in uscita, il secondo il segnale stesso. Out riceve un segnale e lo spedisce al bus audio 0, che indica, come gi osservato, un canale della scheda audio (si ricordi: nei bus audio, prima gli output, poi gli input). Il segnale che spedisce gli viene fornito da SinOsc: un caso di patching, di innesto di una UGen (SinOsc in unaltra (Out). SinOsc genera una segnale sinusoidale: laddove non si specichino
22

Generato utilizzando le classi dot di Rohann Drape.

6.5166

la frequenza e la fase, queste varranno rispettivamente 440 (Hz) e 0.0 (radianti).

SinOsc

440

Out

Fig. 6.9 Rappresentazione dello UGen-graph.

.send(s): la synthDef, di per s, non serve a nulla se non caricata sul server. Il server convenzionalmente assegnato alla variabile globale s. Dunque il codice .send(s) chiama il metodo .send denito per la synthDef, e dice alla synthDef di spedirsi al server s. Attenzione: tutto ci che sta in una synthDef costituito da speciche istruzioni per la sintesi del segnale.

Oltre a send, vi sono molti metodi deniti per la classe SynthDef, che permettono ad esempio di scrivere la denizione su le (si tratta del metodo writeDeFile): le synthDef cos memorizzate verrano caricate ad ogni accensione del server, e saranno perci subito disponibili. Tornando allesempio, ora il server ha pronto il progetto bee per poter creare dei synth di tipo bee.

6.5.2 UGen e UGen-Graph

6.5167

La denizione della synthDef dellesempio originale invece la seguente.


1 2 3 SynthDef.new("sineMe", { arg out = 0, amp = 0.25, kfreq = 5 ; Out.ar(out, SinOsc.ar(kfreq*50, mul: LFPulse.kr(kfreq, width: 0.25))*amp); }).send(s);

Si noti come in questo caso lo UGen-Graph preveda alcuni argomenti in entrata che ne permettono il controllo in tempo reale. Essi sono out, amp, kfreq, tutti dotati di valori predeniti. Ogni synth di tipo sineMe metter a disposizione dellutente i tre controlli equivalenti in entrata. La riga 2 descrive il patching tra UGen (Out, SinOsc, LFPulse). Si osservi come questultima aggiorni i propri valori ricalcolando il valore in uscita a tasso di controllo, secondo quanto previsto dal messaggio kr inviato a LFPulse. opportuno ora soffermarsi di pi su una UGen, in particolare su SinOsc. Per sapere come si comporta ci si pu evidentemente rivolgere allhelp le relativo. Unaltra opzione, utile in fase di studio almeno, consiste nellaccedere alla denizione nel codice sorgente.
1 2 3 4 5 6 7 8 9 10 SinOsc : UGen { *ar { arg freq=440.0, phase=0.0, mul=1.0, add=0.0; ^this.multiNew('audio', freq, phase).madd(mul, add) } *kr { arg freq=440.0, phase=0.0, mul=1.0, add=0.0; ^this.multiNew('control', freq, phase).madd(mul, add) } }

6.5168

Si noti come SinOsc erediti da UGen, la sopraclasse generica di tutte le UGen. In pi, denisce soltanto due metodi di classe, ar e kr. Lasciando perdere lultima riga di ogni metodo, si nota come i metodi prevedano un certo numero di argomenti a cui pu essere passato un valore da fuori. Si noti anche come tutti gli argomenti tipicamente abbiano un valore predenito. Cos
SinOsc.ar

del tutto identico a


SinOsc.ar(freq: 440.0, phase: 0.0, mul: 1.0, add: 0.0)

ovvero a
SinOsc.ar(440.0, 0.0, 1.0, 0.0)

Gli ultimi due argomenti sono mul e add, e sono condivisi dalla maggior parte delle UGen: mul un moltiplicatore del segnale mentre add un incremento (positivo o negativo) che viene sommato al segnale. Si considerino gli esempi seguenti

{SinOsc.ar(220,

mul: 1, add:0)}.scope ; ;

{SinOsc.ar(220)}.scope

SinOsc genera una sinusoide a 220Hz . Il segnale generato da una UGen tipicamente normalizzato, la sua ampiezza oscilla in [1, 1] (altre volte compreso in [0, 1]). Largomento mul denisce un moltiplicatore che opera sullampiezza cos denita, mentra add un incremento che si aggiunge allo stesso segnale. Nella prima riga il segnale moltiplicato per 1 e sommato a 0. Il segnale cio immutato. Si noti che i valori specicati sono quelli predenti, quindi si potrebbe scrivere la riga successiva ed ottenre esattamente lo stesso risultato. A scanso di equivoci,

6.5169

moltiplicare e aggiungere signica che ogni campione del segnale moltiplicato e sommato per i valori specicati nei due argomenti. Invece in questo esempio

{SinOsc.ar(220,

mul: 0.5, add:0)}.scope ;

il segnale risulta moltiplicato per 0.5 (e sommato a 0, ma irrilevante): la sua ampiezza sar compresa in [1.0, 1.0] 0.5 = [0.5, 0.5]. Inne nellesempio seguente

{SinOsc.ar(220,

mul: 0.5, add:0.5)}.scope ;

il segnale precedente viene sommato a 0.5: la sua ampiezza sar compresa in [1.0, 1.0] 0.5 + 0.5 = [0.5, 0.5] + 0.5 = [0.0, 1.0]. Lassegnazione mul del valore costante 0.5 indica che ogni nuovo campione verr moltiplicato per 0.5. Si potrebbe pensare allora che mul sia un segnale costante. A tal proposito si pu prendere in considerazione la UGen Line. Come dice lhelp le: Line line generator

...
Generates a line from the start value to the end value. I primi tre argomenti di Line sono start, end, dur: Line genera una sequenza di valori che vanno da start a adur in dur secondi. Nel codice seguente

{SinOsc.ar(220)*Line.ar(0.5,0.5,

10)}.scope

Line genera per 10 secondi una sequenza di valori pari a 0.5 (cio una progressione da 0.5 a 0.5). Il segnale in uscita dalloscillatore SinOsc viene moltiplicato per luscita di Line. Ad ogni istante di tempo il campione calcolato dalla prima UGen viene moltiplicato per il campione calcolato dalla seconda (che ha sempre valore 0.5). Si noti che il segnale

6.5170

risultante uguale a quello precedente. chiaro che laspetto interessante nelluso di Line sta proprio nel fatto che i valori che la UGen genera non sono costanti ma variano invece tipicamente secondo una progressione (lineare, appunto). Questo un crescendo dal niente:

{SinOsc.ar(220)*Line.ar(0.0,1.0,

10)}.scope

Il patching appunto linnesto di una UGen in un argomento di unaltra o il calcolo di un segnale a partire dal contributo offerto da pi UGen in una qualche relazione reciproca (qui di moltiplicazione). Lesempio permette di capire come gli argomenti possano essere descritti non da costanti ma da variabili, cio da alti segnali. In altre parole, i segnali possono modicare qualsiasi aspetto controllabile di altri segnali.
kfreq:5

b: 50

SinOsc

freq

phase: 0

LFPulse

freq

iphase: 0.25

width: 0.5

amp:0.25

out:0

Out

bus

channelsArray

Fig. 6.10

Rappresentazione dello UGen-graph.

6.5171

Nella gura 6.10 rappresentato il diagramma di usso della synthDef "sineMe". Gli elementi neri descrivono il usso a tasso audio, tutti gli altri le informazioni a tasso diverso (controllo/evento). LFPulse lavora a tasso di controllo, mentre kfreq, amp, out vengono modicate a tasso di evento (ogni qualvolta un utente modica i parametri). I blocchi * a b indicano blocchi di moltiplicazione. I valori di argomenti nelle UGen non specicati sono indicati attraverso i valori predeniti. Il segnale moltiplicatore prodotto da LFPulse, un generatore di onde quadre, con frequenza "kfreq" (quindi collegata alla frequenza della sinusoide). Il segnale in uscita da LFPulse unipolare, cio compreso nellintervallo [0, 1]. Si pu capire meglio la natura del segnale generato attraverso:

{LFPulse.ar(100,

mul: 0.5)}.scope

Come si vede, il segnale prevede solo due valori di ampiezze, 0.0 e 0.5 (a causa dellargomento mul), e oscilla da uno allaltro 100 volte al secondo. Utilizzando un segnale simile come moltiplicatore di un altro segnale, si ha che, quando lampiezza 0.0, il segnale risultante ha ampiezza 0.0 (silenzio), quando lampiezza 0.5, in uscita si ha il segnale di partenza scalato per 0.5. In sostanza, si produce una intermittenza. Dunque, la frequenza della sinusoide correlata alla frequenza di intermitteza (pi acuta la frequenza, pi frequentemente intermittente).

6.5.3 Un synth

Le righe 10 e 11 dellesempio in discussione creano e mettono in funzione un synth. In particolare


aSynth = Synth.new("sineMe")

6.5172

assegna alla variabile aSynth un oggetto di tipo Synth inviando il costruttore new alla classe. Il costruttore prevede come argomento una stringa che indica la synthDef da cui il synth viene fabbricato "sineMe". Nel momento in cui viene create attraverso new, il synth viene attivato (= suona). Questo comportamento tipicamente utile, perch in realt un synth pu essere pensato come uno strumento (un sintetizzatore) ma anche come un evento sonoro (una nota): a pensarlo cos, diventa ovvio che la costruzione del synth equivale alla generazione di un evento sonoro. Il codice seguente assume che la synthDef precedente sia ancora disponibile.
1 ~synth1 = Synth.new("sineMe", [\kfreq, 10]) ; // synth plays, array of args ~synth2 = Synth.newPaused("sineMe") ; // another synth ~synth2.run(true) ; // start playing ~synth1.set(\kfreq, 3) ; // set kfreq to 3 // set 2 args from amp // get kfreq

3 4 6 8 10 12

~synth2.setn(\amp, [20, 0.1]) ;

~synth1.get(\kfreq, { arg val ; val.postln }) ;

~synth2.getn(\out, 3, { arg valArray ; valArray.postln }) ; // get 4 args from out ~synth1.run(false) ; // stop playing ~synth2.run(false) ; // stop playing ~synth1.free ; // free the synth ~synth2.free ; // free the synth

14 15 17 18

6.5173

La riga 1 crea un synth e lo associa alla variabile ~synth1. Dopo il nome, viene specicato un array che contiene coppie composte da nome dellargomento e valore (in questo caso una sola coppia, \kfreq e 10). Se si vuole costruire un synth senza renderlo immediatamente attivo possibile usare il metodo newPaused, come avviene per il synth associato a ~synth2. Il metodo run controlla attivazione/disattivazione (play/pause per cos dire) del synth, attraverso un valore booleano true/false (4, 14, 15). Il metodo set permette di controllare un argomento, passando nome e valore (6). Attraverso setn possibile controllare pi argomenti, passando il nome del primo e un array che contiene i valori degli n argomenti successivi. Nellesempio vengono impostati due argomenti a partire da \amp (incluso), ovvero \amp e \kfreq (10). Simmetricamente i metodi get e getn consentono di recuperare i valori degli argomenti dal synth. Il primo restituisce il valore di un singolo argomento, mentre il secondo un array di n valori (3 nellesempio) a partire dal primo indicato (\out, ovvero i valori di \out, \amp, \kfreq). I valori sono passati come argomenti della funzione seguente, che, negli esempi di riga 10 e 12, chiede semplicemente di stampare i valori stessi sulla post window. Una synth non pi utilizzato pu essere messo in pausa, ma in quel caso continua ad essere residente in memoria, e dunque ad occupare inutilmente risorse del calcolatore. Per eliminare dal server il synth. sufciente inviare alloggetto synth il messaggio free (17, 18).

6.5.4 GUI e controller

Sulla creazione di elementi GUI non c molto da dire. Si tratta di costruire due manopole allinterno di una nestra, secondo una tecnica gi vista pi volte. Di nuovo c la creazione di un pulsante a partire da GUI.button, secondo una sintassi del tutto analoga a quella di GUI.knob.

6.5174

La riga successiva denisce quanti stati ha il pulsante attraverso un array che contiene le propriet di ognuno. Cos lo stato 0 prevede la scritta stop in colore nero Color.black, mentre lo stato 1 prevede la scritta start in colore rosso Color.red. Pi interessanti invece le azioni collegate alla variazione delle manopole e al pulsante. La riga
knob.action_(arg v; aSynth.set("amp", v.value);

denisce la connessione tra il controller GUI knob1 e sintesi audio. Lazione associata alla manopola (knob1.action_)
aSynth.set("amp", v.value)

Ogni volta che cambia il valore della manopola viene chiamato sulloggetto aSynth (il synth) il metodo set che assegna al parametro specicato ("amp") un valore, qui v.value. Il parametro ("amp") viene allora aggiornato dentro lo UGen-Graph. Esso indica il moltiplicatore che posto in fondo alla cascata delle UGen SinOSc e LFPulse: di fatto si comporta come un controllo del volume a cui viene attacata una manopola knob1. Questultima genera valori nellintervallo [0, 1], cio nellescursione dampiezza del segnale audio in forma normalizzata, e il segnale dunque varier tra 0 e il valore massimo in uscita dal patching precedente. Analogamente
aSynth.set("kfreq", v.value*15)

assegna al parametro "kfreq" il valore v.value*15. Lescursione di "kfreq" sar perci compresa in [0, 15]. Inne anche a button viene assegnata unazione secondo una sintassi analoga a JKnob: largomento button indica appunto il pulsante, per cui button.value permette di accedere allo stato del pulsante (ovvero lindice dello stato nellarray degli stati button.states). Ad ogni pressione del pulsante viene richiamata lazione. Lazione valuta il lo stato del pulsante attraverso il costrutto condizionale (34). Se il valore val del pulsante 1 viene chiamato il metodo

6.5175

run(false) su aSynth, che mette in pausa il synth. Nel caso negativo (e dunque se val 0) il synth viene attivatoaSynth.run. Quando si esegue il codice, il synth attivo e lo stato 0. Alla prima pressione lo stato diventa 1, la GUI viene aggiornata e viene eseguito il ramo condizionale che contiene aSynth.run(false).

6.5176

7 Controlli e canali

7.1 Inviluppi

Si gi discusso degli inviluppi dampiezza a proposito del calcolo di segnali in tempo differito. In particolare si era avuto modo di notare come la classe Env fosse specializzata nella generazione di segnali dinviluppo. In realt un oggetto di tipo Env non un segnale, ma una sorta di stampo del prolo temporale dilatabile (o comprimibile) a piacere. Tant che per ottenere un segnale in tempo differito era stato necessario inviare il messaggio asSignal. In tempo reale un oggetto Env una forma temporale a disposizione di una UGen specializzata che, leggendo il contenuto di Env, genera un segnale: la UGen EnvGen appunto specializzata in questo compito. In effetti, largomento times del costruttore di Env, che richiede di specicare un array di durate, deve essere inteso non come una specicazione assoluta, quanto piuttosto proporzionale. Le durate specicate indicano cio la proporzione tra parti dellinviluppo (si ricordi che le durate sono qui riferite agli intervalli tra

7.1177

valori dampiezza), non necessariamente una indicazione cronometrica. Si consideri la denizione nel sorgente SC del metodo *ar di EnvGen (che uguale, tasso a parte, a quella di *kr):
*ar

arg envelope, gate = 1.0, levelScale = 1.0, levelBias = 0.0,

timeScale = 1.0, doneAction = 0; }

Come specica ulteriormente dallhelp le relativo, largomento envelope unistanza di Env. Gli argomenti levelScale, levelBias, timeScale sono tutti operatori che trasformano linviluppo passato come primo argomento. Si prenda come esempio linviluppo
1 2 3 4 e = Env.new( levels:[0.0, 1.0, 0.5, 0.5, 0.0], times: [0.05, 0.1, 0.5, 0.35] ).jplot ;

levelScale: moltiplica i valori dampiezza dellinviluppo. Il valore predenito 1.0, che lascia i valori di levels in e immutati levelBias: viene sommato ai valori dampiezza. Se questi sono compresi in entrata nellescursione [0.0, 1.0], in uscita essi saranno compresi in [0 + levelBias, 1.0 + levelBias]. Anche qui il valore di default (0.0) non modica i valori di e timeScale: moltiplica i valori di durata. La durata del nuovo inviluppo sar perci timeScale times. Il totale degli intervalli in e pari a 1, dunque la durata dellinviluppo generato da EnvGen sar pari a 1 timeScale. Come in precedenza, il valore predenito (1.0) non cambia i valori dellinviluppo passato come primo argomento.

7.1178

Come si vede EnvGen pu stirare la forma dellinviluppo in lungo e in largo. In effetti, conviene utilizzare come escursione sia per lampiezza che per la durata di un inviluppo Env lintervallo normalizzato [0.0, 1.0]. Si potr poi agire sugli argomenti di EnvGen. Nellesempio seguente viene modicato un esempio tratto dallhelp le. Qui linviluppo il tipico inviluppo percussivo senza sostegno, accessibile inviando a Env il messaggio perc
1 2 // explicit signal multiplication { EnvGen.kr(Env.perc, 1.0, doneAction: 0) * SinOsc.ar(mul: 0.1) }.play ; // effect of timeScale { EnvGen.kr(Env.perc, 1.0, timeScale: 10, 0.1) }.play ; // using mulAmp { SinOsc.ar(mul: 0.1 * EnvGen.kr(Env.perc, 0) ) }.play ;

4 5

doneAction: 0) * SinOsc.ar(mul:

7 8

1.0, timeScale: 10,

doneAction:

10 11

// gate = 0 { EnvGen.kr(Env.perc, 0.0, doneAction: 0) * SinOsc.ar(mul: 0.1) }.play ; // controlling the gate { EnvGen.kr(Env.perc, SinOsc.kr(4), doneAction: 0) * SinOsc.ar(mul: 0.1) }.play ; JMouseBase.makeGUI ; // SwingOSC { EnvGen.kr(Env.perc, GUI.mouseX.kr(-1,1), doneAction: 0) * SinOsc.ar(mul: 0.1) }.play ;

13 14

16 17

Si confrontino i segnali risultanti dalle righe 2 e 5 di codice. Si noter leffetto di timeScale, che dilata gli intervalli temporali di un fattore 10.

7.1179

Lesempio di riga 3 illustra unimplementazione tipica dellinviluppo: i due segnali vengono moltiplicati tra di loro ed, essendo un unipolare e laltro bipolare, il risultato sar un segnale bipolare (la sinusoide inviluppata, come si vede sostituendo a play il metodo jplot). Laltra implementazione tipica in SC (che produce lo stesso risultato) illustrata nella riga successiva in cui il segnale di inviluppo generato da EnvGen il valore dellargomento mul. Gli altri due argomenti sono:

gate: largomento gate specica un trigger. Un trigger un segnale di attivazione, qualcosa che fa partire qualcosa. Per la precisione, un trigger funziona come una fotocellula: ogniqualvolta registra un passaggio, invia un segnale. Tipicamente in SC il passaggio che fa scattare il trigger il passaggio dallo stato di partenza, che ha valore 0, ad uno stato con valore > 0. In sostanza, EnvGen genera al tasso prescelto un segnale dampiezza pari a 0 nch non riceve il trigger. A quel punto legge linviluppo. Nei tre esempi alle righe 1-7 il valore di gate 1.0 (che anche quello di default): essendo superiore a 0 fa scattare il trigger. Lattivazione del trigger dice alla UGen EnvGen di leggere linviluppo. Dunque, eseguendo il codice si ascolta il segnale inviluppato. Lesempio successivo prevede come valore di gate 0, e il trigger non scatta. Se si sostituisce ad una costante un segnale il trigger viene attivato tutte le volte che viene sorpassato il valore di soglia 0. Nella riga 12 una sinusoide con frequenza 4Hz che controlla il trigger gate. Tutte le volte che supera lasse delle ascisse (entrando nel semipiano positivo sopra lo 0) il trigger viene attivato. Ci avviene 4 volte al secondo. Inne, nellesempio seguente gate un segnale generato daGUI.mouseX. Lasse orizzonatale della tavola graca costruita da JMouseBase.makeGUI viene ripartito nellintervallo [1, 1]. Lo 0 perci a met lungo lasse. Tutte le volte che lo si supera il trigger viene attivato.

7.1180

{ EnvGen.kr(Env.perc, 1.0, doneAction: 0) * SinOsc.ar(mul: 0.1) }.play ; // > SC works for you ( SynthDef.new("sinePerc", { Out.ar(0, EnvGen.kr(Env.perc, 1.0, doneAction: 2) * SinOsc.ar(mul: 0.1)) }).send(s); ) Synth.new("sinePerc") ;

3 5 6 7 8 9 10 11 13

doneAction: un suono percussivo quale quello generato inviluppando una sinusoide, ad esempio con Env.perc, genera il tipico inviluppo percussivo senza sostegno. Ma che succede quando linviluppo terminato? necessario ricordare che nellesempio di partenza, riportato qui sopra nella riga 1, SC svolge un lavoro oscuro ma sostanziale, che riportato dalla riga 6 in avanti: crea una synthDef istanzia da questa un synth e lo mette in funzione Se si considera questo secondo blocco di codice, si pu meglio apprezzare il problema. Che ne fa il synth? Senza deallocazione esplicita, resta attivo: EnvGen continua a generare un segnale dampiezza nulla, e unaltra UGen responsabile della sinusoide, SinOsc un altro segnale. Spetta infatti allutente rimuovere il synth un volta che questi ha generato il segnale percussivo richiesto. Una sequenza di 20 suoni percussivi allocherebbe un numero analogo di synth, con intuibile spreco di RAM e di CPU. Largomento doneAction permette

7.1181

di evitare il lavoro di deallocazione, poich esso viene preso in carico direttamente da scsynth. Largomento doneAction permette di specicare che cosa scsynth debba fare del synth in questione una volta terminata la lettura dellinviluppo. I valori possibili sono attualmente 14. Ad esempio con doneAction = 0 scsynth non fa nulla e il synth resta allocato e funzionante. Il valore pi utilizzato doneAction = 2, che dealloca il synth. In sostanzia quel synth non c pi e non c da preocuparsi della sua esistenza. Ovviamente il synth non pi disponbile. Se si volesse generare unaltra sinusoide percussive sarebbe necessario costruire un altro synth dalla stessa synthDef. Si consideri lesempio seguente in cui il mouse funziona come un trigger. Se doneAction = 0 il synth residente e ad ogni passaggio del mouse pu essere attivato grazie al trigger. Se doneAction = 2 dopo la prima attivazione il synth deallocato e il comportamento del mouse non pu inviare alcun messaggio. Nellinterfaccia graca del server visibile il numero dei synth attivi: si nota allora lincremento del numero quando creato il synth (implicitamente) e il diverso comportamento (persitenza/decremento) in funzione di doneAction.
1 3 4 JMouseBase.makeGUI ; // SwingOSC // after reading Env.perc synth is still resident { EnvGen.kr(Env.perc, GUI.mouseX.kr(-1,1), doneAction: 0) * SinOsc.ar(mul: 0.1) }.play ; // after reading Env.perc synth is freed { EnvGen.kr(Env.perc, GUI.mouseX.kr(-1,1), doneAction: 2) * SinOsc.ar(mul: 0.1) }.play ;

6 7

7.2182

7.2 Generalizzazione degli inviluppi

Sebbene luso degli inviluppi sia tipico (e sia stato originato) per lampiezza, va comunque ricordato che EnvGen di fatto un lettore di tabelle. Un oggetto Env in effetti del tutto analogo ad una cosiddetta tabella in cui sono tabulati punti che descrivono un prolo. EnvGen allora ununit di lettura di tabelle speciali (di tipo Env) il cui contenuto pu essere utilizzato per controllare parametri di diverso tipo. Si consideri il codice seguente:

7.2183

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 27

( SynthDef.new("sinEnv", { // defining an env var levels, times, env ; levels = Array.fill(50, { arg x ; sin(x)*x }).normalize ; times = Array.fill(49, 1).normalizeSum ; env = Env.new(levels, times) ; // using it extensively Out.ar(0, Pan2.ar( SinOsc.ar( freq: Latch.kr( EnvGen.kr(env, timeScale: 50,levelScale: 100, levelBias:30 ).poll .midicps.poll, LFPulse.kr( EnvGen.kr(env, timeScale: 50, levelScale: 5, levelBias: 10)) ), mul: LFPulse.kr(EnvGen.kr(env, timeScale: 50,levelScale: 10, levelBias: 15 )) ), EnvGen.kr(env, timeScale: 50, levelScale: 2, levelBias: -1), 0.4 ) ) }).send(s) ; ) Synth.new("sinEnv") ;

Alcune considerazioni sulla synthDef:

Inviluppo Le righe 5-9 deniscono un inviluppo env, costituito da 50 punti dampiezza intervallati da 49 durate. In particolare levels un

7.2184

array che contiene un frammento di sinusoide che cresce progressivamente in ampiezza. La progressivit data dal moltiplicatore *x, dove x lindice dellelemento calcolato. Si provi:
Array.fill(50,

arg x ; sin(x)*x

}).normalize.plot

Il metodo normalize scala i valori dellarray nellintervallo [0.0, 1.0].


1 3 [1,2,3,4,5].normalize [ 0, 0.25, 0.5, 0.75, 1 ]

Nellesempio dalla post window lintervallo in cui sono compresi i valori del array sono riscalati tra [0.0, 1.0]. I punti dampiezza calcolati per levels sono pensati come equispaziati. per questo che la denizione di times produce semplicemente un array riempito dal valore 1. Il metodo normalizeSum restituisce un array array/array.sum, dove a sua volta il metodo sum restituisce la somma degli elementi dellarray in questione.
1 3 5 7 [1,2,3,4,5].sum 15 [1,2,3,4,5].normalizeSum [ 0.066666666666667, 0.13333333333333, 0.2, 0.26666666666667, 0.33333333333333 ] [1,2,3,4,5].normalizeSum.sum 1

9 11

7.2185

Come si vede nellesempio dalla post window, la somma degli elementi nellarray 15 e dunque normalizeSum divide per 15 ogni elemento dellarray. Poich 5 d un contributo pari a un terzo di 15, nellarray restituito da normalizeSum il valore relativo pari ad un terzo di 1 (0.3 . . .). I due array levels e times sono allora nella forma normalizzata preferibile. Lidea alla base della synthDef quella di utilizzare lo stesso inviluppo, opportunamente modicato, per controllare aspetti diversi del segnale. Prima di addentrarsi nella descrizione delle relazioni tra le UGen conviene avere sottomano un diagramma di usso che ne visualizzi le relazioni, quale quello in Figura 7.1. Il diagramma semplicato rispetto al codice, pich non vi sono rappresentati i valori numerici degli inviluppi (che altrimenti risulterebbero in un graco illeggibile).
Impulse freq: 10 phase: 0 EnvGen gate: 1 levelScale: 100 levelBias: 30 timeScale: 50 doneAction: 0 :1 :1 : -99 : -99 :0 :1 :1 :0 EnvGen gate: 1 levelScale: 5 levelBias: 10 timeScale: 50 doneAction: 0 :1 :1 : -99 : -99 :0 :1 :1 :0 Impulse freq: 10 phase: 0 Poll trig in label: -1 trigid: 12 : 85 : 71 : 101 : 110 : 40 : 69 : 110 : 118 : 71 : 101 : 110 : 41 MIDICPS a LFPulse freq iphase: 0 width: 0.5 EnvGen gate: 1 levelScale: 2 levelBias: 10 timeScale: 50 doneAction: 0 :1 :1 : -99 : -99 :0 :1 :1 :0

Poll

trig

in

label: -1

trigid: 17

: 85

: 71

: 101

: 110

: 40

: 85

: 110

: 97

: 114

: 121

: 79

: 112

: 85

: 71

: 101

: 110

: 41

Latch

in

trig

MIDICPS

SinOsc

freq

phase: 0

LFPulse

freq

iphase: 0

width: 0.5

EnvGen

gate: 1

levelScale: 2

levelBias: -1

timeScale: 50

doneAction: 0

:1

:1

: -99

: -99

:0

:1

:1

:0

Pan2

in

pos

level: 0.4

Out

bus: 0

channelsArray

nil

Fig. 7.1

Diagramma di usso (semplicato).

Out.ar(0, Pan2.ar(SinOsc.ar. . . Si consideri la Figura 7.2 che riporta lultima parte del diagramma di usso. In nero rappresentato il usso del segnale audio. Iniziando dal fondo, la UGen nale Out che invia al bus 0 quanto previsto dallargomento successivo, Pan2.ar. Questultima UGen gestisce il panning stereo, ovvero la distribuzione tra due anali del segnale: il suo metodo *ar cos descritto nel codice sorgente:
*ar

arg in, pos = 0.0, level = 1.0;

7.2186

99

:0

:1

:1

:0

EnvGen

gate: 1

levelScale: 5

levelBias: 10

timeScale: 50

doneAction: 0

:1

:1

: -99

: -99

:0

:1

:1

:0

: 101

: 110

: 41

MIDICPS

LFPulse

freq

iphase: 0

width: 0.5

EnvGen

gate: 1

levelScale: 2

levelBias: 10

timeScale: 50

doneAction: 0

:1

:1

: -99

: -99

:0

:1

:1

:0

: 112

: 85

: 71

: 101

: 110

: 41

Latch

in

trig

MIDICPS

SinOsc

freq

phase: 0

LFPulse

freq

iphase: 0

width: 0.5

EnvGen

gate: 1

levelScale: 2

levelBias: -1

timeScale: 50

doneAction: 0

:1

:1

: -99

: -99

:0

:1

:1

:0

Pan2

in

pos

level: 0.4

Out

bus: 0

channelsArray

nil

Fig. 7.2 Diagramma di usso (semplicato): estratto. Pan2 riceve cio in entrata un segnale in e lo posiziona tra i due canali attraverso largomento pos: se pos vale 1 allora il segnale tutto sul canale sinistro, se pos vale 1 tutto sul destro, se pos vale 0 ripartito tra i due in misura uguale. Ovviammente tutti i valori intermedi sono possibili. Largomento level permette di specifcare un moltiplicare generale per cui vengono scalati entrambi i segnali prima di andare in uscita. Dunque ad Out richiesto attraverso Pan2 di inviare alla scheda audio una coppia di segnali ottenuta scalando in modo complementare lampiezza del segnale in. Si noti come venga specicato un unico bus (0), ma il segnale sia distribuito su due canali ( stereo). Se ne discuter pi avanti, per ora si osservi come al panner venga fornito in entrata un segnale generato da un oscillatore sinusoidale (opportunamente) scalato: ci che avviene dunque che una sinusoide (opportunamente) elaborata viene (opportunamente) distribuita dal panner sui due canali. Nella synthDef, mentre level vale 0.4 (riga 22), il primo uso dellinviluppo env nel controllo del panning, cio come argomento di pos (riga 21, cfr. 7.2). Qui EnvGen legge env espandendolo temporalmente di un fattore 50 (timeScale), espandendone lescursione dei valori dalloriginario [0.0, 1.0] a [0.0, 2.0] (levelScale), aggiungendovi 1 (levelBias), cos che lescursione nale sia [1.0, 1.0], ovvero quella richiesta da Pan2.

7.2187

Il panning seguir linviluppo env a sinusoide crescente, oscillando sempre di pi sbilanciato tra i due canali.

SinOsc.ar. . . Il blocco compreso tra le righe 13 e 20 contiene la denizione del segnale che viene distribuito dal panner: si tratta di un oscillatore SinOsc. Se si comincia dalllargomento mul si nota come sia occupato da un generatore di onde impulsie a bassa frequenza, secondo un esempio gi discusso in un altro caso. Lunica variante che la frequenza degli impulsi che modulano la sinusoide varia seguendo env (20). In modo analogo a quanto visto sopra, il segnale generato da EnvGen in questo caso varier tra [15.0, 25.0], sempre lungo 50 secondi. Si noti che lincremento della densit delle pulsazioni correlato allo spostamento verso il canale destro ( lo stesso inviluppo). freq: Latch.kr. . . Se si osserva nuovamente la Figura 7.1 si nota come largomento freq di SinOsc sia controllato da una UGen, Latch. Dunque, la frequenza varia in funzione del segnale generato da Latch. Questultima implementa in SC un algoritmo classico, detto sample and hold. Si osservi la sintassi del metodo *kr dal codice sorgente:
*kr

arg in = 0.0, trig = 0.0;

Il primo argomento, in, un segnale da cui Latch preleva un valore. Il segnale in uscita da Latch costituito da un valore costante pari al valore campionato no al prelievo successivo: di qui il nome tipico di sample (campiona) and hold (tieni). Il campionamento dipende da trig: un segnale trigger che attiva il prelevamento ogni volta che si ha passaggio da negativo a positivo. Si considerino i due esempi seguenti:

7.2188

1 2 4 5

// line going from 1 to 0 in 20 secs { Line.kr(1, 0, 20) }.scope // sampled & held by a latch { Latch.kr(Line.ar(1, 0, 20), SinOsc.ar(0.5)) }.scope

La prima funzione genera un segnale i cui valori diminuiscono linearmente da 1 a 0 nellarco di 20 secondi. La seconda campiona attraverso Latch il primo segnale pescando ad una frequenza che dipende dal trigger denito da SinOsc. Si ha triggering tutte le volte che il segnale passa da negativo a positivo: poich la sinusoide con i valori predenti oscilla tra [1, 1] un simile passaggio si ha tutte le volte che la sinusoide inizia il ciclo (con valore 0). Attenzione, il passaggio da positivo a negativo a met del ciclo non funziona come trigger. La frequenza della sinusoide pari ad un ciclo ogni due secondi. Dunque, il valore campionato da Latch verr tenuto per due secondi. Se si eseguono le righe di codice si nota come nella prima ci sia un decremento continuo nel tempo, mentre nella seconda, che mostra loutput del Latch, si vede attraverso gli scatti che laggiornamento procede a frequenza 0.5Hz (una volta ogni due secondi, appunto). Nellesempio di partenza Latch campiona e mantiene il segnale in uscita da EnvGen (riga 15). Si noti timeScale = 50 che dilata linviluppo env in un tempo di 50 secondi. Per quanto concerne i valori dampiezza essi saranno compresi in [0, 1] 100 + 30 = [30, 130]: env infatti compreso tra [0, 1], ma viene prima moltiplicato per levelScale e quindi sommato a levelBias. Tralasciando momentaneamente il messaggio poll si noti il messaggio successivo, midicps, che chiede di convertire i valori in

7.2189

uscita da valori midi a frequenze (cps, o Hz : quantitativamente lo stesso). Il valore midi del la sopra il do centrale 69, e la sua frequenza 440Hz . Dunque:
1 3 5 7 69.midicps 440 440.cpsmidi 69

In forma pi complessa:
1 3 4 Array.series(7, 10,1).postln.midicps [ 10, 11, 12, 13, 14, 15, 16 ] [ 14.56761754744, 15.433853164254, 16.351597831287, 17.323914436055, 18.354047994838, 19.44543648263, 20.601722307054 ]

Tornando allescursione dellesempio:


1 2 [30, 130].midicps [ 46.249302838954, 14917.240368579 ]

Questultima lescursione in Hz in cui si muove linviluppo. Questinviluppo viene campionato a tasso variabile, poich la frequenza di campionamento di Latch controllata da un generatore di impulsi, LFPulse, la cui frequena di pulsazione a sua volta determinata da

7.2190

un EnvGen a cui fornito linviluppo env. Anche questo env dilatato in 50 secondi, mentre la sua escursione (la frequenza di LFPulse) si estende in [10, 15] (Hz ).

poll Larchitettura client/server di SC pone un problema di rilievo che spesso risulta nascosto. Il processo di sintesi controllato dal client, ma realizzato dal server. Il cliente dice cosa fare al fornitore di servizi il quale svolge la sua azione ottemperando alle richieste. Come per pu sapere il client cosa succede dentro il server? Come si fa a sapere se non si sono commessi errori di implementazione? Il solo feedback audio non sufciente (il fatto che il risultato sia interessante indipendentemente dalla correttezza dellimplementazione senzaltro positivo, ma la serendipit non aiuta il debugging . . .). Il feedback visivo (attraverso i metodi scope e plot ad esempio) non analitico, cio non comunica direttamente cosa succede in termini di campioni audio calcolati. Il metodo poll, denito sulle UGen, dice al server di inviare indietro al client il valore di un campione audio, ad un tasso impostabile come argomento del metodo, e di stamparlo sulla post window. Questo permette di monitorare cosa succede a livello campione nel server in uscita da ogni UGen. In sostanza, il metodo pu essere concatenato dopo i metodi ar e kr. Ad esempio:

7.2191

1 3 4 5 6 7 8 9 10 11 12 13 14 15

{SinOsc.ar(Line.ar(50, 10000, 10).poll).poll}.play Synth("temp__1198652111" : 1001) UGen(Line): 50.0226 UGen(SinOsc): 0.00712373 UGen(Line): 149.523 UGen(SinOsc): -0.142406 UGen(Line): 249.023 UGen(SinOsc): -0.570459 UGen(Line): 348.523 UGen(SinOsc): -0.982863 UGen(Line): 448.023 UGen(SinOsc): -0.616042 UGen(Line): 547.523 UGen(SinOsc): 0.676455

Per ogni UGen, poll stampa sullo schermo il valore del segnale in uscita. Si noti come SinOsc oscilli in [1, 1] mentre Line inizi la progressione lineare da 50 a 10000. La sequenza poll.midi.poll nellesempio precedente stampa prima il valore in uscita da EnvGen, quindi la sua conversione in Hz .

7.3 Sinusoidi & sinusoidi

Lipotesi alla base dellutilizzo di un inviluppo dampiezza sta nel controllo di un segnale da parte di un altro segnale in modo da ottenere un risultato pi complesso, naturale, interessante etc. Un segnale di inviluppo un segnale unipolare, ma evidentemente possibile utilizzare segnali bipolari. In particolare la sinusoide non soltanto la forma donda che produce lo spettro pi semplice ma anche la forma tipica che assume una variazione regolare intorno ad un punto di equilibrio.

7.3192

Dunque, una sinusoide descrive opportunamente un fenomeno di oscillazione intorno ad un valore medio. Si considerino due casi:
1 2 4 5 7 9 11 12 13 14 16 17 18 19 // minimal tremolo { SinOsc.ar(mul: 0.5+SinOsc.kr(5, mul: 0.1)) }.play // minimal vibrato { SinOsc.ar(freq: 440+SinOsc.kr(5, mul: 5)) }.play // with mouse control JMouseBase.makeGUI ; // SwingOSC // tremolo { SinOsc.ar(mul: 0.5 + SinOsc.kr( freq: GUI.mouseX.kr(0, 10), mul: GUI.mouseY.kr(0.0, 0.5))) }.play // vibrato { SinOsc.ar(freq: 440 + SinOsc.kr( freq: GUI.mouseX.kr(0, 10), mul: GUI.mouseY.kr(0, 10))) }.play

tremolo: in musica un tremolo una variazione periodica della dinamica, ovvero dellampiezza come dimensione percettiva. Limplementazione di un tremolo evidentemente semplice. sufciente sommare allampiezza del primo oscillatore il segnale prodotto da un oscillatore di controllo: lincremento varier periodicamente con frequenza pari a quella delloscillatore di controllo da 0 no al massimo (lampiezza dello stesso oscillatore), discender a 0 e al massimo negativo per ritornare inne a 0. Nellesempio largomento mul contiene una costante (0.5) a cui vine sommato il segnale in uscita da un oscillatore che

7.3193

varia 5 volte al secondo nellescursione [0.1, 0.1]. Dunque, con la stessa frequenza lampiezza delloscillatore portante (audio) varier nellintervallo [0.4, 0.6]. Il contributo delloscillatore di controllo allora una variazione periodica dellampiezza del segnale controllato, variazione il cui periodo specicato dalla frequenza delloscillatore di controllo. Il suono sintetico acquista cos una caratteristica tipica dellesecuzione strumentale (per esempio, dei ati). Nellesempio alle righe 11-14 viene utilizzato il mouse per controllare i due parametri del tremolo. [Lavori in corso: introducendo il mouse ricordare che l'origine sulla tavola in alto a sx]

vibrato: se si applica il ragionamento svolto per il tremolo questa volta non allampiezza ma alla frequenza, si ottiene un vibrato. Un oscillatore di controllo controlla un incremento (minimo) della frequenza delloscillatore principale. Supponendo che f1 , amp1 , f2 , amp2 siano frequenza e ampiezza rispettivamente delloscillatore audio e di quello di controllo, la frequenza delloscillatore audio (f1 , nora costante) dopo lincremento varia periodicamente (secondo la frequenza delloscillatore f2 di controllo) tra f1 amp2 e f1 + amp2 . Si ricordi che luscita del segnale di controllo infatti sempre una variazione dampiezza amp2 : questa variazione si somma in questo caso alla frequenza delloscillatore controllato f1 . Nellesempio (5), la variazione di frequenza data dalla somma alla costante 440 del segnale di un oscillatore che 5 volte al secondo oscilla tra [5, 5] (si veda mul): dunque per 5 volte al secondo la frequenza audio varier tra [435, 445]. Analogamente a quanto visto per il tremolo, fornito anche un esempio in cui frequenza e ampiezza del vibrato sono controllabili via mouse. Il risultato musicale di una simile variazione periodica dellaltezza di una nota viene denito vibrato. Il periodo del vibrato dipende dal periodo delloscillatore di controllo: si pensi

7.3194

a un violinista che sposta di poco ma continuamente il suo dito attorno alla posizione della nota prescelta. Ancora: nella tipica tecnica del canto operistico si mettono in atto simultaneamente tremolo e vibrato. Un esempio riassuntivo il seguente:
1 2 3 4 5 6 7 8 9 10 11 13 14 16 17 18 19 20 ( SynthDef("tremVibr", { arg freq = 440, mul = 0.15, tremoloFreq = 5 , tremoloMulPercent = 5, vibratoFreq = 10, vibratoMulPercent = 5 ; var tremoloMul = mul*tremoloMulPercent*0.01 ; var vibratoMul = freq*vibratoMulPercent*0.01 ; var tremolo = SinOsc.kr(tremoloFreq, 0, tremoloMul) ; var vibrato = SinOsc.kr(vibratoFreq, 0, vibratoMul) ; var sinOsc = SinOsc.ar(freq+vibrato, 0, mul+tremolo) ; Out.ar(0, sinOsc) ; }).send(s) ; ) ( // pure sinusoid var aSynth ; aSynth = Synth.new("tremVibr", [ "tremoloMulPercent", 0, "vibratoMulPercent", 0]) ; )

La synthDef "tremVibr" prevede argomenti per il controllo delloscillatore audio e di tremolo e vibrato (si noti come tutti abbiano valori predeniti). Sia di tremolo che di vibrato possibile controllare la frequenza e lincidenza. Le prima in entrambi i casi descritta in termini assoluti, cio attraverso i cicli al secondo (insomma, in Hz ). Come si vede alle righe 8 e 9, tremolo e vibrato sono due segnali in uscita da due oscillatori

7.3195

sinusoidali che hanno appunto come freq rispettivamente tremoloFreq e vibratoFreq. Pi interessante la descrizione dellincidenza dei due parametri (ovvero di quanto variano il segnale audio). In entrambi i casi lincidenza relativa alla stato del segnale da controllare. in altre parole, lincidenza proporzionale ed descritta percentualmente. Si consideri
var tremoloMul = mul*tremoloMulPercent*0.01 ;

tremoloMul calcolato assumendo che tremoloMulPercent, il parametro controllabile dal synth, corrisponda ad una percentuale dellampiezza del segnale. Se mul = 0.5 e tremoloM ulP ercent = 10 allora tremoloM ul sar pari al 10% di mul, cio a 0.05. Dunque, il segnale assegnato a tremolo sar compreso nellescursione [0.05, 0.05] e lampiezza del segnale audio assegnato alla variabile sinOsc osciller nellintorno [0.45, 0.55]. Si ricordi che sinOsc appunto una variabile, da non confondere con la UGen SinOsc): attraverso la variabile SinOsc il segnale viene passato come argomento ad Out. Un discorso analogo al calcolo del tremolo vale per il vibrato con il calcolo di vibratoM ul. Nellesempio, viene quindi creato un synth aSynth in cui viene annullato in fase di creazione il contributo di tremolo e vibrato assegnando un valore pari a 0 ai due argomenti "tremoloMulPercent" e "vibratoMulPercent" (non c incidenza di tremolo e vibrato poich i due segnali di controllo hanno ampiezza nulla). A partire da una simile synthDef possibile costruire una interfaccia graca per controllare gli argomenti di un synth. Lidea di partenza avere per ogni parametro un cursore e di visualizzarne il nome e il valore. Dato il numero esiguo di parametri si potrebbe semplicemente costruire linterfaccia artigianalmente, denendo cio singolarmente tutti i componenti GUI necessari. Vale la pena per sperimentare un altro approccio, pi automatizzato, secondo quanto gi visto nella prima interfaccia graca realizzata, quella inutile. Le classi di elementi necessarie sono due:

7.3196

1. GUI.slider: permette di costruire un cursore (in inglese, uno slider): la sintassi del costruttore (22) piuttosto ovvia, giacch richiede una nestra di riferimento (qui window 23) e un rettagolo in cui viene disegnato il cursore. 2. GUI.staticText: un campo di testo per la visualizzazione, che per non prevede riconoscimento dellinput (scrive testo sullo schermo ma non serve per immetterlo). La sintassi del costruttore quella consueta, e denisce un riquadro ove verr stampato sullo schermo il testo. In pi (17 e 28) viene chiamato sullistanza cos ottenuta il metodo string_ che denisce il testo da stampare. Si tratta di denire un array che contenga i nomi degli argomenti, e a partire da questo di 1. generare gli oggetti graci 2. denire le azioni associate a questi ultimi Lunica difcolt sta nel fatto che nel connettere valori in uscita dal cursore e argomenti del synth necessario tenere in conto di un mapping disomogeneo. Ogni cursore ha unestensione compresa in [0, 1]. La frequenza freqdelloscillatore audio pu avere unescursione compresa in [50, 10000], mentre la sua ampiezza mul invece compresa in [0, 1], le frequenze di tremolo e vibrato sono tipicamente comprese in un registro sub-audio, [0, 15], mentre le due incidenze devono essere espresse percentualmente, dunque in [0, 100]. perci necessario un lavoro di scalatura dei valori espressi da ogni cursore in quanto relativo ad un preciso parametro. Non opportuno passare i valori non scalati al synth includendo dentro la synthDef le operazioni necessarie allo scaling: in questo modo si verrebbe infatti a denire una dipendenza
23

Si noti come window sia resa pi alta del richiesto aggiungendovi 30 2 pixel in altezza.

7.3197

della synthDef dallinterfaccia graca. Nel lavoro con l einterfacce grache invece bene assumere che il modello e i dati siano indipendenti dallinterfaccia di controllo/display. In questo modo, si pu buttare via linterfaccia senza dover modicare il modello (qui, la synthDef). Daltra parte, nella denizione di un algoritmo di sintesi quale previsto dalla synthDef del tutto fuori luogo prevedere elementi che invece concernono la GUI. La soluzione proposta dellesempio prevede luso di un IdentityDictionary, ovvero di una struttura dati che associa in maniera univoca ad una chiave un valore, secondo il modello del dizionario che prevede per ogni lemma (la chiave) una denizione (il valore): controlDict associa ad ogni stringa-argomento un array che ne denisce lescursione (secondo il modello [minVal, maxVal]). La parte di codice compresa tra 13 e 30 denisce tre array che contengono un numero pari alla dimensione di controlDict rispettivamente di blocchi di testo per la visualizzazione del nome del parametro, cursori e blocchi di testo deputati alla visualizzazione del valore degli argomenti. Quindi viene istanziato un synth. Inne si tratta di denire la funzionalit della GUI attraverso un ciclo su ognuno degli elementi contenuti in controlDict. Nel ciclo value indica lelemento di destra (lescursione), ed a partire da questa si ottiene la chiave name attraverso linvocazione del metodo controlDict.findKeyForValue(value). Ad esempio, se value [50, 1000], la chiave associata a name sar "freq". Quindi viene calcolata unescursione range tra [0, 1] come differenza tra gli estremi (nellesempio, 1000 50 = 950) e il minimo viene considerato come scarto di partenza (offset).
labelArr[index].string_(name)

assegna allelemento di testo index in labelArr la stringa name che vi risulta visibile (nellesempio, "freq"). Inne
slidArr[index].action =

...

7.3198

assegna allelemento index di slidArr unazione attraverso il consueto meccanismo di una funzione che viene valutata ogni qualvolta si registra una variazione nello stato del cursore (un movimento della leva graca). Nella funzione largomento (qui: theSlid) come di consueto listanza dellelemento graco su cui denita la funzione. Lazione realizza quattro comportamenti, ognuno descritto da unespressione alle righe 39-42. 1. dichiarando la variabile paramValue se ne calcola il valore. Nellesempio precedentemente relativo a "freq", poich theSlid.value sempre compreso in [0, 1] ed essendo range = 50, lescursione varia appunto tra [0, 950] + 50 = [50, 1000] 2. la seconda azione consiste nello stampare sullo schermo il nome del parametro ed il suo valore 3. si tratta di controllare il synth impostando per largomento name il valore paramValue (ovvero: aSynth.set(freq, 123.4567890) 4. inne il valore paramValue viene scritto nellelemento di testo index in valueArr.

7.3199

1 2 4 5 6 7 8 9 10 11 13 14 16 17 18 19 21 22 23 24 25 27 28 29 30 32 33 34 35 36 37 38 39 40 41 42 43 44 46

( var aSynth = Synth.new("tremVibr") ; var controlDict = IdentityDictionary[ "freq" -> [50, 1000], "mul" -> [0,1], "tremoloFreq" -> [0, 15], "tremoloMulPercent" -> [0, 50], "vibratoFreq" -> [0, 15], "vibratoMulPercent" -> [0, 50] ]; var window = GUI.window.new("Control Panel", Rect(30,30, 900, controlDict.size+2*30)) ; var labelArr = Array.fill(controlDict.size, { arg index ; GUI.staticText.new( window, Rect( 20, index+1*30, 200, 30 )) .string_( 0 ) ; }) ; var slidArr = Array.fill(controlDict.size, { arg index ; var slid = GUI.slider.new( window, Rect( 240, index+1*30, 340, 30 )); slid ; }) ; var valueArr = Array.fill(controlDict.size, { arg index ; GUI.staticText.new( window, Rect( 600, index+1*30, 200, 30 )) .string_( 0 ); }) ; controlDict.do({ arg value, index ; var name = controlDict.findKeyForValue(value) ; var range = value[1]-value[0] ; var offset = value[0] ; [value, index].postln; labelArr[index].string_(name) ; slidArr[index].action = { arg theSlid ; var paramValue = theSlid.value*range + offset ; [name, paramValue].postln ; aSynth.set(name, paramValue) ; valueArr[index].string_(paramValue.trunc(0.001) ) ; 7.3200 } }) ; window.front ;

Si noti come linterfaccia dipenda dal dizionario che connette la GUI al modello (la synthDef) denendo per ogni argomento il mapping opportuno. Aggiornando la struttura dati per una nuova synthDef la GUI cambia di conseguenza. Nellesempio che segue stata denita una nuova SynthDef: per costruire un pannello di controllo stato necessario istanziare un synth con il nome della synthDef e aggiornare il controlDict. Il resto del codice stato omesso perch uguale a precedente.
1 2 3 4 5 6 7 8 9 11 12 14 15 16 17 18 19 20 21 23 25 ( SynthDef("resonantDust", { arg rlpfFreq = 300, dustFreq = 100, rq = 0.01, mul = 10, delay = 0, decay = 0 ; Out.ar(0, CombL.ar(RLPF.ar(Dust.ar(dustFreq), rlpfFreq, rq, mul), 1, delay, decay)) } ).send(s) ; ) ( var aSynth = Synth.new("resonantDust") ; var controlDict = IdentityDictionary[ "rlpfFreq" -> [50, 1000], "mul" -> [0,20], "dustFreq" -> [0, 300], "rq" -> [0, 0.1], "delay" -> [0, 10], "decay" -> [0, 10] ]; [] )

7.3201

Un certo numero di informazioni potrebbero poi essere desunte dalla stessa synthDef se assegnata a variabile. Nellesempio seguente la synthDef "resonantDust" assegnata ad a: se si invia il messaggio name ad a questultima restituisce il suo nome.
1 2 3 4 5 6 7 9 11 a = SynthDef("resonantDust", { arg = 10, delay = 0, decay = 0 ; Out.ar(0, CombL.ar(RLPF.ar(Dust.ar(dustFreq), rlpfFreq, rq, mul), 1, delay, decay)) } ).send(s) ; a.name ; resonantDust rlpfFreq = 300, dustFreq = 100, rq = 0.01, mul

Si potrebbero allora desumere in automatico dalla synthDef i nomdi degli argomenti che costituiscono le chiavi di controlDict. In particolare gli argomenti di una UGen-Graph function prendono il nome di control names.
1 3 a.allControlNames [ ControlName P 0 rlpfFreq control 300, ControlName P 1 dustFreq control 100, ControlName P 2 rq control 0.01, ControlName P 3 mul control 10, ControlName P 4 delay control 0, ControlName P 5 decay control 0 ]

Dallarray restituito da allControlNames, che prevede una sintassi specica, non allora difcile ottenere le stringhe necessarie per le chiavi di controlDict

7.3202

1 2 3 4 5 6 7 9 10 12 13 15 16 18 19 21 22

a.allControlNames.do({ arg item ; item.postln ; item.asString.split($ )[4].postln ; "\n".postln }) ControlName P 0 rlpfFreq control 300 rlpfFreq ControlName dustFreq ControlName rq ControlName mul ControlName delay ControlName decay P 1 dustFreq control 100

P 2 rq control 0.01

P 3 mul control 10

P 4 delay control 0

P 5 decay control 0

Nel codice prima viene stampato item, cio il nome intero. Nella riga successiva, lo stesso esplicitamente convertito in stringa (asString), quindi in suddiviso blocchi separati da un carattere di spaziatura : la denizione del carattere prevede luso del presso $, e il metodo restituisce un array che contiene i singoli blocchi ottenuti come elementi. Ad esempio:
1 3 "to be or not to be".split($ ) [ to, be, or, not, to, be ]

7.3203

Dellarray cos ottenuto si prende lelemento numero 4, ovvero il nome del parametro, che viene scritto sulla post window. Lunico problema di un approccio simile che il mapping tra domini numerici (tra escursione di ogni cursore e escursione del parametro correlato) non pu essere automatizzato, ma deve essere denito caso per caso. Dunque, luso di un dizionario, oltre ad esemplicare la struttura dati, si rivela una scelta interessante. Lunica controindicazione sta nel fatto che un dizionario non una struttura ordinata: nel ciclo, lordine in cui vengono lette le chiavi infatti deciso internamente da SC (e, tra laltro, varia per ogni esecuzione del codice). In effetti, modicando il codice, sarebbe forse pi opportuno utilizzare un array di array cos:
1 2 3 4 5 6 7 8 [ ["rlpfFreq", 50, 1000], ["mul", 0,20], ["dustFreq", 0, 300], ["rq", 0, 0.1], ["delay", 0, 10], ["decay", 0, 10] ]

Inne, si pu ristrutturare il problema. Ci che richiesto una GUI automatica a partire dalla synthDef che prevede un controllo per ogni argomento, la cui escursione deve essere impostabile dallutente. Per intanto la synthDef pu essere assegnata ad una variabile globale ~aDef, cos da essere richiamata in seguito.

7.3204

1 2 3 4 5 6 7 8 9 10 11 13 14

( ~aDef = SynthDef("tremVibr", { arg freq = 440, mul = 0.15, tremoloFreq = 0 , tremoloMulPercent = 0, vibratoFreq = 0, vibratoMulPercent = 0 ; var tremoloMul = mul*tremoloMulPercent*0.01 ; var vibratoMul = freq*vibratoMulPercent*0.01 ; var tremolo = SinOsc.kr(tremoloFreq, 0, tremoloMul) ; var vibrato = SinOsc.kr(vibratoFreq, 0, vibratoMul) ; var sinOsc = SinOsc.ar(freq+vibrato, 0, mul+tremolo) ; Out.ar(0, sinOsc) ; }).send(s) ; )

A questo punto (esempio successivo) possibile istanziare subito un synth a partire dal nome della synthDef (~aDef.name) (2). Recuperando il codice gi discusso in precendenza, a partire dalla lista dei controlNames si denisce un controlArr: un array che contiene per ogni argomento un array [nome dell'argomento, valore minimo, valore massimo]. In fase di costruzione massimi e minimi sono impostati rispettivamente a 1 e a 0 (11). Viene quindi costruito un secondo array guiArr i cui elementi sono correlati uno ad uno con controlArr: larray che contiene gli oggetti GUI relativi a ciascun argomento della synthDef. Per ogni elemento in controlArr viene creato un elemento in guiArr. Tra questi ci sono anche due campi numerici che accettano in entrata un valore impostabile dallutente, due istanze di GUI.numberBox. La sintassi piuttosto intuitiva: si noti che viene loro assegnato il valore di massimo e minimo contenuto nel corrispettivo elemento di controlArr, attraverso value_(item[. . .]) (22 3 24). Per ogni argomento c un elemento nellarray controlArr che contiene

7.3205

1. il nome 2. il minimo 3. il massimo e c un elemento in guiArr che contiene (19-27) 1. 2. 3. 4. 5. un campo di testo dove scrivere il nome un campo numerico dove inserire il minimo un campo numerico dove inserire il massimo un cursore di controllo un campo di testo dove scrivere il valore attuale dellargomento

Creata linterfaccia graca, si tratta a questo punto di associare agli elementi le azioni opportune, secondo quanto avviene nelle righe 31-49. Per ognuno degli elementi di controlArr si recupera un elemento di guiArr (32-33). Questo elemento un array che contiene tutti gli oggetti relativi allargomento che elemento di controlArr. La denizione delle azioni concerne soltanto tre degli elementi graci, quelli che devono rispondere allinterazine con lutente. Questi deve poter muovere il cursore dopo aver determinato massimi e minimi di ciascun argomento.

Lazione collegata ai campi numerici (guiElement[1] e guiElement[2]) assegna il valore immesso (value) dallutente ai rispettivi campi di massimo e minimo dellelemento correlato in controlArr. Se dunque lutente imposta nei campi numerici relativi a "freq"] i valori 20 e 10000, lelemento relativo in controlArr sar ["freq", 20, 10000] Inne guiElement[3] il cursore che controlla gli argomenti ipostandoli sul synth. Il codice uguale allesempio precedente con ununica differenza. Le variabili sono dichiarate allinterno della funzione che controlla lazione. Questo passaggio si rende necessario perch range e offset dipendo dai massimi e dai minimi, e questi possono essere variati dallutente. necessario perci che ad ogni movimento

7.3206

del cursore si aggiornino i valori di massimo e minimo leggendoli dallarray controlArr.

7.3207

1 2 4 6 8 9 10 11 13 14 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47

( var aSynth = Synth.new(~aDef.name) ; var controlArr = [], guiArr = [] ; var window ; ~aDef.allControlNames.do({ arg item ; var name = item.asString.split($ )[4] ; controlArr = controlArr.add([name, 0.0, 1.0]) }) ; window = GUI.window.new(~aDef.name++" Control Panel", Rect(30,30, 900, controlArr.size+2*30)).front ; // GUI creation controlArr.do({ arg item, ind ; var index = ind + 1; // add some space var guiElement = [ GUI.staticText.new( window, Rect( 20, 30*index, 200, 30 )) .string_( item[0] ), GUI.numberBox.new( window, Rect( 240, 30*index, 50, 30 )) .value_(item[1]), GUI.numberBox.new( window, Rect( 300, 30*index, 50, 30 )) .value_(item[2]), GUI.slider.new( window, Rect( 370, 30*index, 340, 30 )), GUI.staticText.new( window, Rect( 720, 30*index, 200, 30 )) .string_( 0.0 ) ] ; guiArr = guiArr.add(guiElement) ; }) ; // GUI action definition controlArr.do({ arg item, index ; var guiElement = guiArr[index] ; guiElement[1].action = { arg minBox ; item[1] = minBox.value ; } ; guiElement[2].action = { arg maxBox ; item[2] = maxBox.value ; } ; guiElement[3].action = { arg slider ; var name = item[0] ; var range = item[2].value 7.3208 - item[1].value ; var offset = item[1].value ; var paramValue = slider.value*range + offset ; [name, paramValue].postln ; aSynth.set(name, paramValue) ;

7.4 Segnali pseudo-casuali

La discussione precedente in relazione al controllo di un oscillatore da parte di un altro oscillatore pu essere intuibilemente estesa alla pi generale prospettive del controllo di una UGen da parte di un altra UGen. I segnali di controllo precedenti erano sinusoidi: in particolare la forma donda sinusoidale era appunto il prolo temporale della variazione indotta nel segnale. Un prolo di variazione pu evidentemente assumere forme diverse. Negli esempi seguenti le prime funzioni contengono un segnale di controllo che verr impiegato per controllare il vibrato di un oscillatore (con frequenza sub-audio e a tasso di controllo) nelle seconde. Attraverso il metodo jscope possibile visualizzre il prolo dei primi segnali di controllo (per comodit di visualizzazione la frequenza stata modicata a 1000 Hz). In generale le UGen che iniziano con LF sono specializzate nella generazioni di segnali di controllo.
1 2 4 5 7 8 10 11 { SinOsc.ar(1000) }.scope ; { SinOsc.ar(freq: 440+SinOsc.kr(2, mul: 50), mul: 0.5) }.play ; { LFSaw.ar(1000) }.scope ; { SinOsc.ar(freq: 440 + LFSaw.kr(2, mul: 50), mul: 0.5) }.play ; { LFNoise0.ar(1000) }.scope ; { SinOsc.ar(freq: 440 + LFNoise0.kr(2, mul: 50),mul: 0.5) }.play ; { LFNoise1.ar(1000) }.scope ; { SinOsc.ar(freq: 440 + LFNoise1.kr(2, mul: 50), mul: 0.5) }.play ;

7.4209

Si nota immediatamente la differenza tra i due segnali periodici, la sinuoside e londa a dente di quadra, proprio in relazione al prolo udibile della variazione. Le due Altre UGen impiegate sono specializzate nella generazione di segnali pseudo-casuali. In particolare LFNoise0 generata ad una certa frequenza valori dampiezza che tiene per la durata di ogni periodo (cio, no quando non calcola un un nuovo valore): se si osserva la nestra attivata quando jscope invocato sulla UGen si nota landamento a gradoni. Come LFNoise0, LFNoise1 genera segnali pseudo-casali alla frequenza impostata, ma interpola tra gli estremi successivi. In altre parole, non salta da un valore al successivo, ma vi arriva gradualmente. Lesempio seguente minimale ed il comportamento dei due generatori ne risulta chiaramente illustrato.
1 3 {SinOsc.ar(LFNoise1.ar(10, mul:200, add: 400))}.play {SinOsc.ar(LFNoise0.ar(10, mul:200, add: 400))}.play

In entrambi i casi la frequenza delloscillatore controllata da un generatore di numeri casuali che aggiorna il suo stato 10 volte al secondo. In considerazione degli argomenti mul e add il segnale di controllo della frequenza varia casualmente allora nellescursione [1, 1] 200 + 400 = [200, 600]. Con LFNoise0 si ha un gradatum (di tipo scalare), con LFNoise1 un continuum (un glissando). Si potrebbe pensare a LFNoise0 come ad una versione di LFNoise1 sampled and held: si prende un campione di un segnale che interpola tra valori pseudo-casuali quale quello generato da LFNoise1 ad un ogni intervallo di tempo prestabilito e lo si tiene no al prelevamento successivo. Come si visto, un simile comportamento realizzato da Latch. Lesempio seguente una variazione di un esempio dallhelp le di Latch. In entrambi i casi il

7.4210

segnale in uscita varia la sua frequenza ad un tasso di 9 volte al secondo scegliendo casualmente nellintervallo [100, 900].
1 3 5 { SinOsc.ar(LFNoise0.kr(9, 400, 500), 4, 0.2)}.play // the same but less efficient { SinOsc.ar(Latch.ar(LFNoise1.ar, Impulse.ar(9)) * 400 + 500, 4, 0.2) }.play;

Per la sua natura discreta LFNoise0 pu essere utilizzato come componente di un sequencer, cio come generatore di sequenze discrete di valori. Due variazioni sul tema sono presentate nellesempio seguente.
1 2 3 4 5 6 8 9 10 11 12 13 14 15 16 17 18 SynthDef("chromaticImproviser", { arg freq = 10 ; Out.ar(0, SinOsc.ar( freq: LFNoise0.kr(freq, mul:15, add: 60).round.midicps, mul: EnvGen.kr(Env.perc(0.05), gate: Impulse.kr(freq), doneAction:2) ) ) }).play SynthDef("modalImproviser", { arg freq = 10; var scale = [0, 0, 0, 0, 3, 3, 4, 5, 5, 6, 6, 7, 7, 7, 10]+60 ; var mode = scale.addAll(scale+12).midicps ; var range = (mode.size*0.5).asInteger ; Out.ar(0, SinOsc.ar( freq: Select.kr(LFNoise0.kr(freq, mul: range, add: range).round, mode), mul: EnvGen.kr(Env.perc(0.05), gate: Impulse.kr(freq), doneAction:2) ) ) }).play

7.4211

I due improvvisatori generano sequenze casuali di altezze ad un tasso predenito di 10 note al secondo. In entrambi i casi il generatore audio e un oscillatore sinusoidale con un inviluppo percussivo. Il primo, vagamente hard bop, genera una sequenze di altezze cromatiche comprese tra in midi tra [45, 75]. Nel midi sono numerate linearmente le altezze, come se si trattasse di assegnare un indice ai tasti (bianchi e neri) di un pianoforte. In notazione midi il do centrale 60, dunque le altezze variano tra il quindiciesimo tasto che lo precede, corrispondente al la al di sotto dellottava inferiore, e il quindicesimo tasto seguente, il mi bemolle al di sopra dellottava superiore. Infatti la frequenza delloscillatore audio data dalla sequenza di valori in uscita da LFNoise0, moltiplicati per 15 e sommati a 60 (secondo quanto previsto da mul, add). Per rappresentare i tasti del pianoforte sono utilizzati in midi i numeri interi, mentre i valori intermedi corrispondono a frazioni di semitono. Siccome qui interessano le altezze temperate (trenta tasti di pianoforte), il segnale viene arrotondato allintero tramite il metodo round (3). Il secondo gestisce le altezze in maniera diversa. In primo luogo scale denisce una scala che comprende le altezze indicate nellarray. Si noti che allarray viene aggiunto 60, dunque le altezze (in notazione midi) saranno:
1 2 [0, 0, 0, 0, 3, 3, 4, 5, 5, 6, 6, 7, 7, 7, 10]+60 [ 60, 60, 60, 60, 63, 63, 64, 65, 65, 66, 66, 67, 67, 67, 70 ]

ovvero do, mi bemolle, mi, fa, fa diesis, sol, sib, alcune delle quali ripetute. Il modo (cio la sequenza effettiva) su cui il generatore improvvisa denito come scale a cui viene aggiunta una copia di se stesso trasposta di unottava. Si ricordi che in midi il salto di ottava, pari ad un raddoppio della frequenza, equivale ad un incremento di 12 (ovvero: 12 tasti sul pianoforte). Larray mode viene quindi convertito in Hz attraverso

7.4212

midicps (10). La scelta della frequenza da generare determinata dalla UGen Select, la sui sintassi prevede
*ar

arg which, array;

In sostanza, dato un array, Select ne restituisce lelemento which. A scanso di equivoci, si ricordi che, essendo una UGen, Select restituisce un segnale, cio una sequenza, a tasso audio o di controllo, composta dallelemento which di array. Lidea quella di far selezionare casualmente a Select una frequenza dallarray mode. Per fare ci viene utilizzato LFNoise0. Questultimo, a frequenza freq, genera un numero compreso in [range, +range] + range = [0, 2range], dove la variabile range denita come la met intera di della dimensione di mode (11). Quindi, se mode.size = 26, allora range = 13, e LFNoise osciller in [0, 26]. Essendo i valori in uscita da LFNoise0 gli indici dellarray da cui Select legge, devono essere interi: di qui linvocazione del metodo round. Si notato che mode contiene pi volte gli stessi valori: un modo semplice (un po primitivo, ma efcace. . .) per fare in modo che certe altezze vengano scelte con frequenze no a quattro volte superiori ad altre. La scala enfatizza allora il primo, e poi il terzo minore, il quarto, il quarto aumentato e il quinto grado, ed per questo (assai) vagamente blueseggiante. Inne, unultima annotazione sullinviluppo dampiezza. Il segnale in uscita dalloscillatore continuo e deve essere inviluppato ogni volta che si produce un cambiamento di altezza, ovvero alla stessa frequenza di LFNoise0, in modo tale da sincronizzare altezze e ampiezza in un evento notale: per questo largomento gate riceve come valore il segnale in usicta da Impulse, ovvero una sequenza di impulsi che attivano linviluppo alla stessa frequenza freq di LFNoise0. Nellesempio precedente lutilizza di EnvGen richiede di specicare per largomento gate un trigger che operi alla stessa frequenza di LFNoise0: a tal ne stata impiegata la UGen Impulse. Un segnale pseudo-casuale spesso utilizzato come trigger quello generato da Dust: questultima

7.4213

produce una sequenza di impulsi, nellescursione [0, 1], che distribuita irregolarmente (stocasticamente) nel tempo, alla densit media al secondo denita dal primo argomento density.
*ar

arg density = 0.0, mul = 1.0, add = 0.0 ;

Se negli esempi precedenti si sostituisce a Impulse Dust, lasciando inalterto il resto del codice, si ottien che gli inviluppi verrano generati in un numero medio pari a freq al secondo, manon in una serie cronometricamente regolare. Nellesempio seguente le ascisse del mouse controllano la densit, che varier nellescursione [1, 500].
1 2 JMouseBase.makeGUI ; // SwingOSC { Dust.ar(GUI.mouseX.kr(1, 500)) }.play

Lutilizzo di Dust come trigger piuttosto diffuso in SC. Lesempio seguente tratto dallhelp le di TRand, un altra UGen interessante: TRand genera un valore pseudo-casuale compreso nellescursione descritta dai due primi arogmenti:
*ar

arg lo = 0.0, hi = 1.0, trig = 0.0;

La generazione avviene ogni qualvolta il trigger cambia stato, passando da negativo a positivo. Nellesempio seguente la prima parte prevede una come trigger Impulse: ad ogni nuovo impulso un Trand genera un nuovo valore utile alla frequenza di SinOsc. Si noti che un altro modo per ottenere leffetto sample & hold implementato con Latch. Se si sostituisce Impulse con Dust, si ottiene una sequenza di impulsi (e dunque di inviluppi) in una quantit media pari a freq.

7.4214

1 2 3 4 5 6 7 8 10 11 12 13 14 15 16 17

( { var trig = Impulse.kr(9); SinOsc.ar( TRand.kr(100, 900, trig) ) * 0.1 }.play; ) ( { var trig = Dust.kr(9); SinOsc.ar( TRand.kr(100, 900, trig) ) * 0.1 }.play; )

7.5 Espansione e riduzione multicanale

Nellesempio relativo alla generalizzazione degli inviluppi si fatto riferimento a Pan2, che riceveva in ingresso un segnale mono e forniva come output una coppia di segnali risultanti da una varia distribuzione dellampiezza del segnale. Un segnale stereo, come quello in uscita da Pan2, in effetti una coppia di segnali che devono essere eseguiti in parallello, tipicamente su due (gruppi di) altoparlanti. Tuttavia non stato necessario specicare nella sintassi di Out una coppia di bus, bens soltanto lindice 0. Nella fattispecie, SC distribuisce automaticamente i due

7.5215

segnali sul necessario numero di bus contigui (in questo caso 0 e 1). Questoperazione prende il nome di multichannel expansion. Pi in generale, importante sapere che SC opera una espansione multicanale tutte le volte che un argomento di una UGen costituito da un array. Cos nella prima synthdef dellesempio seguente (multi1) largomento freq costituito non da un segnale o da un numero, ma da un array: dunque SC generer una coppia di sinuosodi con frequenza 100 e 1000 che verrano inviate rispettivamente sui bus 0 e 1. In altre parole la synthDef identica nei risultati a quella successiva, multi2, in cui esplicitamente vengono inviati due segnali diversi ai due bus 0 e 1. Ma anche il terzo esempio, multi3, equivalente: in esso Out riceve un array come valore dellargomento channelArray, ed esegue di conseguenza una espansione multicanale, secondo quanto esplicitamente prescritto nella synthDef multi2.
1 2 3 5 6 7 8 10 11 12 13 SynthDef( "multi1", { Out.ar(0, SinOsc.ar([100, 1000])) }).play ; SynthDef( "multi2", { Out.ar(0, SinOsc.ar(100)) + Out.ar(1, SinOsc.ar(1000)) }).play ; SynthDef( "multi3", { Out.ar(0, [SinOsc.ar(100), SinOsc.ar(1000)]) }).play ;

. Lespansione multicanale avviene soltanto ed esclusivamente quando agli argomenti di una UGen viene passato un oggetto di tipo , Array, non per le sue sopraclassi n per le sue sottoclassi. A pensarci, questo

7.5216

tipo di costrutto sintattico estremamente potente. Si consideri quanto avviene nellesempio seguente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 SynthDef("multi16", { var bFreq = 100 ; // base freq var sigArr = Array.fill(16, { arg ind ; var index = ind+1 ; SinOsc.ar( freq: bFreq*index+(LFNoise1.kr( freq: index, mul: 0.5, add: 0.5)*bFreq*index*0.02) ) }) ; Out.ar(0, sigArr) } ).play ;

Il diagramma di usso fornito in Figura 7.3, mentre un dettaglio riprodotto ingrandito in Figura 7.4. Partendo dal fondo del codice, e come si vede nelle Figure 7.3 e 7.4, si pu osservare come Out riceva come argomento di channeslsArray larray sigArr, costituito da 16 segnali.
Out.ar(0, sigArr)

Questultimo ottenuto chiamando il metodo costruttore fill a cui viene indicato un numero (16, il numero di posti dellarray) e una funzione. Ogni chiamata della funzione restituisce un oggetto SinOsc.ar, ovvero un segnale sinusoidale, in cui il valore dellargomento freq correlato allindice progressivo dellarray. Nellesempio seguente sono aggiunti come commento i valori risultanti dai primi due cicli dellarray. Come si

7.5217

LFNoise1

:2

LFNoise1

:3

LFNoise1

:4

LFNoise1

:5

LFNoise1

:6

LFNoise1

:7

LFNoise1

:8

LFNoise1

:9

LFNoise1

: 10

LFNoise1

: 11

LFNoise1

: 12

LFNoise1

: 13

LFNoise1

: 14

LFNoise1

: 15

LFNoise1

: 16

LFNoise1

:1

MulAdd

nil

: 0.5

: 0.5

MulAdd

nil

: 0.5

: 0.5

MulAdd

nil

: 0.5

: 0.5

MulAdd

nil

: 0.5

: 0.5

MulAdd

nil

: 0.5

: 0.5

MulAdd

nil

: 0.5

: 0.5

MulAdd

nil

: 0.5

: 0.5

MulAdd

nil

: 0.5

: 0.5

MulAdd

nil

: 0.5

: 0.5

MulAdd

nil

: 0.5

: 0.5

MulAdd

nil

: 0.5

: 0.5

MulAdd

nil

: 0.5

: 0.5

MulAdd

nil

: 0.5

: 0.5

MulAdd

nil

: 0.5

: 0.5

MulAdd

nil

: 0.5

: 0.5

MulAdd

nil

: 0.5

: 0.5

b: 100

b: 100

b: 100

b: 100

b: 100

b: 100

b: 100

b: 100

b: 100

b: 100

b: 100

b: 100

b: 100

b: 100

b: 100

b: 100

b: 2

b: 3

b: 4

b: 5

b: 6

b: 7

b: 8

b: 9

b: 10

b: 11

b: 12

b: 13

b: 14

b: 15

b: 16

MulAdd

nil

: 0.02

: 100

MulAdd

nil

: 0.02

: 200

MulAdd

nil

: 0.02

: 300

MulAdd

nil

: 0.02

: 400

MulAdd

nil

: 0.02

: 500

MulAdd

nil

: 0.02

: 600

MulAdd

nil

: 0.02

: 700

MulAdd

nil

: 0.02

: 800

MulAdd

nil

: 0.02

: 900

MulAdd

nil

: 0.02

: 1000

MulAdd

nil

: 0.02

: 1100

MulAdd

nil

: 0.02

: 1200

MulAdd

nil

: 0.02

: 1300

MulAdd

nil

: 0.02

: 1400

MulAdd

nil

: 0.02

: 1500

MulAdd

nil

: 0.02

: 1600

SinOsc

freq

phase: 0

SinOsc

freq

phase: 0

SinOsc

freq

phase: 0

SinOsc

freq

phase: 0

SinOsc

freq

phase: 0

SinOsc

freq

phase: 0

SinOsc

freq

phase: 0

SinOsc

freq

phase: 0

SinOsc

freq

phase: 0

SinOsc

freq

phase: 0

SinOsc

freq

phase: 0

SinOsc

freq

phase: 0

SinOsc

freq

phase: 0

SinOsc

freq

phase: 0

SinOsc

freq

phase: 0

SinOsc

freq

phase: 0

Fig. 7.3 Espansione multicanale: diagramma di usso. 7.5218


Out bus: 0 channelsArray nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil

:6

LFNoise1

:7

LFNoise1

:8

LFNoise1

:9

LFNoise1

: 10

LFNoise1

: 11

: 0.5

: 0.5

MulAdd

nil

: 0.5

: 0.5

MulAdd

nil

: 0.5

: 0.5

MulAdd

nil

: 0.5

: 0.5

MulAdd

nil

: 0.5

: 0.5

MulAdd

nil

: 0.5

: 0.5

b: 100

b: 100

b: 100

b: 100

b: 100

b: 100

b: 6

b: 7

b: 8

b: 9

b: 10

b: 11

nil

: 0.02

: 600

MulAdd

nil

: 0.02

: 700

MulAdd

nil

: 0.02

: 800

MulAdd

nil

: 0.02

: 900

MulAdd

nil

: 0.02

: 1000

MulAdd

nil

: 0.02

: 1100

SinOsc

freq

phase: 0

SinOsc

freq

phase: 0

SinOsc

freq

phase: 0

SinOsc

freq

phase: 0

SinOsc

freq

phase: 0

SinOsc

freq

phase: 0

Out

bus: 0

channelsArray

nil

nil

nil

nil

nil

nil

nil

nil

nil

nil

nil

nil

nil

nil

nil

Fig. 7.4

Espansione multicanale: diagramma di usso (dettaglio).

vede largomento bFreq una frequenza di base che viene moltiplicata progressivamente per un intero da ind + 1 (in modo tale da evitare la moltiplicazione per 0, che produrrebbe un segnale dampiezza nulla). A questa fondamentale (riga 7) viene aggiunta una variazione (un vibrato) pseudo-casuale la cui frequenza proporzionale allindice (riga 9), e la cui ampiezza proporzionale al 5% della frequenza fondamentale per lindice (righe 12-13). Nel primo caso, loscillatore ha frequenza bF req 1 = 100Hz a cui si aggiunge una variazione del 2%, ovvero [2, 2]Hz ad un tasso pari a 1Hz . Per ottenere una variazione pi continua stato qui impiegato LFNoise1. Se si vuole semplicare lesempio eliminando la variazione pseudo-casuale basta eliminare dallesempio commentato le righe 8-13.

7.5219

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

SynthDef("multi16commented", { var bFreq = 100 ; // base freq var sigArr = Array.fill(16, { arg ind ; // 0 1 var index = ind+1 ; // 1 2 SinOsc.ar( freq: bFreq*index // 100 200 +(LFNoise1.kr( // + freq: index, // freq: 1 mul: 0.5, add: 0.5) // amp: 1 1 (fixed) *bFreq // bFreq: 100 (fixed) *index*0.02) // *1*0.02 = 2 *2*0.05 = 4 ) }) ; Out.ar(0, sigArr) } ).play ;

Dunque, ogni elemento di sigArr un oggetto SinOSc.ar, un segnale generato da una UGen SinOsc. Poich Out riceve un array, alloca un numero di bus pari alla dimensione dellarray, a partire da quello indicato, cio da 0. Le 16 sinusoidi (dotate di diversi parametri) sono distribuite dai bus consecutivi 0 15 in 16 canali audio: se la scheda audio in grado di gestire 16 canali, allora ad ognuno di essi verr inviata una delle sinusoidi. Senza lespansione multicanale, sarebbe necessario descrivere esplicitamente attraverso il codice la struttura riprodotta dal diagramma di usso di Figura 7.3, un lavoro oneroso che invece SC fa per lutente, espandendo ogni elemento dellarray in un opportuno ramo del diagramma come meglio si vede in Figura 7.4. I rami hanno la stessa struttura, ma parametri differenti. Se si ascolta su un normale dispositivo stereo (quale quello di un pc) il risultato della valutazione del codice precedente, saranno udibili soltanto

7.5220

le due prime sinusoidi, quelle con frequenza fondamentale pari a 100 e 200Hz . A tal proposito SC mette a disposizione dellutente una UGen specializzata nella riduzione multicanale, ovvero nel missaggio di pi segnali in un unico segnale: Mix. Questultima semplicemente prevede come argomento un array di segnali che restituisce missati in un unico segnale. La versione seguente modica lesempio in discussione fornendo ad Out un unico segnale, grazie allintervento di Mix (riga 14). SI noti che questultima UGen semplicemente somma loutput dei segnali che compongono larray che riceve come argomento.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 SynthDef("multi16mix", { var bFreq = 100 ; // base freq var sigArr = Array.fill(16, { arg ind ; var index = ind+1 ; SinOsc.ar( freq: bFreq*index+(LFNoise1.kr( freq: index, mul: 0.5, add: 0.5)*bFreq*index*0.02) , mul: 1/16 ; ) }) ; Out.ar(0, Mix.new(sigArr)) } ).play ;

Se tutte le sinusoidi hanno ampiezza normalizzata (come nel caso in discussione), allora lescursione del segnale in uscita da Mix sar compresa in [1, 1] 16 = [16, 16], dunque abbondantemente oltre lescursione rappresentabile dellampiezza. perci opportuno scalare lampiezza di ognuno dei segnali sinusoidali per un fattore pari ad 1/n

7.5221

(riga 11), dove n il numero dei segnali (la dimensione dellarray), in modo che lampiezza complessiva sia 1/n n = 1. Attraverso Mix si persa la spazializzazione ottenuta attraverso la distribuzione nei 16 canali (ancorch inudibili), ottenendo un unico segnale (mono, per cos dire). Si tratta ora di utilizzare le possibilit dellespansione multicanale per una distribuzione stereo. Nellesempio seguente viene generato un array di 16 segnali come negli esempi precedenti. Si noti per che SinOsc.ar diventato il segnale in entrata di una UGen Pan2. Come si visto, Pan2 distribuisce un segnale in entrata su un fronte stereo in funzione dellargomento pos. Per ogni sinusoide, pos varia casualemente in [1, 1] con una frequenza pari a index (riga 15). Dunque, pi elevato lindice, pi elevata la frequenza delloscillatore, pi rapidamente varia la distribuzione spaziale. Si noti che stata utilizzata la UGen LFNoise1, che interpola tra valori successivi, per evitare salti di posizione, simulando invece un progressivo movimento tra i valori assunti dalla distribuzione. In sostanza, larray sigArr ora contiene 16 segnali generati da Pan2, cio sinusoidi la cui frequenza e la cui frequenza di spostamento sul fronte stereo incrementa progressivamente in funzione dellindice dellarray. Ma Pan2 restituisce in uscita una coppia di segnali. Ogni segnale infatti stereo, cio costituito da una coppia di segnali la cui ampiezza varia in funzione di pos. Si pu vericare quanto osservato aggiungendo un riga
sigArr.postln ;

prima di riga 16 nella synthDef "multi16commented". Si otter stampato sulla post window:
1 [ a SinOsc, a SinOsc, a SinOsc, a SinOsc, a SinOsc, a SinOsc, a SinOsc, a SinOsc, a SinOsc, a SinOsc, a SinOsc, a SinOsc, a SinOsc, a SinOsc, a SinOsc, a SinOsc ]

7.5222

Se si fa lo stesso con "multi16mixPan", togliendo il marcatore di commento alla riga 18, si ottiene invece:
1 [ [ an OutputProxy, an OutputProxy ], [ an OutputProxy, an OutputProxy ], [ an OutputProxy, an OutputProxy ], [ an OutputProxy, an OutputProxy ], [ an OutputProxy, an OutputProxy ], [ an OutputProxy, an OutputProxy ], [ an OutputProxy, an OutputProxy ], [ an OutputProxy, an OutputProxy ], [ an OutputProxy, an OutputProxy ], [ an OutputProxy, an OutputProxy ], [ an OutputProxy, an OutputProxy ], [ an OutputProxy, an OutputProxy ], [ an OutputProxy, an OutputProxy ], [ an OutputProxy, an OutputProxy ], [ an Oetc

La denominazione OutputProxy il modo con cui SC chiama loutput di alcune UGen che hanno uscite multiple (come Pan2). A parte questo aspetto, qui irrilevante, interessante osservare che ogni elemento dellarray a sua volta un array composto di due elementi, uno per canale. Si consideri il prossimo esempio:
1 3 4 6 7 { Out.ar(0, Pan2.ar(SinOsc.ar, Line.kr(-1, 1, 10), 0.5)) }.play; // only left channel (non multichannel expansion) { Out.ar(0, Pan2.ar(SinOsc.ar, Line.kr(-1, 1, 10), 0.5)[0]) }.play; // only right channel (non multichannel expansion) { Out.ar(0, Pan2.ar(SinOsc.ar, Line.kr(-1, 1, 10), 0.5)[1]) }.play;

Alla riga 1 una sinusoide si sposta progressivamente dal canale sinistro a quello destro: pos un segnale generato dalla UGen Line che linearmente muove nellarco di 10 secondi tra 1 e 1. Questo movimento si traduce sul fronte stereo in decremento dellampiezza del segnale sul canale sinistro e in un incremento della stessa sul canale destro. Le righe 4 e 6 inviano ad Out soltano uno dei due canali, rispettivamente sinitro

7.5223

e destro (dunque, non c espansione multicanale). Si noti la sintassi che quella per il recupero degli elementi in un array, attraverso [0] e [1] chiamati sullarray restituito da Pan2. Lo spostamento sul fronte stereo analiticamente scomposto in un crescendo del segnale a destra ed in un diminuendo a sinistra. Tornando a multi16mixPan, la disposizione delgi elementi in sigArr perci
[[sig0sx, sig0dx], [sig1sx, sig1dx], [sig3sx, sig3dx] sig15sx]]

. . .,

[sig15sx,

Come gi visto, chiamando il metodo flop (riga 19) si ottiene un nuovo array con questa struttura
[[sig0sx, sig1sx, sig2, sx, sig3sx, sig2, dx, sig3dx,

. . .,

sig15sx], [sig0dx, sig1dx,

. . .,

sig15dx]]

che composta di due array, uno per canale, contenenti in contributi di ognuno dei segnali sinusoidali a quel canale. allora possibile missare ognuno dei due array ottenendo due singoli segnali, per il canale sinistro e per quello destro (righe 20 e 21), assegnati alle variabili left, right. Inviando a Out larray [left, right] si produce di nuovo unespansione multicanale, per cui il primo (left) verr inviato, attraverso il bus 0, allaltoparlante sinistro, il secondo (right), attraverso il bus 1, allaltoparlante destro.

7.5224

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24

SynthDef("multi16mixPan", { var bFreq = 100 ; // base freq var left, right ; var sigArr = Array.fill(16, { arg ind ; var index = ind+1 ; Pan2.ar( in: SinOsc.ar( freq: bFreq*index+(LFNoise1.kr( freq: index, mul: 0.5, add: 0.5)*bFreq*index*0.02) , mul: 1/16 ; ), pos: LFNoise1.kr(freq: index) ) }) ; // sigArr.postln ; sigArr = sigArr.flop ; left = Mix.new(sigArr[0]) ; right = Mix.new(sigArr[1]) ; Out.ar(0, [left, right]) } ).play ;

7.5225

8 Sintesi, II: tecniche di generazione del segnale audio

8.1 Oscillatori e tabelle

Discutendo di algoritmi di sintesi del segnali non in tempo reale, pi volte si discusso della generazione del segnale a partire dal calcolo di una funzione, tipicamente sin(x). Sebbene concettualmente elegante, il computo diretto della funzione si rivela assai poco efciente dal punto di vista computazionale: costringe infatti lelaboratore a calcolare il valore della funzione per un numero di volte al secondo pari al tasso di campionamento prescelta (cio 44.100 nel caso di qualit CD, il tasso di campionamento predento in SC). possibile per impiegare un altro metodo, che ha una lunga tradizione in computer music: si tratta cio di costruire un oscillatore digitale. Finora il termine stato impiegato senza fornirene una denizione: si trtta ora di vedere pi in dettaglio che cosa esso indichi. Loscillatore digitale un algoritmo fondamentale nella computer music, poich non soltanto impiegato per generare direttamente un segnale ma anche ununit componente di molti altri

8.1226

generatori di suono. Tornando al problema della generazione di una sinusoide, e ragionando diversamente, si pu osservare come un suono sinusoidale (ma in generale la parte stazionaria di ogni suono periodico) sia caratterizzato da una grande prevedibilit: esso si ripete uguale ad ogni periodo. Dato un periodo del segnale, si pu allora pensare di prelevarne il valore in n punti equidistanti. Questi valori campionati possono essere immessi in una tabella di n punti: alla lista dei punti corrisponde unaltra lista con i valori della funzione. Una tabella di questo tipo prende il nome di wavetable (tabella dellonda o forma donda tabulata).
1

1 0 1 2 3 4 5 6 7 8 9 Time (s) 10 11 12 13 14 15 16

Fig. 8.1

Forma donda campionata

Ad esempio il campionanamento di una sinusoide in ampiezza normalizzata (Figura 8.1) d origine alla seguente tabella: [00] = 0.19509032 [08] = -0.19509032 [01] = 0.55557024 [09] = -0.55557024 [02] = 0.8314696 [10] = -0.8314696 [03] = 0.98078525 [11] = -0.98078525

8.1227

[04] = 0.98078525 [12] = -0.98078525 [05] = 0.8314696 [13] = -0.8314696 [06] = 0.55557024 [14] = -0.55557024 [07] = 0.19509032 [15] = -0.19509032 Lalgoritmo delloscillatore digitale prevede fondamentalmente il semplice svolgimento di due operazioni: leggi i valori della tabella dallarea di memoria in cui questa conservata; 1. arrivato allultimo indirizzo (il 15 nellesempio), riparti dal primo (0). Questultima operazione si denisce wrappping around. Il metodo di sintesi descritto (Table Look-Up Synthesis) estremamente efciente: la lettura di un dato dalla memoria infatti un processo assai pi veloce che non il calcolo del valore di una funzione. agevole osservare come una simile operazione di stoccaggio di una forma e suo successivo reperimento sia tra laltro lo stesso approccio preso in considerazione nel momento in cui si discusso di una generalizzazione degli inviluppi. La tabella costituisce infatti un modello (statico e caricato in fase di inizializzzione) della forma donda: sta allutente decidere ampiezza e frequenza del segnale sintetizzato dalloscillatore. Per quanto concerne lampiezza loperazione ormai ovvia. Ad esempio, se il segnale tabulato ha ampiezza compresa in [1, 1], moltiplicando nelloscillatore tutti i valori della tabella per 0.5, il segnale in uscita avr unampiezza dimezzata, che osciller nellescursione [0.5, +0.5]. Come ormai noto, moltiplicare un segnale per un numero k produce come risultato un segnale in cui ognuno dei valori risulta moltiplicato per k . Quindi, ognuno dei valori del segnale in entrata (la forma donda tabulata) viene scalato per 0.5 (Figura 8.2).

8.1228

0.5

0.5

1 0 1 2 3 4 5 6 7 8 9 Time (s) 10 11 12 13 14 15 16

Fig. 8.2

Scalatura dellampiezza

Passando allimplementazione in SC, si noti che, se il problema fosse la generazione di una sinusoide, in SC sarebbe sufciente utilizzare le UGen specializzate SinOsc e FSinOsc:

SinOsc implementa lalgoritmo di table look-up, ma non richiede di specicare una tabella poich la prevede internamente (di 8192 punti) FSinOsc implementa un approccio diverso al problema della generazione di una sinusoide, in modo molto efciente (di qui la F di Fast) seppur con qualche limitazione duso.

Tuttavia lutilizo di tabelle non si limita evidentemente alla tabulazione di una sinusoide e vale perci la pena di soffermacisivi. Una delle UGen che si occupano esplicitamente di leggere da tabelle Osc, il cui primo argomento una tabella, la cui dimensione deve essere un multiplo di 2 (per ragioni di ottimizzazione), e il secondo la frequenza alla quale loscillatore legge la tabella. Se la tabella rappresenta il periodo di un segnale, la frequenza indicher la frequenza del segnale risultante. La tabella pu essere fornita a Osc attraverso un buffer. Come si visto

8.1229

discutendo il server, un buffer una locazione di memoria temporanea, allocata dal server nella RAM, in cui possono essere memorizzati dati: ogni buffer (come ogni bus) indirizzato attraverso un numero intero univoco attraverso il quale vi si pu far riferimento. SC implementa una classe Buffer che si occupa della gestione dei buffer. In altre parole con un buffer si dice al server di riservare un certo spazio per certi dati. La classe Buffer prevede alcuni metodi per allocare buffer, per leggervi dentro un le audio, ma anche per riempire direttamente un buffer con specici tipi di segnali, di larga utilit.
1 2 4 5 7 8 10 11 13 14 16 18 19 ( var buf ; buf = Buffer.alloc(s, 2.pow(10)) ; buf.sine1([1]) ; {Osc.ar(buf.bufnum, 440)}.play ) ( var buf ; buf = Buffer.alloc(s, 2.pow(10)) ; buf.sine1( Array.fill(20, { 1.0.rand }) ) ; buf.plot ; {Osc.ar(buf.bufnum, 440, mul: 0.6)}.jscope )

Il codice precedente contiene due esempi. Nel primo alla variabile buf viene assegnato un oggetto Buffer ottenuto chiamando sulla classe il costruttore alloc. Il metodo dice al server s 24 di allocare un buffer di una

8.1230

certa dimensione. La dimensione di per s arbitraria: tuttavia nellimplementazione degli oscillatori viene tipicamente richiesto (non solo in SC) per questioni di ottimizzazione che la dimensione della tabella sia una potenza di 2 (2.pow(10)). Quindi viene inviato a buf il messaggio sine1 che riempie il buffer con una somma di sinusoidi armonicamente relate. La sintassi analoga al metodo waveFill della classe Array: larray contiene le ampiezze della armoniche. Nella fattispecie c solo una sinusoide (la fondamentale) con ampiezza unitaria. Osc richiede come primo argomento un indicativo del buffer da leggere (la tabella, appunto). Questo indicativo non pu essere il nome della variabile perche sconosciuta al server, risiedendo sul lato di sclang: deve invece essere lindice del buffer assegnato dal server. Per ottenerlo sufciente inviare a buf il messaggio bufnum, che restituisce lindice del buffer (7): in questo modo lutente non si deve preoccupare di ricordare esplicitamente quale sia lindicativo assegnato ad ogni buffer. La tabella memorizzata nel buffer viene quindi letta con una frequenza pari a 440 volte al secondo. Nel secondo esempio, larray di ampiezze generato attraverso il metodo fill invocato su Array, che restituisce un array di 20 numeri pseudo-casuali compresi in [0.0, 1.0]: essi rappresentano le ampiezze stocastiche delle prime 20 armoniche. Come si vede, possibile visualizzare i dati contenuti nel buffer attraverso il metodo plot (16). altres possibile fare in modo che Osc legga da una tabella che contiene un segnale audio residente sul disco rigido. Nellesempio seguente viene dapprima generato un segnale attraverso la classe Signal, a cui viene inviato il messaggio sineFill, che genera la consueta somma di sinusoidi: si noti che la dimensione dellarray (primo argomento di sineFill) immediatamente stabilita in una potenza di 2, ovvero 2 = 65536 (riga 6). Inoltre, per poter essere letto da Osc, il segnale deve essere convertito in un formato apposito, quale quello previsto dalla classe Wavetable: di qui la
16

24

Si ricordi che alla variabile globale e riservato per default il server audio

8.1231

conversione sig = sig.asWavetable (riga 7). la synthDef seguente semplicemente ingloba in una UGen Out una UGen Osc, prevedendo come argomento bufnum: questo indica lidenticativo (numerico) del buffer, che serve per potervi far riferimento ( il nome del buffer). Quindi la synthDef viene spedita al server (22). Inne, dopo aver memorizzato il segnale sotto forma di le audio (10-14), possibile caricarlo in un buffer: la riga 30 utilizza il metodo readche alloca un buffer sul server s e vi legge il le indicato dal percorso. Il buffer viene assegnato alla variabile buf. La dimensione del buffer (quanta memoria occupa) inferita direttamente da SC a partire dalla dimensione del le. Cos, il nuovo Synth che viene generato (33) utilizza il buffer buf, il cui indenticativo sul server buf.bufnum.

8.1232

1 2 3 5 6 7 9 10 11 12 13 14 15 17 18 19 20 21 22 23 25 26 27 29 30 32 33 34

( var sig ; var soundFile ; //> generating a signal sig = Signal.sineFill(2.pow(16), [1]) ; // 65536 sig = sig.asWavetable ; // mandatory ! //> writing an audiofile soundFile = SoundFile.new ; soundFile.headerFormat_("AIFF").sampleFormat_("int16").numChannels_(1) ; soundFile.openWrite("sounds/signalTest.aiff") ; soundFile.writeData(sig) ; soundFile.close ; ) ( //> writing a synthDef SynthDef("tableOsc",{ arg bufnum = 0, freq = 440, amp = 0.4 ; Out.ar(0, Osc.ar(bufnum, freq, mul: amp)) }).send(s) ; ) ( var freq = 440 ; var buf, aSynth; //> allocating a buffer and directly reading in an audio file buf = Buffer.read(s, "sounds/signalTest.aiff") ; //> a tableOsc synth readding the table from the buffer aSynth = Synth.new("tableOsc", ["bufnum", buf.bufnum]) ; )

8.1233

La caratteristica precipua delloscillatore soltanto quella di generare campioni sulla base della lettura della tabella: la forma donda tabulata non devessere necessariamente sinusoidale. possible infatti tabulare qualsiasi forma, come pure importare una forma donda da un qualsiasi segnale preesistente. Nellesempio seguente la tabella riempita con valori pseudo-casuali: in particolare la riga 12 sceglie per ogni elemento dellarray (di tipo Signal) un valore-pseudocasuale tra [0.0, 2.0] 1 = [1.0, 1.0]: il risultato viene quindi converito in una Wavetable. Il resto del codice assume la synthDef precedente ( "tableOsc" e riprende quanto gi visto nel caso della generazione di una sinusoide. Una variabile di rilievo exp (11), lesponente di 2 che determina la dimensione della tabella (riga 12). Un oscillatore che non si arresti dopo una lettura della tabella (nel qual caso il concetto di frequenza di lettura non avrebbe molto senso) produce per denizione un segnale periodico: infatti reitera la lettura della tabella e conseguentementemente il segnale si ripete uguale a se stesso. Pi la tabella grande, maggiore linformazione sulla sua forma (il dettaglio temporale) pi il segnale rumoroso. Viceversa, tabelle di dimensioni ridotte producono segnali il cui prolo temporale pi semplice (meno valori pseudo-casuali), e dunque maggiormente intonati. Si provi ad esempio a variare exp nellescursione [1, 20], e a visualizzare il contenuto del buffer cos ottenuto con jplot.

8.1234

1 2 3 4 6 7 8 10 11 12 14 15 16 17 18 19 20 22 23 24 25 26

/* Periodic/Aperiodic oscillator reading from a pseudo-random filled table */ ( var sig, exp ; var soundFile; // table generation exp = 6 ; // try changing it bewteen [1, 20] sig = Signal.fill(2.pow(exp), {2.0.rand-1}).asWavetable ; //> writing the audio file soundFile = SoundFile.new ; soundFile.headerFormat_("AIFF").sampleFormat_("int16").numChannels_(1) ; soundFile.openWrite("sounds/signalTest.aiff") ; soundFile.writeData(sig) ; soundFile.close ; ) ( var buf, aSynth ; buf = Buffer.read(s, "sounds/signalTest.aiff") ; aSynth = Synth.new("tableOsc", ["bufnum", buf.bufnum, "amp", 0.1]) ; )

Una modulazione della frequenza delloscillatore indica allora una variazione della velocit di lettura della tabella. La synthDef seguente prevede una variazione pseudo-casuale della frequenza con LFNoise0, secondo una modlait abbondantemente descritta. In particolare, con una tabella sinusoidale la synthDef riproduce esempi gi visti. Si noti invece come nel caso di una tabella che contenga valori pseudo-casuali la variazione di velocit produca una variazione dellaltezza percepita soltanto

8.1235

entro certi limiti, approssimativamente entro exp = 10. Oltre a tale soglia, la variazione nella tabella non mantiene pi lidentit del prolo temporale che la tabelle descrive.
1 2 3 4 5 6 7 ( // modulating the freq SynthDef("tableOsc",{ arg bufnum = 0, freq = 440, amp = 0.4 ; Out.ar(0, Osc.ar(bufnum, LFNoise0.ar(10, mul: 400, add: 400), mul: amp)) }).send(s) ; )

8.2 Campionamento

Il metodo di sintesi concettualmente pi semplice il campionamento. Fondamentalmente, con lespressione prelevare un campione si indica lottenimento di un segnale di breve durata o attraverso la registrazione diretta, oppure attraverso unoperazione di editing da un le audio. In ogni caso, nella sintesi per campionamento lidea centrale quella del playback di materiale sonoro preesistente.

8.2.1 Campionamento semplice

8.2236

Nel campionamento semplice si ripropone lidea che abbiamo incontrato discutendo delloscillatore: la lettura di una tabella. Se per in quel caso la forma donda, oltre a essere estremamente semplice (ma sarebbe stato possibile costruire una forma qualsiasi), era tipicamente ridotta a un unico periodo, nel campionamento invece la forma donda tabulata rappresenta un suono complesso dotato di inviluppo, la cui durata dipende soltanto dalle limitazioni imposte dallhardware. Come intuibile, lorigine del campione (per registrazione diretta o per estrazione da un le preesistente) non fa alcuna differenza. Una volta messo in memoria, il campione pu essere richiamato ogni qualvolta lutente lo desideri attraverso un dispositivo di controllo. Nonostante la semplicit concettuale, evidente la potenza di un simile approccio, poich si pu avere agevolmente a disposizione una ricca tavolozza di suoni preregistrati: ad esempio un intero set di percussioni, cos come molte altre librerie di campioni 25. Sebbene possa essere senzaltro possibile utilizzare un oscillatore come Osc per leggere un le audio preventivamente caricato in un buffer, la UGen specializzata nella lettura di un buffer PlayBuf. Essa non incorre nel limite di avere una tabella la cui dimensione sia multiplo di 2, ma pu invece leggere un buffer di dimensioni qualsiasi.

25

Non questo il luogo per una sitograa, ma vale la pena di segnalare una risorsa di grande interesse, The Freesound Project, http://freesound.iua.upf.edu/ che mette a disposizione con licenza Creative Commons decine di migliaia di le audio, tutti indicizzati, scaricabili o ascoltabili on-line.

8.2237

1 2 3 4 5 6 7 9 10 11 13 14

( SynthDef("playBuf", { arg bufnum = 0, loop = 0; Out.ar(0, PlayBuf.ar(1, bufnum, loop: loop) ) }).send(s) ) ( var buf, aSynth ; buf = Buffer.read(s,"sounds/a11wlk01-44_1.aiff") ; aSynth = Synth("playBuf", ["bufnum", buf.bufnum, "loop", -1]) )

Nellesempio precedente la synthDef avvolge semplicemente playBuf di cui dimostra tre argomenti, i primi due e lultimo.

Il primo argomento indica il numero dei canali (tipicamente, mono o stereo): se il suo valore 1, allora il buffer mono il secondo specica lindice del buffer, qui controllabilem come argomento bufnum della synthDef. largomento loop indica la modalit di lettura e pu avere due valori: 0 indica ununica lettura del buffer, 1 una lettura ciclica.

Nel seguito viene allocato un buffer buf che legge dal disco sso, nella cartella sound dellinstallazione locale di SC, il le a11wlk01-44_1.aiff. Si noti che il le stato campionato ad un tasso di campionamento pari a 44100Hz (come dice anche il nome). SC lo legge alla sua frequenza di campionamento, predenita a 44100Hz : essendo le due uguali, non c problema. Se invece fossero diverse sarebbe necessario tenerne conto.

8.2238

8.2.2 Resampling e interpolazione

La semplice lettura del buffer, secondo quanto descritto per PlayBuf, riproduce il segnale tale e quale vi stato memorizzato. Ripensando al caso di Osc si pu tuttavia notare come fosse specicabile una frequenza: questa frequenza indicava il numero di letture al secondo della tabella contenuta nel buffer da parte delloscillatore. Se la tabella fosse di dimensione ssa, si potrebbe leggere pi o meno velocemente la tabella, variando opportunamente il tasso di campionamento di scsynth. Ma poich un buffer ha una dimensione ssa ma in compenso anche la sample rate non modicabile, per poter variare la frequenza del segnale uscente necessario lavorare sulla tabella in un altro modo. Questo lavoro comune anche a PlayBuf e permette cos diintonare anche un suono campionato in una tabella. Il procedimento pi usato a tale scopo usualmente denito resampling, che equivale, come indica il nome, a un vero e proprio ricampionamento. Si consideri nuovamente la tabella di Figura ??, che rappresenta una forma donda. Se si leggono tutti i punti della tabella, si ottiene il segnale originale. Si supponga invece, allo stesso taso di campionamento, di leggere soltanto un punto ogni due. Si ottiene come risultato un segnale dalla frequenza doppia rispetto alloriginale. Infatti, il segnale risultante risulta costituito di un numero di campioni pari alla met del segnale originale, e dunque il suo periodo pari alla met del periodo originale, T2 = T1 /2. Data la relazione f = 1/T , ne consegue che f2 = 1/T2 , cio 1/T1 /2: allora il rapporto R = f2 /f1 uguale al rapporto tra 1/T1 /2 e 1/T1 , ovvero R = 2. Si supponga di avere una tabella di 16 punti quale quella di Figura ?? e che il tasso di campionamento sia di 44100 campioni al secondo. Supponendo una semplice lettura della tabella sia che il periodo T (ovvero quanto dura un ciclo della forma donda tabulata)

8.2239

pari a T = 16/44100 = 0.00036281179138322sec. Ci indica che la frequenza del segnale risultante f = 1/T = 2756.25Hz . Se invece si legge un campione ogni due, ovvero il passo di lettura 2, allora il periodo del nuovo segnale T = 16/2 8/44100 = 0.00018140589569161sec, e dunque la frequenza f = 1/T = 5512.5Hz . ovvero il doppio della frequenza originale. Rispetto alla tabella la lettura procede come illustrato qui di seguito e produce il risultato visibile in Figura 8.3. Tabella Segnale [00] = 0.19509032 [00] [01] = 0.55557024 non utilizzato [02] = 0.98078525 [01] [13] = -0.8314696 non utilizzato [14] = -0.55557024 [07] [15] = -0.19509032 non utilizzato
1

1 0 Time (s) 0.000362812

Fig. 8.3 Lettura con passo = 2 della tabella. In Figura 8.4(a) rappresentato un segnale sinusoidale di 440Hz . Leggendo un punto ogni due, si ottiene il segnale di Figura 8.4(b-c): in (b) si osserva come nella stessa durata il numero di picchi e gole nel segnale risultante raddoppi, mentre in (c) si pu osservare come ogni ciclo del

8.2240

segnale sottocampionato sia rappresentato da met del numero di campioni rispetto alloriginale. Il segnale sottocampionato ha frequenza pari a 880Hz e durata pari a met del segnale originale, come si vede nelle Figure 8.4(d-e).
1 1

1 0 Time (s) 0.02

1 0 Time (s) 0.02

1 0 0 1 Time (s) 1

1 0 Time (s) 0.01

1 0 Time (s) 1

Fig. 8.4 Sinusoide a 440 Hz (a), segnale sottocampionato leggendo un campione ogni due (b-c), segnale originale e sottocampionato rappresentati nella durata complessiva (d-e). Ancora, se il passo di lettura 4 allora T = 16/4 4/44100 = 0.00018140589569161sec, per una frequenza pari a f = 1/T = 5512.5Hz . Musicalmente una rapporto 2 : 1 corrisponde a una trasposizione allottava superiore, mentre 4 : 1 indica una trasposizione pari a quattro ottave. Loperazione n qui descritta si chiama downsampling. Lupsampling il procedimento simmetrico attraverso il quale possibile diminuire la frequenza del campione originario: a ogni punto della tabella vengono fatti corrispondere non uno, ma due campioni (nel senso di punti di campionamento) nel segnale derivato.

8.2241

Tabella > Array [00] = 0.19509032 > [00] [01]? [01] = 0.55557024 > [02] [03]? [02] = 0.98078525 > [04] [15] = -0.19509032 > [30] [31] In sostanza, come se tra un campione e laltro della tabella originale venisse messo un nuovo campione. Rispetto alla tabella originale, ci sar tra ogni coppia di punti un punto intermedio in pi, secondo quanto avviene in Figura 8.5.
1

1 0 Time (s) 0.000362812

Fig. 8.5 Lettura con passo = 0.5 della tabella. La frequenza risultante la met di quella originale, il che equivale musicalmente a una trasposizione al grave di unottava. I valori dei campioni aggiunti non ci sono nella tabella originale e devono essere inferiti in qualche modo, intuibilmente a partire dai valori contigui. Tipicamente ci avviene per per interpolazione tra i due valori originali che precedono e seguono ogni campione aggiunto. Intuibilmente se il primo valore 0 e il secondo 1, il valore interpolato sar 0.5. Se si osserva la Figura

8.2242

8.6 si capisce come il ragionamento possa essere generalizzato attraverso il ricorso alla proporzionalit tra triangoli. Nella Figura 8.6 sono noti (cio, sono valori numerici dati):

x0, y 0, x1, y 1, x.
Il problema trovare y . Si osserva come siano omogenei i triangoli ABQ e AP O , e dunque valga una relazione di omogeneit tra i lati.

ABQ AP O P O/BQ = AO/AQ


Sosituendo ai termini i valori delle variabili si ottiene:

(y y 0)/(y 1 y 0) = (x x0)/(x1 x0)


ovvero

y=

(xx0)/(y 1y 0) (x1x0)

+ y0

B (x1, y1) P (x, y) A (x0, y0) O Q

Fig. 8.6 Interpolazione lineare come relazione tra triangoli Nel caso precedente, in cui un campione viene aggiunto in mezzo a due precedenti, rispetto alla gura si ha che i campiono noti sono rappresentati dai punti A e B , che costituiscono i campioni 0 e 2 del nuovo segnale. Si tratta di calcolare il valore del campione con indice 1, ovvero

8.2243

del punto P = (1, ?). Si hanno dunque le seguenti sostituzioni numeriche rispetto alla formula:

(x0, y 0) = (0, 0) (x1, y 1) = (2, 1) x=1


E dunque:

y=

(10)/(10) (20)

+0=

1 2

+ 0 = 0.5

Questo tipo di interpolazione detto lineare perch intuibilmente cerca sulla retta che connette i due punti noti A e B il valore ignoto corrispondente alla ordinata della nuova ascissa 26. Lutilizzo dellinterpolazione permette di determinare il valori di campioni che avrebbero un indice frazionario: cio, di poter prendere non solo un campioni ogni due, tre etc, ma anche ogni 0.9, 1.234 etc. Ci signica poter trasporre il campione originale non solo allottava, ma a tutte le altre frequenze corrispondenti alle altre note di una tastiera, per i quali necessario considerare anche rapporti numerici non interi tra il numero dei punti iniziali e e quello nale. Poich il rapporto tra due frequenze a distanza di semitono (sulla tastiera, tra due tasti consecutivi: ad esempio, tra do e do) 2 1.06, come si vede in SC calcolando il rapporto tra le frequenze (si ricordi che in notazione midi 60 indica il do e lunit rappresenta il semitono):
12

26

Esistono altre forme di interpolazione, che qui non si discutono, ma che si basano sullo stesso principio geometrico di determinare una certa curva, non necessariamente una retta, tra due punti e determinare il valore di una ordinata per una ascissa intermedia nota.

8.2244

1 2

61.midicps/60.midicps 1.0594630943593

Per ottenere una trasposizione di semitono sarebbe necessario prelevare un punto ogni 1.06. Questa operazione possibile calcolando per interpolazione il nuovo valore. Inne ricordiamo che, poich dal punto di vista timbrico raddoppiare la frequenza ben diverso da trasporre strumentalmente allottava (vedi Capitolo 1), se si vuole fare della sintesi imitativa opportuno campionare tutte le diverse regioni timbriche (i registri) dello strumento prescelto. In un oscillatore come Osc lidea quella di fornire un periodo di un segnale e di poter controllare la frequenza del segnale risultante. La frequenza viene espressa in termini assoluti, poich indica il numero di volte in cui la tabella viene letta al secondo, e dunque il numeroi di cicli per secondo. PlayBuf invece un lettore di buffer, impiegato tipicamente quando il buffer contiene non un ciclo ma un segnale complesso, di cui infatti non necessario conoscere la dimensione: la frequenza di lettura viene allora pi utilmente espressa in termini di proporzione tra i periodi. Il rapporto viene espresso in termini della relazione T1 , dove T1 il numero di campioni 2 contenuti nel buffer e T2 quello del segnale risultante. La stessa relazione pu essere espressa in temrini di frequenza con f2 , dove f1 indica 1 la frequenza del segnale di partenza e f2 quella del segnale risultante. PlayBuf permette di specicare un simile tasso attraverso largomento rate: se rate = 1 allora il buffer viene letto al tasso di campionamento del server, se rate = 0.5 allora T2 = 2, cio il buffer viene stirato su una durata doppia, e di conseguenza il segnale ha una frequenza f2 pari alla met delloriginale, ovvero scende di unottava. Se rate negativo, allora il buffer viene letto in senso inverso, dallultimi campione al primo. Nellesempio seguente largomento rate gestito da un segnale che risulta dalluscita di una UGen Line. Questultima denisce un
f T

8.2245

segnale che decresce linearmente in [1, 1]. Questa progressione determina una lettura del buffer inizialmente senza modicazioni rispetto alloriginale (rate = 1), a cui segue un progressivo decremento che si traduce in un abbassamento della frequenza (e, si noti, in un incremento della durata) no a rate = 0. Quindi il valore cresce ma con segno inverso: cos fa la frequenza, ma il buffer viene letto allinverso (dalla ne allinizio). Il valore nale del segnale generato da Line 1, che equivale ad una velocit di lettura pari a quella normale, ma al contrario.
1 2 3 4 5 6 7 8 10 11 12 14 15 ( SynthDef("playBuf2", { arg bufnum = 0; Out.ar(0, PlayBuf.ar(1, bufnum, rate: Line.kr(1, -1, 100), loop: -1) ) }).send(s) ; ) ( var buf, aSynth ; buf = Buffer.read(s,"sounds/a11wlk01-44_1.aiff") ; aSynth = Synth("playBuf2", ["bufnum", buf.bufnum]) ; )

Lesempio seguente dimostra la potenza espressiva di SC (oltre alla sua efcienza) e riprende molti aspetti di un esempio gi visto (che non si ridiscuteranno). Larray source contiene 100 segnali stereo che risultano dalla spazializzazione attraverso Pan2 di PlayBuf. In ognuna delle UGen PlayBuf largomento rate controllato da una UGen LFNoise0, cos che lescursione possibile dello stesso [2.0, 2.0], ovvero no

8.2246

ad unottava sopra ed anche al contrario. I 100 Out leggono lo stesso buffer, ognuno variando pseudo-casualmente la velocit di lettura. I segnali stereo sono quindi raggruppati per canale (attraverso flop, riga 13), missati (14-15) e inviati come array di due elementi a Out (16).
1 2 3 4 5 6 7 8 9 10 11 13 14 15 16 17 18 20 21 22 24 25 ( SynthDef("playBuf3", { arg bufnum = 0; var left, right ; var num = 30 ; var source = Array.fill(num, { arg i ; Pan2.ar( in: PlayBuf.ar(1, bufnum, rate: LFNoise0.kr(1+i, mul: 2), loop: -1), pos: LFNoise0.kr(1+i), level: 1/num) ; }) ; source = source.flop ; left = Mix.new(source[0]) ; right = Mix.new(source[1]) ; Out.ar(0, [left, right]) }).send(s) ; ) ( var buf, aSynth ; buf = Buffer.read(s,"sounds/a11wlk01-44_1.aiff") ; aSynth = Synth("playBuf3", ["bufnum", buf.bufnum]) ; )

8.3247

8.3 Sintesi additiva

Il teorema di Fourier stabilisce che ogni segnale periodico per quanto complesso pu essere rappresentato come la somma di semplici segnali sinusoidali. Lidea alla base della sintesi additiva quello di procedere in senso inverso, ottenendo un segnale complesso a partire dalla somma di semplici segnali sinusoidali, ognuno dei quali dotato di un suo inviluppo dampiezza. La Figura 8.7 illustra un banco di oscillatori che lavorano in parallelo: essi generano n segnali, che vengono sommati e che risultano in un segnale complessivo spettralmente pi ricco. Nella sintesi additiva classica gli oscillatori sono intonati a partire da una frequenza fondamentale: le loro frequenze (f 2 . . . f n) sono cio multipli interi della frequenza fondamentale (f 1).
harmonics f1 f2 Out f3 f4 oscils

fn

Fig. 8.7

Banco di oscillatori.

Si tratta di uno dei metodi di sintesi in uso da pi antica data proprio perch le due operazioni (sintesi di segnali sinusoidali e loro somma) sono di implementazione molto semplice. Se in teoria del segnale si parla di somma di segnali, pi tipicamente in ambito audio ci si riferisce alla stessa cosa in termini di missaggio (mixing, miscelazione). Come

8.3248

si visto, la UGen specializzata nel missaggio in SC Mix, che riceve in entrata un array si segnali, li somma e restituisce un nuovo segnale (mono). Il missaggio in digitale propriamente e banalmente una somma. Si comporta cos segnale 1: 0 - 1 - 2 - 3 - 2 - 1 - 1 segnale 2: 0 - 3 - 1 - 2 - 1 - 1 - 0 mix 1 + 2: 0 - 4 - 3 - 5 - 3 - 1 - 1 Per dirla in termini pi vicini a SC, si consideri il seguente esempio:
1 2 3 4 6 7 8 ( // ramp in [-1.0, 1.0] a = Array.series(20).normalize*2-1 ; b = Array.fill(20, { rrand(-0.5, 0.5) }) ; // rand in [-0.5, 0.5] c = a + b ; // sum // plot [a, b, c].flop.flat.plot(numChannels:3, discrete:true, minval: -1, maxval:1) )

I due array a e b contengono 20 valori. Il primo viene riempito di con una serie che progredisce da 0 a 20, viene normalizzato nellescursione [0, 1], quindi moltiplicato per 2 (lescursione ora [0.0, 2.0]), inne sottraendo 1 i valori sono compresi in [1.0, 1.0]. Il secondo contiene valori pseudo-casuali nellintervallo [0.5, 0.5]. Il terzo (c) contiene la somma (valore per valore) dei primi due. Il risultato visibile attraverso la riga 7. Passando alla sintesi, lesempio seguente per quanto minimale istruttivo rispetto alla potenza espressiva di SC. Come si vede, la riga 3 introduce un oggetto Mix: esso deve riceve come argomento un array di

8.3249

segnali che sommera restituendo un segnale unico. A Mix viene passato un array che contiene 20 segnali (generati da SinOsc. Si noti che i il contatore e viene utilizzato come moltiplicatore della frequenza degli oscillatori, che saranno perci multipli di 200Hz . Poich i inizializzata a 0 il primo oscillatore avrebbe una frequenza nulla (= assenza di segnale), e dunque necessario aggiungere 1 a i. Nel missaggio, la somma dei segnali lineare: ci vuol dire che n segnali normalizzati nellintervallo [1.0, 1.0] verranno missati in un segnale risultante la cui ampiezza sar compresa tra n [1.0, 1.0] = [n, n]: intuibilmente, ci risulta in un abbondante clipping. Nellesempio, il problema risolto attraverso mul:1/20: ogni segnale ha al massimo ampiezza pari a 1/20, e dunque, pure nella circostanza in cui tutti i segnali sono simultaneamente al massimo dellampiezza, il segnale missato non supera i massimi previsti.
1 2 3 4 5 6 7 ( { Mix.new // mix ( Array.fill(20, { arg i ; // 20 partials SinOsc.ar(200*(i+1), mul: 1/20)})) }.scope ; )

Per verdere che cosa avviene a livello spettrale sufciente valutare:


1 s.freqscope // assuming that s is the server in use

Vista lattitudine algoritmica di SC, il linguaggio mette a disposizione un metodo fill denito direttamente sulla classe Mix. Il codice seguente esattamente identico nei risultati al precedente.

8.3250

1 2 3 4 5 6 7

( { // the same Mix.fill(20, { arg i ; SinOsc.ar(200*(i+1), mul: 1/20)}) }.scope ; )

Negli approcci precedenti ogni sinusoide inizia il suo ciclo nello stesso momento. Questo determina un picco visibile nella forma donda, che si traduce in una sorto di impulso. In generale, la fase irrilevante nella parte stazionaria del segnale, mente assume importanza critica nei transitori e nelle discontinuit del segnale. Poich quanto qui interessa soltanto la relazione tra le componenti (in termini stazionari), meglio evitare allineamenti di fase. Nellesempio qui di seguito ogni oscillatore riceve una fase iniziale casuale attraverso 2pi.rand.
1 2 3 4 5 6 7 ( { // avoiding phase sync Mix.fill(20, { arg i ; SinOsc.ar(200*(i+1), 2pi.rand, mul: 1/20)}) }.scope ; )

Variando lampiezza delle armoniche componenti, si modica evidentemente il peso che queste hanno nello spettro. Qui di seguito, un segnale stereo che risulta dalla somma di 40 sinusoidi a partire da toHz viene modulata nellampiezza in modo differente sul canale destro e su

8.3251

quello sinistro:nel primo caso lampiezza dei componenti varia casualmente con una frequenza di 1 volta al secondo, nel secondo caso segue una oscillazione sinusoidale con frequenza casuale nellintervallo [0.3, 0.5]Hz . Gli argomenti mul e add rendono il segnale unipolare (compreso nellintervallo [0.0, 1.0]). Lampiezza divisa per 20: secondo quanto vista prima, dovrebbe essere divisa per 40, ma, a causa delle variazioni delle ampiezze dei segnali, una divisione (empirica) per 20 sufciente ad evitare il clipping.
1 2 3 4 5 6 7 8 9 ( { // stereo spectral motion Mix.fill(40, { arg i ; var right = { LFNoise1.kr(1, 1/20) } ; var left = { SinOsc.kr(rrand(0.3, 0.5), 2pi.rand, mul: 0.5, add: 0.5) } ; SinOsc.ar(50*(i+1), [2pi.rand, 2pi.rand], mul: [left/20, right]) }) }.scope ; )

Lesempio seguente scala lampiezza di ogni componente in proporzione inversa al numeto di armonica: pi il numero grande, minore lampiezza, secondo un comportamente sicamente comune negli strumenti musicali. La variabile arr contiene prima una sequenza di interi da 1 a 20, il cui ordine viene invertito (attraverso il metodo reverse), il cui valore viene normalizzato in modo che la somma di tutti i componenti sia pari a 1. In altre parole, sommando tutti gli elementi dellarray arr si ottiene 1. QUesto vuol dire che se si usano gli elementi dellarray come moltiplicatori delle ampiezze non si incorre in fenomeni di clipping. La frequenza deloscillatore espressa in notazione midi (60 = do centrale), che viene quindi convertita in cicli al secondo (midicps).

8.3252

1 2 3 4 5 6 7 8

( { var arr = Array.series(20, 1).reverse.normalizeSum ; Mix.new // mix ( Array.fill(20, { arg i ; // 20 partials SinOsc.ar(60.midicps*(i+1), 2pi.rand, mul: arr[i])})) }.scope ; )

Attraverso la sintesi additiva possono essere generate proceduralmente molte forme donda di tipo armonico. Ad esempio, londa quadra pu essere descritta come una somma innita di armoniche dispari la cui ampiezza proporzionale allinverso del numero darmonica, secondo la relazione f1 1, f3 1/3, f5 1/5, f7 1/7 . . . La Figura 8.8 confronta unonda quadra (in alto) con la somma pesata delle prime 3 armoniche (al centro) e delle prime 10 armoniche (in basso), mentre la Figura 8.9 dimostra i cambiamenti nella forma donda (a sinistra) e nello spettro (a destra, nel sonogramma) in relazione allincremento delle componenti armoniche. Come si nota, la ripidit degli spigoli nella forma donda proporzionale al numero dei componenti. Dunque, larrotondamento indica un basso numero di componenti e, allopposto, la presenza di transizioni ripide un numero alto. La discussione sulla sintesi additiva ha introdotto non a caso la classe Mix, perch in effetti la sintesi additiva pu essere generalizzata nei termini di un micro-missaggio/montaggio in cui un insieme arbitrario di componenti viene sommato per produrre un nuovo segnale. Si parla in questo caso di somma di parziali, intendendo come parziale un componente di frequenza arbitraria in uno spettro (e che perci non deve essere relata armonicamente alla fondamentale, e che pu includere anche segnali rumorosi). Restando nellambito dei componenti sinusoidali,

8.3253

1 0 Time (s) 0.03

1 0 Time (s) 0.03

1 0 Time (s) 0.03

Fig. 8.8
1

Onda quadra, prime 3 e prime 10 armoniche.


1

2104

1 0 Time (s) 1

1 0 Time (s) 1

1 0 Time (s) 1

1 0 Time (s) 1

Frequency (Hz) 0 0

1 0 Time (s) 1

1 0 Time (s) 1

6 Time (s)

Fig. 8.9 Progressione nelle componenti e approssimazione progressiva allonda quadra

8.3254

si pu descrivere larmonicit nei termini di un rapporto tra le frequenze delle componenti (ratio, in latino e in inglese). In caso di armonicit, si ha integer ratio tra fondamentale e armoniche (il rapporto espresso da un numero intero), nel caso di un parziale si ha non-integer ratio, poich parziale/f 1 non intero. Nellesempio seguente lunica novit la riga 10, che introduce un incremento casuale (e proporzionale al numero di armonica) nelle frequenze delle armoniche. Dunque lo spettro sar inarmonico. Tra laltro, una componente ridotta di inarmonicit tipica di molti strumenti acustici ed di solito auspicabile (si parla allora di quasi-integer ratio). Si noti che ogni volta che il codice viene valutato, nuovi valori vengono generati con conseguente cambiamento dello spettro.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ( // Varying envelopes with quasi-integer ratio { Mix.new( Array.fill(50, { arg k ; var incr = 1 ; // quasi-integer. Try to increase to 25..10 etc var env ; i = k+1 ; env = { LFNoise1.ar(LFNoise0.ar(10, add:1.75, mul:0.75), add:0.5, mul:0.5) }; SinOsc.ar(50*i +(i*incr).rand, mul: 0.02/i.asFloat.rand)*env }) )}.scope )

Uno spettro di parziali pu essere ottenuto per addizione di componenti sinusoidali, in numero di 80 nellesempio qui di seguito.

8.3255

1 2 3 4 5 6 7

( // A generic partial spectrum { var num = 80 ; Mix.new( Array.fill(num, { SinOsc.ar(20 + 10000.0.rand, 2pi.rand, 1/num) }) ); }.scope )

possibile sommare sinusoidi in un intervallo ridotto, intorno ai 500Hz :


1 2 3 4 5 6 ( // Sweeping around 500 Hz { Mix.new( Array.fill(20, { SinOsc.ar(500 + LFNoise1.ar(LFNoise1.ar(1, add:1.5, mul:1.5), add:500, mul: 500.0.rand), 0, 0.05) }) ); }.scope )

Inne, in relazione alla generalizzazione della tecnica in termini di montaggio/missaggio, nellesempio seguente quattro segnali ottenuti con tecniche di sintesi diverse (e diverse sono infatti le UGen coinvolte: SinOsc, Blip, HPF, Dust, Formant) sono correlate attraverso la stessa frequenza (f): lampiezza di ognuno di essi controllata da un generatore pseudocasuale (le variabili a, b, c, d. Il segnale poi distributio sul fronte stereo attraverso Pan2.

8.3256

1 2 3 4 5 6 7 9

// Additive in a broad sense: 4 UGens tuned around a frequency and randomly mixed down // (and randomly panned around) ( a = { LFNoise1.ar(1, add:0.15, mul:0.15) }; b = { LFNoise1.ar(1, add:0.15, mul:0.15) }; c = { LFNoise1.ar(1, add:0.15, mul:0.15) }; d = { LFNoise1.ar(1, add:0.15, mul:0.15) }; f = { LFNoise0.ar(LFNoise0.ar(0.5, add:0.95, mul: 0.95), add: 50, mul:50) }; m = { Pan2.ar( Mix.new([ { SinOsc.ar(f, mul:a) }, { Blip.ar(f, mul:b) }, { HPF.ar(Dust.ar(f*0.2), f, mul:c) }, { Formant.ar(f,mul:c) }, ]) , LFNoise1.ar(0.2, mul:1) ) }; m.scope; )

11 12 13 14 15 16 17 18 19 20 21

La sintesi additiva si rivela particolarmente adatta per i suoni ad altezza determinata, in cui sufciente la denizione di un numero limitato di armoniche particolarmente evidenti. In effetti, per costruire uno spigolo (come quello di unonda quadra, o di un onda triangolare o a dente di sega) sono necessarie teoricamente innite componenti sinusoidali. A tal proposito, SC prevede specici generatori, ad esempio Pulse genera onde impulseive (tra cui londa quadra) e Saw onde a dente di sega. Questi tipi di segnali sono spesso utilizzati come segnali di controllo ed esistono perci versioni LF di queste UGen, cio versioni che sono specializzate nel lavorare a tasso di controllo LFPulse, LFSaw 27. Una somma

8.3257

di sinusoidi, per quanto numerose esse possano essere, produce un segnale aperiodico, ma non rumoroso, ed il risultato tipico un effetto di liquidit.
1 2 3 4 5 6 7 8 9 10 11 12 14 ( // Filling the spectrum with sinewaves: frac of semitones m = { var comp = 500; // number of components. Increase as much as your CPU can var res = 0.1; var base = 30 ; // 30 = rumble Mix.new( Array.fill(comp, { arg i; SinOsc.ar(freq: (base+(i*res)).midicps // resolution: eg. interval of 0.5 Hz // Note that the resulting wave h as a pulse freq of resolution , phase: 2pi.rand, mul: 1.0/comp )}) // random phase )}.scope )

Lesempio crea 500 sinusoidi in parallelo ( un bel numero. . .): il numero degli stessi controllato da comp. La strategia di riempimento gestisce le frequenze attraverso la notazione midi. Questo assicura la possibilit di equispaziare in altezza (percepita) le componenti (e non in frequenza). La variabile res controlla la frazione di semitono che incrementa in funzione del contatore i a partire dalla altezza di base, base. Si noti come storicamente il principale problema della sintesi additiva sia stata la complessit di calcolo richiesta. Infatti un segnale complesso richiede la denizione di almeno 20 armoniche componenti, di ognuna delle quali
27

Ancora, Blip.ar(freq, numharm, mul, add) genera un numero numharm controllabile di componenti armoniche di un fondamentale freq, dotate tutte della stessa ampiezza.

8.3258

necessario descrivere linviluppo in forma di spezzata. Come si vede, il problema computazionale ormai di scarso rilievo, anche perch SC particolarmente sufciente. La sintesi di suoni percussivi, nello spettro dei quali si evidenzia una preponderante componente di rumore (cio di parziali non legate alla serie armonica di Fourier), richiederebbe (al ne di ottenere un risultato discreto) un numero di componenti elevatissimo. In realt, necessario un altro approccio, quale ad esempio quello della sintesi sottrattiva (cfr. infra).

8.4 Sintesi granulare

La sintesi granulare muove dal presupposto che il suono possa essere pensato non solo in termini ondulatori ma anche in termini corpuscolari. usuale il paragone con i procedimenti pittorici del pointillisme: la somma di frammenti sonori di dimensione microscopica viene percepita come un un suono continuo, come la somma di minuscoli punti di colore puro origina la percezione di tutti gli altri colori. Si tratta allora di denire grani sonori la cui durata vari tra 1 e 100ms, ognuno dei quali caratterizzato da un particolare inviluppo dampiezza, che inuisce notevolmente sul risultato sonoro. Come intuibile, se per sintetizzare un secondo di suono sono necessari ad esempio 100 grani, non pensabile la denizione deterministica delle propriet di ognuno di essi. perci necessario ricorrere a un controllo statistico, che ne denisca i valori medi per i diversi parametri, ad esempio durata, frequenza, forma donda, inviluppo: pi complessivamente deve essere stabilita la densit media dei grani nel tempo. La necessit di un controllo dalto livello rispetto al basso livello rappresentato dai grani richiede cos la denzione di un modello di composizione: non a caso la sintesi granulare una delle tecniche che ha maggiormente stimolato un approccio algoritmico

8.4259

alla composizione. Ad esempio si pu organizzare la massa dei grani seguendo la legge di distribuzione dei gas perfetti (Xenakis), oppure attraverso una tipologia degli ammassi che deriva da quella meteorologica delle nuvole (Roads), o inne immaginare le sequenze di grani come dei ussi di particelle di cui determinare le propriet globali tramite maschere di tendenza (Truax). In generale, va notato che per la sintesi granulare richiesto al calcolatore un notevole lavoro computazionale per controllare i valori dei parametri relativi alla sintesi di ogni singolo grano. Esistono molti modi differenti di realizzare la sintesi granulare, che rispondono a obiettivi anche assai diversi tra loro: in effetti, seppur con qualche approssimazione, pu essere descritta come sintesi granulare ogni tecnica di sintesi che si basi sullutilizzo di segnali impulsivi.
1 2 3 4 5 6 7 9 11 12 13 14 15 16 17 18 19 ( // sync granular synthesis var baseFreq = { GUI.mouseX.kr(100, 5000, 1) }; var disp = { GUI.mouseY.kr } ; var strata = 10 ; var minDur = 0.05, maxDur = 0.1 ; var minRate = 10, maxRate = 50 ; JMouseBase.makeGUI ; // if you use SwingOSC { Mix.fill(strata, { SinOsc.ar( baseFreq + LFNoise0.kr(rrand(minRate, maxRate), mul: baseFreq*disp*0.5), mul: 1/strata) } //* LFPulse.kr(1/rrand(minDur, maxDur)) * SinOsc.ar(1/rrand(minDur, maxDur)*0.5).squared ) }.freqscope )

8.4260

Nellesempio precedente implementato un approccio in sincrono alla sintesi granulare. In sostanza, nellalgoritmo di sintesi una sinusoide inviluppata in ampiezza da unonda quadra, generata da LFPulse: poich LFPulse genera un segnale unipolare (la cui ampiezza inclusa in [0.0, 1.0]), quando lampiezza dellonda quadra > 0, lascia passare il segnale, altrimenti lo riduce ad ampiezza 0. Il risultato una nestrazione del segnale. La durata di ogni nestra dipende dal ciclo dellonda quadra, ed gestito da minDur e maxDur (le durate specicano 1 il periodo della nestra, dunque la frequenza di LFPulse sar T , cio 1/rrand(minDur, maxDur)). In questo caso, linviluppo del grano appunto dato da un impulso (il ciclo positivo dellonda quadrata). La riga 16 (commentata) propone un altro inviluppo, ottenuto elevando al quadrato una sinusoide (cfr. supra). Linviluppo avr il doppio dei picchi nellunita di tempo (attraverso il rovesciamento della parte negativa della sinusoide) e dunque la frequenza viene dimezzata attraverso *0.5. Si noter che linviluppo a campana, meno ripido della sinusoide al quadrato riduce lampiezza delle componenti pi elevate dello spettro. Come si vede, attraverso Mix viene missato insieme un numero strata di segnali: in ognuno, baseFreq gestisce la frequenza di base a cui viene aggiunto un valore pseudo-casuale disp, generato da LFNoise0, che rapprenta una percentuale normalizzata della frequenza di base (se baseF req = 100 e disp = 0.5, allora la frequenza delloscillatore varier nellintervallo [100 50, 100 + 50]). Il tasso di questa variazione gestito da minRate e maxRate. Lapproccio precedente implementabile in tempo reale, e parte dallassunto di convertire, per cos dire, segnali continui, in segnali impulsivi a partire da una operazione di nestrazione. Un altro approccio possibile prevederebbe di pensare ad ogni singolo grano come ad un evento di taglia compositiva: lo si vedr discutendo dello scheduling. Un uso tipico delle tecniche granulari sfrutta la granularit non tanto per la sintesi ab nihilo quanto piuttosto per lelaborazione: tipicamente, loperazione chiamata granulazione, e come intuibile consiste nel sottoporre a

8.4261

scomposizione in grani e successiva ricombinazione un segnale preesistente. In qualche misura, loperazione intrinsecamente in tempo differito, perch necessario avere a disposizione una parte di segnale per poterla scomporre e ricomporre. Ci vuol dire che, in tempo reale, necessario quantomeno riempire un buffer (una locazione di memoria temporanea) per poterlo granulare.
1 2 4 5 6 7 8 9 10 11 12 13 15 ( b = Buffer.read(s, "sounds/a11wlk01-44_1.aiff"); SynthDef(\grainBuf, { arg sndbuf; Out.ar(0, GrainBuf.ar(2, Impulse.kr(10), 0.1, sndbuf, LFNoise1.kr.range(0.5, 2), LFNoise2.kr(0.1).range(0, 1), 2, LFNoise1.kr(3) )) }).send(s); ) x = Synth(\grainBuf, [\sndbuf, b])

Lesempio precedente una semplicazione di quello proposto nel le di help della UGen GrainBuf. Nel buffer b viene memorizzato il segnale "sounds/a11wlk01-44_1.aiff", sul quale GrainBuf opera. La sintassi di GrainBuf la seguente:
GrainBuf.ar(numChannels, trigger, dur, sndbuf, rate, pos, interp, pan, envbufnum, mul, add)

Il primo argomento specica evidentemente il numero dei canali. Il secondo il segnale di triggering che innesca un nuovo grano: nellesempio

8.4262

una sequenza di impulsi con frequenza di 10Hz . Si provi a sostituire in Impulse.kr(10) il valore 10 con GUI.mouseX(1, 300) per vericare leffetto della densit dei grani 28. Largomento dur determinata la durata di ogni grano (costante a 0.1), il successivo il buffer (nello specico, sar b). Gli argomenti rate, pos, interp, pan, envbufnum controllano rispettivamente il tasso di lettura del campione (come in PlayBuf), la posizione di lettura del buffer (normalizzata tra [0.0, 1.0], il metodo di interpolazione per il pitchshifting dei grani (qui, 2, ad indicare interpolazione lineare), la posizione sul fronte stereo (in caso di segnali stereo, come in Pan2), un buffer (opzionale) in cui contenuto un inviluppo da utilizzare per inviluppare i grani. Alcuni argomenti sono controlli da generatori LowFrequency. Alcune parole solo su ci che non si visto. Il metodo range permette di scalare un segnale allinterno dellintervallo specicato dai due valori passati: decisamente pi intelligibile del lavoro (istruttivo. . .) con mul eadd 29 La UGen LFNoise2 produce un rumore a bassa frequenza con interpolazione quadratica tra un valore e il successivo, a differenza che nel caso di LFNoise1 in cui linterpolazione lineare. Il risultato un segnale molto arrotondato. Si provi ad ascoltare e a guardare i seguenti segnali:
1 3 5 {LFNoise0.ar(200)}.scope {LFNoise1.ar(200)}.scope {LFNoise2.ar(200)}.scope

28 29

Senza dimenticare JMouseBase.makeGUI ; in caso si usi SwingOSC. Il metodo assume che il segnale sia nellescursione normalizzata, quindi non funziona se gli argomenti mul e add vengono specicati.

8.5263

8.5 Sintesi sottrattiva

Nella sintesi sottrattiva il segnale in input generalmente un segnale complesso, dotato di uno spettro molto ricco. Questo segnale subisce un processo di ltraggio in cui si attenuano le frequenze indesiderate, enfatizzando soltanto alcune regioni (pi o meno estese) dello spettro. Pi in generale, un ltro un dispositivo che altera lo spettro di un segnale in entrata. Un ltro agisce cio enfatizzando o attenuando determinate frequenze del segnale: una modica dello spettro determina a livello percettivo un cambiamento nel timbro del suono. I parametri fondamentali di un ltro sono: il tipo, la frequenza di taglio/centrale, lordine. Si riconoscono usualmente quattro tipi di ltri: passa-basso (lowpass), passaalto (highpass), passa-banda (bandpass) e elimina-banda (bandreject, o notch). I quattro tipi sono schematizzati in Figura 8.10.

Fig. 8.10 Tipi di ltro. Larea grigia rappresenta lintervallo di frequenze che il ltro lascia passare.

8.5264

In un ltro passa-basso o passa-alto ideali, data una frequenza di taglio, tutte le frequenze rispettivamente superiori o inferiori a questa dovrebbero essere attenuate a 0. Allo stesso modo, in un ltro passa-banda o elimina-banda ideali, data una banda di frequenze, tutte le frequenze rispettivamente esterne o interne alla banda dovrebbero essere attenuate a 0. La frequenza di taglio perci quella frequenza a partire dalla quale viene effettuato il ltraggio. Nei ltri passa- o elimina-banda si deniscono sia la larghezza di banda (bandwidth) che la frequenza centrale: data una regione dello spettro, la prima ne misura la larghezza, la seconda la frequenza al centro. Ad esempio, in un ltro che passa tutte le frequenze nellintervallo [100, 110]Hz , la larghezza di banda 10Hz , la frequenza centrale 105Hz . La differenza tra mondo ideale e mondo reale evidente se si valuta:
1 { LPF.ar(WhiteNoise.ar, freq: 1000) }.freqscope

La UGen LPF un ltro passabasso (acronimo di Low Pass Filter), e freq indica la frequenza di taglio. Poich la sorgente un rumore bianco (si noti il patching), il risultato visibile dovrebbe assomigliare alla Figura 8.10, passabasso. In realt, lattenuazione sempre progressiva, e proprio la ripidit della curva un indicatore della bont del ltro (intuibilmente, pi linviluppo spettrale ripido dopo la frequenza di taglio, meglio ). Poich i ltri che rispondano ai requisiti dellidealit non esistono, si considera come frequenza di taglio quella a cui il ltro attenua di 3 dB il livello dampiezza massimo. Se perci il passaggio tra la regione inalterata e quella attenuata dal ltro graduale, un ultimo parametro diventa rilevante: la pendenza della curva. Questultima, misurata in dB per ottava, denisce lordine del ltro. Ad esempio, un ltro del I ordine presenta una attenuazione di 6 dB per ottava, uno del II di 12 dB, del III di 18 dB e cos via.

8.5265

Se si pensa al caso discusso del rumore bianco, si nota come una parte dellenergia (quella relativa alle frequenze tagliate) sia attenuata (idealmente a zero). Questa diminuzione riduzione dellenergia complessiva di un segnale risulta, nel dominio del tempo, in una modica dellampiezza del segnale, con una conseguente diminuzione del volume del suono risultante. Tipicamente, pi il ltraggio consistente, maggiore il decremento dellenergia. Ragion per cui spesso necessario incrementare in uscita lampiezza del segnale, compensando lenergia persa (un processo tipicamente detto balancing). Un altro parametro di rilievo in un ltro passabanda il cosiddetto Q. Il Q intuitivamente, rappresenta il grado di risonanza di un ltro. Pi precisamente:

Q=

fcentrale larghezzaBanda

te) stretta e diventa progressivamente percepibile unaltezza precisa. Il parametro Q indica allora la selettivit del ltro, la sua risonanza. Nella circostanza, usuale nella sintesi sottrattiva, in cui si impieghino pi ltri, il ltraggio pu avvenire in parallelo o in serie: nel primo caso pi ltri operano sullo stesso segnale simultaneamente (analogamente a quanto avveniva per il banco di oscillatori); nel secondo caso, i ltri

= 10, 5. Se manteniamo Q costante, e incrementiamo la frequenza centrale del nostro ltro a 10.500Hz , otteniamo come estremi 11.000Hz e 10.000Hz . La larghezza di banda passata da 10Hz a 1000Hz , conformente con la nostra percezione dellaltezza. Dunque, Q risonanza, perch se Q elevato, la banda (percettivamen-

Q cio il rapporto tra la frequenza centrale e la larghezza di banda. Esprimere il funzionamento del ltro attraverso Q permette di tenere in conto il problema della percezione della frequenza. Mantenere Q costante lungo tutto lo spettro vuol dire infatti adeguare la larghezza di banda allaltezza percepita (allintervallo musicale). Ad esempio: se fcentrale = 105; facuta = 110Hz ; fgrave = 100Hz allora Q =
105 110100

8.5266

sono invece collegati in cascata, e il segnale in entrata passa attraverso pi ltri successivi. Seppur in termini non del tutto esatti, la sintesi sottrattiva pu essere pensata come un procedimento simmetrico a quella additiva: se in questultima si parte generalmente da forme donda semplici (sinusoidi) per ottenere segnali complessi, nella sintesi sottrattiva si parte invece da un segnale particolarmente ricco per arrivare a un segnale spettralmente meno denso. Come sorgenti possono essere utilizzati tutti i generatori disponibili, a parte evidentemente le sinusoidi. In generale, sono di uso tipico segnali spettralmente densi. Ad esempio, il rumore bianco, ma anche quello colorato, tendono a distribuire eneregia lungo tutto lo spettro e ben si prestano ad essere punti di partenza per una tecnica sottrattiva. In Figura 8.11 la larghezza di banda di quattro ltri (con frequenze centrali 400, 800, 1200, 1600 Hz) che operano in parallelo su un rumore bianco viene diminuita da 1000, a 10, no a 1 Hz, trasformando il rumore in un suono armonico.
5000

Frequency (Hz) 0 0 Time (s)

10

Fig. 8.11 Filtraggio: da rumore bianco a spettro armonico Per questioni di efcienza computazionale, le UGen specializzate nel ltraggio prevedono come argomento in SC non Q ma il suo reciproco, indicato come rq. Intuibilmente, varr allora la relazione per cui minore rq pi stretto il ltraggio (maggiore la risonanza). Lesempio seguente implementa la stessa situazione di ltraggio armonico in SC: a un

8.5267

array che contiene 10 valori da utilizzare come frequenze centrali per il ltro passabanda BPF. Poich larray viene passato come argomento a BPF ne consegue una espansione multicanale: ne risultano 10 segnali. Di essi, soltanto due sono udibili (nel caso la scheda audio sia stereo), quelli che risultano dal ltraggio a 100 e 200Hz .Attraverso scope si possono osservare tutti i segnali e notare come il rumore bianco in entrata faccia risuonare ogni ltro intorno alla sua frequenza centrale. Lesempio successivo uguale, ma invia i segnali ad un oggetto Mix: il segnale complessivo, che risulta dalla somma dei dieci ltraggi sul rumore bianco le cui frequenze sono armoniche di 100Hz udibile, e visibile come forma donda e spettro (scope e freqscope).

8.5268

1 2 3 4 5 6 7 8 9 10 11 12 14 15 16 17 18 19 20 21 22 23 24 25 26

// Filtering results in a harmonic spectrum ( var sound = { var rq; i = 10; rq = 0.01; f = 100; // rq = reciprocal of Q > bw/cutoff w = WhiteNoise.ar; // source a = Array.series(i, f, f); // array: [ 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000 ] m = BPF.ar(w, a, rq, i*0.5); // mul is scaled for i m; } ; sound.scope(10) ; // see 10 channels of audio, listen to first 2 ) ( var sound = { var rq; i = 10; rq = 0.01;f = 100; // rq = reciprocal of Q > bw/cutoff w = WhiteNoise.ar; // source a = Array.series(i, f, f); // array: [ 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000 ] n = BPF.ar(w, a, rq, i*0.5); // mul is scaled for i m = Mix.ar(n); // mixDown m; } ; sound.scope ; // mixdown: waveform sound.freqscope ; // mixdown: spectrum )

Un aspetto particolarmente interessante della sintesi sottrattiva sta nella somiglianza che presenta con il funzionamento di molti strumenti acustici, nei quali una fonte di eccitazione subisce pi ltraggi successivi. Ad esempio, la cassa armonica di una chitarra si comporta come un risonatore: vale a dire, opera un ltraggio selettivo attenuando certe frequenze ed enfatizzandone altre. Un discorso analogo vale per la voce

8.5269

umana, nella quale leccitazione glottidale passa attraverso il ltraggio successivo di tutti i componenti del cavo orale: proprio per questo la sintesi sottrattiva il metodo alla base della tecnica standard di simulazione articiale della voce, la cosiddetta codica a predizione lineare (Linear Predictive Coding). Nellesempio successivo, possibile confrontare alcune sorgenti e vericare il risultato del ltraggio gi discusso selezionando quella che interessa.
1 3 4 5 6 7 8 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // Here we change sources // sources { w = Pulse.ar(100, 0.1, mul: 0.1) }.play { w = Dust2.ar(100, mul: 1) }.play { w = LFNoise0.ar(100, 0.1, mul: 1) }.play { w = PinkNoise.ar(mul: 0.1) }.play { w = WhiteNoise.ar(mul: 0.1) }.play ( { i = 10; q = 0.01;f = 100; //w = Pulse.ar(100, 0.1, mul: 0.1); //w = Dust2.ar(100, mul: 1); //w = LFNoise0.ar(100, 0.1, mul: 1); //w = PinkNoise.ar; w = WhiteNoise.ar; a = Array.series(i, f, f); n = BPF.ar(w, a, q, i); m = Mix.ar(n)*0.2; z = n.add(w).add(m); m; }.scope // try .plot too )

8.6270

8.6 Analisi e risintesi

Come si visto, il momento fondamentale sia nella sintesi additiva che nella sintesi sottrattiva il controllo dei parametri che determinano rispettivamente linviluppo delle componenti armoniche e le caratteristiche dei ltri impiegati. Nelle tecniche di sintesi basate su analisi e risintesi sono proprio i dati di controllo che vengono derivati dallanalisi di un segnale preesistente ad essere utilizzati nella fase di sintesi. Il processo viene usualmente scomposto in tre fasi: 1. creazione di una struttura dati contenente i dati ricavati dallanalisi; 2. modica dei dati danalisi; 3. risintesi a partire dai dati danalisi modicati Se questa larchitettura generale, molte sono comunque le possibilit di realizzazione. Il caso del Phase Vocoder (PV) particolarmente interessante e permette di introdurne limplementazione in SC. Nel PV, il segnale viene tipicamente analizzato attraverso una STFT (Short Time Fourier Transform). In un analisi STFT il segnale viene suddiviso in frame (nestre, esattamente nellaccezione di prima in relazione alla sintesi granulare), ognuno dei quali passa attraverso un banco di ltri passabanda equispaziati in parallelo tra 0 e sr (la frequenza di campionamento). Il risultato complessivo dellanalisi di tutti i frame , per ogni ltro, landamento di una componente sinusoidale di frequenza pari alla frequenza centrale del ltro stesso. La struttura di un frame pu essere descritta come segue: FRAME bin: 1 1 freqval 1 2 freqval 2 3 freqval 3

... ...

8.6271

bin: 2 ... bin: 512

ampval 1 freqval 1 ampval 1 ... freqval 1 ampval 1

ampval 2 freqval 2 ampval 2 ... freqval 2 ampval 2

ampval 3 freqval 3 ampval 3 ... freqval 3 ampval 3

... ... ... ... ... ...

In sostanza: 1. si scompone ogni frame del segnale originale in un insieme di componenti di cui si determinano i valori di ampiezza e frequenza; 2. da queste componenti si costruiscono gli inviluppi relativi ad ampiezza e frequenza per tutti i segnali sinusoidali: lampiezza, la fase e la frequenza istantanee di ogni componente sinusoidale sono calcolate interpolando i valori dei frame successivi Gli inviluppi superano perci i limiti del singolo frame. I due inviluppi cos ottenuti possono essere impiegati per controllare un banco doscillatori che riproducono per sintesi additiva il segnale originale. Se il le danalisi non viene modicato, la sintesi basata su FFT riproduce in maniera teoricamente identica il segnale originale, anche se in realt si verica nel processo una certa perdita di dati. Il PV sfrutta laspetto pi interessante dellanalisi FFT: la dissociazione tra tempo e frequenza. cos possibile modicare uno dei due parametri senza per questo alterare laltro. Pi in generale possibile modicare autonomamente i parametri di controllo di tutte le componenti. Unaltra possibilit di grande rilievo lestrazione degli inviluppi di soltanto alcune delle componenti. Essendo il passo di analisi necessariamente antecedente alla sintesi (e costoso computazionalmente), le implementazioni classiche prevedono la scrittura su le dei dati danalisi e la successiva importazion ed elaborazioni dei dati per la risintesi. Lutilizzo in tempo reale richiede nellimplementazione in SC di allocare un buffer in cui vengono scritti

8.6272

progressivamente i dati danalisi. In particolare, la dimensione del buffer (che deve essere una potenza di 2) corrisponde alla dimensione della nestra danalisi (il frame). In sostanza, ogni nestra prelevata sul segnale viene memorizzata nel buffer: ogni nuova nestra sostituisce la precedente. I dati memorizzati nel buffer sono il risultato della conversione dal dominio del tempo al dominio della frequenza effettuata sulla nestra (detto un po approssimartivamente, lo spettro istantaneo della nestra, che conta come ununica unit di tempo). Questa operazione svolta dalla UGen FFT, che effettua appunto una Fast Fourier Transform sulla nestra. I dati conservati nel buffer possono essere a quel punto manipolati opportunamemte attraverso un insieme di UGen estremamente potenti, il cui presso PV_ (Phase Vocoder). Tipicamente la manipolazione lavora sullo stesso buffer, sostituendo i dati precedenti. Manipolazti o meno, il buffer contiene comunque ancora dati che rappresentano il segnale (la nestra prelevata) nel dominio della frequenza. Per essere inviati in uscita, i dati devono perci essere convertiti dal dominio della frequenza a quello del tempo (dallo spettro alla forma donda): loperazione svolta dalla UGen IFFT, ovvero Inverse Fast Fourier Transform. In sostanza, lintero processo prende la forma seguente: segnale in entrata FFT PV_. . . IFFT segnale in uscita Lelemento PV_ in teoria opzionale, nel senso che i dati possono essere risintetizzati senza che siano stati rielaborati. Evidentemente una simile situazione non particolarmente utile, ma permette di spiegare il processo:

8.6273

1 2 4 5 6 7 8 9 10 11 12 13

a = Buffer.alloc(s, 2048, 1) ; b = Buffer.read(s,"sounds/a11wlk01.wav") ; ( SynthDef("noOperation", { arg fftBuf, soundBuf ; var in, chain; in = PlayBuf.ar(1, a.bufnum, loop:1) ; chain = FFT(bufnum, in) ; // time > freq Out.ar(out, IFFT(chain) // freq > time ); }).play(s,[\fftBuf, a, \soundBuf, b]) ; )

tante dalla trasformazione al contrario realizzata da IFFT. In un mondo ideale, non ci sarebbe perdita di dati tra analisi e risintesi ed il segnale in uscita sarebbe una ricostruzione perfetta di quello in entrata: nel mondo reale si assite invece ad alcune trasformazioni del segnale indotte dalla catena di elaborazioni, tipicamente ridotte, ma potenzialmente importanti, soprattutto nel caso di segnali molto rumorosi. Linsieme delle UGen PV_ molto esteso, e molto potente. Qui di seguito verranno soltanto presentate un paio di applicazioni. Si consideri lesempio seguente: la synthDef realizza un noise gate, cio lascia passare il segnale al di sopra di una cert ampiezza selezionabile con il mouse.

211 . Quindi in uscita a Out viene semplicemente inviato il segnale risul-

Nellesempio, il segnale in viene trasformato nel dominio della frequenza dalla UGen FFT (si noti il nome tipico assegnato alla variabile, chain, ad indicare che si tratta di un concatenamento di nestre in successione). I dati danalisi vengono memorizzati nel buffer a (che passato allargomento fftBuf), la cui dimensione una potenza di 2, 2048 =

8.6274

1 3 4 5 6 7 8 10 12 13

c = Buffer.read(s, "sounds/a11wlk01-44_1.aiff") ; SynthDef("noiseGate", { arg soundBufnum ; var sig; sig = PlayBuf.ar(1, soundBufnum, loop:1); sig = sig.abs.thresh(GUI.mouseX.kr(0,1)) * sig.sign; Out.ar(0, sig); }).play(s, [\soundBufnum, c.bufnum]) ; JMouseBase.makeGUI ; // see what's going on s.scope(1) ;

Il codice di rilievo quello di riga 6. La parte negativa del segnale ribaltata in positivo attraverso abs, quindi tresh lascia inalterati i campioni la cui ampiezza superiore alla soglia (gestita da mouseX), mentre restituisce il valore 0.0 se il campione al di sotto della soglia (cio: azzera il segnale al di sotto di una certa soglia). In questo modo, tutta la parte di segnale inferiore ad un certo valore di soglia viene eliminata. Tipicamente ci permette di eliminare i rumori di fondo, che si presentano come segnali con ampiezza ridotta e costante. Tuttavia, il segnale, che ora unipolare, suono pi o meno allottava superiore a causa del ribaltamento. Il metodo sign restituisce 1 se il campione ha valore negativo e 1 se positivo. Moltiplicando ogni campione per il segno del campione originale la parte di segnale negativa ribaltata ritorna ad avere segno negativo. Vale la pena di osservare come, secondo prassi usuale in SC, lo stesso risultato pu essere ottenuto attraverso differenti implementazioni 30. Ad esempio:

30

Un suggerimento per la prima implementazione proviene da Nathaniel Virgo, la seconda stata proposta da Stephan Wittwer, entrambi attraverso la mailing list.

8.6275

1 3 4 5 6 7 8 9 11 13 14

c = Buffer.read(s, "sounds/a11wlk01-44_1.aiff") ; SynthDef("noiseGate2", { arg soundBufnum = 0; var pb, ir, mx; mx = GUI.mouseX.kr; pb = PlayBuf.ar(1, soundBufnum, loop: 1); ir = InRange.ar(pb.abs, mx, 1); Out.ar(0, pb * ir) }).play(s, [\soundBufnum, c.bufnum]); JMouseBase.makeGUI ; // see what's going on s.scope(1) ;

La UGen InRange.kr(in, lo, hi) restituisce 1 se il campione ha valore incluso nellintervallo [lo, hi], 0 altrimenti. Nellesempio, lo gestito da mouseX, mentre hi pari a 1. Dunque, il segnale in uscita ir una sequenza di 0 e 1 in funzione del valore assoluto del campione: se il valore assoluto (abs) superiore a lowqui assegnata alla variabile mx, allora loutput 1. Il segnale sar necessariamente uguale o inferiore a 1, che il massimo nella forma normalizzata. Il segnale originale pb viene moltiplicato per ir, che lo azzera se lampiezza inferiore alla soglia mx (poich lo moltiplica per 0) e lo lascia inalterato se superiore alla soglia (poich lo moltiplica per 1). Si noter che, seppur efcace, il noise gate produce importanti buchi nel segnale. Lesempio seguente sfrutta un altro approccio e realizza quello che si potrebbe denire un (noise) gate spettrale. La UGen PV_MagAbove opera sui dati danalisi assegnati alla variabile chain: lascia invariati i valori dampiezza dei bin la cui ampiezza superiore ad un valore di soglia (qui controllato dal mouseX), mentre azzera quelli la cui ampiezza

8.6276

inferiore. Come si nota, possibile in questo modo eliminare completamente lo sfondo rumoroso che le cui componenti si situano ad un ampiezza ridotta rispetto ad altri elemtni (la voce) in primo piano. Come si nota, loperazione molto potente, ma non indolore, perch le stesse componenti sono rimosse da tutto lo spettro (anche dalla gure in primo piano, per cos dire).
1 2 3 4 5 6 8 9 10 11 12 13 14 15 16 17 19 20 ( // Allocate two buffers, b for FFT, c for soundfile a = Buffer.alloc(s, 2048, 1); b = Buffer.read(s, "sounds/a11wlk01-44_1.aiff"); "done".postln; ) // A spectral gate ( // Control with mouse spectral treshold SynthDef("magAbove", { arg bufnum, soundBufnum ; var in, chain; in = PlayBuf.ar(1, soundBufnum, loop: 1); chain = FFT(bufnum, in); chain = PV_MagAbove(chain, JMouseX.kr(0, 100, 0)); Out.ar(0, 0.5 * IFFT(chain)); }).play(s,[\out, 0, \bufnum, a.bufnum, \soundBufnum, b.bufnum]); JMouseBase.makeGUI ;// for SwingOSC )

Unaltra applicazione che ha un suo corrispettivo nel dominio della frequenza il ltraggio. Intuibilmente, se il segnale nestrato scomposto in frequenza, allora possibile eliminare i bin relativi alle frequenze che si vogliono ltrare (ad esempio, tutti quelli sopra una certa soglia: ltro passa-basso, tutti quelli sotto: ltro passa-alto). Nellesempio

8.6277

seguente il mouse controlla largomento wipe di PV_BrickWall. Se wipe pari a 0, non c effetto, se < 0 la UGen lavora come un ltro passa-basso, se > 0 come un passa-alto. Lescursione possibile compresa nellintervallo [1.0, 1.0] (il che richiede di determinare empiricamente il valore pari alla frequenza desiderata). Il ltro spettrale prende giustamente il nome di brickwall perch la ripidit in questo caso verticale.
1 2 3 4 5 7 8 9 10 11 12 13 14 15 16 18 19 ( // Allocate two buffers, b for FFT, c for soundfile a = Buffer.alloc(s, 2048, 1); b = Buffer.read(s, "sounds/a11wlk01-44_1.aiff"); ) ( // An FFT filter SynthDef("brickWall", { arg bufnum, soundBufnum ; var in, chain; in = PlayBuf.ar(1, soundBufnum, loop: 1); chain = FFT(bufnum, in); chain = PV_BrickWall(chain, JMouseX.kr(-1.0,1.0, 0)); // -1.0 > 0.0: LoPass ; 0.0 > 1.0: HiPass Out.ar(0, IFFT(chain)*10); }).play(s,[\bufnum, a.bufnum, \soundBufnum, b.bufnum]); JMouseBase.makeGUI ; // for SwingOSC )

Una delle applicazioni pi tipiche del PV consiste nellelaborazione della frequenza indipendentemente dal tempo. Tra le molte UGen disponibili, lesempio seguente sfrutta PV_BinShift. PV_BinShift(buffer,

8.6278

stretch, shift) permette di traslare e di scalare tutti i bin, rispettivamente din un fattore shift e stretch. La traslazione (shift corrisponde ad uno spostamento nello spettro: ad esempio, dato un spettro di tre componenti [100, 340, 450], una traslazione +30 risulta in [130, 370, 480]. Nel codice seguente, largomento stretch riceve valore pari a 1 (nessuna trasformazione), mentre shift varia in funzione di mouseX nellintervallo [128, 128]. Osservando lo spettro e in particolare muovendo il mouse verso destra, si nota come lintero spettro si sposti lungo lasse delle frequenze.
1 2 3 4 6 7 8 9 10 11 12 13 15 17 18 ( a = Buffer.alloc(s,2048,1); b = Buffer.read(s,"sounds/a11wlk01.wav"); ) ( SynthDef("fftShift", { arg bufnum, soundBufnum ; var in, chain; in = PlayBuf.ar(1, soundBufnum, loop: 1); chain = FFT(bufnum, in); chain = PV_BinShift(chain, 1, MouseX.kr(-128, 128) ); Out.ar(0, 0.5 * IFFT(chain).dup); }).play(s,[\bufnum, a, \soundBufnum, b]); JMouseBase.makeGUI ; // for SwingOSC s.scope ; )

In maniera analoga, largomento stretch permette di moltiplicare il valore dei bin: come si vede osservando la visualizzazione dello spettro nellesempio seguente (in particolare muovendo progressivamente il mouse da sinistra a destra), dalla variazione di scale nellintervallo [0.25, 4]

8.6279

(con incremento esponenziale) consegue una progressiva espansione spettrale.


1 2 3 4 6 7 8 9 10 11 12 13 15 17 18 ( a = Buffer.alloc(s,2048,1); b = Buffer.read(s,"sounds/a11wlk01.wav"); ) ( SynthDef("fftStretch", { arg bufnum, soundBufnum ; var in, chain; in = PlayBuf.ar(1, soundBufnum, loop: 1); chain = FFT(bufnum, in); chain = PV_BinShift(chain, MouseX.kr(0.25, 4, \exponential) ); Out.ar(0, 0.5 * IFFT(chain).dup); }).play(s,[\bufnum, a, \soundBufnum, b]); JMouseBase.makeGUI ; // for SwingOSC s.scope ; )

Due note per concludere. Il metodo dup(n), denito su Object, resituisce un array che contiene n copie delloggetto stesso. Il valore di default di n 2. Negli esempi precedenti, dup inviato a IFFT restituisce un array [IFFT, IFFT], che dunque richiede una espansione multicanale: essendo il valore predenito di n = 2, il segnale in uscita stereo. Inne, si sar notato come i valori di controllo per le UGen PV non siano espressi in Herz, ma alcune volte in forma normalizzata ( il caso di PV_BrickWall, altre in escursioni che dipendono dallimplementazione delle UGen stesse (si pensi a [128, 128] nel caso di PV_BinShift). In effetti, la situazione, per cui i valori dipendono fondamentalmente

8.6280

dallimplementazione,non chiarissima e deve tipicamente essere risolta in maniera empirica.

8.7 Modulazione

Si ha modulazione quando un aspetto di un oscillatore (ampiezza, frequenza, fase) varia continuamente in relazione a un altro segnale. Il segnale che viene trasformato si denisce portante (carrier), quello che controlla la trasformazione modulante (modulator). Un oscillatore denito da unampiezza e da una frequenza sse: nella modulazione si sostituisce uno dei due parametri con loutput di un altro oscillatore. Tremolo e vibrato costituiscono di fatto due esempi di modulazione, rispettivamente dellampiezza e della frequenza: due segnali di controllo (modulanti) che modicano periodicamente i segnali originali (portanti). Le implementazioni pi semplici delle modulazioni di ampiezza e frequenza sono perci analoghe a quelle del vibrato e del tremolo. Nei casi di tremolo e vibrato per la modulazione avviene al di sotto della soglia di udibilit (subaudio range), il segnale modulante ha cio una frequenza inferiore ai 20 Hz, e la modulazione viene percepita non come trasformazione timbrica, ma espressiva. Se invece la frequenza della modulante cade allinterno del campo uditivo, il risultato della modulazione un cambiamento spettrale che si manifesta percettivamente come cambiamento qualitativo del timbro. In generale, il risultato di una modulazione un nuovo segnale le cui caratteristiche dipendono dalla combinazione dei due parametri fondamentali: frequenza e ampiezza di portante e modulante. La modulazione una tecnica di larghissimo impiego perch estremamente econonomica in termini computazionali: contrariamente a quanto avviene

8.7281

5000

5000

Frequency (Hz)

0 0 Time (s)

Frequency (Hz) 15.343 0 0 Time (s)

13.7314

1 1 Time (s) 1.1

1 1 Time (s) 3

Fig. 8.12 Incremento della frequenza di vibrato (destra) e di tremolo (sinistra), sonogramma (alto) e forma donda (basso): dallespressivit al timbro. per la sintesi additiva, che richiede la denizione di moltissimi parametri di controllo, attraverso la modulazione, anche impiegando soltanto due oscillatori come portante e modulante, possibile creare spettri di grande complessit. Proseguendo nellanalogia con temolo e vibrato, la modulazione pu avvenire nellampiezza e nella frequenza.

8.7.1 Modulazione ad anello e dampiezza

8.7282

Quando il segnale modulante (M) controlla lampiezza della portante (C), si possono avere due tipi di modulazione dampiezza, che dipendono dalle caratteristiche della modulante. Si ricordi che un segnale bipolare si estende tra un massimo positivo e uno negativo, mentre un segnale unipolare invece compreso completamente tra valori positivi.
1 2 {SinOsc.ar}.scope ; {SinOsc.ar(mul: 0.5, add:0.5)}.scope ; // bipolar // unipolar

Un segnale audio tipicamente bipolare (in ampiezza normalizzata oscilla nellintervallo [1.0, 1.0]). Come si vede, per trasformare un segnale bipolare in uno unipolar sufciente moltiplicarlo e aggiungervi un offset (nellesempio: [1.0, 1.0] [0.5, 0.5] [0.0, 1.0]). Se il segnale modulante bipolare si ha una modulazione ad anello (Ring Modulation, RM), se unipolare si ha quella che viene denita per sineddoche modulazione dampiezza (Amplitude Modulation, AM). Supponendo che portante e modulate siano due sinusoidi di frequenza rispettivamente C e M , il segnale risultante da una modulazione ad anello ha uno spettro complesso formato da due frequenze, pari a C M e C + M , chiamate bande laterali (side-bands). Se C = 440Hz e M = 110Hz , il risultato della modulazione ad anello un segnale il cui spettro dotato di due componenti armoniche, 330 e 550Hz . Se invece la modulante unipolare, e si ha una modulazione dampiezza, lo spettro del segnale risultante conserva anche la componente frequenziale della portante, oltre a somma e differenza di portante e modulante. Mantenendo tutti i parametri identici, ma realizzando una AM attraverso limpiego di un segnale unipolare, si avranno perci C M , C , C + M : tre componenti pari a 330, 440, 550Hz . Nel caso in cui la differenza risulti di segno negativo si ha semplicemente uninversione di fase: il che equivale a dire che la frequenza in questione comunque si ribalta al positivo: ad esempio, un risultante di 200Hz sar presente come frequenza di 200Hz . Lesempio seguente mostra due possibili implementazioni,

8.7283

del tutto identiche nei risultati, ma utili come esempio di patching. Nelle prime implementazioni (3, 7) la modulante controlla largomento mul della portante. Nelle seconod, si ha moltiplicazione diretta dei segnali.Largomento mul denisce il valore per cui ogni campione deve essere moltiplicato: il moltiplicatore fornito dal valore del segnale modulante. La moltiplicazione di segnali fa la stessa cosa: ogni nuovo campione della portante viene moltiplicato per un nuovo campione della modulante.
1 2 3 4 6 7 8 // better to select log display for frequencies // RM { SinOsc.ar(440, mul: SinOsc.ar(110))}.freqscope ; { SinOsc.ar(440)*SinOsc.ar(110) }.freqscope ; // the same // AM { SinOsc.ar(440, mul: SinOsc.ar(110, mul: 0.5, add:0.5))}.freqscope ; { SinOsc.ar(440)*SinOsc.ar(110, mul:0.5, add:0.5) }.freqscope ;

8.7.2 Modulazione ad anello come tecnica di elaborazione

La sintesi per modulazione dampiezza (AM) nasce come tecnica per costruire spettri pi complessi a partire da sinusoidi. La modulazione ad anello ha invece unorigine e un lungo passato analogico 31 non tanto come tecnica di generazione ab nihilo ma come tecnica di elaborazione
31

Lo stesso nome deriva dalla congurazione ad anello dei diodi usata per approssimare la moltiplicazione nelle implementazioni in tecnologia analogica.

8.7284

di segnali complessi 32. In primo si pu pensare di modulare un segnale complesso (ad esempio di origine strumentale) con un segnale sinusoidale. Un segnale complesso avr uno spettro composto di un insieme di componenti portanti Cn :

C = C1 , C2 , C3 . . . , Cn
Data una sinusoide M , una modulazione ad anello CM risulta allora nella somma e differenza di tra ogni componente C e M . Se il segnale modulante unipolare, anche le componenti C saranno presenti nello spettro:

C1 M, (C1 ), C1 + M ; C2 M, (C2 ), C2 + M, . . . Cn M, (Cn ), Cn + M


Se M ridotto, allora Cn M e Cn + M saranno vicini a Cn : ad esempio, con uno spettro inarmonico Cn = 100, 140, 350, 470, etc e M = 10, si avr 90, 110, 130, 150, 340, 360, 460, 480, etc M, etc + M . Sostanzialmente linviluppo spettrale non cambia, ma raddopia di densit. Quando invece M elevato, allora si realizza una espansione spettrale. Nellesempio precedente, se M = 2000 allora lo spettro risultante sar 1900, 2100, 1860, 2140, 1650, 2350, 1730, 2470, etc M, etc + M . La situazione rappresentata in Figura 8.13 (tratta da Puckette cit.). Nellesempio seguente un le audio modulato da una sinusoide la cui frequenza incrementa esponenzialmente tra 0 e 10000Hz nellarco di 30 secondi. Si noti allinizio leffetto di raddopio intorno alle frequenze dello spettro e la progressiva espansione spettrale, che porta, nel nale, allemergenza della simmetria spettrale intorno alle frequenze della sinusoide.

32

Alcuni riferimenti e gure da Miller Puckette, Theory and Techniques of Electronic Music, http://www-crca.ucsd.edu/~msp/techniques.htm

8.7285

5.2. MULTIPLYING AUDIO SIGNALS

125

amplitude

(a)

frequency

(b)

(c)

Fig. 8.13 Result of ring modulation of a complex signal by aspetFigure 5.4: Inuenza di M sullinviluppo pure sinusoid: (a) original signals spectrum and spectral trale: thesegnale originale,(1/3 of theenvelope; (b) modulated by a addensamento(c) ed relatively low modulating frequency fundamental); modulated by a higher frequency, 10/3 of the fundamental. espansion (da Puckette cit.).
Multiplying by the signal of frequency gives partials at frequencies equal to:

1 2 4 5 6 7 8

1 + , 1 , . . . , k + , k . ( b = Buffer.read(s,"sounds/a11wlk01.wav"); // buffering the file As before if any frequency is negative we take its absolute value.

PlayBuf.ar(1, b.bufnum, BufRateScale.kr(b.bufnum), loop: 1) envelope and the component frequencies of the result transform by relatively

Figure 5.4 shows the result of multiplying a complex periodic signal (with several components tuned in the ratio 0:1:2: ) by a sinusoid. Both the spectral

simple rules. * The resulting spectrum is essentially the original spectrum combined with SinOsc.ar(XLine.kr(1, 10000, 30)) This combined spectrum is then shifted to its reection about the vertical axis. the right by the modulating frequency. Finally, if any components of the shifted }.freqscope spectrum are still left of the vertical axis, they are reected about it to make ) positive frequencies again. In part (b) of the gure, the modulating frequency (the frequency of the sinusoid) is below the fundamental frequency of the complex signal. In this case

Lo stesso approccio pu essere esteso ai casi in cui M complesso: quanto avveniva tipicamente nelluso analogico dellRM, in cui spesso due segnali strumentali venivano modulati tra loro 33. Nel caso allora in
33

Ad esempio, in alcuni importanti lavori di K.H.Stockhausen (dal vivo): Kontakte, Hymnen, Mikrophonie, Prozession

8.7286

cui C e M siano complessi, si ha addizione/sottrazione reciproca di tutte le componenti, cos che, se C e M hanno rispettivamente i e k componenti spettrali, C M avr i k componenti, un risultato nale evidentemente di grande complessit. Lesempio seguente utilizza lo stesso le audio predenito in SC. Il primo segnale modulante una copia del medesimo le, la cui velocit di lettura modicata attraverso un generatore di segnale esponenziale da 0.1 a 10.0 (si pu ascoltare valutando le righe 5-9). Come si vede, il segnale risultante dalla RM (righe 11-19) presenta un inviluppo spettrale molto complesso, in cui pure resta traccia percettiva dei due segnali di partenza.
1 2 3 5 6 7 8 9 11 12 13 14 15 16 17 18 19 ( b = Buffer.read(s,"sounds/a11wlk01.wav"); // buffering the file ) ( // the modulating signal { PlayBuf.ar(1, b.bufnum, XLine.kr(0.1, 10.0, 30, doneAction:0), loop: 1) }.play ; ) ( {

PlayBuf.ar(1, b.bufnum, BufRateScale.kr(b.bufnum), loop: 1) * PlayBuf.ar(1, b.bufnum, XLine.kr(0.1, 10.0, 30, doneAction:0), loop: 1) *5 }.freqscope )

Nel secondo esempio, lo stesso segnale dal le audio moltiplicato per un onda quadra la cui frequenza incrementa in 30sec da 1 a 10.000Hz .

8.7287

Si noti che nch la frequenza nel registro subaudio (f req < 20), il segnale modulante opera propriamente come un inviluppo dampiezza.
1 2 4 5 7 8 9 10 11 // modulating signal { Pulse.ar(XLine.kr(1, 10000, 30))}.play ; ( b = Buffer.read(s,"sounds/a11wlk01.wav"); // buffering the file PlayBuf.ar(1, b.bufnum, BufRateScale.kr(b.bufnum), loop: 1) * Pulse.ar(XLine.kr(1, 10000, 30)) }.freqscope ) {

Un altro esempio interessante di applicazione della RM si ha quando il segnale portante un segnale complesso armonico con frequenza fondamentale C , mentre la modulante una sinusoide con frequenza M = C M =C 2 . Lo spettro risultante armonico con frequenza 2 , e comprende soltanto armoniche dispari. In sostanza, si tratta dello spettro del segnale portante abbassato di unottava, secondo unelaborazione che prende il nome in ambito pop/rock di octaver. Ad esempio, se C1n = 100, 200, 300, . . . n e M = 50, allora il segnale modulato avr queste componenti: 100 50, 100 + 50, 200 50, 200 + 50, 300 50, 300 + 50 = 50, 150, 150, 250, 250, 350, 350. Si tratta delle armoniche dispari (raddoppiate) di C . I rapporti Tra frequenze risultanti 2 sono infatti 1, 3, 5, 7, . . .. Per recuperare le armoniche pari (altrimenti il segnale pi vuoto rispetto alloriginale), sufciente sommare il segnale originale.

8.7288

1 2 3 4 5 6 7 8 9 10 11 12 14

//RM octaver ( SynthDef.new("RmOctaver", { var in, freq ; in = SoundIn.ar(0) ; // audio from soundcard Pitch.kr(in)[0].poll ; // the retrieved freq Pitch.kr(in)[1].poll ; // is the in sig pitched? freq = Pitch.kr(in)[0] ; // the pitch freq Out.ar(0, SinOsc.ar(freq: freq*0.5)*in+in); // RM freq/2 + source }).send(s) ; ) Synth.new("RmOctaver") ;

La synthDef "RmOctaver" riceve in input il segnale dallingresso della scheda audio (il microfono, tipicamente), attraverso la Ugen SoundIn. Nella UGen, il primo argomento indica il bus richiesto: per convenienza lindice 0 rappresenta sempre il primo ingresso. La UGen Pitch invece una UGen di analisi, che estrae un segnale di controllo che rappresenta la frequenza fondamentale (pitch in inglese) del segnale analizzato. Pitch fornisce in uscita un array che comprende due segnali. Il primo costituito dai valori di frequenza rilevati, il secondo da un segnale che pu avere valore binario [0, 1], ad indicare rispettivamente assenza/presenza di frequenza fondamentale rilevabile. Le righe 6 e 7 chiamano sui due segnali che compongono larray in uscita da Pitch il metodo poll che stampa sullo schermo il valore del segnale. La frequenza freq 2 viene utilizzata per controllare un oscillatore 34 che moltiplica il segnale in, inne in viene sommato al segnale risultante. In modo analogo poi possibile utilizzare una modulante con M =
34

Si moltiplica per 0.5 per questioni di efcienza computazionale. Le divisioni costano al calcolatore molto di pi delle moltiplicazioni.

8.7289

n C . In questo caso si ha un incremento delle componenti superiori dello spettro del segnale portante. Se M = n C si ha un ribattimento delle armoniche. Si consideri: C = 100, 200, 300, n 100 e M = 200(n 2). Lo spettro risultante sar: 100 200 = 100, 100+ 200 = 300, 200 200 = 0, 200 + 200 = 400, . . ..
8.7.3 Modulazione di frequenza

Nella modulazione di frequenza (FM) la frequenza della portante che viene modicata dalla modulante. Si coinsiderino due oscillatori che producano due sinusoidi di frequenza C e M . Alla frequenza C delloscillatore portante viene sommato loutput delloscillatore modulante. In questo modo la frequenza C subisce una serie di variazioni che la fanno deviare verso lacuto (quando loutput di M positivo, C viene incrementata) e verso il grave (inversamente, quando loutput di M negativo, C decrementa). Con il termine frequenza di deviazione di picco (peak frequency deviation) o semplicemente deviazione (D ) si indica lescursione massima, misurata in Hz , subita dalla frequenza della portante, che dipende dallampiezza della modulante. La differenza fondamentale tra FM da una parte e RM e AM dallaltra sta nel fatto che con la FM non si producono solo 2 o 3 bande laterali, ma una serie teoricamente innita data da tutte le frequenze C nM . Se C = 220 e M = 110, si ha con n = 1: 330 e 110; con n = 2: 440 e 0; con n = 3: 550 e 110 (110 invertito di fase), e cos via. Valgono a proposito le considerazioni sugli spettri gi svolte per la AM. Con la FM si apre allora la possibilit di creare spettri anche molto complessi attraverso una tecnica di grande semplicit computazionale. Proprio per questo motivo la FM diventata la prima tecnica di sintesi digitale di grandissimo successo commerciale, grazie a una serie, fortunatissima negli

8.7290

anni80, di sintetizzatori della Yamaha (soprattutto il celebre DX7). La FM stata usata dallinizio 900 nelle telecomunicazioni (come noto, di uso comune la locuzione radio in modulazione di frequenza): sul nire degli anni 60, alluniversit di Stanford, John Chowning sperimenta inizialmente con rapidissimi vibrato, implementa quindi una versione digitale, e ne esamina a fondo le possibilit (1973).Turenas (1972) il primo brano ad usare estensivamente la FM, anche se il primo tentativo in assoluto Sabelithe del 1966, completato nel 1988. Chowning ottiene con due oscillatori risultati timbrici pari alluso di 50 oscillatori in sintesi additiva. Quindi, la Yamaha compra il brevetto (ancora adesso quello pi redditizio per Stanford...) e applica la tecnica sui sintetizzatori. Dunque, la FM si caratterizza per la ricchezza timbrica. In realt, delle innite bande teoricamente presenti, poche sono quelle signicative: per determinarne il numero approssimativo utile il cosiddetto indice di modulazione I . La relazione che lega deviazione, frequenza moduD lante e indice di modulazione la seguente: I = M . In questo senso, D rappresenta la profodit della modulazione. Il valore dato da I + 1 viene considerato un buon indicatore del numero approssimativo di bande laterali signicative. Lutilit della formula sta in questo: se si considerano D e M costanti (poich dipendono da modulante e portante), lindice fornisce una misura della ricchezza in bande laterali dello spettro. Dalla relazione precedente discende che lampiezza della modulante data dalla relazione: D = I M . Se I = 0, non c deviazione, poich lincremento fornito dalla modulante alla portante (la deviazione) nullo, e il segnale portante non viene modulato. Lincremento dellindice di modulazione indica un incremento della ricchezza dello spettro. Riassumendo: nella FM,la natura dello spettro generato (cio, la posizione delle bande laterali) determinata dalla relazione tra la portante e la modulante, mentre la ricchezza dello spettro (cio, il numero di bande laterali) proporzionale allampiezza del segnale modulante.

8.7291

60 60 Sound pressure level (dB/Hz) Sound pressure level (dB/Hz)

60 Sound pressure level (dB/Hz)

40

40

40

20

20

20

0 Frequency (Hz)

22050

0 Frequency (Hz)

22050

0 Frequency (Hz)

22050

Fig. 8.14

I = 1, 3, 7

Nellesempio seguente, il mouse controlla frequenza e ampiezza del segnale modulante M , mentre C , ch costante, pari a 1000Hz . Posizionando il mouse nellorgine (si ricordi che langolo in alto a sinistra), il contributo di M pari a 0 (lampiezza del segnale nulla): dunque, si ascolta soltanto la sinusoide C a 1000Hz . Spostando leggermente in basso il mouse (incrementando lampiezza della modulante) e mouvendosi verso destra si ascolta e si osserva nello spettro il contributo dato da M la cui frequenza incrementa da 0 a 1000. Come si nota aumenta la distanza tra le bande laterali in relazione alla formula C M . Spostando verso il basso il mouse, aumenta invece lenergia associata alle bande laterali (i picchi diventano pi prominenti, lo spetto pi ricco).
1 3 4 5 6 7 8 9 10 11 12 JMouseBase.makeGUI // required on SwingOSC ( { SinOsc.ar( 1000 // C freq + SinOsc.ar( // modulator freq: GUI.mouseX.kr(0, 1200), // M freq mul: GUI.mouseY.kr(0, 10000) // M amp ), mul: 0.3 )}.freqscope )

8.7292

8.7.4 C:M ratio

Oltre allampiezza della modulante, laltro fattore che va tenuto in considerazione nello studio delle caratteristiche spettrali del segnale risultante da una modulazione il rapporto tra le frequenze di portante e modulante. Tale fattore di particolare rilievo per la modulazione di frequenza o nella modulazione ad anello di segnali complessi: in entrambi casi, a differenza di quanto avviene nella AM o RM in cui C e M siano sinusoidali, il segnale in uscita risulta costituito da numerose componenti, e non soltanto da due (RM) o tre (AM). Il rapporto tra le frequenze dei due segnali viene usualmente denito come C:M ratio (di qui in poi: cmr ). Poich le frequenze componenti lo spettro del segnale comprendono somma e differenza di C e M , se cmr un intero lo spettro sar armonico, comprendendo due multipli del massimo comun divisore tra C e M . Ad esempio, se C = 5000Hz e M = 2500Hz (cmr = 2 : 1), si avranno (in AM) 2500, 5000 e 7500Hz : uno spettro armonico formato da fondamentale (2500) e dalle prime due armoniche (i multipli 5000 e 7500Hz ). Due casi interessanti in RM e in FM si hanno quando cmr = 1 : 2 e C : M ratio = 1 : 1. Nel primo caso sono presenti soltanto le armoniche dispari: con C = 1000 e M = 2000, si ha in RM ()1000, 3000, a cui si aggiungono in FM: ()3000, 5000, ()5000, 7000, ecc. Nel secondo caso, tutte le armoniche sono presenti nel segnale modulato: con C = M = 1000, si ha in RM 0, 2000, e in FM 0, ()1000, 3000, ()2000, 4000 ecc. Se la frazione ha denominatore 1 (come nellesempio precedente), allora le frequenze ottenute sono multipli della modulante, che diventa fondamentale del segnale generato. Se il denominatore maggiore di 1, il massimo comun divisore tra C e M a diventare la fondamentale del segnale risultante dalla modulazione: con C = 3000

8.7293

e M = 2000 (cmr = 3 : 2) la nuova fondamentale il massimo comun divisore, ovvero 1000Hz , lo spettro essendo composto da 1000 Hz (3000 2000), cui si aggungono (in AM) 3000Hz (C ), e 5000Hz (3000 + 2000). A differenza di questo caso, la fondamentale pu anche essere apparente. Ad esempio, se C = 5000 e M = 2000, (cmr = 5 : 2) la nuova fondamentale sempre 1000Hz (il MCD tra 5000 e 2000), ma lo spettro composto da 3000Hz (5000 2000), 5000 (in AM), e 7000 (5000 + 2000). La fondamentale 1000Hz apparente perch pu essere ricostruita dallorecchio, pur non essendo presente sicamente nel segnale, come la fondamentale di cui sono presenti le armoniche III, V e VII. La fondamentale viene ricostruita dallorecchio se cade nel campo di udibilit e se sono presenti un numero sufciente di armoniche. Questultima considerazione vale per la modulazione di frequenza, poich in RM e AM semplici (dove C e M sono segnali sinusoidali) le bande laterali sono rispettivamente sempre solo 2 e 3. Nellesempio precedente, sempre con C = 5000Hz ma con M = 2000, si avranno (in AM) frequenze risultanti pari a 3000, 5000, 7000Hz : terzo, quinto, settimo armonico di 1000Hz . La C:M ratio perci unindicatore dellaarmonicit dello spettro: pi semplice la frazione (cio minore il prodotto C M ), pi vicini sono gli armonici risultanti. Se la C:M ratio quasi intera (ad esempio, 2.001 : 1) si ottiene un suono che non solo viene ancora percepito come armonico, ma che risulta invece meno articiale proprio perch simula le inarmonicit presenti negli strumenti acustici. In generale, si ha che ratio pari a N : 1, 1 : N riproducono lo stesso spettro. Il numero dei parziali pu essere calcolato a partire dai componenti della ratio. Ad esempio, se la ratio 2 : 3, si ha allora |2 3 n| = 1, 2, 4, 5, 8, 11 . . .. Se C > 1, allora ci sono bande laterali inarmoniche (o fondamentale mancante). Ad esempio, se cmr = 2 : 5, lo spettro risultante sar 2, 3, 7, 8, 12, 13 . . .: come si vede, manca la fondamentale (la componente 1) e molte armoniche. In pi, lo spettro si allontana allacuto.Si consideri cmr = 5 : 7: si produce uno spettro

8.7294

distintamente inarmonico: 2, 5, 9, 12, 16, 19, 23, 26, . . .. La synthDef seguente permette di controllare una modulazione di frequenza utilizando, oltre la frequenza di base freq, ovvero C , i parametri derivati dalla cmr , ovvero c, m, a. I primi due indicano numeratore e denominatore della cmr , il terzo lampiezza della modulante.
1 2 3 4 5 6 7 8 9 10 11 12 13 ( SynthDef("cm", { arg f = 440, c = 1, m = 1, a = 100, amp = 0.3 ; Out.ar(0, SinOsc.ar( f // base freq + SinOsc.ar( // modulator freq: f * m / c, // M freq mul: a // M amp ), mul: amp) ) }).send(Server.local) ; )

Linterfaccia graca seguente permette di controllare i parametri f, c, m, a attraverso cursori e campi dinserimento numerico.

8.7295

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 19

( var w = GUI.window.new("C:M player", Rect(100, 100, 250, 420)).front ; //sliders var sl1 = GUI.slider.new(w, Rect(10, 10, 50, 350)) ; var sl2 = GUI.slider.new(w, Rect(60, 10, 50, 350)) ; var sl3 = GUI.slider.new(w, Rect(110, 10, 50, 350)) ; var sl4 = GUI.slider.new(w, Rect(160, 10, 50, 350)) ; // numberboxes var nb1 = GUI.numberBox.new(w, Rect(10, 360, 40, 20)) ; var nb2 = GUI.numberBox.new(w, Rect(60, 360, 40, 20)) ; var nb3 = GUI.numberBox.new(w, Rect(110, 360, 40, 20)) ; var nb4 = GUI.numberBox.new(w, Rect(160, 360, 40, 20)) ; // labels var l1 = GUI.staticText.new(w, Rect(10, 390, 40, 20)).string_("freq") ; var l2 = GUI.staticText.new(w, Rect(60, 390, 40, 20)).string_("c") ; var l3 = GUI.staticText.new(w, Rect(110, 390, 40, 20)).string_("m") ; var l4 = GUI.staticText.new(w, Rect(160, 390, 40, 20)).string_("a") ; /* to be continued */

Si tratta ora di creare un synth e di stabilire alcuni valori di riferimento


1 2 3 4 5 /* here */ var cmsynth = Synth("cm") ; var freq = 2000 ; // f: 0-2000 Hz var num = 30 ; // range for c:m /* still to be continued*/

Inne, la parte di codice che segue piuttosto lunga (come tipico con le interfacce grache), ma cio che fa semplicemente associare ad ogni elemento graco lazione di controllo sul synth (cmsynth.set) e aggiornare lelemento correlato (cambiando il valore del cursore, cambia anche

8.7296

quello del campo numerico, e viceversa). Il codice in un blocco unito (ed eseguibile direttamente) accessibile qui:

8.7297

1 2 3 4 5 6 7 9 10 11 12 13 15 16 17 18 19 20 22 23 24 25 26 28 29 30 31 32 33 35 36 37 38 39 41 42 43 44 45 46

/* here*/ // base freq sl1.action = { arg sl ; var val = sl.value*freq ; cmsynth.set("f", val) ; nb1.value = val ; } ; nb1.action = { arg nb ; var val = nb.value ; // 0-1000 Hz cmsynth.set("f", val) ; sl1.value = val/freq ; } ; // C sl2.action = { arg sl ; var val = (sl.value*(num-1)).asInteger+1 ; cmsynth.set("c", val) ; nb2.value = val ; } ; nb2.action = { arg nb ; var val = nb.value.asInteger ; cmsynth.set("c", val) ; sl2.value = val/num ; } ; // M sl3.action = { arg sl ; var val = (sl.value*(num-1)).asInteger+1 ; cmsynth.set("m", val) ; nb3.value = val ; } ; nb3.action = { arg nb ; var val = nb.value.asInteger ; cmsynth.set("m", val) ; sl3.value = val/num ; } ; // a sl4.action = { arg sl ; var val = sl.value*10000 ; cmsynth.set("a", val) ; nb4.value = val ; } ;

8.7298

In conclusione, le tecniche per modulazione hanno dalla loro parte la capacit di creare spettri di grande complessit unitamente ad unestrema economicit in termini computazionali: si pensi alla complessit spettrale ottenuta ad esempio in FM a partire da due sinusoidi. Daltra parte, non ovvia la relazione tra controllo e risultato. In pi, le tecniche per modulazione scontano una inapplicabilit analitica: estremamente difcile lestrazione funzionale dei parametri controllo dallanalisi di materiale sonoro preesistente.

8.7299

9 Suono organizzato: (minimal) scheduling in SC

9.1 Server side: attraverso le UGen

Il controllo dellinformazione e dei suoi processi, attraverso il linguaggio SC, e la sintesi del segnale, attraverso la gestione delle UGen nelle synthDef, sono due momenti fondamentali che devono essere integrati: mutuando unespressione di Edgar Varse, si tratta di arrivare al suono organizzato. La locuzione interessante perch unisce il lavoro sulla materia sonora alla sua organizzazione temporale: ne consegue una denizione in fondo molto generale di musica e composizione, come un insieme di eventi sonori. In termini molto generali, lo scheduling appunto lassegnazione di risorse per la realizzazione di un evento in un certo momento 35. Come usuale, in SC esistono potenzialmente modi diversi di realizzare lo scheduling. Una opzione, che potrebbe derivare
35

Ad esempio, http://en.wikipedia.org/wiki/Scheduling. Un termine italiano possibile programmazione, nel senso di gestione nel tempo delle risorse, ma in generale il termine italiano molto meno specico.

9.1300

da sintetizzatori analogi, quella di gestire il sequencing (la messa in sequenza) degli eventi attraverso segnali. Lesempio seguente, pur nella sua banalit, dimostra un aspetto che si gi discusso: un inviluppo applicato ad un segnale continuo lo pu trasformare in un insieme di eventi discreti.
1 2 3 4 5 6 7 8 9 10 11 13 ( SynthDef.new("schedEnv", { Out.ar(0, Pan2.ar( // usual panner InRange.ar(LFNoise2.ar(10), 0.35, 1).sign * Formant.ar(LFNoise0.kr(1, mul: 60, add: 30).midicps, mul:0.5), LFNoise1.kr(3, mul:1) ) // random panning ) }).send(s) ; ) Synth.new("schedEnv") ;

In questo caso, alla UGen Formant (si veda il le di help) genera un segnale che viene inviluppato dal segnale descritto dal codice di riga 4. Si gi visto in precedenza il funzionamento di InRange, che restituisce un segnale il cui valore dipende dallinclusione o meno del valore del campione in entrata nellintervallo specicato (qui [0.35, 1]). Se il valore incluso, InRange restituisce il valore stesso, altrimenti restituisce 0. Il metod sign restituisce 1 se il segnale positivo, 0 se pari a 0. Dunque, il segnale in uscita sar composto da 1 o 0 in funzione rispettivamente del superamento o meno della soglia di 0.35. Un simile segnale dinviluppo buca il segnale audio tutte le volte che vale 0, di fatto trasformando un segnale continuo in un insieme di eventi. Si valuti quanto segue per capire cosa succed in 10 secondi.

9.1301

{InRange.ar(LFNoise2.ar(10),

0.35, 1).sign}.plot(10)

Leffetto di generazione di eventi sottolineato nel caso di istanziazione multipla di pi synth 36. Lesempio, gi conosciuto, chiarisce un punto generale in relazione allo scheduling: il parallelismo (voci, strati, come li voglia chiarire) gestito da SC semplicemente istanziando pi synth, esattamente come quando si valuta il codice interattivamente.
1 2 3 4 5 6 7 8 9 10 11 13 14 15 16 17 ( SynthDef.new("schedEnv", { Out.ar(0, Pan2.ar( // usual panner InRange.ar(LFNoise2.ar(10), 0.35, 1).sign * Formant.ar(LFNoise0.kr(1, mul: 60, add: 30).midicps, mul:0.5), LFNoise1.kr(3, mul:1) ) // random panning ) }).send(s) ; ) ( 100.do({ Synth.new("schedEnv") ; }) )

Si gi discussa la UGen Select.kr(which, array), che implementa a livello server una funzione tipica del sequencing: a tasso di controllo, ogni volta che calcola un nuovo valore selezione un elemento which dellarray array. Vale la pena di riprendere la discussione. Il codice seguente tratto dal le di help.
36

Il clicking, che produce una solta di polvere di sfondo rispetto agli altri oggetti sonori quasi vocali, dipende da unapplicazione dellinviluppo che non si cura delle transizioni. Nel singolo synth non era particolarmente evidente.

9.1302

1 2 3 4 5 6 7 8 9 10 11 12 13 14

( { var a, cycle; a = Array.fill(32, { rrand(30,80) }).postln.midicps; cycle = a.size * 0.5; Saw.ar( Select.kr( LFSaw.kr(1.0, 0.0, cycle, cycle), a ), 0.2 ); }.play; )

Larray a contiene 32 frequenze, a caso tra le altezze 30 e 80 in notazione MIDI (riga 4). La riga 7 specica che il segnale in uscita sar un onda a dente di sega, la cui ampiezza sar pari a 0.2 (riga 12). La frequenza gestita invece da Select. Allargomento array passato a,cio le altezze prescelte proverrano dallarray precedentemente creato. La parte pi interessante la riga 9, che determina quale sar lindice dellelemento di a da prelevare. La riga 6 ha assegnato a cycle valore pari a met della dimensione di a, ovvero 16. La UGen LFSaw genera unonda a dente di sega, con frequenza 1 nellesempio: in sostanza, una volta al secondo la forma donda incrementa linearmente tra 1.0 e 1.0. Senonch largomenti mul e add valgono cycle. Dunque, il segnale una retta che incrementa tra 0 e 31, e il valore del campione (convertito internamente in intero) utilizzato da Select come indice dellelemento da selezionare in a 37. Lutilizzo di segnali simili (ramp, a rampa) tipico nei synth

37

Unaltra UGen che permette di generare eventi discreti in maniera analoga Stepper.

9.2303

analogici 38, in cui spesso lo scheduling gestito attraverso segnali continui opportunamente generati.

9.2 Server side: le UGen Demand

Un approccio allo scheduling molto peculiare implementato nelle UGen di tipo Demand, o demand rate, secondo una locuzione come si vedr signicativa. La UGen Demand.ar(trig, reset, [..ugens..]) opera in relazione ad un trigger (trig). Ogni qualvolta un segnale di trigger ricevuto 39, la UGen richiede un valore ad ognuna delle altre UGen incluse nellarray [..ugens..]. Queste UGen devono essere di tipo speciale, ovvero di tipo Demand: sono infatti delle UGen che generano un valore (ed uno solo) a richiesta. Nellesempio seguente, il segnale di trigger per Demand generato dalla UGen Impulse. Ogni impulso prevede una transizione tra minimo (0) e massimo (qui 1), e dunque un trigger. Inutile discutere della sintesi (riga 7), salvo notare che la frequenza di Pulse gestita da freq. La riga 6 assegna a freq attraverso una UGen Demand. Ogni qualvolta un trigger ricevuto, Demand chiede il prossimo valore nellarray demand a. Questultimo riempito da Dseq (riga 4), che produce una sequenza di valori quale quella prevista nellarray fornito come primo argomento ([1,3,2,7,8]), ripetuta in questo caso per 3 volte. Ascoltando il risultato, ci si accorge come la sequenza a sia appunto costituita dalla ripetizione per 3 volte di un segmento: quando il segmento concluso, Demand restituisce lultimo valore della sequenza.

38

39

Limplementazione pi usuale prende il nome di phasor: si veda la UGen omonima in SC. Si ricordi che si ha un evento di triggering ogni qualvolta si verica una transizione da 0 ad valore positivo.

9.2304

1 2 3 4 5 6 7 9 10

( { var a, freq, trig; a = Dseq([1, 3, 2, 7, 8]+60, 3); trig = Impulse.kr(4); freq = Demand.kr(trig, 0, a.midicps); LPF.ar(Mix.fill(10, { Pulse.ar(freq+5.rand)}), 1500) * 0.1 }.play; )

Come si vede, in sostanza le UGen di tipo Demand sono generatori di valori a richiesta, e si differenziano tra loro per i pattern che possono produrre. Ad esempio, come si visto Dseq genera sequenze di valori costruite iterando n volte un array. Invece, una UGen come Drand riceve due argomenti: il primo un array di valori, il secondo un un numero che rappresenta il numero di valori pescati a caso nellarray fornito. Nellesempio, larray lo stesso del caso precedente, mentre freq una sequenza di durata inf di valori prelevati pseudo-causalmente sullo stesso array a 40.
1 2 3 4 5 6 7 9 10 ( { var a, freq, trig; a = Drand([1, 3, 2, 7, 8]+60, inf); trig = Impulse.kr(10); freq = Demand.kr(trig, 0, a.midicps); LPF.ar(Mix.fill(10, { Pulse.ar(freq+5.rand)}), 1500) * 0.1 }.play; )

40

Inoltre la frequenza di triggering stata incrementata da 4 a 10.

9.2305

La potenza espressiva delle UGen di tipo Demand sta nella possibilit di innesto ricorsivo. Lesempio seguente del tutto analogo al primo caso qui discusso. senonch uno degli elementi dellarray su cui opera Dseq Drand.
1 2 3 4 5 6 7 8 9 10 11 12 ( { var freq, trig, reset, seq; trig = Impulse.kr(10); seq = Dseq( [42, 45, 49, 50, Dxrand([78, 81, 85, 86], LFNoise0.kr(4).unipolar*4) ], inf).midicps; freq = Demand.kr(trig, 0, seq); RLPF.ar(Pulse.ar(freq + [0,0.7])* 0.1, 1500, 0.01); }.play; )

Ci che avviene che seq una sequenza che ripete innite volte (inf) un pattern costituito dai numeri 42, 45, 49, 50, e da un quinto elemento denito da Dxrand: analogamente a Drand, questultima UGen pesca a caso nellarray fornito ([78, 81, 85, 86]), ma la sequenza in uscita non prevede ripetizioni dello stesso elemento. La dimensione della sequenza in uscita controllata da LFNoise0: in sostanza, varia pseudo-casualmente nellescursione [0, 4]. Nel primo caso, il contributo di Dxrand nullo, nel secondo consiste in tutti e quattro i valori, in ordine pseudo-casuale.

9.2306

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 17 18 19 20 21 22

( SynthDef("randMelody", { arg base = 40, trigFreq = 10; var freq, trig, reset, seq; var structure = base+[0, 2, 3, 5, 6, 8] ; trig = Impulse.kr(trigFreq); seq = Dseq( structure.add( Dxrand(structure+12, LFNoise0.kr(6).unipolar*6)) , inf).midicps; freq = Demand.kr(trig, 0, seq); Out.ar(0, LPF.ar( Mix.fill(5, {Saw.ar(freq +0.1.rand + [0,0.7])* 0.1}), 1500)); }).send(s); ) ( 15.do({ arg i ; Synth.new("randMelody", [\base, 20+(i*[3, 5, 7].choose), \trigFreq, 7+(i/10) ]) }) )

Nellesempio precedente una synthDef costruita in maniera analoga al caso precedente prevede come argomenti base e trigFreq: il primo rapprsenta la frequenza di base in notazione MIDI, il secondo la frequenza di triggering. Il ciclo successivo sovrappone dieci voci: in ognuna la frequenza di base incrementata di i per un valore a scelta tra [3, 5, 7]. In pi la frequenz di triggering incrementa di una quantit pari a i/10. Questultimo aspetto permette di realizzare un progresivo dephasing: i tempi degli strati sono cio lievemente differenti, e il sincronismo iniziale si disperde progressivamente. Lidea alla base delle UGen che si basano sul meccanismo Demand

9.2307

quella di fornire la possibilit di annidare allinterno delle synthDef aspetti tipicamente di pi alto livello. In pi, la possibilit dellincassamento di una UGen nellaltra estremamente potente. Ma in realt forse concettualmente pi lineare separare due aspetti, che lavorano tipicamente a tassi di aggironamento diverso: la sintesi (ad audio rate) e lo scheduling (a event rate). Non a caso, le UGen Demand sono strettamente relate con i cosidetti Pattern, strutture per il controllo compositivi di alto livello sul lato del linguaggio. In qualche modo, le UGen Demand costituiscono una sorta di versione sul lato server dei Pattern. Al di l dei Pattern, su lato del linguaggio lo scheduling si basa fondamentalmente sulle Routine.

9.3 Lang side: Routines

In effetti, il modo pi usuale (e forse pi sensato) per effettuare lo scheduling degli eventi consiste nel gestirlo dal lato del linguaggio. La struttura di controllo fondamentale in proposito in SC la routine. Di per se, le routine sono strutture di controllo che estendono il funzionamento delle funzioni, e il loro uso non limitato allo scheduling. In realt, lo scheduling soltanto una delle applicazioni possibili delle routine, anche se la pi tipica. Lesempio seguente mostra una routine minimale. Come si vede, la routine riceve come argomento una funzione. La funzione contiene un ciclo che per 10 volte stampa rallentando, quindi assegna a time valore apri a i*0.1. Lunico messaggio non conosciuto wait, ricevuto da i, che un numero a virgola mobile. Intuibilmente, wait concerne lo la gestione del tempo, cio lo scheduling. La routine r denisce cio una programmazione che deve essere eseguita, in cui il messaggio wait ricevuto da un numero indica un tempo di attesa (pari al ricevente del messaggio) prima di proseguire nellesecuzione della

9.3308

espressione successiva. La gestione del compito nel tempo afdata ad un orologio, nella circostanza SystemClock, cio ad una classe specializzata nel tenere il tempo alla massima precisione possible. Alorologio chiesto di eseguire (play) una routine (r). Lorologio interpreta gli oggetti che ricevono il messaggio waitco,q quantita temporali in cui sospendere lesecuzione della sequenza di esperessioni. Quando questa riprende, le espressioni successivi sono valutate il pi in fretta possibile. Nella circostanza, la routine r attende un tempo time che cresce progressivamente. Realizzato il ciclo, r prescivere di attendere 1 secondo e di scrivere , poi un altro secondo e di scrivere n.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 18 ( // Minimal routine r = Routine.new( { var time ; 10.do ({ arg i ; "rallentando".postln ; time = (i*0.1).postln ; time.wait ; }) ; 1.wait ; "fi".postln ; 1.wait ; "ne".postln ; } ) ; ) SystemClock.play(r)

Si noti che la routine ricorda il suo stato interno: se si valuta nuovamente SystemClock.play(r) si ottiene di ritorno loggetto SystemClock, poich la routine ormai terminata.

9.3309

1 3

SystemClock.play(r) SystemClock

Per riportare lo stato interno della routine alla condizione iniziale necessario inviarle il messaggio reset.
1 3 r.reset a Routine

A questo punto possible rieseguire da capo la routine. Una sintassi alternativa fornita dal metodo play denito direttamente per le routine, che riceve come argomento un orologio.
1 2 4 5 6 // the same: r.play(SystemClock) rallentando 0 [etc]

chiaro come una routine costituisca la base per la gestione di processi temporali. Semplicando, quando si vogliono eseguire espressioni secondo una certa progressione temporale sufciente avviluparle in una routine ed inframezzare opportunamente espressioni del tipo n.wait.

9.4 Orologi

9.4310

Il prossimo esempio discute la generazione di una GUI che funziona da semplice cronometro, e permette di introdurre alcuni elementi in pi nella discussione sullo scheduling. Una volta eseguito il codice, si apre una piccola nestra che visualizza lavanzamento del tempo a partire da 0: il cronometro parte e viene interrotto nel momento in cui la nestra viene chiusa.
1 2 3 4 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ( var w, x = 10, y = 120, title ="Tempus fugit" ; // GUI var var clockField ; var r, startTime = thisThread.seconds ; // scheduling w = GUI.window.new(title, Rect(x, y, 200, 60)) ; clockField = GUI.staticText.new(w, Rect(5,5, 190, 30)) .align_(\center) .stringColor_(Color(1.0, 0.0, 0.0)) .background_(Color(0,0,0)) .font_(GUI.font.new("Century Gothic", 24)); r = Routine.new({ loop({ clockField.string_((thisThread.seconds-startTime) .asInteger.asTimeString) ; 1.wait }) // a clock refreshing once a second }).play(AppClock) ; w.front ; w.onClose_({ r.stop }) ; )

Le prime righe dichiarano le variabili. Come si vede, fondamentalmente il codice prevede due tipi di elementi: elementi GUI (righe 2, 3) e elementi che si occupano di gestire laggiornamento dei valori temporali. In particolare, la riga 4 assegna a startTime il valore di thisThread .seconds: thisThread un oggetto particolare, una pseudo-variabile che contiene una istanza della classe Thread, che tiene il conto di di quanto

9.4311

tempo passato dallinizio della sessione dellinterprete. Se si valuta un paio di volte la riga seguente si noter come il valore restituito dal metodo seconds incrementi di conseguenza (e corrisponda ai secondi passati da quando si avviata lapplicazione).
1 thisThread.seconds

Dunque, startTime contiene un valore che rappresenta il tempo in cui il codice viene valutato (il momento 0 del cronometro). Inutile dilungarsi analiticamente sulla costruzione dellinterfaccia graca che non ha nulla di particolare. Si noti soltanto lo stile tipico di programmazione nelle GUI che prevede il concatenamento dei messaggi (righe 8-11) per impostare le diverse propriet grache. Le righe 12-17 deniscono invece la routine che si occupa ogni secondo di calcolare il nuovo valore del cronometro e di aggiornare il campo dellelemento GUI. La funzione contenuta nella routine contiene un ciclo innito (si ricordi che loop un sinonimo di inf.do) che esegue due espressioni: la prima aggiorna il campo dellora impostando la propriet string di clockField (righe 13-14), la seconda richiede di attendere un secondo prima di ripetere il ciclo (1.wait, riga 16). Il nuovo valore del tempo da visualizzare vience calcolato in tre passi. In primo luogo viene chiesto allinterprete il valore attuale del tempo passato attraverso una chiamata di thisThread.seconds: ad esso viene sottratto il tempo di partenza startTime per ottenere la differenza. Quindi il risultato viene convertito in numeri interi (qui non interessano i valori decimali): dunque nel numero intero di secondi pasasati da quando il cronometro esistito. Inne, il metodo, denito per la classe SimpleNumber, asTimeString restituisce una stringa nella forma ore:minuti:secondi. Ad esempio:

9.4312

1 3

20345.asTimeString 5:39:05

Il metodo play riceve come argomento un oggetto di tipo Clock, ma non SystemClock: infatti, questultimo non pu essere utilizzato nel caso di GUI. AL suo posto deve essere utilizzato AppClock. La soluzione obbligata perch dipende dalla gestione delle priorit: nel caso di scheduling, i messaggi al server audio hanno la priorit su altre funzioanlit, GUI inclusa. Questo non permetterebbe di gestire da una stessa routine il controllo di un synth e di un elemento GUI: una simile operazione possibile utilizzando allinterno della routine il metodo defer implementato dalle funzioni. In sostanza, nella routine le espressioni che concernono la GUI vengono raccolte allinterno di una funzione (racchiuse tra parentesi graffe) a cui viene inviato il messaggio defer che permette di differire il risultato della loro valutazione nel momento in cui vi siano risorse computazionali disponibili (ovvero: senza sottrarne alla computazione audio). Inne, la riga 19 denisce una propriet della nestra w: onClose prevede come valore una funzione che viene eseguita nel momento in cui w chiusa. La funzione contiene r.stop: la routine r viene ci arrestata alla chiusura della nestra. Il codice seguente una variante del precedente che dimostra le ultime considerazioni. Laggiornamento della GUI racchiuso in una funzione a cui inviato defer (riga 14-17): dunque, possibile utilizzare SystemClock (riga 18). stata poi rimossa lassegnazione della funzione onClose e un messaggio postln permette di stampare laggiornamento del tempo sulla post window. Se ora si chiude la nestra, la routine continua ad essere operante (allinnito), come si vede dalla post window. Inoltre, la routine r chiede di impostare la stringa che contiene lora nel campo graco: poich questo non esiste pi (la GUI stata chiusa), la post window segnala un errore.

9.4313

1 2 3 4 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

( var w, x = 10, y = 120, title ="Tempus fugit" ; // GUI var var clockField ; var r, startTime = thisThread.seconds ; // scheduling w = GUI.window.new(title, Rect(x, y, 200, 60)) ; clockField = GUI.staticText.new(w, Rect(5,5, 190, 30)) .align_(\center) .stringColor_(Color(1.0, 0.0, 0.0)) .background_(Color(0,0,0)) .font_(GUI.font.new("Century Gothic", 24)); r = Routine.new({ loop({ { clockField.string_((thisThread.seconds-startTime) .asInteger.asTimeString.postln) ; }.defer ; 1.wait }) // a clock refreshing once a second }).play(SystemClock) ; w.front ; )

9.5 Sintetizzatori/eventi

Lapproccio precedente permette a tutta evidenza di essere esteso allaudio. Se si eccettua il problema della priorit, non c infatti niente di peculiare alle interfacce grache nella gestione dello scheduling discussa precedente. La synthDef seguente si basa su una sequenza di impulsi di densit density (riga 7), che vengono ltrati da un ltro risonante passabasso RLPF (righe 6-9), riverberate attraverso la UGen FreeVerb (righe 5-10), e inne distributie sul fonte streo attraverso Pan2 (righe 4-12).

9.5314

Prevede come argomenti la densit degli impulsi (density), la frequenza di taglio del ltro (ffreq), il reciproco di Q (rq) e um moltiplicatore dampiezza (amp).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ( SynthDef("filtImp", { arg density = 30, ffreq = 200, rq 1; Out.ar(0, Pan2.ar( FreeVerb.ar( RLPF.ar( Impulse.ar(density), ffreq, rq ), room:1 )*amp*5, LFNoise1.ar(2)) ) }).send(s) )

= 0.1, amp =

Una volta costruito il synth (synth), lo scheduling gestito attraverso una routine innita (loop), che ad ogni iterazione imposta (synth.set) i valori degli argomenti del synth. La routine sfrutta abbondantemente valori pseudo-casuali: ad esempio, la densit degli impulsi che mettono in risonanza il ltro varia nellescursion [5, 20]. Pi interessante il calcolo di ffreq, che seleziona un numero casuale in [70, 80] e lo moltiplica per un fattore pari a 0.75, quindi converte da valori midi a frequenza. Ogni valore midi prescelto sar perci un multiplidi 0.75 semitoni, ovvero lungo una scala di 3 di tono. Anche il tempo in secondi che inter8 corre tra una iterazione e la successiva generato pseudo-casualmente nellintervallo [0.1, 1.0].

9.5315

1 2 4 5 6 7 8 9 10 11 12 13 14 15 16

( var synth = Synth(\filtImp) ; Routine.new({ loop ({ synth.set( \density, rrand(5, 20), \ffreq, (rrand(70, 80)*0.75).postln.midicps, \rq, rrand(0.001, 0.005), \amp, 0.5.rand + 0.5 ) ; rrand(0.1, 1).wait ; }) ; } ).play(SystemClock) )

Nellesempio precedente lidea fondamentale quella di costruire un syntt (uno strumento) e controllarlo attraverso una routine. La synthDef seguente permette di introdurre un secondo approccio. Essa prevede una semplice sinusoide a cui aggiunto un vibrato ed un inviluppo dampiezza. I parametri di entrambi sono controllabili dallesterno: a, b, c rappresentano punti dellinviluppo, vibrato e vibratoFreq i due parametri del vibrato.

9.5316

1 2 3 4 6 7 8 9 10 11 12 13 14 16 17

( SynthDef("sineMe1",{ arg out = 0, freq = 440, dur = 1.0, mul = 0.5, pan = 0, a, b, c, vibrato, vibratoFreq; var env; env = Env.new([0, a, b, c, 0],[dur*0.05, dur*0.3,dur*0.15,dur*0.5], 'welch'); Out.ar(out, Pan2.ar( SinOsc.ar( freq: freq+SinOsc.kr(mul:vibrato, freq: vibratoFreq), mul:mul ) * EnvGen.kr(env, doneAction:2) ), pan) }).send(s); )

Linviluppo dampiezza utilizzato da una UGen EnvGen, il cui argomento doneAction riceve un valore 2. Ci signica che, una volta concluso linviluppo, il synth viene deallocato sul server. Questo implica che il synth non esiste pi: esso si comporta perci non tanto come uno strumento ma come un evento. Si osservi cosa avviene nello routine:

9.5317

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 31 32

( var r = Routine.new({ inf.do({ arg i ; var env, dur = 0.5, freq, end, mul, pan, vibrato, vibratoFreq ; a = 1.0.rand ; b = 0.7.rand ; c = 0.5.rand ; pan = 2.0.rand-1 ; freq = ([0, 2, 3, 5, 6, 8, 10, 12, 13, 15, 16, 18].choose+70).midicps; // 13 pitches on a non-8ving modal fragment dur = rrand(0.015, 0.5); mul = rrand(0.05, 0.8); vibrato = (dur-0.015)*100; vibratoFreq = dur*10; Synth.new("sineMe1", [ "vibrato", vibrato, "vibratoFreq", vibratoFreq, "a", a, "b", b, "c", c, "freq", freq, "dur", dur, "mul", mul, "env", env] ) ; end = 0.15.rand; (dur+end).wait; }); }); SystemClock.play(r); )

Ad ogni iterazione del ciclo viene istanziato un synth che si dealloca nel momento in cui linviluppo concluso (dunque non neppure necessario chiamare il metodo free). In questo secondo esempio, loggetto

9.5318

synth non trattato come uno strumento, cio come un dispostivo persistente (una tromba, un basso, un fagotto) il cui comportamento va controllato in funzione del presentarsi di nuovi eventi. Piuttosto, qui il synth diventa un evento sonoro, lequivalente di una nota musicale. Nuovamente, la synthDef fa largo uso di valori pseudo-casuali, che offrono alla sinusoide una qualit tipicamente schiata. Lunico aspetto di rilievo il controllo della frequenza (riga 9). Larray denisce una sequenza di altezze che descrivono un modo non ottavizante di 13 altezze, ne seleziona una stocasticamente, e vi aggiunge 70.

9.6 Routine/Task

Le routine possono essere riportate nella condizione iniziale attraverso il messaggio reset e interrotte attraverso il messaggio stop.
1 2 3 4 5 7 8 10 11 13 14 15 16 17 ( r = Routine({ inf.do({ arg i ; i.post; ": going on".postln; 2.wait}) }).play ) // wait a bit r.reset // had enough r.stop // start again // first reset r.reset //then r.play

9.6319

Tuttavia, nel momento in cui ricevono il messaggio stop, per poter essere di nuovo attivate devono prima ricevere il messaggio reset che le riporta nella condizione di partenza. Questo aspetto costituisce una limitazione potenzialmente importante allo uso musicale. La classe Task implementa questo comportamento: un processo che pu essere messo in pausa (pauseable process).
1 2 3 4 5 7 8 10 11 13 14 16 17 19 20 22 23 ( t = Task({ inf.do({ arg i ; i.post; ": going on".postln; 2.wait}) }) ; ) // start t.play // pause: internal state is stored t.pause // start from last state t.resume // reset to 0 t.reset // stop: the same as pause t.stop // the same as resume t.play

Come si vede, i metodi stop/pause, e play/resume si comportano esattamente nello stesso modo. Lesempio seguente mette in luce le differenze tra task e routine. La synthDef "bink" utilizza un ltro risonante Resonz, sollecitato da un rumore bianco. Il tutto inviluppato da un inviluppo percussivo e distribuito sul fronte stereo attraverso Pan2.

9.6320

1 2 3 4 5 6 7 8 9 10 12 13 14 15 16 17 18 19 20 21 22 23 25 27 29 31 32 33 34 35 36 37 38 39 40 42 44

( SynthDef("bink", { arg freq, pan = 1; Out.ar(0, Pan2.ar( EnvGen.kr(Env.perc, doneAction:2)* Resonz.ar(WhiteNoise.ar, freq, 0.001)), LFNoise1.kr(pan) ) }).send(s) ; ) ( var pitch ; var arr = Array.fill(8, { arg i ; Task({ inf.do({ arg k ; pitch = (i*[3, 5, 7].choose+(k*0.5)+40).clip2(200) ; Synth(\bink, [\freq, pitch.midicps, \pan, pitch/50 ]) ; ((9-i)/8).wait ; }) }) }) ; var isPlaying ; arr.do({ |t| t.play}) ; // play all // first time evaluate until up here 8.do({ arg ind ; isPlaying = true ; Routine({ inf.do({ if (isPlaying) { arr[ind].pause; isPlaying = false } { arr[ind].play; isPlaying = true } ; (Array.series(6, 0, 1/6).choose).wait }) }).play }) )

9.6321

Alla variabile arr assegnato un array che contien 8 task. Ognuno di essi permette di generare uno voce in cui le altezze dipendono dal contatore (in sostanza le voci tendono a differenziarsi per registro). Il task sceglie un valore tra [3, 5, 7], lo moliplica per lindice (il registro della voce, per cos dire) e incrementa progressivamente di un quarto di tono laltezza. Il punto di partenza la nota 40, e la progressione viene bloccata al valore 200. Ogni strato ha un suo tasso di sviluppo autonomo: il pi grave si muove pi lentamente, il pi acuto pi velocemente, nellintervallo di [0.125, 1] secondi. Si noti che si tratta di una quantizzazione ai trentaduesimi con tempo pari a 60. La progressione procede inesorabilmente se si valuta il codice no alla riga 27. Fino a qui lutilizzo dei task in fondo accessorio: sarebbe stato perfettamente possibile usare routine. La riga 31 introduce un nuovo processo di scheduling. Il ciclo seleziona progressivamente gli indici dei task contenuti in arr. Quindi esegue una routine che modica lo stato del task selezionato valutando la variabile isPlaying (la quale reinizializzata per ogni task, riga 32). In sostanza, la routine verica se il task in esecuzione: se cos , lo arresta (e aggiorna isPlaying), se fermo, lo fa ripartire (e aggiorna isPlaying). Inne attende per una durata scelta a caso tra le frazioni multiple di un sesto di secondo (appositamente: in modo da essere asimmetrico rispetto alla suddivisione binaria precedente).

9.7 Micro/macro

Lutilizzo delle routine e dei task permette di chiarire meglio lutilit della classi di tipo Demand. In effetti, rispetto allo scheduling, esse non sono alternative alle classi appena discusse. Piuttosto permettono di gestire

9.7322

quello che potrebbe essere denito come micro-scheduling: uno scheduling interno al synth. Lopzione particolarmente interessante se un simile micro-scheduling viene incassato in uno scheduling, si potrebbe dire macro-, gestito da una routine o da un task. La synthDef seguente una variazione di un esempio gi discusso. Qui density gestisce la frequenza di un segnale impulsivo che fa da trigger per una Demand (righe 9-10). Queste ultime ciclano su dFreq a cui assegnata una UGen Drand, la quale, a sua volta, genera una sequenza innita di elementi selezionati casualmente dallarray fornito. Questultimo utilizza un insieme di intervalli a quarti di tono che vengono trasposti in funzione dellargomento midinote. Gli stessi valori, convertiti in frequenze, controllano sia la frequenza del generatore di impulsi (riga 9) che la frequenza di taglio del ltro (riga 10). In assenza della riga 13, un synth di tipo filtImp2 genera una sequenza continua: ma la presenza di EnvGen e dei sui argomenti Env.perc e doneAction:2, in seguito allapplicazione di un inviluppo percussivo, libera il synth. Ne consegue un oggetto sonoro composito, che prevede una variabilit interna, in questo caso una variazione del prolo melodico, ma che resta un singolo oggetto.

9.7323

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

( SynthDef("filtImp2", { arg density = 5, midinote = 90, rq = 0.1, amp = 1; var trig = Impulse.kr(density) ; var dFreq = Drand([0, 2, 3.5, 5, 6, 6.5, 11.5, 12]+midinote, inf) ; Out.ar(0, Pan2.ar( FreeVerb.ar( RLPF.ar( Impulse.ar(Demand.kr(trig, 0, dFreq.midicps)), Demand.kr(trig, 0, dFreq.midicps), rq ), room:1 ) * EnvGen.kr(Env.perc, doneAction:2) * amp, LFNoise1.ar(density*0.25)) ) }).send(s) )

Lesempio seguente utilizza una routine per gestire lo scheduling di oggetti filtImp2. Lesempio anche interessante perch utilizza largomento contatore i della funzione associata alla routine per denire una trasformazione progressiva, attraverso le variabili correlate j e k. Inoltre, realizza un parameter mapping, una associazione tra i diversi parametri: in sostanza, per ogni nuovo synth, i valroi degli argomenti dipendono sempre da i, j, k.

9.7324

1 2 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

( var j, k ; Routine.new({ inf.do({ arg i; k = i.clip2(40) ; j = i.clip2(60) ; [i, k, j].postln ; Synth(\filtImp2, [ \density, rrand(5, 15)+(k/4), \midinote, rrand(40, 50)+j, \rq, rrand(0.001, 0.005)-(k/20000), \amp, 0.1.rand + (((60-j)/60)*0.5) ] ) ; (rrand(0.1, 0.3+(60-j*0.05))).wait ; }) ; } ).play(SystemClock) )

9.7325

Você também pode gostar