+ All Categories
Home > Documents > DOMNode Magnitude SCNSObjectAbstract Inspector … · Document GetStringDialog WiiNunchukGUI...

DOMNode Magnitude SCNSObjectAbstract Inspector … · Document GetStringDialog WiiNunchukGUI...

Date post: 18-Feb-2019
Category:
Upload: vudang
View: 218 times
Download: 0 times
Share this document with a friend
328
Obj Player88 PowerO GraphParser Model EZSlider En Grap NB SCWindow Collapse Quar SOS SimpleContro Date Exception KeyCodeRe PatternCondu GetFileDialog FreqScope SoundFileViewProgressWindow SoundFileFormats TChoos Insets EventStreamClea TDuty_old UpdateListener Instr OSCResponderQueue Abstr PitchCollection ControlName UnicodeResponder FlowVar Runner GraphView MIDIRespo JKeyState Sc SerialPort MIDIEndPoint GeneralHIDSlot SwingDialog LRUNumberAllocator SoundIn AbstractPlayControl StackNumberAllocator DOMNode KeyMapper Toolbox NodeMapSetting Harmonics PathName PhilologusGuiTable BroadcastServer Actant FreqShift Philologus Gradient SelectXFocus GuidoArt WiiMoteGUI JSoundFi Condition AutoClassHelp Rect UGenInstr Color UniqueID GuidoScor HIDInfo Histor Messa Quarks Helper TopicHe JPen Env SCIBDrag MIDIIn JPeakMeterMa HIDDevice DOMImplementation Vol Staff D MelFilter SkipJack SCIBTo LilyPond Spee SCIBAreaSelection EZNumber Co RingNumberAllocato PMOsc Praat SCNSObjectAb ClassBrowser ResponderArray Quark AudioIn Graph SimpleKD MultiTap BusSynthDefs JMouseBase PhilologusGuiScore Def HistoryGui ContiguousBlock ModalFreqUGe GeoAudio MyClass InBus SystemSynthDefs O AutoCompMetho TaskProxyE Keyboarder Link FlowLayout CmdString GuidoVo FormantCha Praa OSC Inspect PatchIn CtkProtoNotes PitchClass Pin BeatSched JSpeech SCIBViewP Mono NetAd PowerOfTwoBlock J GeneralHID Spec GuidoTimeSig HIDDeviceService EnvGate KeyCodeRes SOScore CmdPeriod Splay SynthDef DocParser JStethoscope PriorityQueue PageLayout GuidoTime Position ClientFunc HiliteGradient Help Crucial ControlPrototypes Pen E Glyph NumChannels RunnerGUI Client XFader GuidoMark CtkNoteObject MLIDbrowser CocoaDialog OSCresponderNode VolumeGui Tempo MIDIEvent SlotInspector RunnerMixer QuarkDepe WiiRemoteGU CocoaGUI Page Chronometer EventTyp CtkObj Post WiiMote ParserGUI SpeechChannel Stethoscope NodeProxyEdit Mix RelativeToPa GeoClock CtkTimer ProxyMixer XSession Module JFreq Spla Gui EZRanger QuarkSVNRepository Warp MXHIDAbsInfo Quant No RecordProxyMixer B MIDIClient AutoDocTestClas JSCWindow LocalQuarks IODesc Monitor JFont SCVie Midi2FreqUGen XIn Notificat NodeCon SCIBPanelWindow Unix EnvironmentRed XFader4 IndexL ModalDialog TempoBus File Bu SynthDesc SynthDescLib SoundFile Manuscrip Updater PrettyState PatchOut XInFeedback XFaderN GraphGenerator InspManager HIDDeviceElement Sheet Painter MXHID Scribe Node NodeIDAllocator SelectButtonSet StartUp Document GetStringDialog WiiNunchukGUI Pitc NotificationRegistration Layout ContiguousBlockAlloca MIDIClockOut GUI Clock Bus JKnob Scheduler AbstractConsole Editor MultiPageLayout AbstractConstrain JavaObj Ar UI Impulsar SOGui TaskPro MXHIDSlot ProxyMoni OSCService Do MultiChanRecorder Swi AGProcessor SOScoreNodeBox Semaphore Insp GeoGrapher UGenHelper GeoGraphSynth MethodQuote InterplEnv S GlyphWriter InstrAt SwingGUI GeneralHIDDevi Grapher ClassHelper WiiMoteIRObject Point JSpeechChan Au ServerOptions GraphBuilder WiiCalibrationInfo Platform Font MIDIOut ImageView Sample TestDep SelectX RawPointer Magnitude Boolean Nil Symbol Finalize Abstrac Interpreter Process Frame FunctionDef Collection Class andrea valle
Transcript

ObjectPlayer88 PowerOfTwoAllocator

GraphParser

ModelEZSlider

Env

GraphicalScore

NB

SCWindow

InterplChord

Collapse

QuarkView

SOScoreProcessing

SimpleControllerDate

Exception

KeyCodeResponder

PatternConductor

GetFileDialog

FreqScope

SoundFileViewProgressWindow

SoundFileFormats

TChoose

Insets

EventStreamCleanup

TDuty_old

UpdateListener

Instr

OSCResponderQueue

AbstractNodeWatcher

PitchCollectionControlName

UnicodeResponderFlowVar

Runner

GraphView

MIDIResponder

JKeyState

Score

SerialPort

MIDIEndPoint

GeneralHIDSlot

SwingDialog

LRUNumberAllocator

SoundIn

AbstractPlayControl

StackNumberAllocatorDOMNode

KeyMapper

Toolbox

NodeMapSetting

Harmonics

PathName

PhilologusGuiTable

BroadcastServer

Actant

FreqShift

Philologus

Gradient

SelectXFocus

GuidoArt

WiiMoteGUI

JSoundFileViewProgressWindowCondition

AutoClassHelper

Rect

UGenInstr

Color

UniqueID

GuidoScore

HIDInfo

HistoryMessage

Quarks

Helper

TopicHelper

JPen

EnvirDispatch

SCIBDrag

MIDIIn

JPeakMeterManager

HIDDeviceDOMImplementation

Volume

Staff

DebugFrame

MelFilter

SkipJack

SCIBToolboxWindow

LilyPond

Speech

SCIBAreaSelection

EZNumber

Cocoa

RingNumberAllocator

PMOsc

PraatFormant

SCNSObjectAbstractClassBrowser

ResponderArray

Quark

AudioIn

Graph

SimpleKDRUnit

MultiTap

BusSynthDefs

JMouseBase

PhilologusGuiScore Def

HistoryGui

ContiguousBlock

ModalFreqUGenGeoAudio

MyClass

InBus

SystemSynthDefs

OSCresponder

AutoCompMethodBrowser

TaskProxyEditor

Keyboarder

LinkedListNode

FlowLayout

CmdString

GuidoVoiceFormantCharter

Praat

OSCBundle

Inspector

PatchInCtkProtoNotes

PitchClass

PingPongBeatSchedJSpeechSCIBViewPallatte

Mono

NetAddr

PowerOfTwoBlock

JSCView

GeneralHID

Spec

GuidoTimeSig

HIDDeviceService

EnvGate

KeyCodeResponderStack

SOScore

CmdPeriod

Splay SynthDef

DocParser

JStethoscope

PriorityQueue

PageLayout

GuidoTime

Position

ClientFunc

HiliteGradient

Help

Crucial

ControlPrototypes

Pen

Enveloper2

Glyph

NumChannels

RunnerGUI

Client

XFader

GuidoMark

CtkNoteObject

MLIDbrowser

CocoaDialogOSCresponderNode

VolumeGui

Tempo

MIDIEventSlotInspector

RunnerMixer

QuarkDependencyWiiRemoteGUI

CocoaGUI

Page

Chronometer

EventTypesWithCleanupCtkObj

Post

WiiMote

ParserGUI

SpeechChannel

Stethoscope

NodeProxyEditor

Mix

RelativeToParentGeoClock

CtkTimer

ProxyMixer

XSessionModule

JFreqScope

SplayZ

GuidoEventEZRanger

QuarkSVNRepositoryWarp

MXHIDAbsInfo

Quant

NodeMap

RecordProxyMixer

Buffer

MIDIClient

AutoDocTestClass2JSCWindow

LocalQuarks

IODesc

Monitor

JFont

SCViewHolder

Midi2FreqUGenXIn

NotificationCenter

NodeControl

SCIBPanelWindow

Unix

EnvironmentRedirect

XFader4

IndexL

ModalDialog

TempoBus

FileReader

BufferProxy

SynthDesc

SynthDescLib

SoundFile

ManuscriptUpdater

PrettyState

PatchOut

XInFeedback

XFaderN

GraphGenerator

InspManager

HIDDeviceElement

Sheet

Painter

MXHIDScribe Node

NodeIDAllocator

SelectButtonSetStartUp

Document

GetStringDialog

WiiNunchukGUI

PitchInterval

NotificationRegistration

Layout

ContiguousBlockAllocatorMIDIClockOut

GUI

Clock

Bus

JKnob

SchedulerAbstractConsole

EditorMultiPageLayout

AbstractConstraint

JavaObject

ArrayPlayer

UI

Impulsar

SOGui

TaskProxyAllGui

MXHIDSlot

ProxyMonitorGuiOSCService

DoMultiChanRecorder

SwingOptions

AGProcessor

SOScoreNodeBox

Semaphore

Insp

GeoGrapher

UGenHelper

GeoGraphSynth

MethodQuote

InterplEnv

SCView

GlyphWriter

InstrAt

SwingGUI

GeneralHIDDevice

Grapher

ClassHelper

WiiMoteIRObject

Point

JSpeechChannel

AutoClassHelperTest

ServerOptions

GraphBuilder

WiiCalibrationInfo

Platform

FontMIDIOut

ImageView

SampleListGenerator

TestDependant

SelectX

RawPointerMagnitude

Boolean

Nil

Symbol

Finalizer

AbstractFunction

Interpreter

Process

Frame

FunctionDef

Collection

Class

andrea valle

1

2

Quest’opera è stata rilasciata sotto la licenza Creative Commons Attribuzione-Non commerciale-Condividi allo stesso modo 2.5 Italia. Per leggere una copiadella licenza visita il sito web http://creativecommons.org/licenses/by-nc-

sa/2.5/it/ o spedisci una lettera a Creative Commons, 171 Second Street, Suite300, 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 SuperCol-lider (http://www.python.org/)TEX: tipografia 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 grafi che rappresentano le relazioni di ereditarie-tà tra classi in SuperCollider, diagrammi di flusso per le unità audio(http://www.graphviz.org/)TeXShop: tutto l’editing basato su TEX(http://www.uoregon.edu/~koch/texshop/)NodeBox: immagine di copertina(http://nodebox.net/)

Sito: http://www.cirma.unito.it/andrea/sc.htmlSussuri e grida: [email protected]

Andrea Valle

tSCIRMAtheSuperColliderItalianManualatCIRMA

(→ DRAFT: 9 aprile 2008)

3

4

Sommario1 Introduzione 81.1 Ciò di cui si parla: SuperCollider 81.2 Obiettivi 91.3 Fonti 91.4 Convenzioni grafiche 10

2 SuperCollider grado 0 122.1 Che cos’è SuperCollider 122.2 Alcuni buoni motivi (a scelta) per usare SC 152.3 Disponibilità rispetto ai sistemi operativi 162.4 Dove reperire SuperCollider 172.5 Interfacce grafiche 182.6 Informazioni di base sull’ambiente di sviluppo 202.7 Salvare ed aprire documenti, help file compresi 23

3 Object Oriented Programming in SC: fondamenti 253.1 Minima objectalia 253.2 Oggetti in SC 283.3 Metodi e messaggi 343.4 I metodi di tipo post e dump 413.5 Numeri 44

4 Sintassi: elementi fondamentali 484.1 (Parentesi) 484.2 // Commenti (/*di diverso tipo*/) 494.3 ”Stringhe” 494.4 Variabili 504.5 Simboli 544.6 Espressioni ; 554.7 • Errori 574.8 {Funzioni} 58

5

4.9 Classi, messaggi/metodi e keyword 654.10 Esempio 674.10.1 Il pattern GUI 674.10.2 Il codice 704.10.3 Introduzione 714.10.4 Creazione degli elementi GUI 724.10.5 Interazione 734.10.6 Per finire 754.11 Controlli di flusso 754.12 Ancora un esempio GUI 804.12.1 La funzione di generazione dei valori colore 814.12.2 Modularizzare la GUI 824.12.3 Controllo delle azioni 86

5 Sintesi, I: fondamenti 955.1 Poche centinaia di parole d’acustica 975.2 Algoritmi di sintesi 1015.3 Nota sul metodo play 1115.4 Altri segnali e altri algoritmi 1125.5 Funzione valore assoluto e funzione quadratica 1245.6 Ancora sull’elaborazione di segnali 1275.7 Segnali di controllo 1325.8 Conclusioni 143

6 L’architettura e il server 1446.1 L’architettura 1446.2 Esempi 1486.2.1 SwingOSC 1486.2.2 Graphista! 1516.3 Il client sclang 1516.4 Un impianto chimico per la produzione di liquidi

e un server audio in tempo reale 152

6

6.5 Appetizer: un esempio di sintesi e controllo intempo reale 163

6.5.1 Una synthDef 1656.5.2 UGen e UGen-Graph 1676.5.3 Un synth 1726.5.4 GUI e controller 174

7 Controlli e canali 1777.1 Inviluppi 1777.2 Generalizzazione degli inviluppi 1837.3 Sinusoidi & sinusoidi 1927.4 Segnali pseudo-casuali 2097.5 Espansione e riduzione multicanale 215

8 Sintesi, II: tecniche di generazione del segnale audio 2268.1 Oscillatori e tabelle 2268.2 Campionamento 2368.2.1 Campionamento semplice 2368.2.2 Resampling e interpolazione 2398.3 Sintesi additiva 2488.4 Sintesi granulare 2598.5 Sintesi sottrattiva 2648.6 Analisi e risintesi 2718.7 Modulazione 2818.7.1 Modulazione ad anello e d’ampiezza 2828.7.2 Modulazione ad anello come tecnica di elaborazione 2848.7.3 Modulazione di frequenza 2908.7.4 C:M ratio 293

9 Suono organizzato: (minimal) scheduling in SC 3009.1 Server side: attraverso le UGen 3009.2 Server side: le UGen Demand 3049.3 Lang side: Routines 308

7

9.4 Orologi 3109.5 Sintetizzatori/eventi 3149.6 Routine/Task 3199.7 Micro/macro 322

1.1–8

1 Introduzione

1.1 Ciòdicuisiparla:SuperCollider

SuperCollider (SC) è un pacchetto software per la sintesi e il controllodell’audio in tempo reale1. Attualmente rappresenta lo stato dell’artenell’ambito della programmazione audio: non c’è altro software di-sponibile che sia insieme così potente, efficiente, flessibile. L’unicopunto a sfavore di SC è che non è “intuitivo”: richiede competenzerelative alla programmazione, alla sintesi del segnale audio, alla com-posizione musicale (nel senso più lato, aperto e sperimentale del ter-mine). Ci si potrebbe chiedere d’altra parte in che senso un violino,una chitarra elettrica, una batteria presentino un’interfaccia utente“intuitiva”: non c’è forse bisogno di un duro lavoro per cavare fuoriuna nota da un violino o per tenere un tempo suonando una batte-ria? Il disagio che il neofita prova con SC dipende però forse da unaltro fatto: SC è preciso ed efficiente ma è puntiglioso ed ha un forte

Si vedrà più avanti che cosa indica specificamente “pacchetto software”1

1.3–9

senso della responsabilità. Ciò che spetta al programmatore, spetta alui solo: SC non lo aiuterà in caso di sue mancanze. Per chiudere ilparallelo, SC è come uno Stradivari per il violinista: dipende da que-st’ultimo farlo suonare come quello sa fare. Non è come un carillonin cui basta caricare la molla, poiché tutta la competenza relativa allagenerazione e al controllo del suono è in fondo già ascritta al mecca-nismo.Anche se a dire il vero, come si vedrà, SC può tramutare un carillonin uno Stradivari.

1.2 Obiettivi

L’obiettivo di quanto segue è duplice:

1. introdurre alcuni aspetti fondamentali relativi a quell’ambito cheviene usualmente indicato, con una definizione a volte imprecisae a volte impropria, computer music

2. fornire una breve panoramica ed insieme un’introduzione a Su-perCollider in italiano.

L’ipotesi di partenza (e indipendentemente dai risultati) è quella difar interagire i due ambiti, attraverso una reciproca specificazione

1.3 Fonti

1.4–10

Il materiale presentato è, in qualche misura, “originale”. La parte re-lativa alla sintesi riprende alcune parti di Audio e multimedia2 e vi in-clude parti di materiali provenienti dai corsi degli anni precedenti.Trattandosi di materiale introduttivo, è chiaro che il testo si affidasaldamente al suo intertesto costituito dai molti testi analoghi, piùcomplessi e più completi, che lo precedono. Questo vale a maggiorragione per la parte dedicata a SuperCollider. Non è una traduzio-ne in senso stretto di scritti già esistenti: tra l’altro, attualmente, nonesiste un libro di riferimento per SuperCollider3. E tuttavia è chiaroche il testo scritto è fittamente intessuto di prestiti provenienti daglihelp file, dai tutorial di James McCartney, Mark Polishook, Scott Wil-son, dal testo di David Cottle, dalle osservazioni preziosi fornite dallaSuperCollider mailing list, dalle informazioni accessibili attraverso ilSuperCollider wiki. La lettura di queste fonti non è in nessun modoresa superflua dal testo seguente, il quale ha invece semplicementeun ruolo propedeutico rispetto alle stesse, perché quantomeno evitaal lettore italiano la difficoltà supplementare della lingua straniera.

1.4 Convenzionigrafiche

Il manuale prevede tre distinzioni:

Lombardo, V. e Valle, A., Milano, Apogeo 2008 (3ed.).2

È in corso di pubblicazione (l’uscita è prevista per la fine del 2008) il progetto di un3

SuperCollider Book, sull’esempio del Csound Book, per gli stessi tipi della MIT press.

1.4–11

1. testo: in carattere nero normale, senza particolarità, esattamentecome quanto scritto qui

2. codice: è scritto in carattere typewriter, utilizza lo schema co-lori della versione MacOSX per la sintassi, è riquadrato in blu ele righe sono numerate. Al di sotto di ogni esempio è presenteun marcatore interattivo. Esso permette di accedere al file sor-gente dell’esempio che è incluso nel pdf, e di aprirlo direttamentecon l’applicazione SuperCollider. Per garantire la funzione è beneusare Acrobat Reader (che è gratuito e multipiattaforma). Alla pri-ma apertura Reader richiede di impostare il suo comportamentonei confronti dell’allegato

1 // ad esempio2 "a caso".postln ;

3. post-window: è scritto in nero; con carattere typewriter, e riportauna parte di sessione con l’interprete SuperCollider. È riquadrato inarancio e le righe sono numerate.

1 "così".postln

code/introduzione/adEsempio.sc

2.1–12

2 SuperCollidergrado0

2.1 Checos’èSuperCollider

SuperCollider (SC) è un pacchetto software per la sintesi e il control-lo dell’audio in tempo reale. La definizione di “pacchetto software”tuttavia si rivela piuttosto vaga. Per arrivare ad una definizione piùanaliticamente ed esauriente, è meglio partire dalla definizione di SCche appare sulla homepage di Sourceforge:

“SuperCollider is an environment and programming languagefor real time audio synthesis and algorithmic composition. Itprovides an interpreted object-oriented language which func-tions as a network client to a state of the art, realtime soundsynthesis server”

(http://supercollider.sourceforge.net/)

Più analiticamente:

2.1–13

1. an environment: SC è un’applicazione che prevede più componentiseparate. Di qui l’utilizzo del termine “ambiente”.

2. and: SC è anche un’altra cosa del tutto diversa.

3. a programming language: SC è infatti anche un linguaggio di pro-grammazione. Come si dice in seguito, appartiene alla famigliadei linguaggi ”orientato agli oggetti”, ed è, tra l’altro, tipologica-mente vicino a Smalltalk. Il codice del linguaggio SC, per essereoperativo (“per fare qualcosa”), deve essere interpretato da un in-terprete. Un interprete è un programma che ”capisce” il linguag-gio e agisce di conseguenza. SC è anche l’interprete del linguaggioSC.

4. for realtime sound synthesis: SC è ottimizzato per la sintesi del se-gnale audio in tempo reale. Questo lo rende ideale per un’utilizzostrumentale (performance live) così come per la realizzazioni diinstallazioni/eventi. È senz’altro possibile utilizzare SC non intempo 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 fat-to che permette due approcci complementari e opposti alla sintesiaudio. Da un lato, permette di svolgere operazioni di basso livel-lo sul segnale audio. Dall’altro, permette al compositore di espri-mersi ad alto livello, cioè non in termini di campioni audio, ma distrutture che rappresentino oggetti per la composizione musicale(ad esempio: scale, pattern ritmici, etc.). In questo senso, si rive-la ideale per la composizione algoritmica, ovvero per un approc-cio alla composizione musicale basato sull’utilizzo di procedureformalizzate. In SC questo tipo di operazioni può essere svoltointerattivamente ed in tempo reale.

2.1–14

6. [the] language [. . .] functions as a network client to a [. . .] server: l’applicazioneche interpreta il linguaggio SC è anche un cliente che comunica, at-traverso una rete, con un server, un fornitore di servizi.

7. a state of the art: attualmente SC rappresenta lo stato dell’arte nell’ambitodella programmazione audio: non c’è altro software disponibileche sia insieme così potente, efficiente, flessibile (e ormai ancheportabile).

8. sound synthesis server: SC è un fornitore di servizi, in particolare diservizi audio. La locuzione può sembrare misteriosa. Si traducecosì: SuperCollider genera audio in tempo reale su richiesta. Inquesto senso, SC fornisce audio su richiesta: chi richiede audio aSC è un suo cliente (client).

Riassumendo: quando si parla di SC si possono indicare (generandouna certa confusione) cinque cose diverse. Queste cose sono:

1. un server (→ un fornitore di servizi) audio2. un linguaggio di programmazione per l’audio3. l’interprete (→ il programma interprete) per il linguaggio4. l’interprete in quanto cliente del server audio5. il programma (→ l’applicazione complessiva) che comprende tut-

te le componenti 1-4

La situazione è schematizzata in 2.1. L’applicazione SC prevede dueparti: una è il server audio (denominato scsynth), l’altra è l’interpreteper il linguaggio (denominato sclang) che, oltre a interpretare il lin-guaggio SuperCollider, svolge il ruolo di client rispetto a scsynth.Può sembrare complicato. In effetti lo è.Installare SC vuol dire perciò installare un’applicazione complessivache comprende un server audio e un interprete del linguaggio/clientdel primo. Si vedrà in seguito meglio che cosa indicano i termini: per

2.2–15

Client Server

Linguaggio

Interprete

sclang scsynth

SuperCollider application

1

2

3

4

5

Fig. 2.1 Struttura di SC.

ora si tenga a mente che esistono due programmi distinti, e che quan-do si installa SC si ottengono due programmi al costo di 1 (il costo sicalcola così: 2×0 = 0. Come recita un madrigale di Cipriano de Rore,“mia benigna fortuna”).

2.2 Alcunibuonimotivi(ascelta)perusareSC

Alcune applicazioni di SC, in ordine sparso:

• costruire un proprio sistema per fare musica live, interfaccia gra-fica compresa

http://www.dewdrop-world.net/sc3/index.php

• fare musica dance (nel senso più vago del termine)http://crucial-systems.com/SuperCollider

2.3–16

• allestire un dj sethttp://www.durftal.com/music/cylob/bf/

• fare composizione elettroacustica (nel senso più vago del termine)http://www.bridgerecords.com/pages/catalog/9210.htm

• sonificare datihttp://www.sonification.de/

• controllare un complesso sistema di altoparlanti (> 170) dal vivohttp://www.woutersnoei.nl/

• ricostruire in audio binaurale il Poème électronique (ovvero: la dif-fusione di 3 tracce in movimento su 350 altoparlanti)

http://www.edu.vrmmp.it/vep/

• integrare audio e video dal vivohttp://www.klippav.org/

• praticare live codinghttp://www.toplap.org/

Una ricerca su YouTube permette agevolmente di vedere all’operaSuperCollider in situazioni molto diverse. Alcuni video sono qui rac-colti qui:

http://supercollider.sourceforge.net/videos

2.3 Disponibilitàrispettoaisistemioperativi

2.4–17

SuperCollider è stato originariamente sviluppato da James McCart-ney su piattaforma Macintosh. In particolare la versione 2 era forte-mente integrata con il sistema operativo Mac OS9. SuperCollider 3(che è insieme molto simile e molto diverso da SC 2) è stato sviluppa-to per il sistema operativo Mac OSX ed è ora un software open source,sviluppato da una consistente comunità di programmatori a partiredal lavoro di James McCartney. La comunità di sviluppatori ha co-sì effettuato il porting anche per le piattaforme Windows e Linux4.Queste ultime due versioni di SC differiscono principalmente per di-fetto rispetto alla versione per OSX, nel senso che alcune funzionalitàpresenti in quest’ultima non sono state portate nelle altre due. Tutta-via, nell’arco della seconda metà del 2007 è stato compiuto un grandesforzo dalla comunità si sviluppatori, in vista dell’uscita (prossima)del SuperCollider Book. La versione attuale (3.2), che è quella di riferi-mento per il libro, ha apportato notevoli migliorie al software, oltread un notevole incremento della documentazione ed un aumentatacompatibilità tra le piattaforme. Si può dire che le differenze tra lepiattaforme sia ormai limitate agli ambienti di sviluppo (necessaria-mente dipendenti dal sistema operativo)5.

2.4 DovereperireSuperCollider

SuperCollider può essere scaricato da Sourceforge, in particolare all’indirizzohttp://supercollider.sourceforge.net/downloads

In particolare, Stefan Kersten è il principale autore della versione Linux, mentre la4

versione Windows è stata implementata da Christopher Frauenberger.I riferimenti, scarsi, a piattaforme specifiche in questo testo si riferiscono alle versioni5

MacOSX e Windows, non a Linux.

2.5–18

Nel caso si utilizzino i file binari, l’installazione in sé è un processobanale, poiché prevede un file .dmg su Mac6 e un file .exe per Win-dows7.In generale, il sito ufficiale presso Sourceforge

http://sourceforge.net/projects/supercollidercostituisce il punto di riferimento della comunità di utilizzatori e svi-luppatori, ed è utile farvi riferimento. In particolare, vale la penamenzionare la mailing list degli utilizzatori, attraverso la quale è pos-sibile confrontarsi rapidamente con i migliori programmatori di SC.

http://www.create.ucsb.edu/mailman/listinfo/sc-usersGli archivi della mailing list sono poi una risorsa preziosa, in cui mol-to spesso è già disponbile una risposta ai propri quesiti. L’interfacciaweb via Nabble è forse di più agevole consultazione:

http://www.nabble.com/Supercollider—User-f535.htmlInfine, il sito di James McCartney ha un valore eminentemente stori-co:

http://www.audiosynth.com/

2.5 Interfaccegrafiche

Esistono versioni compilate anche per versioni ormai superate del sistema operativo6

(10.2) così come per processori più datati (G3). Per ottenerle si può consultare il sito ela mailing list.La versione per Windows prende il nome di PsyCollider. Il nome deriva da Py + SCol-7

lider (con metatesi), poiché l’implementazione utilizza il linguaggio Python. A diffe-renza di quanto avveniva inizialmente, l’utente finale non è tenuto né conoscere néad avere Python installato sulla propria macchina per potre eseguire Psycollider.exe.Python è incluso nei file binari ed è utilizzato internamente da Psycollider.

2.5–19

La principale differenza tra le versioni per i diversi sistemi operativiconcerne le classi per lo sviluppo di interfacce grafiche (in SC è pos-sibile programmare la GUI così come si programma la generazionedell’audio), rispetto alle quali ogni piattaforma ha le sue specificità.È importante sottolineare che SC permette di programmare gli ele-menti della GUI facendo astrazione dal pacchetto grafico prescelto.In sostanza in questo modo è possibile costruire pulsanti, cursori, fi-nestre riferendosi genericamente ad essi e selezionando un gestoregrafico che si occuperà di costruirle. Si pensi alle “skin´´ in cui unastessa funzione è gestita secondo modalità grafiche diverse. In SC siseleziona un gestore grafico tra quelli disponibili e questi costruiràl’oggetto prescelto. A dopo il dettaglio in termini di programmazio-ne. I gestori grafici non sono molti. L’interfaccia grafica nativa su Ma-cOSX è Cocoa, mentre su Linux il primo gestore grafico è stato SCUM.Recentemente si è reso disponibile il server grafico SwingOSC

http://www.sciss.de/swingOSC/. SwingOSC è un server grafico del tutto autonomo rispetto a SC(può essere utilizzato anche da altre applicazioni), ma, ed è quan-to qui interessa, può essere utilizzato da dentro SC (in altre parolel’utente può praticamente dimenticarsi dell’esistenza del meccani-smo client/server). SwingOSC è scritto in Java e per funzionare ri-chiede che sia installata il Java Runtime Environment. Per controllarelo stato della propria macchina e installare la versione più recente diJava si può visitare il sito dedicato:

http://java.com/en/download/installed.jspSwingOSC permette la costruzione di interfacce grafiche di qualitàsuperiore rispetto a quelle originariamente sviluppate per MacOSX(almeno, a parere di chi scrive, anche se probabilmente meno efficien-ti computazionalmente). In più, SwingOSC, essendo basata su Java, ècompletamente multipiattaforma, essendo disponibile per Mac, Win-dows, Linux. La versione per Windows (detta “PsyCollider”) integra

2.6–20

in forma predefinita SwingOSC, che è l’unica interfaccia grafica di-sponibile. Se si guarda la Post Window durante l’avviamente di Psy-Collider si potrà notare come il server SwingOSC venga avviato au-tomaticamente (booting java -jar SwingOSC. . . etc).Gli esempi del testo verranno scritti in forma astratta rispetto al gesto-re grafico, ma chi scrive utilizza sempre (pur essendo su Mac) Swin-gOSC.

2.6 Informazioni di base sull’ambiente disviluppo

SC funziona in tempo reale ed in forma interattiva attraverso un’interfacciatestuale: tutte le comunicazioni tra utente e programma avvengonoattraverso testo. Si osservino le figure 2.2 e 2.3. Eseguendo il pro-gramma si apre appunto un editor di testo. In particolare si apre laPost Window, la finestra che visualizza i messaggi che SC indirizzaall’utente (2.2 e 2.3, 1). In Windows, la finestra SC Log stampa gli stes-si messaggi di un terminal aperto in parallelo (2.3, 4 all’interno peròdell’applicazione PsyCollider, 2.3, 5). Entrando in esecuzione, SC ef-fettua alcuni passaggi di inizializzazione (di cui si discuterà più avan-ti), il cui risultato viene scritto sulla Post Window. Nel caso di Psy-Collider, viene immediatamente avviato il server audio (si vede benesul terminal, 2.3, booting 57110, 6): si notino i messaggi relativi al ri-levamento della scheda audio (Device options:, Booting with:).L’avviamento del server audio avviene invece per mano dell’utente

2.6–21

in OSX, attraverso due interfacce grafiche gemelle: in particolare, so-no disponibili due server distinti, local e internal8 (2.2, 4 e 5). In Win-dows viene altresì avviato il server grafico SwingOSC (2.3, 7, bootingjava -jar SwingOSC. . . etc). È possibile utilizzare la Post Window perscrivere il proprio codice (almeno in fase di test) ma è sempre megliocreare una nuova finestra (2.2 e 2.3, 2). A questo punto, l’utente im-mette del codice e chiede all’interprete di valutarlo. Ad esempio, ilcodice "Hello World".postln richiede di stampare sullo schermo lastringa "Hello World". Si noti la sintassi colorata che distingue trala stringa e il metodo .postln;9. Se l’interpretazione va a buon fine,SC risponderà con un certo comportamento che dipende dal codiceimmesso: in questo caso, stampando sulla Post Window la stringa"Hello World" (2.2 e 2.3, 3). Altrimenti segnalerà l’errore attraversola Post Window.L’editor di testo (sia in OSX che in Windows) prevede un insieme dicaratteristiche tipiche di ogni editor, ed è inutile soffermarvicisi. Altrefunzioni sono utili ad un utente più avanzato, ed esulano da quantoqui previsto.Alcune indicazioni minimali:

• MacOSX

− Valutazione del codice: selezionare il codice e premere Enter (nonReturn). Se il codice consiste in una sola riga, è sufficiente po-sizionare il cursore in un punto della riga e premere enter.

− Arresto dell’audio: Apple + .

Si veda dopo.8

Gli schemi colori per le due piattaforme sono differenti (chissà perché). Qui si segue9

quella originale di OSX

2.6–22

1

4

5

2

3

Fig. 2.2 SC in Mac OSX.

45

61

7

2

3

Fig. 2.3 SC in Windows (PsyCollider).

− Aiuto: Apple + ?. Se la combinazione è premuta contestual-mente alla selezione di codice SC, si apre il file di aiuto corre-lato (laddove esistente).

− Pulizia della Post Window: Apple + Shift + k

• Windows

2.7–23

− Valutazione del codice: selezionare il codice e premere Ctrl + Re-turn (invio). Se il codice consiste in una sola riga, è sufficienteposizionare il cursore in un punto della riga e premere Ctrl +Return (invio).

− Arresto dell’audio: Alt + .− Aiuto: F1. Se la combinazione è premuta contestualmente alla

selezione di codice SC, si apre il file di aiuto correlato (laddoveevistente).

− Pulizia della Post Window: Alt + P

2.7 Salvare ed aprire documenti, help filecompresi

Il contenuto della finestra può essere salvato su file e riaperto in un’altrasessione di lavoro, per essere modificato o semplicemente per esse-re rieseguito. Una caratteristica importante di SC è che gli help filescontengono codice valido che può essere valutato interattivamente.Quando si apre un file di help è allora possibile sperimentare con gliesempi contenuti per capire esattamente il funzionamento descritto-vi. Rispetto al formato dei files predefinito MacOSX e Windows pre-vedono alcune differenze:

• MacOSX: il formato file prescelto in origine è una versione spe-ciale del formato di testo RTF. In MacOSX gli help file sono in for-mato HTML, ma vengono convertiti automaticamente in RTF, esono documenti validi per SC: in altre parole, è possibile valutareil codice che vi è contenuto. L’utilizzo del formato RTF consente

2.7–24

di avere formattazione avanzata nei documenti di SC (font, co-lor, dimensione, etc.). Il formato è però nativo soltanto per Mac,e in caso di utilizzo su un’altra piattaforma, la formattazione de-ve essere eliminata (il file deve essere convertito in testo sempli-ce). In ogni caso è perfettamente possibile e consigliabile (salvospecifici interessi tipografici) utilizzare per i propri file il formatoASCII (cioè: testo semplice), in particolare scegliendo l’estensionescd (SuperCollider Document), per evitare ambiguità.

• Windows: il formato file è soltanto ASCII (si può aprirlo con No-tepad/Blocconote), e spetta all’utente scegliere l’estensione (evi-dentemente, .scd è una scelta sensata). Quando si vuole eseguire ilcodice contenuto nei file di help in HTML è necessario convertireil formato HTML in codice selezionando “HTML To Code Windo-w” dal menu “File” (Ctrl+T). Si apre una finestra in cui il formatoHTML viene convertito in ASCII (la nuova finestra è una finestradi codice, la sintassi è colorata): a questo punto, è possibile valu-tarlo.

3.1–25

3 Object Oriented Programming inSC:fondamenti

3.1 Minimaobjectalia

Nella programmazione orientata agli oggetti si assume che l’utente,per programmare il comportamento di un calcolatore, manipoli enti-tà 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 ope-razioni. Per poter essere riconosciute dal linguaggio le entità devo-no appartenere ad un insieme finito di tipi: un oggetto è del tipo A,l’altro del tipo B e così via. I tipi vengono chiamati “classi” in OOP.Un oggetto è dunque una particolare istanza di una classe: la classepuò essere pensata come il tipo astratto, ma anche come lo stampoda cui si fabbricano le istanza. Da un unico conio (la classe) si stampaun un numero indefinito di monete uguali (gli oggetti). È nella classe

3.1–26

che si definiscono i metodi di cui tutti gli oggetti di quel tipo sarannodotati. Una classe descrive anche il modo in cui creare un oggetto apartire dalla classe. Le classi sono organizzate gerarchicamente: ogniclasse può derivare da un’altra classe e ogni classe può avere del-le classi derivate. Questo principio prende il nome di “ereditarietà”.Ad esempio un conio è una ulteriore specificazione di un più gene-rico “stampo”: lo stampo è la sopraclasse del conio, e il conio è unasottoclasse dello stampo. Un sigillo (per la ceralacca) è un altro stam-po, ma di un tipo completamente diverso dal conio: il sigillo è unasottoclasse dello stampo, da cui eredita alcuni aspetti che condividecon il conio (la capacità di impressione), ma da cui si differenza peraltri (prevede un’impugnatura manuale, mentre il conio viene battu-to a martello).L’ereditarietà va pensata in termini genetici: i caratteri del padre sonopresenti (come patrimonio genetico, appunto) nei figli, ma, a scansodi equivoci, si noti che qui ereditarietà è intesa in termini sistemati-ci, non evolutivi.La relazione di ereditarietà prende infatti a modellole tassonomie naturalistiche. Ad esempio, il grafo seguente illustrala posizione tassomica dell’ippopotamo. L’ippopotamo appartieneal subordine dei Suina (ad esempio, intuibilmente, i maiali), con iquali condivide alcuni tratti, che differenziano entrambi dai Rumi-nantia (ad esempio, le mucche), pur essendo Ruminantia e Suina en-trambi Actiodactyla (e distinguendosi entrambi dai Perissodactyla,ad esempio i cavalli). Se le classi sono astratte (ad esempio, la spe-cies dell’ippopotamo), gli oggetti (gli ippopotami da classificare) so-no concreti.Tornando alla programmazione, nel paradigma object-oriented il mon-do si presenta al programmatore come un insieme di oggetti che siprestano, sotto determinate condizioni, ad essere manipolati. In par-ticolare, per manipolare un oggetto –per chiedergli di fare qualcosa–

3.1–27

Fig. 3.1 Tassonomia dell’ippopotamo.

è necessario inviargli un “messaggio”. Un oggetto, per poter rispon-dere al messaggio, deve conoscere un metodo. In sostanza, può ri-spondere ad una richiesta (messaggio) attraverso una competenza(metodo). L’oggetto che riceve il messaggio è il “ricevente” di quelmessaggio e vi può rispondere se implementa un metodo corrispon-dente. Riassumendo:

• oggetto e metodo concernono la definizione dell’oggetto dall’interno

• messaggio e ricevente concernono la comunicazione con l’oggettodall’esterno

L’insieme dei messaggi a cui un oggetto può rispondere prende ilnome di “interfaccia”: ed è un’interfaccia in senso proprio, perché èciò che l’oggetto rende disponibile all’utente per l’interazione, dovel’utente può essere anche un altro oggetto. Nella maggior parte dei

3.2–28

linguaggi ad oggetti, la sintassi tipica per passare un messaggio ad unoggetto utilizza il punto (.) e prende la forma oggetto.messaggio. Larelazione tra oggetto e messaggio non va pensata come una descri-zione alla terza persona (“l’oggetto fa una certa cosa”), ma piuttostocome una coppia vocativo/imperativo: “oggetto, fai qualcosa!”. Adesempio, conio.imprimi, o anche: ippopotamo.nuota.

3.2 OggettiinSC

SuperCollider (qui intendendo: sclang) è un linguaggio orientato aglioggetti. Lo è per di più in termini molto “puri”, poiché ha come suomodello storico, e come parente tipologico assai prossimo, il linguag-gio Smalltalk. In Smalltalk, come in SC, letteralmente ogni entità pos-sibile è un oggetto. Questa radicalità può essere spiazzante inzial-mente, ma è un punto di forza poiché garantisce che tutte (propriotutte) le entità potranno essere controllate dall’utente secondo un uni-co principio: tutte avranno attributi e metodi, a tutte sarà possibileinviare dei messaggi poiché presenteranno all’utente una certa inter-faccia. Si prenda il caso delle strutture dati: SC possiede una grandericchezza di strutture dati, cioè di classi che funzionano da contenito-ri di altri oggetti, ognuna dotati di particolari capacità e specializzataper certi tipi di oggetti. Ad esempio un “array” è un contenitore ordi-nato di oggetti. Si scriva Array. Se si richiama la colorazione della sin-tassi, si nota come venga applicato il blu, colore che contraddistinguele classi (oltre che le parole riservate var, arg, this e alcune altre).SC sa che Array è una classe perché la prima lettera è maiuscola: tuttociò che inizia con la maiuscola per SC indica una classe. Se si esegueil codice, SC restuituisce (per ora si intenda: stampa sullo schermo) la

3.2–29

classe stessa. Se si richiama l’help file, si nota come immediatamen-te venga indicata la sopraclasse, Superclass: ArrayedCollection. L’helpfile fornisce alcune indicazioni sui metodi disponibili per gli oggettidi tipo Array. Il codice:

z = Array.new;

costruisce un nuovo array vuoto attraverso il messaggio new invo-cato sulla classe Array. Tipicamente un messaggio viene inviato adun’istanza particolare e non ad una classe. Ma prima di poterlo fare,è necessario avere un’istanza a cui inviare un messaggio. Al messag-gio new rispondono allora tutte le classi restituendo una loro istanza.Il metodo new è il “costruttore” della classe: il metodo cioè che istan-zia un oggetto a partire dalla classe. Possono esserci molti metodi co-struttori, che restituiscono un oggetto secondo modalità specifiche:sono tutto metodi della classe perché, invocati sulla classe, restitui-scono un oggetto. Uno di essi è il messaggio newClear, che prevedeanche una parte tra parentesi tonde:

z = Array.newClear(12);

Nell’esempio le parentesi contengono una lista di “argomenti” (unosolo, in questo caso), che specificano ulteriormente il messaggio new-Clear, ovvero “oggetto, fai qualcosa (così)! ”10. In particolare new-Clear(12) prevede un argomento (12) che indica che l’array dovràcontenere al massimo 12 posti. È possibile indicare esplicitamentel’argomento: “oggetto, fai qualcosa (nel modo: così)! ”. Ogni argo-mento ha un nome specifico, 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);

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

3.2–30

è identico al precedente ma esplicita la keyword dell’argomento. Infi-ne, z = indica che l’array verrà assegnato alla variabile z. Si noti chela lettera utilizzata è minuscola: se si scrivesse Z = SC intepreterebbeZ come una classe (inesistente)11 e solleverebbe un errore. Adesso zrappresenta un array vuoto di capienza 112: è un’istanza della classeArray. Si può chiedere a z di comunicare la classe a cui appartieneinvocando il metodo class:

1 z.class2 Array

Il metodo class (1) restituisce la classe di z: Array (2). Traducendoin italiano, la frase equivalente potrebbe essere: “z, dichiara la tuaclasse!”. Quando si usano degli array molto spesso si è insoddisfattidei metodi elencati nell’help file: sembra che manchino molti metodiintuitivamente utili. È molto difficile che sia veramente così: moltospesso il metodo cercato c’è, ma è definito nella sopraclasse ed ere-ditato dalle classi figli. A partire da Array si può navigare la struttu-ra degli help file risalendo a ArrayedCollection, SequenceableCollec-tion, Collection: sono tutte sopraclassi (di tipo sempre più astratto)che definiscono metodi che le sottoclassi possono ereditare. Se si pro-segue si arriva a Object.

superclass: nil

Object is the root class of all other classes. All objects areindirect instances of class Object.

La Z verrebbe colorata di blu.11

3.2–31

Tutti le classi in SC ereditano da Object. Ad esempio, il metodo classche è stato chiamato su z nell’esempio precedente è definito a livellodi Object ed ereditato, lungo l’albero delle relazioni di ereditarietà,da Array, così che un’istanza di quest’ultima classe (z) vi possa ri-spondere. Al di là della navigazione nella struttura degli help files,SC mette a disposizione dell’utente molti metodi per ispezionare lastruttura interna del codice: è la capacità che tipicamente viene defini-ta “introspezione”. Ad esempio i metodi dumpClassSubtree e dump-SubclassList stampano su schermo rispettivamente una rappresen-tazione gerarchica delle sottoclassi della classe su cui è invocato ilmetodo e una lista in ordine alfabetico. Le due rappresentazioni so-no equivalenti. Nella prima sono più chiari i rapporti di parentela trale classi attraverso la struttura ad albero, nella seconda è invece possi-bile percorrere -per ognuna delle sottoclassi della classe - la strutturadell’albero lungo i rami ascendenti fino a Object. Si prenda la classeCollection, 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.2–32

1 Collection.dumpClassSubtree;2 Collection3 [4 Array2D5 Range6 Interval7 MultiLevelIdentityDictionary8 [9 LibraryBase

10 [ Archive Library ]11 ]12 Set13 [14 Dictionary15 [16 IdentityDictionary17 [18 Environment19 [ Event ]20 ]21 ]22 IdentitySet23 ]24 Bag25 [ IdentityBag ]26 Pair27 TwoWayIdentityDictionary28 [ ObjectTable ]29 SequenceableCollection30 [31 Order32 LinkedList33 List34 [ SortedList ]35 ArrayedCollection36 [37 RawArray38 [39 DoubleArray40 FloatArray41 [ Wavetable Signal ]

43 […]

45 ]46 Collection

3.2–33

1 Collection.dumpSubclassList2 Archive : LibraryBase : MultiLevelIdentityDictionary : Collection :

Object3 Array : ArrayedCollection : SequenceableCollection : Collection :

Object4 Array2D : Collection : Object5 ArrayedCollection : SequenceableCollection : Collection : Object6 Bag : Collection : Object7 Collection : Object8 Dictionary : Set : Collection : Object9 DoubleArray : RawArray : ArrayedCollection : SequenceableCollection :

Collection : Object10 Environment : IdentityDictionary : Dictionary : Set : Collection :

Object11 Event : Environment : IdentityDictionary : Dictionary : Set :

Collection : Object12 FloatArray : Raw

14 […]

16 36 classes listed.17 Collection

Con Collection.dumpClassSubtree si vede la posizione di Array inrelazione ai suoi vicini (47). È sullo stesso livello di RawArray (36),entrambi sono sottoclassi di ArrayedCollection (34). Quest’ultimaclasse appartiene alla famiglia delle SequenceableCollection (28). Ilmetodo Collection.dumpSubclassList lista le classi in ordine alfa-betico: è agevole trovare Array (2) per poi seguire i rami dell’albero(lungo la stessa riga) fino a Object.La figura 3.2 è una visualizzazione tramite un grafo ad albero dellastruttura delle classi di Collection, ottenuta elaborando automatica-mente l’output di Collection.dumpSubclassList. L’esempio è trat-to dall’help file Internal-Snooping, che è dedicato all’introspezione inSC. Anche l’help file Class è particolarmente interessante al proposito.

3.3–34

Array2D

Collection

Range Interval

Archive

LibraryBase

Library

MultiLevelIdentityDictionary

Event

Environment

IdentityDictionary

Dictionary

Set

IdentitySet IdentityBag

Bag Pair

ObjectTable

TwoWayIdentityDictionary

Object

Fig. 3.2 Grafo ad albero delle sottoclassi di Collection, apartire da Object.

L’ultima riga che SC stampa è in entrambi i caso l’oggetto 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 strut-tura delle classi di SC. La figura 3.3 riporta una rappresentazione ra-diale della struttura di tutte delle classi di SC, ottenuta elaborando ilrisultato di Object.dumpSubclassList12. Si noti come un punto di ad-densamento sia rappresentato da UGen, la sopraclasse diretta di tuttele classi che generano segnali in SC. È una classe comprensibilmentemolto numerosa.

3.3 Metodiemessaggi

SC è scritto per la maggior parte in SC (con l’eccezione di un nucleodi primitive scritte in linguaggio C per ragioni di efficienza): il codi-ce stesso del programma è trasparente al programmatore, nel sensoche è scritto in un linguaggio comprensibile per chi parla SC. Questo

Propriamente si tratta delle classi installate sulla macchina di scrive.12

3.3–35

Env

Object

SCWindowDate

BinaryOpFailureErrorDoesNotUnderstandError

MethodErrorImmutableError

MustBeBooleanErrorNotYetImplementedErrorOutOfContextReturnErrorPrimitiveFailedError

ShouldNotImplementErrorSubclassResponsibilityError

ErrorException

FormantTablePatternConductorJClassBrowserOSCResponderQueue

AnnotatedDebugNodeWatcherDebugNodeWatcher

BasicNodeWatcher

NodeWatcher

AbstractNodeWatcherUnicodeResponderScoreSwingDialog

CXSynthPlayerControl

CXPlayerControlAbstractPlayControl

PatternControl

StreamControl

SynthDefControl

SynthControlProxyNodeMapSetting

NodeMapSettingScaleHarmonics

Help

PathNameBroadcastServerFlowVarConditionRectColorUniqueIDJPen

Public

EnvirDispatchDotViewerSpeechClassBrowserResponderArray

BendResponder

TouchResponderCCResponder

MIDIResponder

NoteOffResponder

NoteOnResponder

AudioInMultiTapPMOscBusSynthDefsSystemSynthDefs

AutoCompClassSearch

AutoCompClassBrowser

AutoCompMethodBrowserLinkedListNodeFlowLayout

MixedBundle

OSCBundleFunctionPlayerPingPong

OSCSched

BeatSched

BundleNetAddr

NetAddrJSoundFileViewProgressWindow

JSC2DTabletSlider

JSC2DSlider

JSCSliderBase

JSCButton

JSCControlView

JSCCompositeView

JSCContainerView

JSCDragBoth

JSCDragView

JSCDragSinkJSCDragSource

JSCStaticTextBase

JSCFreqScope

JSCScope

JSCHLayoutView

JSCLayoutView

JSCKnob

JSCSlider

JSCListView

JSCMultiSliderViewJSCView

JSCNumberBox

JSCTextEditBase

JSCPlugContainerView

JSCPlugView

JSCPopUpMenu

JSCRangeSlider

JSCSoundFileView

JSCStaticText

JSCTextField

JSCTextView

JSCTopView

JSCUserView

JSCVLayoutView

SC2compat

KDRMaskTester

SimpleKDRUnitKeyCodeResponderStackKeyCodeResponder

InstrSynthDef

SynthDef

ProxySynthDef

JStethoscopePriorityQueue

JMouseButton

JMouseBase

JMouseXJMouseY

TChooseControlNamePositionCrucialControlPrototypesPen

ResponderClientFunc

ClientFunc

LocalClient

ClientJEZNumberJEZSliderMLIDbrowserCocoaCocoaDialogOSCresponderNode

OSCpathDispatcher

OSCMultiResponder

OSCresponder

OSCpathResponder

TempoSlotInspector

ClassInspector

ObjectInspectorFrameInspector

Inspector

FunctionDefInspectorMethodInspectorStringInspector

StethoscopeMixPeepModuleJFreqScopeSplayZSplay

CosineWarp

LinearWarpCurveWarp

Warp

DbFaderWarpExponentialWarpFaderWarp

SineWarp

ArraySpec

HasItemSpecBufferProxySpec

ScalarSpec

EnvSpecMultiTrackAudioSpec

AudioSpec

Spec

ObjectSpec

PlayerSpec

SampleSpec

StaticIntegerSpec

StaticSpec

NoLagControlSpec

ControlSpec

StreamSpec

TempoSpec

TrigSpec

ProxyNodeMap

NodeMapBufferJSCWindowJFontMidi2FreqUGenModalFreqUGenNodeControlUnix

ProxySpace

LazyEnvir

EnvironmentRedirectTempoBus

CSVFileReader

FileReader

TabFileReaderArrayBuffer

BufferProxy

Sample

AbstractSample

SynthDescLibSynthDescIODescSoundFilePhrase

PrettyEat

PrettyState

PrettyEcho

Post

AudioPatchOut

ControlPatchOut

PatchOut

UpdatingScalarPatchOut

ScalarPatchOut

ScalarPatchIn

ControlPatchIn

AudioPatchIn

PatchInMonitorXInFeedbackXInInBusHIDDeviceServiceHIDDeviceElementHIDDevice

RootNode

Group

Node

Synth

SelectButtonSetStartUpCmdPeriod

EnvirDocument

DocumentGetStringDialogGetFileDialogNotificationRegistrationNotificationCenterUpdaterContiguousBlockAllocatorContiguousBlockRingNumberAllocatorStackNumberAllocatorLRUNumberAllocatorPowerOfTwoAllocatorPowerOfTwoBlockNodeIDAllocatorGUI

SharedBus

BusJKnobScheduler

AppClock

Clock

SystemClock

TempoBusClock

TempoClock

SoundFileFormats

SaveConsole

AbstractConsole

SynthConsole

CXBundle

BooleanEditor

NumberEditorEnvEditor

Editor

IntegerEditorIrNumberEditor

PopUpEditor

KrNumberEditor

PageLayoutModalDialogSheetMultiPageLayout

Any

EveryConstraint

AbstractConstraint

CountLimit

SeenBeforeIsEvenIsNil

IsNotIn

IsInIsOddNotNotNilXor

JavaObjectUIImpulsarEnveloper2MonoXFaderNXFader4XFaderNumChannelsJSlotInspector

JClassInspector

JObjectInspectorJFrameInspector

JInspector

JFunctionDefInspectorJMethodInspectorJStringInspector

OSCServiceDoDefSwingOptionsTestCaseInspManagerInspGeoGrapherGeoGraphSynthDebugFrameMethodQuoteMessage

BeatClockPlayerGui

AbstractPlayerGui

BooleanEditorGui

EditorGui

CXMenu

SCViewHolder

ClassGui

CXObjectInspector

ObjectGui

ClassNameLabel

ActionButton

DualSeriesEfxGui

XFaderPlayerGui

EnvEditorGui

InspectorLink

InstrSpawnerGui

PatchGuiInterfaceGui

KrNumberEditorGui

NumberEditorGui

MethodGuiMethodLabel

Midi2FreqGui

KrPlayerGui

ModalFreqGui

ModelImplementsGuiBodyPlayButton

PlayPathButton

PlayerAmpGui

HasSubjectGui

PlayerBinopGui

PlayerEffectGui

PlayerEfxFuncGuiPlayerMixerGuiPlayerPoolGuiPlayerUnopGui

PopUpEditorGui

SCViewAdapter

SFPGui

AbstractSFPGui

SampleGui

SelectorLabel

ServerErrorGuiServerGui

SimpleTriggerGui

StartRow

StreamKrDurGui

TempoGui

Tile

SCButtonAdapter

ToggleButton

XPlayPathButton

EZNumberEZSliderFreqScopeSoundFileViewProgressWindowHiliteGradientGradient

ArgNameLabel

CXAbstractLabel

CXLabel

FlowView

SCLayoutView

SC2DTabletSlider

SC2DSlider

SCSliderBase

SCButton

SCControlView

SCDragBoth

SCDragSink

SCDragView

SCDragSource

SCStaticTextBase

SCEnvelopeEdit

SCEnvelopeView

SCMultiSliderView

SCFreqScope

SCScope

SCHLayoutView

SCKnob

SCSlider

SCListView

SCMovieView

SCView

SCPopUpMenu

SCRangeSlider

SCSoundFileView

SCTabletView

SCTextField

SCNumberBox

SCTextView

SCTopView

SCCompositeView

SCContainerViewSCUserView

SCVLayoutView

VariableNameLabel

SCStaticText

UGenInstrInstrAt

InterfaceDef

Instr

Lines

PointArray

PolygonZigZag

PointServerOptionsEnvGateGraphBuilderFontMIDIOutMIDIInMIDIEventMIDIClientMIDIEndPointTestDependantSimpleController

Server

Model

SwingOSC

RawPointer

Association

Magnitude

Char

Complex

Number

Float

SimpleNumber

Integer

Polar

False

Boolean

True

NilSymbolFinalizer

A2K

UGen

APF

TwoPole

AbstractPlayerEffect

HasSubject

AllpassC

CombN

AllpassLAllpassN

AmpCompA

AmpCompAmplitude

AudioInPlayer

AbstractPlayer

BPZ2

LPZ2

BRF

BPF

Filter

BRZ2

Balance2

Panner

Ball

BeatClockPlayerKrPlayer

BiPanB2

BinaryOpFunction

AbstractFunction

BinaryOpPlug

AbstractOpPlug

BinaryOpStream

Stream

BinaryOpUGen

BasicOpUGen

BinaryOpXStream

Blip

BrownNoise

WhiteNoise

BufAllpassC

BufCombN

BufAllpassLBufAllpassN

BufChannels

BufInfoUGenBase

BufCombCBufCombLBufDelayC

BufDelayN

BufDelayLBufDurBufFramesBufRateScale

BufRd

MultiOutUGen

BufSampleRateBufSamples

BufWr

CCPlayerMIDIPlayer

COsc

CleanupStream

Clip

InRange

ClipNoise

CoinGate

CombCCombL

CompanderCompanderD

ControlRate

InfoUGenBaseConvolutionConvolution2Crackle

CuspL

CuspN

ChaosGen

DC

DecayDecay2

DecodeB2

DegreeToKey

Delay2

Delay1

DelayC

DelayN

DelayL

Demand

DemandEnvGen

DetectSilence

Dgeom

Dibrown

Dbrown

DiskIn

DiskOut

Diwhite

DwhiteDone

Drand

ListDUGen

DseqDser

DseriesDswitch1DustDust2

Dxrand

DynKlank

EmbedOnce

EnvGen

EnvelopedPlayer

AbstractSinglePlayerEffect

ExpRand

FBSineC

FBSineN

FBSineL

FFT

FOS

FSinOsc

Fdef FuncProxy

Fold

Formant

Formlet

FreeFreeSelfFreeSelfWhenDone

Ref

FuncStream

FuncStreamAsRoutine

Routine

FunctionGBinaryOp

GenFilter

GBinaryOpX

GBind

Gen

GBindF

GCat

GClumpGCollect

GCycGFibGFuncGGeomGInfGIter

GKeepGKeepUntilGKeepWhile

GLace

GRepeatGResetGSelect

GSeries

GStutterGUnaryOp

GZip

Gate

Latch

GbmanL

GbmanN

GenStream

Gendy1Gendy2Gendy3

GrayNoise

HPF

LPF

HPZ1

LPZ1

HPZ2Hasher

HenonC

HenonN

HenonL

IFFTIRand

ImageWarp

Impulse

In

AbstractIn

InFeedback

InRect

InTrig

Integrator

Interface

AbstractPlayerProxy

JScopeOutK2AKeyStateKlangKlank

LFClipNoise

LFNoise0

LFCub

LFSaw

LFDClipNoiseLFDNoise0LFDNoise1LFDNoise3LFNoise1LFNoise2LFPar

LFPulse

LFTri

Lag2

Lag

Lag3

LagControl

Control

LagIn

LastValueLatoocarfian

LatoocarfianC

LatoocarfianN

LatoocarfianL

LeakDC

LeastChange

MostChangeLimitedWriteStream

CollStream

Limiter

Normalizer

LinCongC

LinCongN

LinCongL

LinExpLinLin

LinPan2

Pan2

LinRand

LinXFade2

XFade

LineLinen

LocalIn

LocalOut

AbstractOut

Logistic

LorenzL

MIDIFreqPlayerMIDIHoldsNotes

MIDIGatePlayer

SynthlessPlayer

MantissaMask

MedianMidEQ

Midi2Freq

ModalFreqMonoAudioIn

MouseButton

MouseY

MouseXMulAdd

MultiTrackPlayer

MultiplePlayers

NAryOpFunction

NAryOpStream

NRand

NdefNodeProxy

NoahNoise

NumAudioBusesNumBuffersNumControlBusesNumInputBusesNumOutputBusesNumRunningSynths

ObjectNotFoundSilence

OffsetOut

Out

OneShotStream

OneZero

OnePole

OscOscNOutputProxyPSinGrain

PV_Add

PV_MagMulPV_BinScramblePV_BinWipePV_BrickWallPV_ConformalMap

PV_CopyPhase

PV_DiffuserPV_HainsworthFootePV_JensenAndersen

PV_LocalMax

PV_MagAbove

PV_MagBelowPV_MagClip

PV_MagFreeze

PV_MagNoise

PV_MagSquared

PV_MagShift

PV_BinShiftPV_MagSmear

PV_MaxPV_MinPV_Mul

PV_PhaseShift

PV_PhaseShift270PV_PhaseShift90

PV_RandCombPV_RandWipePV_RectCombPV_RectComb2

Padd

Pset

Paddp

PsetpPaddpre

Psetpre

Pan4PanAzPanBPanB2

PausePauseSelfPauseSelfWhenDone

Pavaroh

FilterPattern

Pbind

Pattern

PbindProxy

Pbindef

Pdef

Pbindf

PbinopPbrown

Pbus

Pchain

Pclump

Pcollect

FuncFilterPattern

Pconst

EventPatternProxy

TaskProxyPdefn

PatternProxy

PdegreeToKey

Pdfsm

ListPattern

Pdict

Pdrop

PdurStutter

Pstutter

PeakFollower

PenvirPevent

PfadeOut

PfadeInPfinPfindurPflattenPflow

Pfset

Pfsm

PfuncPfuncn

Pfx

Pgeom

Pgroup

Pgtpar

Pgpar

Ppar

Phasor

PhidPindex

PinkNoise

Pipe

UnixFILE

Pitch

PitchShift

Place

Pseq

Plag

PlayBuf

PlayerAmp

PlayerBinopPlayerEffectSocket

PlayerSocket

PlayerInputProxy

PlayerMixer PlayerPool

PlayerUnop

PlazyEnvirNPlazyEnvir

PlazyPmono

Pmul

Pmulp

Pmulpre

Pn

Pnaryop

Ppatlace

Ppatmod

Pplayer

Pprob

Prand

Preject

Pretty

IOStream

PrettyPrintStream

PrewriteProrate

Proutine

Prout

Pseed

Pseg

Pstep

Pselect

Pser

Pseries

PshufPslide

Pstep2addPstep3add

PstepNadd

PstepNfunc

Pstretchp

Pstretch

Pswitch1

Pswitch

Psync

Ptime

Ptpar

Ptrace

Ptuple

PulsePulseDivider

Punop

PwalkPwhile

Pwhite

Pwrand

Pwrap

Pxrand

QuadC

QuadN

QuadL

RHPF

RLPF

RadiansPerSample

Ramp

RandRandIDRandSeed

RecNodeProxy

RecordBuf

RefCopy

ReplaceOut

ResonzRingz

Rotate2

Thread

RunningMax

Peak

RunningMin

PulseCountRunningSum

SOS

SampleDurSampleRate

Saw

Schmidt

ScopeOutScoreStreamPlayer

EventStreamPlayer

PauseStream

ScurryableInstrGateSpawnerInstrGateSpawnerInstrSpawnerPatchHasPatchIns

SelectSendTrig

SetResetFF

Shaper

Index

SharedIn

SharedNodeProxy BusPlug

SharedOut

Silent

SimpleTrigger

SinOscSinOscFB

SlewSlope

Spring

StandardL

StandardN

Stepper

Stream2TrigStreamKrDur

StreamClutch

BusDriver

SubsampleOffset

SweepSyncSawTBall

TDelay

Trig1

TDuty

DutyTExpRand

TGrains

TIRandTPulseTRandTWChooseTWindexTap

TaskTdef

TempoPlayer

Thunk

TimerToggleFFTrapezoid

Trig

TrigControl

TwoZero

UnaryOpFunction

UnaryOpPlug

UnaryOpStreamUnaryOpUGen

VOscVOsc3

VSFPSFPAbstractSFP

VarSawVibrato

WrapWrapIndexXFade2

XLine

XOut

XY

ZArchive

File

ZeroCrossing

Interpreter

Main

ProcessFrame

Method

FunctionDef

Archive

LibraryBase

Array

ArrayedCollection

Array2D

Collection

DoubleArray

RawArray

Event

Environment

IdentityDictionary IdentityBag

BagDictionary

Set

IdentitySet

Int16ArrayInt32ArrayInt8Array

Interval

Library

MultiLevelIdentityDictionaryLinkedList

SequenceableCollection

ObjectTable

TwoWayIdentityDictionary

OrderPairRange

Signal

FloatArray

SortedList

List

StringSymbolArray

Wavetable

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

ovviamente è molto diverso da capire esattamente cosa viene dettoattraverso il linguaggio: ma in ogni caso sbirciando i sorgenti si posso-no recuperare molte informazioni interessanti. È questo fatto che per-mette a SC un grande potere di introspezione. Dopo aver selezionatoArray attraverso il menu Lang è possibile accedere alla definizionedella classe Array attraverso Open Class Def.

3.3–36

1 Array[slot] : ArrayedCollection {

3 *with { arg … args;4 // return an array of the arguments given5 // cool! the interpreter does it for me..6 ^args7 }8 reverse {9 _ArrayReverse

10 ^this.primitiveFailed11 }12 scramble {13 _ArrayScramble14 ^this.primitiveFailed15 }16 mirror {17 _ArrayMirror18 ^this.primitiveFailed19 }

21 // etc

23 sputter { arg probability=0.25, maxlen = 100;24 var i=0;25 var list = Array.new;26 var size = this.size;27 probability = 1.0 - probability;28 while { (i < size) and: { list.size < maxlen }}{29 list = list.add(this[i]);30 if (probability.coin) { i = i + 1; }31 };32 ^list33 }

35 // etc36 }

3.3–37

Senza addentrarsi nel merito si noti come la prima riga (1) definisca laclasse Array come una sottoclasse di ArrayedCollection(Array[slot]: ArrayedCollection ). Fa seguito (da 3 in poi) la lista dei metodi im-plementati per la classe (width, reverse, scramble, ognuno chiusotra una coppia di graffe). Un modo agevole per ottenere una lista deimetodi implementati consiste nello sfruttare il potere di introspezionedi SC. SC fornisce molti metodi per conoscere informazioni relativi alsuo stato interno. I metodi dumpInterface, dumpFullInterface, dum-pMethodList visualizzano sulla Post Window informazioni sui methodiimplementati per l’interfaccia di una classe.

• dumpInterface: stampa tutti i metodi definiti per la classe

• dumpFullInterface: come prima, ma include anche i metodi ereditatidalle sopraclassi della classe

• dumpMethodList: come il precedente, ma con i metodi in ordine alfa-betico e l’indicazione 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ù lun-ghe.

code/oggetti/Array.sc

3.3–38

1 Array.dumpInterface2 reverse ( )3 scramble ( )4 mirror ( )5 mirror1 ( )6 mirror2 ( )7 stutter ( n )8 rotate ( n )9 pyramid ( patternType )

10 pyramidg ( patternType )11 sputter ( probability, maxlen )12 lace ( length )13 permute ( nthPermutation )14 allTuples ( maxTuples )15 wrapExtend ( length )16 foldExtend ( length )17 clipExtend ( length )18 slide ( windowLength, stepSize )19 containsSeqColl ( )20 flop ( )21 multiChannelExpand ( )22 envirPairs ( )23 shift ( n )24 source ( )25 asUGenInput ( )26 isValidUGenInput ( )27 numChannels ( )28 poll ( interval, label )29 envAt ( time )30 atIdentityHash ( argKey )31 atIdentityHashInPairs ( argKey )32 asSpec ( )33 fork ( join, clock, quant, stackSize )34 madd ( mul, add )35 asRawOSC ( )36 printOn ( stream )37 storeOn ( stream )38 prUnarchive ( slotArray )39 jscope ( name, bufsize, zoom )40 scope ( name, bufsize, zoom )41 Array

3.3–39

A partire da un metodo è possibile risalire alle classi che lo implemen-tano: è necessario selezionarlo e scegliere dal menu “Lang” la voce “Im-plementations of”: SC apre una finestra dove sono listati le classi cherispondono al metodo e la lista delle keyword.Tornando a z, un metodo più agevole per creare un array consiste sem-plicemente nello scrivere l’array in questione tra parentesi quadre (se-condo una notazione molto diffusa). Ad esempio, z = [1,2,3,4] asse-gna a z l’array [1,2,3,4] (1). Come si è visto nella definizione di classe,uno dei metodi che la classe Array prevede è reverse: intuibilmente, ilmetodo prende l’array e ne inverte l’ordine. Nel momento in cui si passail messaggio reverse a z che ne diventa il ricevente (5), z cerca il metodoreverse tra quelli che sono definiti nella sua classe, e si comporta con-seguentemente. Nel caso in questione, come l’operazione di inversionevenga svolta da SC non è interessante in questa sede (ne è molto interes-sante di per sé): in ogni caso, se si vede la definizione del metodo (supra,8-11), si nota come il metodo chiami una riga misteriosa, _ArrayReverse(supra, 9): l’operazione di inversione è allora realizzata da una primitivadi SC, scritta in linguaggio C e non in SC. Al contrario il metodo sput-ter è scritto completamente in SC. I metodi restituiscono delle entitàcome risultato delle operazioni svolte: queste entità sono oggetti a tuttigli effetti. Ad esempio, z.reverse restituisce un nuovo array, invertitorispetto a z (7). Poiché è un oggetto diverso, se si intende riutilizzar-lo, è necessario assegnarlo ad una variabile, la stessa z ad esempio (12).L’ultimo oggetto restituito viene stampato da SC sullo schermo comerisultato del processo di intepretazione.

3.3–40

1 z = [1,2,3,4]

3 [ 1, 2, 3, 4 ]

5 z.reverse

7 [ 4, 3, 2, 1 ]

9 z10 [ 1, 2, 3, 4 ]

12 z = z.reverse13 [ 4, 3, 2, 1 ]14 z15 [ 4, 3, 2, 1 ]

17 z.mirror18 [ 4, 3, 2, 1, 2, 3, 4 ]

20 z21 [ 4, 3, 2, 1 ]

23 z.reverse.mirror.mirror24 [ 1, 2, 3, 4, 3, 2, 1, 2, 3, 4, 3, 2, 1 ]

In una nuova sessione con l’interprete viene valutato il codice z = [1,2,3,4](1): SC restituisce l’array [ 1, 2, 3, 4 ] (2) e lo assegna a z. Nella rigasuccessiva z.reverse (5) chiede a z di svolgere le operazioni previsteda reverse: il risultato è [ 4, 3, 2, 1 ], che non è assegnato ad alcu-na 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 èl’array z invertito (15). Il metodo mirror genera un nuovo array a par-tire 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.4–41

senza assegnarlo a z, come prima (20-21). L’ultima riga (23) di codicemette in luce un aspetto importante di SC: il cosiddetto concatenamen-to dei messaggi (“message chaining”). Sul risultato di z.reverse vienecalcolato mirror, su questo secondo risultato viene calcolato mirror dinuovo. Si consideri il risultato stampato sulla Post Window (24). Questisono i passaggi a partire da z = [ 4, 3, 2, 1 ] (valore iniziale più tremessaggi):

[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 direndere di difficile lettura il codice.

3.4 I metodiditipo post e dump

Si è detto che tutti i metodi restituiscono un oggetto. A scanso di equi-voci va ricordato il comportamento dei metodi che permettono di ot-tenere informazioni attraverso la Post Window. Esempi già visti sonodumpClassSubtree e dumpSubclassList, e dumInterface, dumpFullIn-terface, dumpMethodList. Il metodo più usato per ottenere genericheinformazioni su quanto sta avvenendo è postln, che stampa una strin-ga di informazione relativa all’oggetto su cui è chiamato. Ad esempio siconsideri questo codice:

1 Array.postln2 Array3 Array

3.4–42

Viene chiamato il metodo postln su Array (1). SC esegue il codice ilquale prevede di stampare informazioni su Array: in questo caso, es-sendo Array una classe, viene semplicemente stampato il nome del-la classe, Array appunto (2). Infine SC stampa sempre sullo schermoun’informazione sull’ultimo oggetto su cui è stato invocato un meto-do: in sostanza SC chiama sull’ultimo oggetto proprio postln. E infattisi ottiene di nuovo Array (3). Se in questo caso l’utilità di postln è vir-tualmente nulla, si consideri invece questo caso:

1 z = [ 4, 3, 2, 1 ]

3 [ 4, 3, 2, 1 ]

5 z.postln.reverse.postln.mirror.postln.mirror

7 [ 4, 3, 2, 1 ]8 [ 1, 2, 3, 4 ]9 [ 1, 2, 3, 4, 3, 2, 1 ]

10 [ 1, 2, 3, 4, 3, 2, 1, 2, 3, 4, 3, 2, 1 ]

12 z.postln.reverse.postln.mirror.postln.mirror.postln

14 [ 4, 3, 2, 1 ]15 [ 1, 2, 3, 4 ]16 [ 1, 2, 3, 4, 3, 2, 1 ]17 [ 1, 2, 3, 4, 3, 2, 1, 2, 3, 4, 3, 2, 1 ]18 [ 1, 2, 3, 4, 3, 2, 1, 2, 3, 4, 3, 2, 1 ]

Come nel caso precedente, a z è assegnato l’array [ 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 schermoil risultato intermedio resituito da ognuno dei metodi invocati. Si no-ti come sia inutile (in questo caso) concatenare di nuovo postln dopo

3.5–43

l’ultimo mirror (come avviene in 12), visto che per definizione SC stam-pa il risultato dell’ultima computazione (ciò che mirror restituisce). Cisi potrebbe aspettare che, poiché postln serve a stampare sullo scher-mo, ciò che questo metodo restituisce sia un oggetto di tipo stringa, uninsieme di caratteri. Non è così: postln

• stampa una stringa sullo schermo

• restituisce l’oggetto su cui è chiamato il metodo

Questo comportamento è assolutamente utile in fase di correzione deglierrori, perché permette di concatenare messaggi di stampa per verifica-re i comportamenti dei metodi invocati, ma senza per questo interferirecon il processo. Se infatti il metodo postln restituiscce una stringa, al-lora in z.postln.reverse il messaggio reverse sarebbe ricevuto da unoggetto di tipo stringa e non da un oggetto array, come nell’esempioseguente.

1 "[ 1, 2, 3, 4 ]".reverse

3 ] 4 ,3 ,2 ,1 [

Questo vale per tutti metodi di introspezione. Si osservino gli esempidi Collection.dumpClassSubtree, Collection.dumpSubclassList,Array.dumpInterface . In tutti e tre i casi l’ultima riga stampa l’oggettoche il metodo restituisce: si noti come venga restituita la classe, secon-do quanto stampato nell’ultima riga delle rispettiva schermate (Collec-tion, Collection, Array). Questo tipo di comportamento vale tipica-mente per tutti i metodi di stampa e di introspezione. Sono molti: adesempio varianti di postln sono post, postc, postcln.

3.5–44

3.5 Numeri

L’interprete di SC può essere utilizzato come calcolatore. Ad esempio,si cosideri questa sessione interattiva:

1 2.3*22 4.6

4 4/35 1.3333333333333

7 4**38 64

10 4+2*311 18

Due cose sono di rilievo. La prima concerne l’ordine di esecuzione. Sesi osserva 4+2*3 si nota come non ci sia gerarchia tra gli operatori: adesempio, la moltiplicazione non viene valutata prima dell’addizione.Nella riga prima viene valutato 4+2, quindi *3 che viene riferito al ri-sultato dell’operazione precendente (4 + 2 = 6× 3 = 18). Il secondoaspetto che dovrebbe stupire il lettore è che la sintassi utilizzata con-traddice il presupposto per cui in SC ogni cosa è un oggetto dotato diun’interfaccia, per cui ogni operazione dovrebbe seguire il modello ge-nerale oggetto.metodo. Qui in effetti SC fa un’eccezione, almeno per lequattro operazioni, che possono essere scritte in modo più intuitivo nel-la consueta forma funzionale. Ciò non toglie che i numeri (interi, a vir-gola mobile, etc.) siano oggetti a tutti gli effetti. Se si chiama il metodo

3.5–45

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

1 5.class2 Integer

Si può allora chiamare su Integer il metodo superclasses che resti-tuisce un array (attenzione, questa volta il metodo restituisce un array)contenente tutte le sopraclassi fino a Object.

1 Integer.superclasses

3 [ class SimpleNumber, class Number, class Magnitude, class Object ]

Intuibilmente, a parte Object, che è sopraclasse di tutto, Magnitude è laclasse che più in generale si occupa di grandezze (tra cui i numeri).

1 Magnitude.allSubclasses

3 [ class Association, class Number, class Char, class Polar, classComplex, class SimpleNumber, class Float, class Integer ]

Con Magnitude.dumpClassSubtree si accede ad una rappresentazionead albero delle sottoclassi di Magnitude: tutte le classi che si occupanodi grandezze: Integer -i numeri interi- è vicino a Float -i numeri a vir-gola mobile-, poiché sono due sottoclassi di SimpleNumber. Qust’ultimaclasse fa parte del più vasto insieme delle sottoclassi di Number, i numeriin generale, compresi quelli polari e quelli complessi (Polar, Complex,che qui non interessano).

3.5–46

1 Magnitude.dumpClassSubtree2 Magnitude3 [4 Association5 Number6 [7 Polar8 Complex9 SimpleNumber

10 [ Float Integer ]11 ]12 Char13 ]14 Magnitude

In quanto oggetto, è possible passare ai numeri, il 3 ad esempio, il mes-saggio postln, il quale stampa il numero e restituisce il numero stesso.

1 3.postln2 33 3

5 3.postln * 4.postln6 37 48 12

Per numerose operazioni matematiche è disponibile una doppia nota-zione, funzionale e ad oggetti. Ad esempio sqrt(2) (10) chiede di ese-guire la radice quadrata di 2: in notazione ad oggetti si tratta di chia-mare su 2 il metodo sqrt (14), che restituisce il risultato dell’operazioneradice quadrata applicata all’oggetto su cui è chiamato. Analogamen-te, l’elevamento a potenza può essere scritto funzionalmente come 4**3

3.5–47

(18), oppure come 4.pow(3) (22): si chiama il metodo pow con argomen-to 3 sull’oggetto 4. Ovvero, “oggetto 4, èlevati a potenza con esponente3”.

1 sqrt(2)

3 1.4142135623731

5 2.sqrt

7 1.4142135623731

9 4**3

11 64

13 4.pow(3)

15 64

4.2–48

4 Sintassi:elementifondamentali

4.1 (Parentesi)

Negli esempi di codice SC si troveranno spesso le parentesi tonde, (),utilizzate come delimitatori. Le parentesi tonde non sono esplicitamentepreviste a tal fine nella sintassi di SC. Tuttavia è una convenzione che laloro presenza indichi un pezzo di codice che deve essere valutato tuttoinsieme (ovvero, selezione di tutte le linee e valutazione). In Mac OSXil doppio-click dopo una parentesi tonda di apertura permette la sele-zione di tutto il blocco di codice: le parentesi agevolano perciò di moltol’interazione dell’utente con l’interprete. Nell’esempio di pagina 71 leparentesi alle righe 1 e 20 indicano il che il codice 2-19 è un blocco unico.In generale la presenza delle parentesi è per convenzione un’indicazionenon all’interprete SC quanto all’utente (magari a se stessi il giorno dopoche si è scritto il codice).

4.3–49

4.2 //Commenti(/*didiversotipo*/)

La colorazione della sintassi nell’esempio in discussione evidenzia inrosso i commenti (“comments”), le parti di codice che l’interprete nontiene in considerazione e che si rivelano molto utili per rendere il codiceleggibile fino all’autodocumentazione. In SC i commenti sono di duetipi:

a. // indica un commento che occupa una riga o la parte terminale diessa;

b. la coppia /* */, che delimita un commento multilinea: tutto ciò cheè incluso tra i due elementi è ignorato dall’interprete.

4.3 ”Stringhe”

Una sequenza di caratteri delimitata da doppi apici è una stringa. Lestringhe possono occupare più linee. La classe String è una sottoclassedi RawArray: le strighe sono cioè sequenze di oggetti a cui è possibileaccedere.

4.4–50

1 t = "stringa"2 stringa

4 t[0]5 s

7 t[1]

9 t

11 t.size12 7

Così, t[0] chiede il primo elemento dell’array "stringa", ovvero s, ecosì via.È nella classe String che vengono definiti i metodi post e affini. Quan-do si invia ad un oggetto il messagio di post, SC tipicamente chiedeall’oggetto una sua rappresentazione in stringa, e sulla stringa richiamail metodo post. Ad esempio il metodo di concatenazione di stringhe ++vale anche se gli oggetti concatenati alla prima stringa non sono strin-ghe: ++ chiede internamente a tutti gli oggetti una loro rappresentazionecome stringa: . . .++ 5 equivale cioè a . . .++ (5.asString).

1 "stringa"++5

3 stringa5

4.4 Variabili

4.4–51

In SC è necessario dichiarare le variabili che si intendono utilizzare. Inomi di variabili devono iniziare con un carattere alfabetico minuscoloe possono contenere caratteri alfanumerici (caratteri maiuscoli, numeri).La dichiarazione delle variabili richiede la parola riservata var (che dun-que non può essere usata come nome di variabile). È possibile assegnareun valore ad una variabile mentre la si dichiara.

1 var prima, seconda;2 var terza = 3;3 var quarta;

5 nil

Nell’esempio si noti che l’inteprete resituisce nil. Nel valutare una se-rie di espressioni, l’interprete resituisce sempre il valore dell’ultima: inquesto caso quello della variabile quarta, a cui non è stato ancora asse-gnato alcun valore, come indicato da nil. La dichiarazione può ancheoccupare più righe, purché iniziali e consecutive. Le lettere a-z sono giàriservate da SC per le variabili d’ambiente: in altre parole si possonoutilizzare (ad esempio in fase di testing) senza bisogno di dichiararle.Nell’esempio (pagina 52, 1) l’utilizzo della variabile a in a = [1,2,3]è ammissibile senza dichiarazione: l’espressione è valutata e alla varia-bile viene assegnato l’array (SC restituisce l’array, 2). Nella valutazionedell’espressione seguente

4.4–52

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

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

10 nil

l’interprete solleva un errore, poiché riconoscendo un’assegnazione divalore a una variabile, rileva che la variabile in question (array) non èstata dichiarata (• ERROR: Variable 'array' not defined.). Il proble-ma si risolve dichiarando la variabile:

1 (2 var array ;3 array = [1,2,3]4 )5 [ 1, 2, 3 ]

Si noti l’uso delle parentesi tonde ad indicare che le riga di codice van-no valutate tutte insieme. L’esistenza della variabile vale soltanto per ilmomento in cui viene valutato il codice. In altre parole, se si esegue ilseguente codice:

1 array.postln

Si ottiene di nuovo:

4.5–53

1 • ERROR: Variable 'array' not defined.2 in file 'selected text'3 line 1 char 5:4 array•.postln5 ———————————–6 nil

Durante una sessione interattiva può essere desiderabile mantenere l’esistenzadi variabili per poterle riutillizare in un secondo momento. A tal fine,si possono utilizzare le variabili d’ambiente13. Come si è visto, i carat-teri alfabetici sono per convenzione interna assegnabili senza dichiara-zione. In più, ogni variabile il cui primo carattere sia è una variabiled’ambiente. Una volta dichiarata in questo modo

1 ~array = [1,2,3]

la variabile array è persistente per tutta la sessione14.

1 ~array

3 [ 1, 2, 3 ]

Una nota più avanzata. Le variabile sono “d’ambiente” perché sono persistenti13

nell’intera sessione interattiva di lavoro con SC. Sebbene possa sembrare, non sonointese come variabili globali (che sono molto pericolose per la programmazione, sem-plicemente perché sono sempre accessibili, e dunque scarsamente controllabili). Ser-vono 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è dell’ambito di validità delle14

variabili.

4.5–54

4.5 Simboli

Un simbolo è un nome che rappresenta qualcosa. Può essere pensatocome un identificativo assoluto. È cioè un nome che rappresenta univo-camente un oggetto. Si scrive tra apici semplici, o, nel caso la sequenzadi caratteri non preveda spazi al suo interno, preceduto da un \

1 a = \symbol

3 symbol

5 b = 'sono un simbolo'

7 sono un simbolo

9 [a.class, b.class].postln

11 [ class Symbol, class Symbol ]

Una stringa è una sequenza di caratteri. Ad esempio, qui a e b sono duestringhe.

1 a = "symbol"

3 symbol

5 b = "symbol"

7 symbol

4.6–55

Le due stringhe sono equivalenti (detto approssimativamente: “hannolo stesso contenuto”).

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

ma non sono lo stesso oggetto.

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

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

1 a = \symbol

3 symbol

5 b = 'symbol' // scrittura equivalente

7 symbol

9 a == b10 true

12 a === b // lo stesso oggetto?

14 true // sì

4.6 Espressioni;

4.6–56

Un’espressione in SC è un enunciato finito e autonomo del linguaggio:una frase conclusa, si potrebbe dire. Le espressioni in SC sono delimitatedal ;. Ogni blocco di codice chiuso da un ; è dunque un’espressione diSC. Quando si valuta il codice SC nell’interprete, se il codice è compostodi una riga sola è possibile omettere il ;.

1 a = [1,2,3]

3 [ 1, 2, 3 ]

5 a = [1,2,3] ;

7 [ 1, 2, 3 ]

In generale, meglio prendere l’abitudine di mettere il ; anche quando siesperimenta interattivamente con l’interprete riga per riga. Quando sivalutano più linee di codice la presenza del ; è l’unica informazione di-sponibile all’interprete per sapere dove finisce un’espressione e ne iniziaun’altra. Le due espressioni seguenti sono uguali, poiché l’a capo non èrilevante per SC (questo permette di utilizzare l’a capo per migliorarela leggibilità del codice). Si noti come in questo caso l’assenza del pun-to in virgola nella seconda versione non crei problemi. In assenza del ;SC considera un’espressione tutto ciò che è selezionato: in questo casoil codice selezionato è effettivamente un’unica espressione “sensata”, edunque l’inteprete non segnala errori.

4.7–57

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

5 (6 a = [ 1,7 2,8 3 ]9 )

L’ordine delle espressioni è l’ordine in cui SC esegue le istruzioni fornite.

4.7 •Errori

Si riconsideri l’esempio precedente:

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

L’esempio illustra il modo in cui SC segnale gli errori. SC è molto fiscale:è un inteprete decisamente poco caritatevole. Questo richiede una par-ticolare attenzione ai principianti, che rischiano di impiegare un tempointeressante prima di riuscire a costruire un’espressione corretta. In più,la segnalazione degli errori è piuttosto laconica in SC: se nel caso pre-cedente è decisamente chiara, in altri può esserlo meno. In particolare

4.8–58

può essere poco agevole individuare dov’è che si trova l’errore. Di so-lito la parte di codice segnalata da SC mentre riporta l’errore è il puntoimmediatamente successivo a dove si è verificato l’errore. Nel caso inesame, 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 perchi non arrivi da un background informatico. Si consideri la definizionefornita dall’help file:

“A Function is an expression which defines operations to be per-formed when it is sent the ’value’ message.”

La definizione è precisa ed esaustiva. Una funzione è:

1. un’espressione

2. che definisce operazioni

3. che vengono effettuate soltanto nel momento in cui la funzione rice-ve il messaggio value. Una funzione è perciò un oggetto: implemen-ta un metodo value con cui risponde al messaggio value (si proviFunction.dumpInterface).

Una funzione può essere pensata come un oggetto (fisico) capace di unfare certe cose. Nel momento in cui la si dichiara si dice a SC di costruirel’oggetto, non di farlo funzionare. A quel punto l’oggetto c’è: si tratta poidi metterlo in funzione quando serve attraverso il messaggio value.Le definizioni delle funzioni sono racchiuse tra parentesi graffe {}.

4.8–59

1 f = { 5 } ;

3 a Function

5 f.value ;6 5

La funzione f è un oggetto che butta fuori a richiesta il valore 5. La de-finizione stocca l’oggetto funzione il cui comportamento viene attivatoa richiesta attraverso il messaggio value (5, 6).

1 g = { arg input; input*2 } ;

3 a Function

5 g.value(2) ;

7 4

9 g.value(134) ;

11 268

Un uso più interessante delle funzioni prevede l’uso di “argomenti”:gli argomenti possono essere pensati come gli input dell’oggetto. Gliargomenti vengono definiti attraverso la parola riservata arg cui fan-no seguito i nomi degli argomenti separati da una , e delimitati da un;. Ad esempio la funzione g è definita come { arg input; input*2} (8): g accetta un argomento e restituisce il risultato dell’operazionesull’argomento. In particolare g restituisce il doppio del valore inputche gli viene dato in entrata. La funzione g è come un frullatore: si mettel’uovo input in entrata e il frullatore restituisce il risultato di frullato-re.sbatti(uovo) in uscita.

4.8–60

1 h = { arg a, b; (a.pow(2)+b.pow(2)).sqrt } ;

3 a Function

5 c = h.value(4,3) ;

7 5

Infine 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 due cateti a e b e restituisce l’ipotenusa c, secondola relazione c =

√a2 + b2 (24, 26). Si noti tra l’altro come la radice

quadrata sia un messaggio inviato all’intero risultante dal calcolo dellaparentesi.In una seconda versione (qui di seguito) la definizione non cambia in so-stanza ma permette di definire ulteriori aspetti. Al codice fa seguito unasessione della Post window in cui la funzione definita viene utilizzata.

1 (2 h = { // calcola l'ipotenusa a partire dai cateti3 arg cat1, cat2 ;4 var hypo ;5 hypo = (cat1.pow(2)+cat2.pow(2)).sqrt ;6 "hypo: "++hypo } ;7 )

code/sintassi/simpleFunc.sc

4.8–61

1 h.value(4,3) ;

3 hypo: 5

5 h.value(4,3).postln.class

7 hypo: 58 String

Si notino alcuni passaggi:

• i commenti funzionano come al solito all’interno delle funzioni (2);

• i nomi degli argomenti seguono i criteri definiti per le variabili (3);

• a seguito degli argomenti è possibile aggiungere una dichiarazionedi variabili (4). Nel corpo della funzione, specie se complessa, puòessere utile avere a disposizione dei nomi di variabili. In questo ca-so, hypo è un nome significativo che permette di rendere più leggi-bile l’ultima riga, in cui vi si fa riferimento ("hypo: "++hypo). Per levariabili valgono le osservazioni già riportate.

• una funzione restituisce un unico valore (sia esso un numero, unastringa, un oggetto, un array, etc): il valore dell’ultima espressio-ne definita nel corpo della funzione. L’ultima espressione è alloral’output della funzione. In particolare la funzione restituisce una strin-ga composta da "hypo: " a cui è concatenato attraverso ++ il conte-nuto della variabile hypo. Ciò che la funzione restituisce in questocaso è una stringa (cfr. 5-8 della Post window).

Quest’ultimo punto ha conseguenze di rilievo. Se si ridefinisce h -nellasua prima versione- secondo quanto proposto dall’esempio seguente, sene altera radicalmente il funzionamento. L’aggiunta dell’espressione a

4.8–62

in coda alla definizione fa sì che la funzone h restituisca in uscita a (cioèil primo argomento in entrata (1)).

1 h = { arg a, b; (a.pow(2)+b.pow(2)).sqrt ; a } ;

3 a Function

5 h.value(4,3) ;

7 4

9 h.value(3,4)

11 3

In definitiva una funzione ha tre parti, tutte e tre opzionali, ma in ordinevincolante:

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 ogget-to che accetta entità in entrata, ma non fa nulla. La funzione i accettaa in entrata, ma alla dichiarazione degli argomenti non fanno seguitoespressioni: 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.8–63

1 i = {arg a;}2 a Function

4 i.value(3)5 nil

7 l = {}

9 a Function

11 l.value

13 nil

La situazione può essere schematizzata come in figura 4.1, dove le fun-zioni sono rappresentate come moduli, che possono essere dotati di en-trate ed uscite. Il testo in grassetto nell’ultima riga rappresenta il codiceSC, il testo in corpo normale i possibili input e output.

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

a+b5nilnil

a+b5

a b a b

{ }

...arg a, barg a, b

Fig. 4.1 Funzioni.

L’introduzione delle funzioni permette di affrontare il problema dell’ambitodi visibilità delle variabili (“scope”). Nell’esempio seguente seguente,

4.9–64

func.value restituisce 8 perché la variabile val, essendo dichiarata fuo-ri della funzione func, vale anche al suo interno.

1 (2 var val = 4 ;3 var func = { val*2 } ;4 func.value ;5 )

Qui di seguito invece func restituisce sempre 8, ma è ciò dipende dalladichiarazione di val all’interno di func.

1 (2 var val = 4 ;3 var func = { arg val = 4 ; val*2 } ;4 func.value ;5 )

Tant’è che l’esempio seguente solleva un errore perché a val (dichiaratadentro func non è assegnato valore.

1 (2 var val = 4 ;3 var func = { arg val ; val*2 } ;4 func.value ;5 )

In sostanza, l’ambito di visibilità delle variabili procede dall’esterno all’interno.Una variabile è visibile fintanto che non viene lo stesso nome non è di-chiarato più internamente.

code/sintassi/scope1.sc
code/sintassi/scope2.sc
code/sintassi/scope3.sc

4.9–65

4.9 Classi,messaggi/metodiekeyword

Si è già visto come in SC le classi siano indicate da una sequenza dicaratteri che inizia con una maiuscola. Si osservi l’esempio seguente diinterazione con la Post Window:

1 superCollider2 • ERROR: Variable 'superCollider' not defined.3 in file 'selected text'4 line 1 char 13 :5 superCollider•6 ———————————–7 nil

9 SuperCollider10 • ERROR: Class not defined.11 in file 'selected text'12 line 1 char 13 :13 SuperCollider•14 ———————————–15 nil

Come già ricordato, un messaggio viene inviato a una classe a ad un og-getto con il .: rispettivamente per il tramite delle sintassi Classe.metodoe oggetto.metodo. I metodi possono in fondo essere pensati come fun-zioni definite per una classe o un oggetto: quando si invoca un metodotramite un messaggio è come se si mandasse un messaggio value a unafunzione. In più, anche i metodi possono avere argomenti che costitui-scono i parametri di entrata. SC tipicamente prevede opportuni valoridi default nel caso in cui il metodo richieda degli argomenti, così che

4.10–66

in molti casi non è necessario specificare gli argomenti. L’uso delle key-word è utile perché consente di selezionare di quale argomento si vuolespecificare il valore, lasciando agli altri i valori predefiniti. Laddove nonsi usino le keyword, l’unico criterio disponibile per SC per poter attri-buire un valore ad un argomento è l’ordine in cui il valore si presentanella lista degli argomenti. Ad esempio il metod plot per gli oggettiArrayedCollection prevede gli argomenti

plot(name, bounds, discrete, numChannels, minval, maxval, parent,

labels)

Il metodo crea una finestra e vi disegna sotto forma di spezzata un og-getto di tipo ArrayCollection. L’argomento name definisce il titolo dellafinestra. Così, la finestra creata da [1,2,3,1,2,3].plot("test") ha co-me titolo “test”. Il metodo consente anche di definire il numero dei ca-nali numChannels. Se il numero è superiore a 1, plot assume che i primin 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 nume-ro 1 dei canali 1 e 2, e così via: plot disegna una finestra per canale15. Sesi volesse specificare che numChannels deve essere pari a 2, senza key-word sarebbe necessario specificare anche gli argomenti precedenti. Adesempio:

[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)

L’utilità del metodo sta nel fatto che i segnali audio multicananle, rappresentabili at-15

traverso un array, sono memorizzati in questa forma. Se il segnale è stereo, plot connumChannels:2 disegna la forma d’onda dei segnali sui due canali.

4.10–67

Infine, l’uso delle keyword è in generale leggermente più costoso da unpunto di vista computazionale ma rende il codice molto più leggibile.

4.10 Esempio

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

4.10.1 IlpatternGUI

La generazione di elementi GUI (Graphical User Interface) richiede dicostruire oggetti GUI, cioè di istanziare oggetti a partire da classi spe-cializzate. Intuibilmente, ogni gestore grafico definisce le sue classi. Adesempio, se si vuole creare una finestra usando il gestore di Mac Cocoa,è necessario istanziare un oggetto dalla classe SCWindow. Se invece (comeavviene necessariamente su Windows) si vuole creare una finestra ana-loga usando SwingOSC bisogna invece utilizzare la classe JSCWindow.Intuibilmente, questa soluzione vincola la programmazione della GUIal gestore grafico. SwingOSC definisce le sua classi a partire da Cocoa,che è stato il primo gestore grafico: come si vede nell’esempio, aggiungeuna J alle classi definite in Cocoa. Così, una strategia di “cerca e sostitui-sci” permette di adattare il codice per usare uno dei due gestori. Sebbenepraticabile, la soluzione non è soddifacente:

4.10–68

• “cerca e sostituisci” lavora sulle stringhe e potrebbe introdurre errorinon previsti

• ne risultano due copie del codice• non vale per altri eventuali gestori grafici

Tuttavia, si potrebbe anche osservare come tutti i gestori grafici sostan-zialmente offrano funzionalità simili: finestre, pulsanti, cursori, etichet-te, campi per l’immissione di testo, e così via. Si possono allora defi-nire astrattamente le funzionalità che un gestore deve/può offrire, perpoi chiedere al gestore attualmente in uso (o disponibile) di realizzarle.Nell’esempio precedente, ciò che interessa è una finestra,indipendentementedla gestore grafico. La classe GUI implementa questo comportamento, edè un esempio interessante di programmazione ad oggetti. Per costruireuna finestra 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 grafico prescelto. Dun-que, GUI riceve un messaggio window, e seleziona la classe opportunapresso il gestore grafico attuale. A questa classe viene inviato il messag-gio new. Si noti la differenza:

4.10–69

1 w = GUI.window

3 JSCWindow

5 w = GUI.window.new

7 a JSCWindow

Nel primo caso (1), SC risponde restituendo la classe che implementa lafunzione astratta window (“finestra”) presso il gestore attuale, cioè JSC-Window (3). Nel secondo caso (5), avviene la stessa cosa, ma attraverso ilconcatenamento dei messaggi, alla classe così ottenuta si invia il messag-gio new, che istanzia un oggetto attuale (7). Poiché il gestore predefinitosu Mac è Cocoa, in quel caso si avrebbe tipicamente:

1 w = GUI.window

3 SCWindow

5 w = GUI.window.new

7 a SCWindow

Attraverso l’uso della classe GUI, può anche cambiare il gestore grafico,ma ciò non richiede di cambiare il codice, che astrae la funzione (“fareuna finestra”) rispetto all’implementazione (“con SwingOSC”, con “conCocoa”, e così via). Nel caso in cui si abbiano più gestori disponibili,è possibile selezionare il gestore attuale attraverso un messaggio allaclasse GUI.

4.10–70

1 GUI.cocoa // seleziona Cocoa (Mac)2 CocoaGUI

4 GUI.swing //selezione SwingOSC (Mac, Win, Linux)5 CocoaGUI

Gli esempi successivi utilizzano sempre il metodo descritto.

4.10.2 Ilcodice

4.10–71

1 (2 /* accoppiamento di view e controller */

4 var window, knob, screen ; // dichiarazione delle variabili usate

6 // una finestra contenitore7 window = GUI.window.new("Knob", Rect(300,300,100, 100)) ;

9 // una manopola nella finestra, range: [0,1]10 knob = GUI.knob.new(window, Rect(30, 30, 50, 50)) ;11 knob.value = 0.5 ;

13 // una finestra-sfondo14 screen = GUI.compositeView.new(window,Rect(0,0, 150,150));15 screen.background = Color.black;

17 // azione associata a knob18 knob.action_({ arg v;19 var red, blue, green ;20 red = v.value ;21 green = red*0.5 ;22 blue = 0.25+(red*0.75) ;23 ["red, green, blue", red, green, blue].postln ;24 screen.background = Color(red, green, blue);25 });

27 // non dimenticarmi28 window.front ;29 )

4.10.3 Introduzione

code/sintassi/simpleKnob2.sc

4.10–72

• 1: il blocco di codice è racchiuso tra parentesi tonde (1 e 31);

• 3: un commento multilinea è utilizzato a mo’ di titolo (3) e molti altricommenti che forniscono alcune informazioni sulle diverse parti delcodice (ad esempio, 7).

• 5: a parte i commenti, il codice inizia con la dichiarazione delle trevariabili utilizzate;

4.10.4 CreazionedeglielementiGUI

• 7-9: la prima cosa da fare è creare una finestra contenitore, cioè unoggetto di riferimento per tutti gli altri elementi grafici che verran-no creati in seguito. È un approccio tipico nei sistemi di creazione diGUI. Alla variabile window viene assegnato un oggetto GUI.window,generato attraverso il metodo costruttore new. A new vengono pas-sati due argomenti: una stringa che indica il titolo visualizzato dallafinestra ("Knob" e un oggetto di Rect, cioè un rettangolo di 100x100pixel, il cui angolo superiore sinistro è nel pixel (300,300).

• 10-12: la costruzione di un elemento grafico a manopola segue unprocedimento analogo a quanto avvenuto per la finestra-contenitore.Alla variabile knob viene assegnato un oggetto GUI.knob (11). Il co-struttore sembra uguale a quello di GUI.window: senonché questavolta è necessario specificare a quale finestra-contenitore vada riferi-to l’oggetto GUI.knob: la finestra-contenitore è window, e il rettangoloche segue prende come punto di riferimento non lo schermo, ma lafinestra window. Dunque un rettangolo 50x50, la cui origine è nel pi-xel (30,30) della finestra window. Il punto di partenza della manopolaè 0.5 (12). Per default l’escursione di valori di un oggetto GUI.knob

4.10–73

varia tra 0.0 e 1.0: dunque attraverso l’attributo knob.value = 0.5 siindica la metà.

• 14-16: si tratta ora di costruire una finestra da “spalmare” sul tuttolo sfondo di window, a cui far cambiare colore attraverso knob. La fi-nestra screen è un oggetto di tipo GUI.compositeView (14). Si notiche è più estesa della finestra-contenitore (è grande 150x150 pixel,a partire dal punto (0,0) di window): è soltanto un trucco per esseresicuri che occupi tutto lo sfondo visibile (si provi a ridurre le dimen-sioni). All’attributo della finestra screen viene assegnato un coloreattraverso un oggetto Color. Anche i colori sono oggetti in SC e laclasse Color prevede alcuni attributi predefiniti, che permettono diavere 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 rappresentaun display i cui attributi dipendono da knob, che opera come con-troller: si può pensare a screen come ad un dispositivo di outputche dipenda dall’input di knob. All’oggetto knob è possibile associa-re un azione: è previsto per definizione che l’azione venga portata atermine tutte le volte che l’utente cambia il valore di knob, cioè muo-ve la manopola. Una funzione rappresenta opportunamente questotipo di situazione, poiché come si è visto è un oggetto che defini-sce un comportamento richiamabile di volta in volta e parametriz-zato da un argomento. Il metodo knob.action_ chiede di attriburea knob l’azione seguente, descritta attraverso una funzione: la fun-zione è tutto quanto è compreso tra parentesi graffe, 19-26. Quanto

4.10–74

avviene è che, dietro le quinte, quando si muove la manopola al-la funzione viene spedito un messaggio value. Il messaggio valuechiede di calcolare la funzione per il valore della manopola, che èl’input della funzione: dietro le quinte cioè viene inviato alla fun-zione il messaggio value(knob.value). Nella funzione l’input è de-scritto dall’argomento v (19): in altre parole, la funzione risponde al-la domanda “cosa bisogna fare quando arriva un certo valore v dallamanopola knob”.

• 20-25: il comportamento previsto richiede di cambiare il colore disfondo di screen. Vengono dichiarate tre variabili (red, green, blue)(20). Esse identificano i tre componenti RGB del colore di screen,che SC definisce nell’intervallo [0, 1]. A red viene assegnato il va-lore in entrata di v (21). A green e blue due valori proporzionali(ma in modo diverso) a v, in modo da definire un cambiamento con-tinuo in funzione di v nelle tre componenti cromatiche. Quindi sidice di stampare su schermo un array composto da una stringa edei tre valori (24): in questo caso SC stampa gli elementi dell’arraysulla stessa riga opportunamente formattati. La stampa su scher-mo permette di capire come vengono calcolati i valori (ed è utilesenz’altro in fase di debugging). Infine, all’attributo background discreen viene assegnato un oggetto Color, a cui sono passate le trecomponenti. Il costruttore di Color, new accetta cioè le tre compo-nenti RGB in escursione [0,1] come definizione del colore da gene-rare. Dov’è new? In generale tutti i metodi costruttori possono essereomessi per semplificare il codice: quando SC vede una classe seguitada una coppia di parentesi contenenti dati assume che si sia invocatoClass.new(argomenti) . È una delle molte abbreviazioni possibili inSC. Dunque, Color(red, green, blue) è equivalente in tutto e pertutto a Color.new(red, green, blue).

4.11–75

4.10.6 Perfinire

• 29: è importante. Tutti i sistemi GUI distinguono tra creazione e vi-sualizzazione. Un conto è creare gli oggetti GUI, un conto è dire chedebbano essere visibili: questa distinzione permette di fare appari-re/sparire elementi GUI sullo schermo senza necessariamente co-struire e distruggere nuovi oggetti. Il metodo front rende window egli elementi che da essa dipendono visibili: in caso d’omissione tuttofunziona uguale, ma nulla è visualizzato sullo schermo.

4.11 Controllidiflusso

In SC il flusso delle informazioni segue l’ordine di lettura delle espres-sioni. I controlli di flusso sono quei costrutti sintattici che possono mo-dificare quest’ordine. Ad esempio, un ciclo for ripete le istruzioni an-nidata per un certo numero di volte, e quindi procede sequenzialmenteda lì in avanti, un condizionale if valuta una condizione rispetto allaquale il flusso di informazioni si biforca (se è vero / se è falso). I control-li di flusso sono illustrati nell’help file “Control structures”, da cui sonotratti (con una piccola modifica) i tre esempi (rispettivamente if, whilee for).

4.11–76

1 (2 var a = 1, z;3 z = if (a < 5, { 100 },{ 200 });4 z.postln;5 )

7 (8 i = 0;9 while ( { i < 5 }, { i = i + 1; [i, "boing"].postln });

10 )

12 for (3, 7, { arg i; i.postln });

14 forBy (0, 8, 2, { arg i; i.postln });

Nel primo caso è illustrato l’uso di if. La sintassi è:

if ( condizione da valutare, { funzione se è vero } , { funzione

se è falso })

In altre parole la valutazione della condizione dà origine a una biforca-zione a seconda che il risultato sia true oppure false. Passando all’esempio,la variabile a (che è dichiarata anche se potrebbe non esserlo) vale 1. Lacondizione è 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 valore100, che viene assegnato a z: z vale 100.Come è noto, la traduzione in italiano di while (in computer science) è”finché”:

while ({ condizione è vera }, { funzione da eseguire } )

Nell’esempio , i vale 0. Finché i è inferiore a 5, viene chiamata la funzio-ne seguente. La funzione incrementa i (altrimenti non si usicrebbe mai

code/sintassi/controlStructures.sc

4.11–77

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

for (partenza, arrivo, { funzione } )

Nell’esempio La funzione viene ripetuta cinque volte (3, . . . 7). Il va-lore viene passato alla funzione come suo argomento in modo che siaaccessibile: la funzione infatti stampa i per ogni chiamata (3, . . . 7). Sinoti che il fatto che l’argomento si chiami i è del tutto arbitrario. Ovvero:

1 for (3, 7, { arg i; i.postln });2 33 44 55 66 77 3

9 for (3, 7, { arg index; index.postln });

11 312 413 514 615 716 3

L’istruzione ForBy richiede un terzo parametro che specifica il passo:

forBy (partenza, arrivo, passo, { funzione } )

L’esempio è una variazione del precedente che stampa l’escursione [0, 8]ogni 2. Esistono altre strutture di controllo. Qui vale la pena di introdur-re do, che itera sugli elementi di una collezione. Si può scrivere così:

4.11–78

do ( collezione, funzione )

ma molto più tipicamente la si scrive come un metodo definito sullacollezione. Ovvero:

collezione.do({ funzione })

l’esempio è tratto dall’help file “Control-structures”, con alcune piccolemodifiche.

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

3 5.do({ arg item; ("item"+item.asString).postln });

5 "you".do({ arg item; item.postln });

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

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

A scanso d’equivoci, l’ultima riga semplicemente restituisce l’array dipartenza. Alla funzione vengono passati l’elemento su cui sta effettuan-do l’iterazione (item) e un contatore (i). Meglio ribadire: i nomi item ei sono totalmente arbitrari. È il loro posto che ne specifica la semantica.Ovvero:

code/sintassi/do.sc

4.12–79

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

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

La funzione stampa un array che contiene il contatore i (colonna di si-nistra delle prime quattro righe) e l’elemento item (colonna di destra).Il metodo do è definito anche sugli interi (n volte.valuta la funzio-ne). Il funzionamento è illustrato nel secondo esempio. Se lo si esegue siottiene:

1 5.do({ arg item; ("item"+item.asString).postln });

3 item 04 item 15 item 26 item 37 item 48 5

L’ultima riga è l’intero su cui è chiamato il metodo. La funzione stampauna stringa costituita dal concatenamento di "item" e della rappresen-tazione sotto forma di stringa restituta dal metodo asString chiamatosul numero intero item (0, . . . 4). Poiché la maggior parte dei cicli foriterano a partire da 0 e con passo 1, molto spesso si trovano scritti inSC attraverso do. La sintassi di do (oggetto.metodo) è più OOP. Infine,l’ultimo esempio dimostra semplicemente che ogni stringa è una colle-zione i cui elementi sono i singoli caratteri alfanumerici che compongola stringa.

4.12–80

4.12 AncoraunesempioGUI

L’esempio seguente presenta il codice di creazione di una GUI, utiliz-zando SwingOSC. Come nel caso precedente, l’esempio è doppiamen-te eccentrico: non utilizza in nessun modo l’audio ed è completamenteinutile (. . .). In ogni caso, il controllo di elementi GUI è particolarmenteinteressante per dimostrare alcuni aspetti della sintassi, e la visualizza-zione del processo è di aiuto alla comprensione.La widget risultante si compone di un certo numero di manopole. Ognimanopola controlla il colore dei quattro riquadri alla sua destra. Il va-lore della manopola viene passato ad una funzione che calcola il valoredelle componenti rossa, verde e blu. I primi tre riquadri visualizzanosoltanto le singole componenti (rossa, verde e blu), il quarto il colorecomplessivo risultante dal contributo di tutte e tre. Quando si muove lamanopola, si ottengono due comportamenti diversi:

• fino ala metà della sua escursione, la manopola attivata controlla lealtre manopole (in sostanza, le manopole sono sincronizzate).

• oltre la metà, ogni movimento della manopola innesca, in ognunadelle altre, la scelta di un valore casuale nell’escursione possibile, apartire dal quale vengono calcolati i colori dei riquadri (secondo lastessa funzione)

Il processo è illustrato in Figura 4.2, dove si rappresenta il caso in cuile manopole siano quattro e la manopola con cui interagisce l’utente siala numero 1 (la seconda dall’alto). Poiché è oltre la meta, ad ogni scattomuove casualmente le altre tre (0, 1, 3).

4.12–81

Red Green Blue All

0

1

2

3

random

Fig. 4.2 Una GUI widget.

4.12.1 Lafunzionedigenerazionedeivaloricolore

Prima di tutto, si può definire la funzione che mappa il valore della ma-nopola su una tripla di colori.

1 colorFunction = { arg val ;2 var red, green, blue ;3 val = val.max(0).min(1) ;4 red = val ;5 green = (val*0.7)+0.3 ;6 blue = 1.0-(val*0.7) ;7 [red, green, blue]} ;

La funzione accetta un valore in entrata. I colori sono definiti in SCnell’intervallo [0, 1]. La funzione assume perciò genericamente che l’argomentoval sia compreso in quell’intervallo. Se così non fosse, la riga 3 provvedea troncare ogni numero minore di 0 a 0 e ogni numero maggiore di 1 a 1.

code/sintassi/ssColorFunc.sc

4.12–82

Il metodo max(0) restituisce il massimo tra il numero sui cui è invocato e0: se val è negativo, max(0) restituisce 0, altrimenti val. Analogamentesi comporta min(1): se val è minore a 1 viene restituio val, altrimenti 1.Si noti che si sarebbe potuto usare un doppio condizionale (if (val <0 . . . ; if (val > 1 . . . ;) ma serebbe stato più complesso da legge-re e meno efficiente computazionalmente. Le righe 4-6 definiscono unatriplice correlazione tra val e le variabili che rappresentano le compo-nenti rgb16. Poiché val è compreso in [0, 1], lo sono anche i valori delletre variabili. Infine (7), la funzione restituisce un array che contiene i trevalori. La funzione riceve dunque in entrata il valore selezionato dallamanopola (che tra l’altro è compreso nell’intervallo [0, 1]) e restituisceuna tripla i cui valori possono essere utilizzati per regolare il colore deiriquadri. Siccome deve essere richiamata più volte, la funzione vieneassegnata alla variabile colorFunction, che deve essere dichiarata.

4.12.2 ModularizzarelaGUI

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 l’unico 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 finestra contenitore alla quale tut-ti gli elementi fanno riferimento (1), assegnata alla variabile window. Se

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

code/sintassi/ssWin.sc

4.12–83

si osserva la Figura 4.2 si nota come tutti gli elementi (manopole e ri-quadri) siano contenuti in un quadrato. In sostanza si trarra di costruireuna griglia basata su un simile quadrato, assumendo perciò come mo-dulo il lato dello stesso. Si potrebbe “blindare” il lato definendolo inpixel ogni volta (ad esempio, 50 pixel). Poiché si tratta di un modulo,un approccio più flessibile consiste nel definire una variabile step a cuiviene assegnato un valore (ad esempio, 50), e nel richiamarla ogni qual-volta sia necessario. Un ragionamento analogo vale per il numero dellemanopole, che non deve necessariamente essere 4, ma può essere arbi-trario: dunque si può assegnarlo ad una variabile n. Quale dovrà esserela dimensione di window? L’altezza dipende dal numero delle manopole,ognuna delle quali è alta step: dunque, n× step. Ogni gruppo mano-pola/riquadri occupa una riga: quattro riquadri a cui si può aggiungereuno spazio equivalente per la manopola, per un totale di 8×step. Dun-que il rettangolo occupato dalla finestra ha il suo vertice in alto a sinistrain (300, 300), è largo step*8 ed è alto step*n.La programmazione di un’interfaccia grafica può essere piuttosto noio-sa e richiedere molte righe di codice. Se si dovessero gestire direttamen-te tutti gli elementi GUI previsti bisognerebbe allocare (e modificare)5 × n variabili (manopola+quattro riquadri per n). Un approccio piùintelligente (e molto usato in SC) consiste nel raccogliere gli elementi inun array. Questo permette di generare gli elementi stessi sfruttando imetodi di costruzione della classe . Il problema di richiamare ogni og-getto (manca infatti una variabile) può essere risolto attraverso l’indicedell’array, che di fatto identifica in termini univoci l’oggetto. Una similetecnica permette di generare gli elementi GUI anche in assenza di unnumero predefinito. Più concretamente, si consideri la riga 1. L’arrayscreenArrRed contiene tutti gli n elementi di tipo GUI.compositeViewche rappresentano la componente rossa. Attraverso il metodo fill vie-ne costruito un array che contiene riquadri (viene riempito di istanze

4.12–84

GUI.compositeView generate da new: ogni riquadro è un quadrato di di-mensioni step × step. L’aspetto più interessante è il posizionamen-to: ogni riquadro ha infatti una ascissa fissa (step*4, inizia dalla se-conda metà della finestra window), mentre l’ordinata varia in funzionedell’argomento i, che è l’indice progressivo 0 . . . n−1. La riga disegnainsomma la prima colonna di riquadri, che sono memorizzati nell’arraycon gli indici 0 . . . n− 1. Analogamente avviene per le altre tre colon-ne, i cui elementi di ognuna sono contenuti in un array dedicato. Infi-ne, l’array delle manopole. Il ragionamento è del tutto analogo, ma conuna aggiunta “decorativa”: il posizionamento orizzontale varia pseudo-casualmente nell’escursione [0, step × 3]. Se step = 50px alloral’ascissa dell’angolo in alto a sinistra può valere al massimo 150px. Sevi si aggiunge il lato (= step) del quadrato si copre la metà della lar-ghezza della finestra window (200px).

4.12–85

1 screenArrRed = Array.fill(n, { arg i ;2 GUI.compositeView.new(window,Rect(step*4, step*i, step,step));3 }) ;

5 screenArrGreen = Array.fill(n, { arg i ;6 GUI.compositeView.new(window,Rect(step*5, step*i, step, step));7 }) ;

9 screenArrBlue = Array.fill(n, { arg i ;10 GUI.compositeView.new(window,Rect(step*6, step*i, step, step));11 }) ;

13 screenArrRGB = Array.fill(n, { arg i ;14 GUI.compositeView.new(window,Rect(step*7,step*i, step,step));15 }) ;

17 knobArr = Array.fill(n, { arg i ;18 GUI.knob.new(window, Rect(rrand(0,step*3), step*i, step,

step)) ;19 }) ;

La situazione è schematizzata in Figura 4.3, dove sono evidenziati i cin-que array: ognnuno rappresenta una colonna). Allo stesso indice corri-spondono poi gli elementi comuni correlati alla manopola.Dunque, la dichiarazione delle variabili è infine la seguente:

1 var colorFunction ;2 var window, step = 50, n = 4 ;3 var knobArr, screenArrRed, screenArrGreen, screenArrBlue, screenArrRGB

;

code/sintassi/ssGuiElement.sc
code/sintassi/ssVar.sc

4.12–86

screenArrRedscreenArrGreen

screenArrBluescreenArrRGB

knobArr

1

2

0

4

5

3

7

8

6

9

Fig. 4.3 Struttura della GUI widget.

4.12.3 Controllodelleazioni

Si tratta ora di collegare ad ogni manopola l’azione opportuna. In parti-colare l’azione deve prevedere due parti:

4.12–87

1. il calcolo della funzione colore per il valore della manopola e l’aggiornamentodei colori dei riquadri;

2. l’aggiornamento delle altre manopole e dei rispettivi riquadri, in fun-zione della soglia 0.5.

In primo luogo, va osservato come la stessa azione debba essere asso-ciata a tutte le manopole. Poiché le manopole sono raccolte in un array,che è una sottoclasse di Collection è possibile utilizzare il metodo do eciclare sugli elementi di knobArr (cioè: su tutti gli oggetti JKnob). È be-ne ribadire il punto: in un array l’ordine degli elementi è vincolante edunque attraverso l’indice dell’elemento è possibile identificare univo-camente ogni elemento. La funzione contenuta in do ha due argomenti:il primo è l’elemento, il secondo l’indice dell’elemento (ovvero un nu-mero 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 definizione del metodo action_ l’argomento(qui k) indica l’istanza della manopola: dunque, k.value accede al va-lore selezionato dall’utente. Nella definizione sono presenti due parti.

1 rgb = colorFunction.value(k.value) ;2 red = rgb[0] ;3 green = rgb[1] ;4 blue = rgb[2] ;

6 screenArrRed[index].background = Color(red,0,0) ;7 screenArrGreen[index].background = Color(0,green,0) ;8 screenArrBlue[index].background = Color(0,0,blue) ;9 screenArrRGB[index].background = Color(red,green,blue) ;

10 ("operating on knob: "+index +" with: "+rgb.round(0.001)).postln ;

code/sintassi/ssAction1.sc

4.12–88

Nella prima viene descritto il punto 1: con il calcolo della funzione co-lore a partire dal valore assunto dalla manopola vengono assegnate letre componenti alle variabili red, green, blue. Si ricorderà che indexè l’indice dell’elemento manopola. Allo stesso indice corrispondono percostruzione negli array dei riquadri i rispettivi elementi. Cioè, ad esem-pio l’indice 3 seleziona nell’array delle manopole knobArr quella corre-lata ai riquadri con indice 3 nei quattro array screenArrRed, scree-nArrGreen, screenArrBlue, screenArr A questo punto, per ognunodei riquadri viene definito il valore opportuno di colore per la propri-tà background (un’unica componte per i primi tre, tutte per l’ultimo).L’ultima riga stampa sulla Post Window alcuni dati che permettono dicontrollare il risultato del processo17.

1 indexArr = Array.series(n) ;2 indexArr.removeAt(index) ;

L’idea è che, mentre si ruota la manopola, le altre manopole girino an-ch’esse e aggiornino i riquadri. A parte la manopola attivamente con-trollata dall’utente, tutte le altre manopole devono eseguire il compor-tamento di aggiornamento È di nuovo il caso di definire un ciclo, co-me quello in cui l’azione è contenuta, ma definito su tutte le manopo-le a parte quella attiva. L’array indexArr contiene una serie numerica0 . . . n − 1, ovvero tutti gli indici degli array (1). Da questo array ènecessario eliminare l’indice relativo alla manopola che che sta control-lando il processo: si tratta dell’argomento index. Il metodo removeAtrimuove l’elemento specificato, “stringe” la collezione su cui è invoca-to, ma restituisce l’elemento stesso: quindi in questo caso non si effettua la

È una funzionalità molto utile in fase di debugging. Si tenga presenta che può essere17

computazionalmente più costosa di quanto immaginato.

code/sintassi/ssIndexArr.sc

4.12–89

riassegnazione dell’array (indexArr = indexArr.removeAt(index) sa-rebbe uguale a indexArr = index). A questo punto, sui restanti n− 1elementi di tuti gli array è necessario definire l’azione di aggiornamento.

1 indexArr.do({ arg index ;2 var iknob, ired, igreen, iblue, itotal ;3 var rgb ;4 var chosenValue ;5 iknob = knobArr[index] ;6 ired = screenArrRed[index] ;7 igreen= screenArrGreen[index] ;8 iblue = screenArrBlue[index] ;9 itotal = screenArrRGB[index] ;

10 if ( knob.value < 0.5,11 { chosenValue = knob.value ;12 },

14 { chosenValue = 1.0.rand ;15 }16 );

18 iknob.value = chosenValue ;19 rgb = colorFunction.value(chosenValue) ;20 ired.background = Color(rgb[0], 0, 0) ;21 igreen.background = Color(0, rgb[1], 0) ;22 iblue.background = Color(0, 0, rgb[2]) ;23 itotal.background = Color(rgb[0], rgb[1], rgb[2]) ;24 ("knob no. "+index.asString+": "+rgb.round(0.001)).postln;

26 });

In primo luogo qui va notato come index sia il nome di un argomento edunque non indichi più l’indice dell’elemento “esterno”: è una questio-ne di ambito di validità delle variabili. Il ciclo è definito per ogni indexin indexArray. Le righe 5-9 attribuiscono a variabili gli elementi numero

code/sintassi/ssAction2.sc

4.12–90

index degli array di manopole e riquadri: in sostanza viene elaborato ilgruppo (la “fila”) index. Il condizionale valuta il valore della manopolaattiva knob, il cui valore, essendo definito fuori del ciclo, è accessibiledentro il ciclo. Se knob.value è inferiore a 0.5, allora il valore prescel-to (chosenValue) è quello di knob, altrimenti è un valore scelto a casonell’intervallo [0, 1]. Il valore prescelto viene attribuito alla manopolaindex passato a colorFunction: i valori rgb così ottenuti permettono didefinire la proprietà background dei riquadri index Infine, di nuovo unastampa su schermo dei dati calcolati (26).

4.12–91

1 (

3 /* The useless GUI:4 demonstrating some5 syntax stuff6 – first part –7 */

9 var colorFunction ;10 var window, step = 50, n = 4 ;11 var knobArr, screenArrRed, screenArrGreen, screenArrBlue, screenArrRGB

;

13 colorFunction = { arg val ;14 var red, green, blue ;15 val = val.max(0).min(1) ;16 red = val ;17 green = (val*0.7)+0.3 ;18 blue = 1.0-(val*0.7) ;19 [red, green, blue]} ;

21 window = GUI.window.new("Some knobs", Rect(300,300, step*8, step*n)) ;

23 screenArrRed = Array.fill(n, { arg i ;24 GUI.compositeView.new(window,Rect(step*4, step*i, step,step));25 }) ;

27 screenArrGreen = Array.fill(n, { arg i ;28 GUI.compositeView.new(window,Rect(step*5, step*i, step, step));29 }) ;

31 screenArrBlue = Array.fill(n, { arg i ;32 GUI.compositeView.new(window,Rect(step*6, step*i, step, step));33 }) ;

35 screenArrRGB = Array.fill(n, { arg i ;36 GUI.compositeView.new(window,Rect(step*7,step*i, step,step));37 }) ;

39 knobArr = Array.fill(n, { arg i ;40 GUI.knob.new(window, Rect(rrand(0,step*3), step*i, step,

step)) ;41 }) ;

4.12–92

code/sintassi/simpleKnob3a.sc

4.12–93

1 /* The useless GUI: – continue –*/

3 knobArr.do( { arg knob, index;4 knob.value = 0.5 ;5 knob.action_({ arg k;6 var indexArr ;7 var rgb, red, blue, green ;8 rgb = colorFunction.value(k.value) ;9 red = rgb[0] ;

10 green = rgb[1] ;11 blue = rgb[2] ;12 screenArrRed[index].background = Color(red,0,0) ;13 screenArrGreen[index].background = Color(0,green,0) ;14 screenArrBlue[index].background = Color(0,0,blue) ;15 screenArrRGB[index].background = Color(red,green,blue) ;16 ("operating on knob: "+index +" with: "+rgb.round(0.001)

).postln ;17 indexArr = Array.series(n) ;18 indexArr.removeAt(index) ;19 indexArr.do({ arg index ;20 var iknob, ired, igreen, iblue, itotal ;21 var rgb ;22 var chosenValue ;23 iknob = knobArr[index] ;24 ired = screenArrRed[index] ;25 igreen= screenArrGreen[index] ;26 iblue = screenArrBlue[index] ;27 itotal = screenArrRGB[index] ;28 if ( knob.value < 0.5,29 { chosenValue = knob.value ;30 },31 { chosenValue = 1.0.rand ;32 }33 );34 iknob.value = chosenValue ;35 rgb = colorFunction.value(chosenValue) ;36 ired.background = Color(rgb[0], 0, 0) ;37 igreen.background = Color(0, rgb[1], 0) ;38 iblue.background = Color(0, 0, rgb[2]) ;39 itotal.background = Color(rgb[0], rgb[1], rgb[2])

; ("knob no. "+index.asString+": "+rgb.round(0.001)).postln;40 });41 }) ;42 }) ;43 window.front ;44 )

4.12–94

code/sintassi/simpleKnob3b.sc

95

5 Sintesi,I:fondamenti

Un suono è una variazione continua della pressione atmosferica perce-pibile dall’orecchio umano. In quanto vibrazione, dipende dall’eccitazionedi corpi del mondo fisico (una chitarra suonata con un plettro, una tendaagitata dal vento, un tavolo battuto con le nocche). Un suono può esse-re registrato in forma di segnale analogico -cioè rappresentato in formacontinua- o digitale -in forma numerica. Si osservino le due figure dedi-cate alla catena dell’audio analogico e digitale (Figura 5.1).

Onda dipressione

Elaborazioneanalogica

SupportoMicrofono

Onda dipressione

Altoparlante

Elaborazioneanalogica

Scrittura

Lettura

trasduzione elettroacustica

trasduzione elettroacustica

Master

es. disco vinile

es. piatto

Onda dipressione

Elaborazioneanalogica

Microfono

Onda dipressione

Altoparlante

Elaborazioneanalogica

Conversione

Conversione

trasduzione elettroacustica

trasduzione elettroacustica

es. hard disk

DAC

ADC

DIGITALE

Supporto

Fig. 5.1 Catena dell’audio: analogico e digitale

Nel caso dell’audio digitale, il segnale, appunto “digitalizzato”, è dispo-nibile sotto forma di informazione numerica, e dunque può essere riela-borato attraverso un calcolatore: si possono cioè fare calcoli a partire dai

5.1–96

numeri che rappresentano il segnale. I passi principali della registrazio-ne digitale sono i seguenti:

1. conversione anologico-digitale: il segnale analogico viene filtratoe convertito dal dominio analogico (in quanto variazione continuadella tensione elettrica, ad esempio prodotta da un microfono) nelformato numerico attraverso l’ADC;

2. elaborazione: il segnale digitalizzato, che ha assunto la forma di unasequenza di numeri, può essere modificato;

3. conversione digitale-analogica: per essere ascoltabile, la sequenzanumerica che compone il segnale viene riconvertita in segnale ana-logico attraverso il DAC: effettuato il filtraggio, si origina di nuovouna variazione continua della tensione elettrica che può, ad esempio,mettere in azione un altoparlante.

L’assunto di partenza della computer music è che il calcolatore possaessere impiegato per sintetizzare direttamente il suono. Il cuore dellasintesi digitale sta nell’escludere il passaggio 1, generando direttamen-te la sequenza numerica che poi verrà convertita in segnale analogico.Questo non esclude affatto la possibilità di lavorare con campionamentiprovenienti da fonti  “esterne”, ma sottolinea piuttosto come l’aspettofondamentale risieda nei metodi e nelle procedure di calcolo che pre-siedono alla sintesi. Se dunque è sempre possibile per il compositore“digitale” lavorare sulla componente analogica (ad esempio registrandoed elaborando analogicamente il segnale), la componente più caratteriz-zante della sua prassi sta nello sfruttare la natura numerica (e dunque“calcolabile”) del segnale digitale (Figura 5.2.

5.1–97

Onda dipressione

Elaborazioneanalogica

SupportoMicrofono

Scrittura

trasduzione elettroacustica

Master

es. disco vinile

Onda dipressione

Elaborazioneanalogica

Microfono

Conversione

trasduzione elettroacustica

es. hard disk

ADC

DIGITALE

Supporto

Fig. 5.2 Composizione analogica e digitale: ambiti

5.1 Pochecentinaiadiparoled’acustica

Un segnale è una rappresentazione di un andamento temporale. Poi-ché rappresenta una sequenza di compressioni/rarefazioni della pres-sione atmosferica, un segnale audio assume la forma di una oscillazionetra valori positivi e valori negativi. Se quest’oscillazione è regolare neltempo, il segnale è periodico, altrimenti è aperiodico: la maggior partedei suoni si colloca in punto tra i due estremi, è cioè più o meno pe-riodica/aperiodica. Il segnale periodico più elementare è la sinusoide,corrispondente al suono di un diapason. Come si vede in Figura 5.3, col-legando un pennino alla lamella del diapason se ne registra l’escursioneed il tracciato che ne consegue è appunto una sinusoide.La periodicità indica che il segnale si ripete nel tempo: la frequenza di unsegnale è il numero delle ripetizioni nell’unità di tempo mentre la du-rata di un ciclo è il periodo. L’escursione tra il massimo positivo e quello

5.1–98

Fig. 5.3 Vibrazione di un diapason e sinusoide

negativo è l’ampiezza del segnale. Le frequenze udibili sono (approssi-mativamente) comprese tra i 16 e i 25.000Hz (Hz -Herz- indica numerodei cicli al secondo, anche cps). Il teorema di Fourier stabilisce che ognisegnale periodico può essere rappresentato come una somma di sinu-soidi di diversa ampiezza: come se un numero (teoricamente infinito)di sinuosoidi di diverso volume suonassere tutte insieme. Se si rappre-senta 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 ognisegnale complesso (non sinusoidale) sia costituito di molte componentidi frequenza diversa. In un segnale periodico queste componenti (det-te “armoniche”) sono multipli interi della frequenza fondamentale (chene è il massimo comun denominatore). Segnali di questo tipo sono adesempio l’onda a dente di segna, l’onda quadra, l’onda triangolare, ed ingenerale la fase stazionaria di tutti i segnali ad altezza musicale riconon-scibile (“intonabile”). In un segnale aperiodico le componenti possonoessere distribuite in frequenza in maniera arbitraria. Quando si parla(molto vagamente) di rumore spesso (ma non sempre) si indicano se-gnali aperiodici. Un segnale digitale è una rappresentazione numericadi un segnale analogico ed è doppiamente discreto: rappresenta cioè va-riazioni d’ampiezza discrete (quantizzazione) in istanti discreti di tempo(frequenza, o meglio tasso di campionamento). Può essere cioè pensatocome una griglia che viene sovrapposta ad un segnale analogico (Figure5.4 e5.5).

5.1–99

Time (s)0 0.00625

–1

1

0

0Time (s)

0 0.00625–1

1

0

Time (s)0 0.00625

–1

1

0

Time (s)0 0.00625

–1

1

0

segnale analogico campionamento

quantizzazione campionamento + quantizzazione

Fig. 5.4 Digitalizzazione del segnale. Forma d’onda,campionamento, quantizzazione, campionamento equantizzazione.

Sebbene rappresentato nei software tipicamente come un curva conti-nua, il segnale digitale è dunque una sequenza ordinata di impulsi (sivedano diverse rappresentazioni possibili in Figura 5.5).

−1

−0.5

0

0.5

1

0 20 40 60 80 100

−1

−0.5

0

0.5

1

0 20 40 60 80 100

−1

−0.5

0

0.5

1

0 20 40 60 80 100

Fig. 5.5 Segnale digitale: rappresentazioni.

Un segnale è descrivibile, nel caso discreto, attraverso una funzione ma-tematica

y = f [x]la funzione indica che per ogni istante discreto di tempo x il segnale hail valore d’ampiezza y. Un segnale digitale è una sequenza di casellex0, x1, x2, . . . a cui corrispondono valori d’ampiezza y0, y1, y2, . . ..

5.2–100

La struttura dati che rappresenta un segnale è allora tipicamente un ar-ray, una sequenza di celle di memoria consecutive e omogenee, conte-nenti cioè lo stesso tipo di dati (il tipo numerico prescelto per il segnalein 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 l’indice(il numero d’ordine che etichetta progressivamente ognuno degli ottovalori) rappresenta x inteso come istante di tempo, mentre il dato nu-merico associato rappresenta y, inteso come il valore d’ampiezza delsegnale nell’istante x:

x = 0→ y = 0x = 1 → y =0.5. . .x = 7 → y =0.35

−1

−0.5

0

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.2–101

5.2 Algoritmidisintesi

Un algoritmo per la sintesi del suono è una procedura formalizzata cheha come scopo la generazione della rappresentazione numerica di unsegnale audio.Il linguaggio SC (sclang) permette di sperimentare algoritmi di sintesidel segnale in tempo differito senza scomodare -per ora- il server au-dio (scsynth). Ad esempio, la classe Array risponde al messaggio plotgenerando una finestra e disegnandovi la curva spezzata ottenuta con-giungendo i valori contenuti nell’array. Il metodo plot è implementatonon solo nella classe Array ma in altre, ed è estremamente utile per ca-pire 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 l’elemento grafico 5.7.

Fig. 5.7 [0, 0.5, 0, -0.25, 0, 0.1, -0.77, 0.35]

5.2–102

Poiché il segnale audio previsto per i CD audio è campionato a 44.100Hz (attualmente lo standard audio più diffuso), se si vuole generare unsegnale mono della durata di 1 secondo a qualità CD, è necessario co-struire un array di 44.100 posti: il processo di sintesi del segnale con-siste allora nel definire ed implementare un algoritmo che permetta di“riempire” ognuno di questi posti (letteralmente numerati) con un va-lore. Così, se si vuole generare un segnale sinusoidale puro, il metodopiù semplice consiste nel calcolare l’ampiezza y per ogni campione xdel segnale in accordo con la funzione del seno e nell’associare il valorey all’indice x dell’arrayA, che rappresenta S. Una funzione periodicasi definisce come segue:

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

y = a× sin(2π × k × x)L’effetto dei parametri a e k è rappresentato in Figura 5.8, dove è di-segnato (in forma continua) un segnale (discreto) composto da 1000campioni.L’algoritmo di sintesi per un segnale sinusoidale, scritto in pseudo-codice(ovvero in un linguaggio inesistente ma essenziale), è dunque il seguen-te:

1 Per ogni x in A:2 y = a*sin(k*x)3 A[x] = y

dove la riga 1 del ciclo calcola il valore y in funzione di due parametri a ek che controllano l’ampiezza e la frequenza della sinusoide, mentre la se-conda riga assegna all’indice x di A il valore y. SC permette agevolmentedi implementare un simile algoritmo. Una classe utile a tal fine è Signal,

5.2–103

−1

−0.5

0

0.5

1

0 200 400 600 800 1000

−1

−0.5

0

0.5

1

0 200 400 600 800 1000

a = 1, k = 1/1000 a = 1, k = 2/1000

−1

−0.5

0

0.5

1

0 200 400 600 800 1000

−1

−0.5

0

0.5

1

0 200 400 600 800 1000

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

che è una sottoclasse di ArrayedCollection (la superclasse più generaledegli oggetti array-like) specializzata per la generazione di segnali. Nelcodice, la riga 3 assegna a sig un array Signal di 44100 celle, ovvero unsecondo di segnale audio mono a qualità CD. Le righe 4-7 sono occupa-te da un ciclo: sig.size restituisce la dimensione dell’array (44100). Persig.size (44100) volte viene valutata la funzione nel ciclo do: poiché xrappresenta l’incremento lungo l’array (che rappresenta il tempo, ovve-ro 0, 1, 2 . . . 44098, 44099), è in funzione di x che viene calcolato ilvalore della funzione (f [x]). Il valore della frequenza del segnale desi-derato indica il numero di cicli (2π) al secondo: se la frequenza deside-rata è 440 Hz (→ cicli al secondo) allora ci devono essere 440×2π cicliogni secondo (2*pi*freq). Questo valore deve essere calcolato per tutti i

5.2–104

posti dell’array (x/44100). Ottenuto il valore val, questo sarà compreso(per definizione trigonometrica) nell’escursione [−1, 1] e dunque puòessere scalato per amp. La riga 6 assegna al posto x di sig il valore val. Sinoti che per sicurezza in SC è sempre meglio, quando si usano gli array,riassegnare alla variabile che contiene l’array: sig = sig.put(x, val)indica che sig è uguale a se stesso, ma con il posto x occupato da val. Ilsegnale viene quindi disegnato nell’escursione d’ampiezza [−1, 1] (8).Infine, è possibile invocare il metodo play su un oggetto Signal: SC svol-ge tutte le operazioni necessarie per poter ascoltare il segnale (il “come”lo si vedrà poi): l’argomento true specifica che l’esecuzione avviene inloop.

1 (2 var sig, amp = 0.75, freq = 440, val ;3 sig = Signal.newClear(44100) ;4 sig.size.do({ arg x ;5 val = amp*sin(2*pi*freq*(x/44100)) ;6 sig = sig.put(x, val) ;7 }) ;8 sig.plot(minval:-1, maxval:1]) ;

10 sig.play(true) ;

12 )

14 (15 var sig, amp = 0.75, freq = 440 ;16 sig = Signal.newClear(44100) ;17 sig.waveFill({ arg x ; amp*sin(x) }, 0, 2pi*freq) ;

19 sig.plot(minval:-1, maxval:1]) ;

21 sig.play(true)22 )

5.2–105

La classe Signal prevede molte possibilità di elaborazione. Ad esempiol’algoritmo precedente è in realtà più agevolmente implementabile uti-lizzando il metodo waveFill che permette di riempire il segnale con ilrisultato di una funzione ({ arg x ; amp*sin(x) }) che viene calcolatotra 0 e 2pi*440. In altre parole, la funzione viene calcolata per 440 volte(440 cicli, 2π) a partire dall’indice 0. Si noti come nel caso di costantidefinite in SC il segno di moltiplicazione possa essere omesso: si puòscrivere 2pi invece che 2*pi.

1 (2 var sig, amp = 0.75, freq = 440 ;3 var soundFile ;

5 sig = Signal.newClear(44100) ;6 sig.waveFill({ arg x, i; amp*sin(x) }, 0, 2pi*440) ; // 1 cycle for

440 times

8 soundFile = SoundFile.new ;9 soundFile.headerFormat_("AIFF").sampleFormat_("int16").numChannels_(1)

;10 soundFile.openWrite("sounds/signalTest.aiff") ;11 soundFile.writeData(sig) ;12 soundFile.close ;13 )

Il segnale ottenuto può essere salvato su hard disk: Signal può così esse-re utilizzato per generare materiali audio utilizzabili in seguito. Sempredal lato client (in sclang) è disponible la classe SoundFile, che crea unfile audio (8), di cui sono specificabili le proprietà (9): il tipo ("AIFF"), la

code/fondamenti/sigSine.sc
code/fondamenti/sigSine3.sc

5.2–106

quantizzazione (16 bit, "int16"), il numero di canali (mono, 1)18. È im-portante specificare la quantizzazione perché SC internamente (e per de-fault) lavora a 32 bit: un formato utile per la precisione interna ma piutto-sto scomodo come formato di rilascio finale. Dopo aver create l’oggettoti tipo file è necessario specificare il percorso del file richiesto (10). Aquesto punto si possono scrivere sul file i dati contenuti nell’array sig(11). Ad operazioni concluse, il file deve essere chiuso (12).Anche nel caso in cui la funzione sia diversa, si tratta di una proceduradi estrema semplicità concettuale. Ad esempio, è possibile generare altrisegnali periodici già ricordati. Per definizione, un onda a dente di segaè un segnale periodico che ha infinite armoniche di frequenza f × n,dove f è la frequenza fondamentale en = 2, 3, 4, . . ., e di ampiezza ri-spettivamente pari a 1/2, 3, 4 . . . (ovvero inversamente proporzionaleal numero di armonica). Il metodo sineFill di Signal permette agevol-mente di osservare questo comportamento. I suoi argomenti sono

1. la dimensione2. un array che specifica una serie di ampiezze3. un array che specifica una serie di fasi

Ampiezze e fasi sono riferite alle armoniche del segnale sinusoidale. Adesempio, un array d’ampiezze [0.4, 0.5, 0, 0.1] indica che verrannocalcolate le prime 4 armoniche, dove f2 avrà ampiezza 0.4, f3 0.5 e cosìvia. Si noti che per eliminare una componente armoniche è sufficientepassare un valore d’ampiezza 0 (è il caso di f4. Il file di help propone ilcodice:

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 econ le sue prime 5 armoniche, come si vede in Figura 5.9.

Si noti il concatenamento dei messaggi: ognuno dei metodi restituisce infatti l’oggetto18

stesso.

5.2–107

−1

−0.5

0

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 Win-dow restituisce:

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

Cioè: un numero diviso un array restituisce un array in cui ogni ognielemento è pari al numero diviso all’elemento di partenza. In altre pa-role è come scrivere [1.0/1, 1.0/2, 1.0/3, 1.0/4, 1.0/5, 1.0/6].L’array contiene dunque una serie di 6 ampiezze inversamente propor-zionali al numero di armonica. Il segnale risultante approssima infattiun’onda a dente di sega19 (Il contributo delle 6 armoniche è visibile nelnumero delle “gobbe” che il segnale presenta). Nell’esempio seguentel’approssimazione è decisamente migliore. Il metodo series, definitoper Array, crea un array ed ha come argomenti size -la dimensione-start e step: l’array è riempito da una serie di size interi successiviche, iniziando da start, proseguono incrementando di step. Dunque,

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

5.2–108

l’array contiene i valori 1, 2, 3, . . . 1000. Il segnale sig genera una si-nusoide e i suoi primi 999 armonici superiori con valore inversamenteproporzionale al numero d’armonica.

1 (2 var sig, arr ;3 arr = Array.series(size: 1000, start: 1, step: 1) ;4 sig = Signal.sineFill(1024, 1.0/arr) ;5 sig.plot ;

7 sig.play(true)

9 )

Un’onda quadra può essere generata nello stesso modo dell’onda a den-te sega, ma aggiungendo soltanto le armoniche dispari (n = 1, 3, 5 . . .).La stessa cosa: un’onda a dente di sega in cui le armoniche pari hannoampiezza nulla. Il codice è riportato nell’esempio seguente (anche Figu-ra 5.10, a).

code/fondamenti/sigSaw.sc

5.2–109

1 (2 var sig, arr, arr1, arr2 ;3 arr1 = Array.series(size: 500, start: 1, step: 2) ;4 arr1 = 1.0/arr1 ;5 arr2 = Array.fill(500, 0) ;6 // arr2 = Array.fill(500, 0) ;7 arr = [arr1, arr2].flop.flat ;8 // arr = [arr1, arr2].lace(1000) ;

10 sig = Signal.sineFill(1024, arr) ;11 sig.plot ;

13 sig.play(true)

15 )

Le ampiezze delle armoniche pari devono essere pari a 0, quelle dispa-ri inversamente proporzionali al loro numero d’ordine. L’array arr1 èl’array delle ampiezze delle armoniche dispari. Si noti che step: 2, eche arr1 è già opportunamente scalato (4). L’array arr2 (5) è creato conil metodo fill che riempie un array della dimensione voluta (qui 500)valutando per ogni posto la funzione. Siccome è necessario un array co-stituito da zeri, la funzione deve semplicemente restituire 0. La riga 6crea il nuovo array arr, ed è più interessante, poiché fa uso dei metodiflop e flat. Si veda l’esempio dalla Post Window seguente:

code/fondamenti/sigSquare.sc

5.2–110

1 a = Array.fill(10, 0) ;

3 [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]

5 b = Array.fill(10, 1) ;

7 [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ]

9 c = [a,b] ;

11 [ [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ] ]

13 c = c.flop ;

15 [ [ 0, 1 ], [ 0, 1 ], [ 0, 1 ], [ 0, 1 ], [ 0, 1 ], [ 0, 1 ], [ 0, 1], [ 0, 1 ], [ 0, 1 ], [ 0, 1 ] ]

17 c = c.flat ;

19 [ 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1 ]

Dopo aver creato due array di 10 posti (1, 5), viene create un nuovo ar-ray 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 me-todo flat “appiattisce” un array “eliminando tutte le parantesi”: si per-de la struttura in sotto-array degli elementi. Rispetto alla riga 9 il ri-sultato è un’alternanza di elementi da uno e dall’altro array (da a eda b). Nell’esempio relativo all’onda quadra il risultato della riga 6 èun’alternanza di elementi di arr1 e di zeri (provenienti da arr2). Co-me accade molto spesso, l’operazione è in realtà praticabile in SC piùsemplicemente attraverso un metodo dedicato, lace (7, commentata):lace(1000) restituisce un array di dimensione 1000 pescando alternati-vamente da arr1 e arr2. Il risultato è disegnato in Figura 5.10, b.

5.3–111

−1

−0.5

0

0.5

1

0 200 400 600 800 1000

−1

−0.5

0

0.5

1

0 200 400 600 800 1000

a bFig. 5.10 Onda a dente di sega e onda quadra.

5.3 Notasulmetodo play

Il metodo play offre la possibilità di ascoltare il contenuto dell’oggettoSignal (come ciò avvenga nel dettaglio lo si vedrà poi). Ci si può chie-dere a quale frequenza. In tempo reale SC per default genera un segnalecon un tasso di campionamento (sample rate, sr) pari a 44.100 campionial secondo. Il contenuto dell’array, dopo essere stato messo in un buf-fer (una locazione di memoria temporanea) viene letto alla frequenza di44.100 campioni al secondo. In altri termini, SC preleva un valore dalbuffer ogni 1/44.100 secondi. Con il metodo play(true) l’esecuzioneè in loop: una volta arrivato alla fine, SC riprende da capo. Dunque, sesize è la dimensione in punti dell’array, il periodo del segnale (“quantodura” in secondi) èsize/sr, e la frequenza è il suo inverso: 1/size/sr =sr/size. Se size = 1000, allora f = 44100/1000 = 44.1HzViceversa, se si intende ottenere un segnale la cui fondamentale sia f ,la dimensione dell’array deve essere size = sr/f . È un calcolo sol-tanto approssimativo perché size deve essere necessariamente intero.Si consideri di nuovo l’esempio:

5.4–112

1 (2 var sig, amp = 0.75, freq = 440 ;3 sig = Signal.newClear(44100) ;4 sig.waveFill({ arg x ; amp*sin(x) }, 0, 2pi*freq) ;

6 sig.plot(minval:-1, maxval: 1) ;

8 sig.play(true)9 )

Se dimensione dell’array è 44.100 (come in molti esempi relativi a wa-veFill) allora il segnale viene letto una volta al secondo. Poiché l’arraycontiene un numero freq di cicli, freq indica effettivamente la frequen-za del segnale.

5.4 Altrisegnaliealtrialgoritmi

Un’onda triangolare può essere generata per approssimazione in manie-ra analoga all’onda quadra (sempre attraverso componenti sinusoidalisuperiori di ampiezza opportuna). Un approccio diverso è invece di ti-po geometrico. Il periodo di un’onda triangolare può essere pensato co-me costituito di quattro segmenti: il primo nell’intervallo [0.0, 1.0], ilsecondo in quello [1.0, 0.0], il terzo in quello [0.0,−1.0] e il quartoin quello [−1.0, 0]. Nell’esempio la variabile size rappresenta la di-mensione degli array che contengono i quattro segmenti, mentre step èl’incremento del valore di ampiezza (ogni segmento copre un’escursionedi 1 campionata nell’array). Il primo segmento è allora un array di tipoSignal riempito da un numero stepdi valori con incremento step: con-tiene valori da 0 a 1− step (8). Il secondo segmento segue il percorso

code/fondamenti/sigSineBis.sc

5.4–113

contrario (il metodo reverse restituisce un array leggendo dall’ultimoal primo elemento l’array sui cui è chiamato). Prima, viene aggiunto unostep ad ognuno degli elementi dell’array: second contine valori da 1 a0+step. I segmenti successivi sono ottenuti generando due array thirde fourth che sottraggono 1 rispettivamente a second e first, cioè li ”tra-slano in basso” (10, 11). Infine l’array total è ottenuto concatenando iquattro segmenti. Si noti che le operazioni di addizione (come le altreoperazioni sugli array) restituiscono un array in cui ognuno degli ele-menti risulta dall’applicazione dell’operazione sul rispettivo elementodell’array di partenza. Ovvero:

1 [1, 2, 3, 4]*22 [ 2, 4, 6, 8 ]

1 (2 // Segment-generated triangle wave

4 var first, second, third, fourth, total ;5 var size = 50 , step;

7 step = 1.0/size ;8 first = Signal.series(size, 0, step) ;9 second = (first+step).reverse ;

10 third = second-1 ;11 fourth = first-1 ;12 total = (first++second++third++fourth) ;

14 total.plot15 )

code/fondamenti/sigTriangle.sc

5.4–114

Il metodo di sintesi geometrico permette in effetti di ottenere risultatimigliori di una somma di sinusoidi che può soltanto ad approssima-re l’onda triangolare, poiché sarebbero necessarie infinite componenti,gli spigoli risultanti sono sempre un po’ “smussati” (Figura 5.11). Gliapprocci di questo tipo alla sintesi del segnale audio sono tipicamentechiamati “sintesi diretta”.

−1

−0.5

0

0.5

1

0 50 100 150 200

−1

−0.5

0

0.5

1

0 200 400 600 800 1000

a bFig. 5.11 Onda a triangolare e somma dei primi 20 armonici con am-piezza pseudo-casuale.

Tornando alla sintesi attraverso componenti sinusoidali, nell’esempioseguente (disegnato in Figura 5.11, b) la variabile harmonics contiene ilnumero degli elementi dell’array arr, cioè il numero delle armoniche. Inquesto caso, il valore d’ampiezza di ognuna delle componente è genera-to dalla funzione {1.0.rand} ed è un valore pseudo-casuale compresotra [0.0, 1.0] (tra assenza e massimo valore normalizzato). Se si ese-gue il codice più volte si noterà che il suono cambia, poiché l’apportodelle componenti dipende dalla funzione del metodo fill. Se si incre-menta o decrementa il valore di partials il segnale rispettivamente siarrichisce o si impoverisce di componenti elevate. La riga 7 permette divisualizzare l’array delle ampiezze come una spezzata che unisce i valo-ri discreti che lo compongono: con jplot2 si possono specificare i valori

5.4–115

d’escursione massimi e minimi ([0, 1]). Si tratta, come si vedrà, di unaforma di sintesi additiva.

1 (2 var sig, arr, harmonics = 20 ;

4 arr = Array.fill(harmonics, {1.0.rand});5 //arr = Array.fill(harmonics, 1.0.rand);

7 sig = Signal.sineFill(1024, arr) ;

9 arr.plot(minval:0, maxval:1) ;10 sig.plot ;

12 sig.play(true)

14 )

Si noti la differenza tra le righe 4 e 5. La riga 5 crea un array riempiendolocon il valore calcolato (una volta sola) da 1.0.rand, mentre è necessarioche ad essere invocata sia una funzione: una funzione è un oggetto che,chiamato dal messaggio value (qui “nascosto”) risponde calcolando unvalore. Dunque, per ogni posto di harmonics viene chiamata la funzione{1.0.rand} che calcola un nuovo valore. Ovvero:

1 Array.fill(3, 1.0.rand)

3 [ 0.31042301654816, 0.31042301654816, 0.31042301654816 ]

5 Array.fill(3, {1.0.rand})

7 [ 0.92207717895508, 0.91672122478485, 0.79339396953583 ]

code/fondamenti/sigAdd.sc

5.4–116

L’introduzione dei numeri pseudo-casuali permette di avvicinare anchei segnali non periodici. Un rumore bianco è un segnale il cui compor-tamento non è predicibile se non in termini statistici. Questo compor-tamento si traduce in una distribuzione uniforme dell’energia su tuttolo spettro del segnale. Un rumore bianco può essere descritto come unavariazione totalmente aperiodica nel tempo: in altre parole, il valore diogni campione è del tutto indipendente da quelli precedenti e da quel-li successivi20. Dunque, il valore di un campione x è indipendente dalvalore del campione precedente x − 1: x può avere qualsiasi valore,sempre, evidentemente, all’interno dello spazio finito di rappresenta-zione dell’ampiezza. Intuibilmente, l’algoritmo di generazione è moltosemplice. In pseudo-codice:

1 Per ogni x in A:2 y = a*rand(-1, 1)3 A[x] = y

Nell’esempio seguente, il metodo fill (6) valuta per ogni campione xuna funzione che restituisce un valore casuale all’interno dell’escursionenormalizzata [−1, 1] (rrand(-1.0,1.0)). Il codice specifica una fre-quenza di campionamento (sr) e una durata in secondi (dur). Il risul-tato (5msec di rumore bianco) viene scalato per amp, e dunque, da-to un valore di 0.75 per amp, si otterrà un’oscillazione (pseudo-)casualenell’escursione [−0.75, 0.75]. Si noti come la funzione (nella definizio-ne di partenza f [x]) in realtà sia calcolata indipendente da x: essendoaperiodica, il suo comportamento non dipende dal tempo.

Si dice perciò che ha autocorrelazione = 0.20

5.4–117

1 (2 /* 5 msec of white noise*/

4 var sig, amp = 0.75, dur = 0.005, sr = 44100 ;

6 sig = Signal.fill(dur*sr, { amp*rrand(-1.0,1.0) }) ;7 sig.plot(minval:-1, maxval:1) ;

9 sig.play(true)10 )

I software rappresentano tipicamente il segnale audio attraverso unacurva continua che connette i valori d’ampiezza. Nel caso del rumore,questa rappresentazione continua -di per sé non corretta rispetto allanatira digitale del segnale ma significativa da un punto di vista acustico-è decisamente meno icastica di una rappresentazione discreta (Figura5.12).

−1

−0.5

0

0.5

1

0 50 100 150 200

−1

−0.5

0

0.5

1

0 50 100 150 200

Fig. 5.12 Rumore bianco: curva continua e dispersione dei valori

È un’ovvietà ma è bene ricordarsi che un segnale digitale è appuntosemplicemente una sequenza di valori, sulla quale è possibile svolge-re operazioni matematiche. Tutta l’elaborazione digitale del segnale na-sce da quest’assunto. I prossimi sono due esempi tratti da Puckette (?).

code/fondamenti/sigWhiteNoise.sc

5.4–118

La funzione seno oscilla periodicamente tra [−1, 1]. Calcolando il suovalore assoluto si ribalta la parte negativa sul positivo: se si osserva lacurva si nota come i due emicicli siano identici: ne consegue un segnalecon frequenza doppia di quello di partenza. In generale l’applicazionedella funzione del valore assoluto è una tecnica rapida per far saltared’ottava un segnale. Poiché la curva occupa soltanto valori positivi (tra[0, 1]), è possibile traslarla in modo da evitare offset: decrementandodi 0.5 l’escursione del segnale diventa [−0.5, 0.5] ed è simmetrica trapositivi e negativi (Figura 5.13).

−1

−0.5

0

0.5

1

0 200 400 600 800 1000

−1

−0.5

0

0.5

1

0 200 400 600 800 1000

Fig. 5.13 y = abs(sin(2π × x)), y = abs(sin(2π × x))− 0.5Nel codice seguente il primo esempio utilizza il metodo waveFill e cal-cola il valore assoluto per ogni campione: è una tecnica potenzialmenteutilizzabile per il tempo reale. Tuttavia la classe Signal implementa mol-ti metodi matematici. Così, il metodo abs invocato su un oggetto Signalrestituisce un altro oggetto Signal in cui ogni elemento è il risultato delvalore assoluto dell’elemento del segnale di partenza (è come chiamareabs per ogni elemento). Lo stesso vale per la sottrazione -0.5 e per lamoltiplicazione *amp: si applicano a tutti gli elementi dell’array.

5.4–119

1 /* Absolute value on a sine */

3 (4 var sig, amp = 0.75 ;

6 sig = Signal.newClear(44100) ;7 sig.waveFill({ arg x, i; (sin(x).abs - 0.5)*amp }, 0, 2pi) ;

9 sig.plot(minval:-1, maxval:1) ;10 )

12 (13 var sig, amp = 0.75 ;

15 sig = (Signal.sineFill(1024, [1]).abs-0.5)*amp ;

17 sig.plot(minval:-1, maxval: 1) ;

19 )

Nella funzione risultante dall’appplicazione del valore assoluto la pre-senza di uno spigolo molto acuto introduce però molte componenti su-periori. L’utilizzo dell’elevamento al quadrato evita questo problemaottenendo un risultato analogo: infatti il quadrato dei valori negatividel segnale è positivo, e si ottiene un ribaltamento della curva analogoa 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 tuttoanalogo) squared.

code/fondamenti/sigFunc.sc

5.4–120

−1

−0.5

0

0.5

1

0 200 400 600 800 1000

−1

−0.5

0

0.5

1

0 200 400 600 800 1000

Fig. 5.14 y = sin(2π × x)2, y = sin(2π × x)2 − 0.5

1 /* Squaring the sine */

3 (4 var sig, amp = 0.75 ;

6 sig = Signal.newClear(1024) ;7 sig.waveFill({ arg x, i; (sin(x).pow(2)-0.5)*amp }, 0, 2pi) ;

9 sig.jplot2(range:[-1,1]) ;10 )

12 (13 var sig, amp = 0.75 ;

15 sig = (Signal.sineFill(1024, [1]).squared-0.5)*amp ;

17 sig.plot(minval:-1, maxval: 1) ;18 )

Come ulteriore esempio di elaborazione del segnale si può considerarel’operazione di “clippping”: in caso di clipping tutti i valori superiori aduna certa soglia s (o inferiori al negativo della stessa) vengono riporta-ti a±s. Il clipping prende anche il nome di distorsione digitale perché

code/fondamenti/sigFunc2.sc

5.4–121

è quanto avviene quando il segnale digitalizzato ha un’ampiezza su-periore a quella rappresentabile dalla quantizzazione. Il clipping è unasorta di limiter (limita infatti l’ampiezza del segnale) radicale e può es-sere usato come “effetto” di tipo distorcente. In SC è definito il metodoclip2(t) che “taglia” un valore fuori dell’escursione [−t, t] a ±t. Siconsideri l’esempio seguente in cui t =3.

1 1.clip2(3)

3 1

5 -1.clip2(3)

7 -1

9 4.clip2(3)

11 312 -4.clip2(3)

14 -3

5.4–122

1 (2 var sig, sig2, sig3, clipFunc ;

4 sig = Signal.sineFill(512, [1]) ;

6 clipFunc = { arg signal, threshold = 0.5 ;7 var clipSig = Signal.newClear(signal.size) ;8 signal.do({ arg item, index;9 var val ;

10 val = if (item.abs < threshold, { item.abs },11 { threshold}) ;12 val = val*item.sign ;13 clipSig.put(index, val) ;14 }) ;15 clipSig ;16 } ;

18 sig2 = clipFunc.value( sig ) ;19 sig3 = clipFunc.value( sig, threshold: 0.75 ) ;

21 //[sig, sig2, sig3].flop.flat.plot(minval:-1, maxval: 1, numChannels:3);

23 sig.play(true) ;24 //sig2.play(true) ;25 //sig3.play(true) ;

27 )

Come esercizio può essere interessante implementare un modulo “clip-per”. Si è detto che una funzione esegue un comportamente quando rice-ve il messaggio value in funzione degli argomenti in entrata. Nell’esempioè appunto definita una funzione che implementa il clipping, clipFunc:gli argomenti in entrata prevedono un segnale (un oggetto Signal) eun valore di soglia (threshold). Ciò che clipFunc restituisce è un altro

code/fondamenti/sigClip.sc

5.4–123

oggetto Signal. La riga 6 dichiara la variabile a cui viene assegnato il se-gnale e gli assegna subito un oggetto Signal di dimensione pari a quelladel segnale in entrata, riempito di 0. L’idea è quella di valutare ognunodegli elementi (che rappresentano campioni audio) di signal: si trattacioè di ciclare su signal (8). Nel ciclo la variabile val rappresenta il va-lore che dovrà essere scritto nell’array clipSig. Il valore da assegnarea val dipende da una condizione. Se il campione valutato è all’internodell’escursione [−threshold, threshold] allora non ci sarà clipping:val ha lo stesso valore del valore di signal in entrata. Se invece il valorein entrata cade all’esterno dell’intervallo, allora in uscita si avrà il valo-re della soglia stessa. Il condizionale è contenuto nelle righe 10-11. Lavalutaione avviene sul valore assoluto di item (item.abs). Avviene cioènell’intervallo [0, 1]. In uscita si ha lo stesso valore assoluto di item op-pure threshold. Se il valore in entrata è negativo, verrebbe così resitutitoun valore positivo. Il segno del valore in entrata viene però recuperatocon la riga 12 in cui il valore val viene moltiplicato per il segno di item:infatti, item.sign restituisce±1.

1 0.216.sign

3 1

5 -0.216.sign6 -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.5–124

Seguono due esempi, uno che sfrutta il valore predefinito di threshold(0.5), l’altro in cui threshold vale 0.75. Il clipping tende a “squadrare”la forma d’onda ed in effetti il segnale risultante tende ad assomigliare-anche all’ascolto- ad un’onda quadra (Figura 5.15).

−1

−0.5

0

0.5

1

0 100 200 300 400 500

−1

−0.5

0

0.5

1

0 100 200 300 400 500

−1

−0.5

0

0.5

1

0 100 200 300 400 500

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

5.5 Funzionevaloreassolutoefunzionequadratica

L’approccio modulare dell’esempio precedente permette di riscrivere icasi relativi al valore assoluto e al quadrato. In particolare si possonoesplicitamente definire due funzioni di trasferimento:

Wabs : f (x) = x2

Wsquare : f (x) = |x|Queste funzioni si comportano come veri moduli di elaborazione delsegnale in entrata. Sono implementate in SC nell’esempio seguente. Nelcodice l’unica cosa di rilievo è l’eliminazione automatica dell’offset. Ilmetodo peak restituisce l’elemento di Signal che ha il valore assoluto piùalto (il massimo d’ampiezza del segnale). Assumendo che il segnale siasimmetrico rispetto allo 0, ogni nuovo valore risultante dall’applicazionedella funzione viene traslato di peak/2. Se il segnale è compreso in

5.5–125

[−0.7, 0.7] allorapeak = 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 escursionein ampiezza risulti simmetricamente intorno allo 0 in [−0.35, 0.35].

5.6–126

1 (2 /* abs and quadratic functions */

4 var sig, sig2, sig3, absFunc, squareFunc ;

6 sig = Signal.sineFill(512, [1]);

8 absFunc = { arg signal ;9 var absSig = Signal.newClear(signal.size) ;

10 var peak;11 signal.do({ arg item, index;12 var val ;13 val = item.abs ;14 absSig.put(index, val) ;15 }) ;16 absSig - (absSig.peak*0.5) ;17 } ;

19 squareFunc = { arg signal ;20 var squareSig = Signal.newClear(signal.size) ;21 var peak ;22 signal.do({ arg item, index;23 var val ;24 val = item.squared ;25 squareSig.put(index, val) ;26 }) ;27 squareSig - (squareSig.peak*0.5) ;28 } ;

30 sig2 = absFunc.value( sig ) ;

32 sig3 = squareFunc.value( sig ) ;

34 [sig, sig2, sig3].flop.flat.plot(minval:-1, maxval:1, numChannels:3) ;

36 )

5.6–127

5.6 Ancorasull’elaborazionedisegnali

La natura numerica del segnale permette di definire operazioni analo-ghe alle precedenti anche su materiale pre-esistente. La classe SoundFi-le permette non soltanto di scrivere sui file ma anche di accedere a fi-le audio disponibili sull’hard-disk. Il codice seguente riempie l’oggettoSignal sig con il contenuto del file audio sFile attraverso il metodoreadData di sFile. Si noti come la dimensione di sig sia stata fatta di-pendere dal numero di campioni di sFile attraverso l’attributo numfra-mes.

1 (2 var sFile, sig ;

4 sFile = SoundFile.new;5 sFile.openRead("sounds/a11wlk01-44_1.aiff");6 sig = Signal.newClear(sFile.numFrames) ;7 sFile.readData(sig) ;8 sFile.close;9 )

L’operazione seguente sfrutta la natura di sequenza numerica del se-gnale audio per implementare una sorta di granulazione. In sostanzail segnale audio importato viene suddiviso in un numero numChunks dipezzi, ognuno dei quali comprende un numero step di campioni. Quin-di i pezzi vengono mischiati a caso e rimontati. Nell’implementazione

code/fondamenti/sigAbsSquare.sc
code/fondamenti/sFileOpen.sc

5.6–128

indices è la sequenza degli indici dei pezzi del segnale ed è una progres-sione a partire da 0. Questa progressione lineare (1, 2, 3, 4 . . .) vienemescolata attraverso il metodo scramble (così da diventare ad esempio9, 1, 3, 7 . . .). Quindi, attraverso il ciclo su ognuno degli indici, vienerecuperato il pezzo corrispondente nel segnale originale e concatenatoin sequenza. È probabile che lo step non si un divisore intero del segna-le in ingresso. La parte che avanza (tail) viene concatenata alla fine delnuovo segnale newSig.

5.6–129

1 ( /* Scrambling a signal */

3 var sFile, sig, newSig ;4 var numChunks, step, rest, indices ;5 var block, tail ;

7 var period = 441;

9 sFile = SoundFile.new;10 sFile.openRead("sounds/a11wlk01-44_1.aiff");11 sig = Signal.newClear(sFile.numFrames) ;12 sFile.readData(sig) ;13 sFile.close;

15 /*16 sig = Signal.newClear(44100) ;17 sig = Signal.sineFill(period, [1]) ;18 (44100/period).do(sig = sig.addAll(sig)) ;19 */

21 step = 50 ; // try: 5, 10, 50, 50022 numChunks = (sig.size/step).asInt ;

24 tail = (sig.size-(step*numChunks)) ;

26 indices = Array.series(numChunks);27 indices = indices.scramble;

29 newSig = Signal.new;30 indices.do({arg item;31 block = sig.copyRange(item*step, (item+1)*step-1) ;32 newSig = newSig.addAll(block) ;

34 }) ;

36 tail = sig.copyRange(sig.size-tail, sig.size) ;37 newSig = newSig.addAll(tail) ;

39 newSig.play(true) ;40 )

5.6–130

Questa forma di granulazione del segnale permette di introdurre infineuna forma di sintesi per permutazione. Attraverso lo “scrambling” delsegnal si produce un nuovo segnale che mantiene più o meno “in memo-ria” il segnale originale. Minore è lo step più grande è la ricombinazionedel segnale. Se un’operazione simile viene svolta su un segnale sinu-soidale si nota ancora più chiaramente la proporzione tra diminuzionedello step e incremento della rumorosità. Lo si può fare togliendo gliindicatori di commento nell’esempio precedente. In quel caso viene pro-dotta una sinusoide di frequenza pari a 1/period. Si noti che la sintesidella sinusoide è ottenuta concatenando cicli di durata pari a periods(in campioni). In questo caso la dimensione del segnale è di 44.100 ele-menti: dunque se il periodo è 441, vuol dire che ci staranno 44100/441cicli dentro un secondo (assumendo la frequenza di campionamento pa-ri a 44.110): la frequenza del segnale sarà 44.100/441 = 100Hz.Nell’esempio seguente viene implementata una forma di sintesi per per-mutazione. Il processo è del tutto analogo a quanto avviene nell’esempioprecedente, con una differenza. Il segnale viene sempre suddiviso inblocchi di durata step. Al posto dell’operazione di scramble viene in-vece 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 dallospettro molto ricco ma che presenta una periodicità che dipende sia dallafrequenza della sinusoide in entrata sia dalla dimensione dello step.

code/fondamenti/sigScramble.sc

5.6–131

1 (2 /* Distorsion via permutation on a sinusoid */

4 var sig, sig2, newSig ;5 var numChunks, step, rest, indices ;6 var block, tail ;7 var period = 441;

9 sig = Signal.new ;10 sig2 = Signal.sineFill(period, [1]) ;11 (44100/period).do(sig = sig.addAll(sig2)) ;

13 step = 50 ; // try: 5, 50, 50014 numChunks = (sig.size/step).asInt ;15 tail = (sig.size-(step*numChunks)) ;

17 a = Array.series((numChunks/2).asInteger, 1,2) ;18 b = Array.series((numChunks/2).asInteger, 0,2) ;19 indices = [a,b].flop.flat ;

21 newSig = Signal.new;22 indices.do({ arg item;23 block = sig.copyRange(item*step, (item+1)*step-1) ;24 newSig = newSig.addAll(block) ;25 }) ;

27 tail = sig.copyRange(sig.size-tail, sig.size) ;28 newSig = newSig.addAll(tail) ;

30 newSig.play(true) ;31 sig.plot ;32 newSig.plot33 )

La periodicità diventa evidente, oltre all’orecchio, se si confronta il pri-mo ciclo della sinusoide, della sinusoide “scrambled” e della sinusoidedistorta per permutazione (Figura 5.16).

code/fondamenti/sigScramble1.sc

5.7–132

−1

−0.5

0

0.5

1

0 100 200 300 400

−1

−0.5

0

0.5

1

0 100 200 300 400

−1

−0.5

0

0.5

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 Segnalidicontrollo

Un segnale sinusoidale, come tutti i segnali perfettamente periodici, man-ca totalmente delle caratteristiche di dinamicità usualmente ascritte adun suono  “naturale” o, meglio,  “acusticamente interessante”: è un suo-no, come direbbe Pierre Schaeffer, senza “forma” temporale, “omoge-neo”. Suoni di questo tipo, peraltro, arredano il paesaggio sonoro dellamodernità meccanica ed elettrica sotto forma di humming, buzzing -e cosìvia- prodotti dai ventilatori, dall’impedenza elettrica, dai motori. A par-te questi casi, tipicamente la “forma” temporale di un suono prende laforma di un profilo dinamico, di una variazione della dinamica del suo-no che è descritta acusticamente sotto forma di una curva di inviluppo,le cui fasi prendono usualmente il nome di attacco/decadimento/sostegno/rilascio:da cui l’acronimo ADSR. Il modo più semplice di rappresentare un si-mile inviluppo consiste nell’utilizzare una spezzata (Figura 5.17).A guardare l’inviluppo si osserva agevolmente come si tratti di altra cur-va, e cioè propriamente di un altro segnale, che si distingue dai segnalifinora considerati per due caratteristiche importanti:

1. non è periodico (si compone di un unico ciclo). Dunque, il perio-do del segnale d’inviluppo è pari alla durata del segnale audio: seil segnale dura 2 secondi (ovvero il periodo dell’inviluppo) allora

5.7–133

Time (s)0 4.23184

–0.8233

0.8881

0

Time (s)0 0.588944

–0.3398

0.3456

0

Fig. 5.17 Descrizione del profilo dinamico attraverso una spezzata (in-viluppo).

l’inviluppo ha una frequenza 1/2 = 0.5Hz. Si noti come la fre-quenza non rientri nel dominio udibile;

2. è unipolare: assumendo che il segnale audio sia nell’escursione nor-malizzata [−1, 1] (bipolarità), l’inviluppo è compreso in [0, 1] (uni-polarità).

Passando dall’analisi del segnale alla sua sintesi, si tratterà perciò di ri-produrre le proprietà dell’inviluppo (la sua “forma”) e di applicare que-sta forma al segnale audio.L’inviluppo è un tipico segnale di controllo: unsegnale che modifica -controlla- un segnale audio.Essendo un segnale, per rappresentare un inviluppo si può utilizzare unarray. Ad esempio, un tipico inviluppo ADSR potrebbe essere descrittodall’array 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 l’ampiezza di un segnale si può (lo siè visto abbondantemente) moltiplicare il segnale per una costante: ognicampione viene moltiplicato per la costante. Ad esempio, se la costante

5.7–134

0

0.25

0.5

0.75

1

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].

Amp = 0.5, l’ampiezza del segnale viene ridotta della metà. Si po-trebbe pensare ad una simile costante nei termini di un segnale: comeun array, di dimensione pari a quella del segnale scalato, che contengasempre lo stesso valore. Ogni valore dell’array da scalare viene moltipli-cato per il rispettivo valore dell’arrayAmp. L’idea smette di essere unacomplicazione inutile nel momento in cui l’array Amp non contienepiù sempre lo stesso valore, ma contiene invece valori variabili che rap-presentano appunto un inviluppo d’ampiezza. Dunque, ogni campionedel segnale audio (ogni elemento dell’array) viene moltiplicato per unvalore incrementale del segnale d’inviluppo ottenuto. La situazione èrappresentata in Figura 5.19.Dovendo descrivere lo sviluppo del segnale audio in tutta la sua durata,la dimensione dell’array d’inviluppo deve essere la stessa del segnaleaudio. Emerge qui un punto fondamentale, su cui si ritornerà: tipica-mente è sufficiente un numero molto minore di punti per rappresentareun inviluppo rispetto ad un segnale audio, ovvero -assumendo che ilsegnale 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 identificaun segnale di controllo.Passando all’implementazione in SC, per intanto si può generare una

5.7–135

0

0.25

0.5

0.75

1

0 2 4 6 8 10

−1

−0.5

0

0.5

1

0 200 400 600 800 1000

−1

−0.5

0

0.5

1

0 200 400 600 800 1000

−1

−0.5

0

0.5

1

0 200 400 600 800 1000

Fig. 5.19 Inviluppo, segnale audio risultante, segnale audio, segnale au-dio inviluppato.

sinusoide di durata pari ad un secondo attraverso il solito metodo wa-veFill. La frequenza prescelta è molto grave perché così sono meglio vi-sibili i cicli del segnale. L’array di partenza è composto da dieci segmen-ti: poiché questi devono essere ripartiti sulla durata del segnale audio,ognuno dei segmenti comprenderà 44100/10 campioni. Questo valo-re è assegnato alla variabile step. Si tratta ora di generare gli array checostituiscono i quattro segmenti ADSR (gli array att, dec, sus, rel),per poi concatenarli in un array complessivo env. Poiché devono esseremoltiplicati per un oggetto Signal allora devono anch’essi essere oggettiSignal. Il metodo series(size, start, step) crea una progressione li-neare di size valori a partire da start con incremento step. L’attaccoparte da 0 e arriva a 0.9, occupa un segmento, cioè 4410 campioni.Il valore 0.9 deve cioè essere raggiunto in 4410 punti: l’incremento diognuno sarà perciò di 0.9/4410. Dunque, 0.9/step. All’ultimo punto

5.7–136

si avrà un valore pari a 0.9/step×step = 0.9. Nel caso di dec si trat-ta di scendere a 0.5 nella durata di un altro segmento. In questo caso laprogressione parte da 0.9 e l’incremento è negativo: bisogna scenderedi 0.5 in uno step, dunque l’incremento è −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. Ilnuovo segnale di inviluppo env è ottenuto concatenando i quattro seg-menti ed è impiegato come moltiplicatore di sig per ottenere il segnaleinviluppato envSig.

1 /* inviluppi, I */

3 (4 var sig, freq = 440, size = 44100, step ;5 var env, att, dec, sus, rel, envSig ;

7 step = 44100/10 ;8 sig = Signal.newClear(size) ;9 sig.waveFill({ arg x, i; sin(x) }, 0, 2pi*50) ;

11 att = Signal.series(step, 0, 0.9/step) ;12 dec = Signal.series(step, 0.9, -0.5/step) ;13 sus = Signal.series(step*4, 0.4, 0) ;14 rel = Signal.series(step*4, 0.4, -0.4/(step*4)) ;

16 env = att++dec++sus++rel ;17 envSig = sig * env ;

19 [sig, env, envSig].flop.flat.plot(minval:-1, maxval: 1], numChannels:3) ;

20 envSig.play(true)21 )

code/fondamenti/sigEnv.sc

5.7–137

Il metodo precedente è piuttosto laborioso. SC prevede una classe Envspecializzata nel costruire inviluppi. Env assume che un inviluppo siauna spezzata che connette valori d’ampiezza nel tempo e fornisce di-verse modalità di interpolazione per i valori intermedi. Si considerino idue array seguenti v e d:

1 v = [0, 1, 0.3, 0.8, 0] ;2 d = [ 2, 3, 1, 4 ] ;

Una coppia simile è tipicamente utilizzata per specificare 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, l’array delle durate contiene sempre un valore di durata in me-no di quello delle ampiezze. Infatti, t[0] (= 2) indica che per andare dav[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 profilo (v)temporale (d). Nell’esempio, resta tuttavia da specificare cosa succedeper ogni campione compreso nei due secondi che intercorrono tra 0 e1. Il modo in cui i campioni sono calcolati dipende dalla modalità dininterpolazione. Nell’esempio seguente e1, e2, e3 sono oggetti Env spe-cificati dalla stessa coppia di array v, d, ma con differenti modalità diinterpolazione (lineare, discreta, esponenziale).

5.7–138

1 (2 /* Using Env*/

4 var v, d, e1, e2, e3 ;

6 v = [0, 1, 0.3, 0.8, 0] ;7 d = [ 2, 3, 1, 4 ] ;

9 e1 = Env.new(v, d,'linear').asSignal ;10 e2 = Env.new(v, d,'step').asSignal ;

12 v = [0.0001, 1, 0.3, 0.8, 0] ;13 e3 = Env.new(v, d,'exponential').asSignal ;

15 [e1, e2, e3].flop.flat.plot(numChannels:3) ;16 )

Si noti che nel caso di un inviluppo esponenziale il valore di partenzanon può essere pari a 0: il primo valore di v viene quindi ridefinito conun valore prossimo allo zero. Il significato dei parametri è illustrato nellaFigura 5.20 che commenta quanto disegnato dal metodo plot.La classe Env eredita direttamente da Object e dunque non è un oggettodi tipo array. Tipicamente vine utilizzata come specificazione di invi-luppo per il tempo reale (come si vedrà). Quando però un oggetto Envriceve il messaggio asSignal Env restituisce un oggetto Signal che con-tiene un inviluppo campionato nel numero di punti che compongono ilnuovo array. La classe Env permette allora di utilizzare anche in tempodifferito una specificazione per gli inviluppi decisamente più comoda.Inoltre, la classe prevede alcuni costruttori che restituiscono inviluppiparticolarmente utili. Ad esempio:

code/fondamenti/ssEnv.sc

5.7–139

0

0.5

1

0 200 400 600 800 1000

0

0.5

0 200 400 600 800 1000

0

0.5

0 200 400 600 800 1000

0.0 1.0 0.3 0.8 0.0v:

2 3 1 4d:

'linear'

'step'

'exponential'

Fig. 5.20 Env: parametri.

• triangle richiede due argomenti: il primo indica la durata, il secon-do il valore di picco di un inviluppo triangolare (il picco cade cioè ametà della durata).

• perc: permette di definire un inviluppo percussivo (attacco + rila-scio). Gli argomenti sono tempo d’attacco, tempo di rilascio, valoredi picco e valore di curvatura.

5.7–140

1 /* inviluppi, I */

3 (4 var sig, freq = 440, size = 1000 ;5 var envT, envP ;

7 sig = Signal.newClear(size) ;8 sig.waveFill({ arg x, i; sin(x) }, 0, 2pi*50) ;

10 envT = Env.triangle(1,1).asSignal(size);11 envP = Env.perc(0.05, 1, 1, -4).asSignal(size) ;

13 [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.L’applicazione di un segnale di inviluppo è ovviamente possibile ancheper un segnale audio di provenienza concreta. Nell’esempio seguenteal segnale sig, ottenuto importando il contento di sFile, viene appli-cato un segnale di inviluppo env: env è ottenuto attraverso due arrayriempiti di numeri pseudo-casuali, v e d. Il primo oscilla nell’intervallo[0.0, 1.0] (è un segnale unipolare). Per evitare offset nell’ampiezza, ilprimo e l’ultimo valore dell’array vengono posti a 0, ed aggiunti dopo.L’array d è composto di un numero di elementi pari a quelli di v, secon-do quanto richiesto dalla sintassi di Env. Gli intervalli di durata varianonell’intervallo [0.0, 4.0].

code/fondamenti/sigEnv2.sc

5.7–141

−1

−0.5

0

0.5

1

0 200 400 600 800 1000

−1

−0.5

0

0.5

1

0 200 400 600 800 1000

−1

−0.5

0

0.5

1

0 200 400 600 800 1000

−1

−0.5

0

0.5

1

0 200 400 600 800 1000

Fig. 5.21 Inviluppi con Env: envT, sig*envT, envP, sig*envP

5.8–142

1 /* inviluppi, II */

3 (4 var sig, freq = 440 ;5 var env, v, d, breakPoints = 10 ;6 var sFile ;

8 sFile = SoundFile.new;9 sFile.openRead("sounds/a11wlk01-44_1.aiff");

10 sig = Signal.newClear(sFile.numFrames) ;11 sFile.readData(sig) ;12 sFile.close;

14 v = Array.fill(breakPoints-2, { arg i ; 1.0.rand ; }) ;15 v = v.add(0) ; v = [0.001].addAll(v) ;16 v.size.postln;17 d = Array.fill(breakPoints-1, { arg i; 4.0.rand ;}) ;

19 env = Env(v, d, 'lin').asSignal(sig.size) ;

21 // Achtung! next line can be computationally expensive22 [sig, env, sig*env].flop.flat.jplot2(minval:-1, maxval: 1, numChannels:

3) ;23 )

Tre inviluppi sono disegnati in Figura 5.22: ad ogni valutazione del codi-ce l’inviluppo assume infatto una forma diversa, a parte per i due ester-mi pari a 0.

0

0.25

0.5

0.75

1

0 20 40 60 80 100

0

0.25

0.5

0.75

1

0 20 40 60 80 100

0

0.25

0.5

0.75

1

0 20 40 60 80 100

Fig. 5.22 Inviluppi pseudo-casuali.

code/fondamenti/sigEnv3.sc

5.8–143

5.8 Conclusioni

L’obiettivo di quanto visto finora era di introdurre il concetto di segnaledigitale, attraverso alcune operazioni che si rendono tipicamente possi-bili grazie alla sua natura numerica. Si è poi avuto modo di osservarecome sia possibile modificare un segnale attraverso un altro segnale. Inparticolare, un segnale di controllo è un segnale che richiede una riso-luzione temporale molto minore del segnale e la cui frequenza di situaal di sotto delle frequenze udibilei (“sub-audio range”). Un segnale dicontrollo tipicamente modifica un segnale audio. È a questo punto op-portuno riprendere ed espandere gli aspetti affrontati attraverso il modusoperandi più tipico di SC, il tempo reale.

6.1–144

6 L’architetturaeilserver

6.1 L’architettura

Come si è avuto modo di osservare, scaricando il programma SC ci siporta a casa due componenti, di principio autonomi, un server e unclient. Il primo viene chiamato scsynth, il secondo sclang (SC-language).Il programma SC sfrutta cioè un’architettura client/server, separandodue funzioni, una di richiesta e l’altra di fornitura servizi, che comuni-cano attraverso una rete. In 6.1 è descritta una generica architettura direte: più client comunicano via rete con un server scambiando messaggi.In SC il client e il server comunicano attraverso la rete attraverso mes-saggi scritti in un protocollo specifico, piuttosto usato nell’ambito del-le 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 è definita a livello astratto.Ciò vuol dire che client e server possono essere in esecuzione sulla stessamacchina: è ciò che avviene quando si manda in esecuzione l’applicazioneSC.

6.1–145

client 1

client 2

client 3

Server

msg

msg

msg

Rete

Fig. 6.1 Architetturaclient/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 livel-lo, potente, efficiente, e non molto intelligente (non ha molta capacità diprogrammazione). Il cliente di questo server è sclang: anche sclang è inrealtà due cose, un linguaggio di programmazione e insieme l’interpretedi questo linguaggio. L’interprete ha due funzioni:

1. è il client: in altre parole, è l’interfaccia che permette all’utente discrivere e spedire messaggi OSC al server. Per scrivere una letteraal server, è necessario avere un foglio di carta e un postino che laconsegni: sclang fa entrambe le cose.

2. è l’interprete del linguaggio: i messaggi OSC sono piuttosto macchi-nosi da scrivere, e condividono con il server la prospettiva di bassolivello. Il linguaggio sclang è invece un linguaggio di alto livello (ti-po Smalltalk). Il codice sclang viene allora tradotto in messaggi OSCdall’interprete e questi vengono così inviati al server. La poesia che

6.1–146

l’utente scrive in linguaggio sclang viene parafrasata in prosa OSCdall’interprete sclang per essere inviata al (prosaico) server.

La situazione è schematizzata in 6.2. La comunicazione tra lato cliente lato server avviene attraverso messaggi OSC che il client (tipicamen-te) spedisce al server. L’interprete sclang spedisce messaggi OSC in duemodi:

1. direttamente. In altre parole, sclang-inteprete è un buon posto perl’utente da dove parlare al server al livello di quest’ultimo (da dovespedire messaggi OSC);

2. indirettamente. Il codice simil-Smalltalk dello sclang-linguaggio (adun livello più astratto) a disposizione dell’utente viene tradotto dall’interpreteautomaticamente in messaggi OSC (a livello server) per il server (èil cosidetto language wrapping).

Riassumendo, a partire dall’applicazione sclang-inteprete si può scrive-re in poesia affidandosi alla traduzione prosastica ad opera dello stes-so inteprete (che è traduttore e postino) o direttamente in prosa OSC(l’inteprete fa solo il postino). Ci si potrebbe chiedere perché complicar-si la vita con una simile architettura. I vantaggi sono i seguenti:

• stabilità: se il client sperimenta un crash, il server continua a funzio-nare (ovvero: l’audio non si ferma, ed è un fatto importante per unconcerto/installazione/performance) e viceversa.

• modularità: un conto è la sintesi, un conto il controllo. Separare ledue funzioni consente ad esempio di controllare scsynth anche daapplicazioni che non siamo sclang: l’importante è che sappiano spe-dire i giusti messaggi al server. Il server è democratico (tutti possono

6.1–147

ottenere servizi audio) e burocratico allo stesso tempo (l’importanteè rispettare il protocollo OSC).

• controllo remoto: la rete di cui si parla può essere sia interna al calco-latore, sia esterna. Quando ci si occupa esclusivamente di sintesi au-dio, tipicamente le due componenti lavorano sullo stesso calcolatoree usando l’indirizzo locale. Ma client e server potrebbero benissimotrovarsi via rete, anche ai due estremi opposti del globo e comunicarevia 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 dell’audio);

2. in caso di alta densità temporale dei messaggi sulla rete, quets’ultimapuò essere sovraccaricata, e la gestione dei messaggi può indurre unritardo.

Va altresì notato che è decisamente raro incorrere in simili problemi.

s.sendMsg

synthDefssynth.play

func.play

messaggi OSC

messaggi OSC

messaggi OSC

sclang

language wrapping

app. esterna

app. esterna

Client side Server side

scsynth

livello messaggi OSCaltri livelli più astratti

Fig. 6.2 Architettura client/server di SC.

6.2–148

Ad esempio, in 6.2 si distingue tra due “lati”, client e server side. Sul latoserver c’è scsynth. Su quello client ci possono essere un numero indefi-nito di applicazioni capaci di parlare via OSC al server (livello dei mes-saggi OSC). Dalla discussione precedente risulta infatti chiaro che sclangè soltanto uno dei possibili client di scsynth. Tuttavia, sclang, essendopensato esplicitamente per lavorare con scsynth, ne è in qualche modo ilclient privilegiato, ad esempio fornendo all’utente un linguaggio di altolivello (altri livelli più astratti) e traducendolo per lui in messaggi OSC.

6.2 Esempi

I due esempi seguenti illustrano l’utilità dell’architettura client/serverdi SuperCollider, che lo rendono particolarmente flessibile nell’interazionecon altri software. Si tratti di aspetti piuttosto complessi a livello intro-duttivo, ma che hanno il solo scopo di far intuire il funzionamento ge-nerale.

6.2.1 SwingOSC

Si è già osservato che SwingOSC è un server grafico in Java, che può es-sere controllato dall’interno di sclang. In sostanza, vale un ragionamen-to analogo a quanto visto per la comunicazione con scsynth. SwingOSCè un server e riceve messaggi via OSC da qualsiasi client che sia in gradodi 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.2–149

Sono allora state predisposte dallo sviluppatore alcune classi che per-mettono dall’interno di sclang un controllo di più alto livello rispettoall’invio diretto di messaggi OSC. Si tratta delle classi già incontrate inprecedenza discutendo di interfacce grafiche. La situazione è raffiguratain 6.3.

scsynth

s.sendMsg

synth.play

func.play

messaggi OSC

messaggi OSC

sclang

language wrapping

app. esterna

Client side Server side

livello messaggi OSCaltri livelli più astratti

SwingOSC

JSCWindow.new

g.sendMsg

messaggi OSC

messaggi OSC

app. esterna

Fig. 6.3 Architettura client/server: sclang, scsynth eSwingOSC.

Ad esempio, il codice sclang GUI.window.new crea un elemento GUI at-traverso SwingOSC, una semplice finestra. Sclang interpreta il codice einvia gli opportuni messaggi a SwingOSC per la creazione dell’elementografico. Il caso più interessante è però quando si crea un elemento chenon è soltanto una “view” ma anche un “controller”: ad esempio unelemento-manopola la cui escursione permmetta di controllare un pa-rametro audio (ad esempio una manopola del volume). In fase di inizia-lizzazione (6.4, 1) è necessario:

6.2–150

1. istanziare un dispositivo di sintesi sul scsynth

2. istanziare un elemento GUI su SwingOSC

3. stabilire la relazione tra variabile di controllo dell’elemento GUI evariabile di controllo dell’elemento audio

In fase di esecuzione, il comportamento dell’utente viene intercettatoda SwingOSC (l’utente muove la manopola che è oggetto istanziato dalserver SwingOSC), il quale comunica di ritorno a sclang il nuovo valoredella manopola (6.4, 2). Sclang a sua volta utilizza il valore ottenuto perinviare a scsynth un messaggio che indichi di modificare quel parametroaudio (6.4, 3).

sclang

scsynth

SwingOSC1

1

2

3

Fig. 6.4 Creazione econtrollo di un elemento GUI.

Sebbene all’utente finale, soprattutto in Windows, la discussione pre-cedente possa sembrare troppo macchinosa, va ricordato che la consa-pevolezza della architettura è l’unica strada per capire esattamente ilfunzionamento di SC. In altre parole, in SC, anche quando non si vedein alcun modo, se c’è segnale audio c’è sempre un server audio che perfunzionare ha bisogno di essere controllato via messaggi OSC.

6.3–151

6.2.2 Graphista!

Un esempio abbastanza sofisticato di sfruttamento della architettura direte di SC è fornito in Figura 6.5. Graphista! è un programma con inter-faccia grafica per la composizione musicale algoritmica inizialmente in-teramente scritto nel linguaggio Python. Non pensato per il tempo reale,è stato ampliato per funzionare anche in RT utilizzando SC come motoreper la sintesi audio. La reimplementazione in SC dell’interfaccia grafica,piuttosto complessa, sarebbe stata complicata. Poiché esiste un modulodi Python che gestisce messaggi OSC, è stato allora possibile connetteredirettamente Graphista! a scsynth: l’utente controlla la sintesi attraver-so SC a partire dall’interfaccia grafica in Python di Graphista! (6.5, a). Infase di implementazione, ci si è però resi conto che la precisione di Py-thon per il controllo della generazione di eventi in tempo reale è piutto-sto limitata. La soluzione è consistita nel trattare anche sclang come unserver (6.5, b). sclang infatti non solo invia, ma può ricevere messaggiOSC. Dunque, Graphista! invia messaggi OSC a sclang: i messaggi con-tengono frammenti di codice sclang. Quest’ultimo intepreta il codice e lotraduce in altri messaggi OSC per scsynth. In questa implementazionel’utente attraverso Graphista! controlla indirettamente scsynth attraver-so sclang (propriamente scsynth è inaccessibile a Graphista!).

6.3 Ilclientsclang

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

6.4–152

synthDefs

Graphista!

OSC messages

Client side Server side

SC scsynth

graph datastructures

User

init init

eventevent

init

audio rate

(Python)

synthDefsGraphista!

OSC messagesClient side Server side

SC scsynth

graph datastructures

User

init init

event event

init

audio rate

SC sclang

variablesupdate

OSC messages

(Python)

a. Prima ipotesi (progetto) b. Seconda ipotesi(implementazione)

Fig. 6.5 Graphista!: uso dell’architettura di rete

1. In primo luogo, sclang è l’interprete di un linguaggio di alto livelloche permette all’utente quasi di dimenticarsi della struttura client/servere di molto di quello visto fin’ora.

2. In secondo luogo, il privilegio maggiore di sclang sta nel fatto chepermette la definizione e l’invio al server delle synthDef. Le synth-Def devono risiedere sul server, ma come si fa a mettercele? Sclangoffre questa possiblità. Non è impossibile farlo da altri client, ma èmolto più complicato.

6.4 Unimpiantochimicoperlaproduzionediliquidieunserveraudiointemporeale

Dunque scsynth è un motore di sintesi audio programmabile e control-labile in tempo reale. Non è agevole di primo acchito riuscire a tenerpresente le relazioni tra tutti gli elementi pertinenti per il server audioscsynth. Conviene perciò introdurre una quadro metaforico e pensareserver come ad un impianto chimico per la sintesi di liquidi. Nella di-scussione seguente si prenda in considerazione la figura 6.6.

6.4–153

Dispositivo

Dispositivo

Dispositivo

Dispositivo

Dispositivo

Dispositivo

Dispositivo

Dispositivo

Macchinario

contenitorecontenitore

Impianto

tubo di collegamento

tubo in uscita

tubo in entrata

Macchinario

Macchinario

tubo in uscita

Fig. 6.6 Il server audio come impianto chimico pergenerazione di liquidi di sintesi.

1. Per sintetizzare un liquido è necessaria un macchinario complesso

2. Un macchinario è costituito di dispositivi specializzati in cui i liquidisubiscono trasformazioni. I dispositivi sono collegati attraverso tubiinterni.

3. Un macchinario deve essere progettato predisponendo le relazionitra dispositivi componenti. A partire da un progetto, può essere co-struito un numero indefinito di macchinari identici.

4. Una volta costruito, il macchinario non può essere modificato nellasua struttura interna

5. Ma un addetto può controllarne il comportamento dall’esterno at-traverso leve e comandi, così come monitorarne il funzionamento

6. Un impianto può comprendere più macchinari che lavorano in pa-rallello

6.4–154

7. Quando l’impianto è in funzione i liquidi scorrono lungo i tubi avelocità costante, senza mai potersi fermarsi.

8. I liquidi possono scorrere nei tubi a due velocità differenti (ma sem-pre costanti), in particolare a velocità di controllo e a velocità di sin-tesi

9. I liquidi possono però essere stoccati in quantità limitate dentro ap-positi contenitori, da cui è possibile attingere quando serve. Questiliquidi di per sé non scorrono, ma, attraverso dispositivi specializza-ti, possono essere riversati in un liquido in scorrimento.

10. Tipicamente (anche se non necessariamente) un macchinario preve-de un dispositivo munito di un tubo che permette di far uscire illiquido all’esterno. Altre volte può avere anche dispositivo con untubo in entrata da cui ricevere un liquido che proviene da altri mac-chinari

11. La circolazione dei liquidi tra l’impianto e l’esterno (l’acqua dall’acquedottoin entrata, il prodotto sintetizzato in uscita) oppure tra i diversi mac-chinari nell’impianto avviene attraverso tubi speciali. I primi sonotubi di entrata/uscita, i secondi di collegamento. Attraverso quesiultimi, i liquidi possono così circolare nell’impianto e sono a dispo-sizione degli altri macchinari. Questi tubi permettono perciò di con-nettere diversi macchinari

12. I tubi di collegamento disperdono il loro liquido (che non è uscitoattraverso i tubi di uscita dell’impianto) negli scarichi dell’impianto.I liquidi non inquinano e la loro dispersione non è rilevante.

È possibile a questo punto riconsiderare i punti precedenti, sostituendoopportunamente i nomi di figura 6.6 con quelli di figura 6.7.

6.4–155

UG UG UG

UG UG UG

UG UG

Synth

Synth

Synthbuffer buffer

SERVER

bus

Out bus

In bus

Out bus

Fig. 6.7 Componenti del server audio.

Osservazione 1

Per sintetizzare un liquido è necessaria un macchinario complesso

Per sintetizzare un segnale audio è necessario un macchinario softwareche in SC prende il nome di Synth: un synth è appunto un sintetizzatoreaudio.

Osservazione 2

Un macchinario è costituito di dispositivi specializzati in cui i liquidisubiscono trasformazioni. I dispositivi sono collegati attraverso tubi in-terni

Per generare segnale audio un synth richiede di specificare quali algo-ritmi di elaborazione/sintesi del segnale da utilizzare. In SC, seguendola tradizione della famiglia di linguaggi Music N, gli algoritmi di ela-borazione/sintesi sono implementati in UGen (→Unit Generator): unaUGen è semplicemente un dispositivo software che elabora o sintetizzasegnale audio. Ad esempio SinOsc è una UGen che genera segnali si-nusoidali: per avere un’idea è sufficiente eseguire questa riga di codice:

6.4–156

{SinOsc.ar}.play. Le UGen costituiscono i componenti di base di unsynth. Un synth è appunto un sintetizzatore, un dispositivo di sintesicostruito con componenti UGen.Le UGen possono avere più entrate, ma hanno sempre soltanto un’uscita.Una UGen può ricevere in entrata un’altra UGen: questo processo sichiama 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, ungrafo di UGen, una struttura che rende conto delle relazioni tra UGen.Poiché le UGen generano segnali lo UGen-graph descrive il flusso deisegnali che dalle diverse sorgenti si “raccolgono” nel segnale. Lo UGen-graph è la cartina geografica di un fiume che raccoglie contributi di di-versi affluenti per poi terminare in mare.

Osservazione 3

Un macchinario deve essere progettato predisponendo le relazioni tradispositivi componenti. A partire da un progetto, può essere costruitoun numero indefinito di macchinari identici

Il client di SC chiede perciò al server di costruire e di far funzionareun synth per lui. Per soddisfare la richiesta il server deve sapere qualipezzi (UGen) utilizzare e in quali relazioni combinarli (patching in unoUGen-graph). Poiché è probabile che all’utente possano servire più voltegli stessi dispositivi, SC prevede un passaggio supplementare. L’utenteprima specifica al server una definizione di un synth (synthDef), unasorta di progetto dettagliato di come deve essere fatto il synth deside-rato, e quindi chiede al server di costruire un synth seguendo quel pro-getto. Una synthDef associa un nomen ad uno UGen-graph u, in modoche si possano creare synth di tipo n che generano segnali attraverso lerelazioni tra UGen previste da u. Una volta create, le synthDef possono

6.4–157

essere memorizzate in formato binario e restare perciò sempre disponi-bili all’utente. L’utente può in altre parole crearsi una libreria di synth-Def (intese come progetti o come stampi da cui creare synth) e, quandoè opportuno, chiedere al server di creare un synth a partire dalla synth-Def.In sostanza per usare SC come motore di sintesi è necessario compierealmeno passi:

1. definire una synthDef (definire il progetto del sintetizzatore)2. istanziare un synth a partire da una synthDef (costruire il sintetizza-

tore)

Osservazione 4

Una volta costruito, il macchinario non può essere modificato nella suastruttura interna

Una synthDef è un diagramma, uno schema: oggetto statico. Una vol-ta spedita al server, è immutabile. Se prevede due entrate, quelle avrà.D’altra parte, è sempre possibile spedire al server una nuova synthDefche prevede le modifiche desiderate.

Osservazione 5

Ma un addetto può controllarne il comportamento dall’esterno attraver-so leve e comandi, così come monitorarne il funzionamento Il proget-to di un sintetizzatore è descritto in una synthDef attraverso lo UGen-Graph. Lo UGen-Graph è descritto sintatticamente attraverso una fun-zione (ed infatti è racchiuso tra parentesi graffe). Come ogni funzio-ne può prevedere argomenti in entrata: questi argomenti sono appun-to parametri per il calcolo, svolto dal corpo della funzione, del valore(l’ampiezza del segnale) che la funzione restituisce in uscita.

6.4–158

Osservazione 6

Un impianto può comprendere più macchinari che lavorano in parallel-lo

Ogni macchinario dell’esempio idraulico rappresenta un synth, ovvero,musicalmente parlando, uno strumento -o in fondo anche una voce. Ilserver audio può gestire un numero indefinito di sintetizzatori in paral-lelo. In altre parole, il numero massimo degli strumenti -o delle voci- chepossono suonare contemporaneamente dipende esclusivamente dallerisorse hardware a disposizione.

Osservazione 7

Quando l’impianto è 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 liqui-do dipende dal fatto che, poiché a questo punto non si sta parlando solodi segnali, ma di segnali in tempo reale, i segnali sono certo sequenze divalori di ampiezza secondo quanto visto finora, ma in più con il vinco-lo che tali campioni devono essere inesorbilmente calcolati ad un tassouniforme nel tempo (tipicamente, ma non necessariamente, nel caso disegnali audio 44.100 volte in un secondo). Ad ogni istante di tempo, unnuovo campione deve essere calcolato nella sequenza: ogni synth effet-tua tutti i calcoli previsti da tutte le UGen che lo compongono e resti-tuisce un valore. In altre parole, ad ogni istante deve essere attraversatotutto lo UGen-Graph, indipendentemente dalla sua complessità. Atten-zione: se si considera l’esempio idraulico, ciò significa che se una gocciaentra da un tubo in entrata nell’istante x l’istante dopo (x+ 1) deve es-sere già attraversato tutto l’impianto, ed essere in uscita. Ovvero: dentro

6.4–159

ogni macchinario, lo scorrimento del liquido lungo i tubi che connettonoi dispositivi è letteralmente istantaneo.

Osservazione 8

I dispositivi possono lavorare a due velocità differenti (ma sempre co-stanti), in particolare a velocità di controllo e a velocità di sintesi

Dunque ad ogni istante una nuova goccia deve uscire da un macchinariodopo aver percorso tutto il complesso dei dispositivi. I dispositivi nonnecessariamente però aggiornano il loro comportamento ad ogni istan-te: possono modificare la loro azione soltanto una volta ogni n istanti.Un segnale di controllo è tipicamente un segnale che cambia meno neltempo di un segnale audio e che quindi può essere calcolato ad una ri-soluzione più bassa. Ad esempio, è inutile calcolare per ogni campioneaudio il valore di un inviluppo d’ampiezza. Se infatti si calcola un valoredell’inviluppo per il quale moltiplicare il segnale audio, e lo si mantienecostante per 10 campioni audio, per poi ricalcolarlo all’undicesimo, dinuovo maneternlo costante per altri 10, e così via, si ottiene un segna-le che è evidentemente più “scalettato” in ampiezza ad un’analisi dellaforma d’onda, ma che di fatto non è sensibilmente diverso da un segnalein cui l’inviluppo d’ampiezza sia stato calcolato per ogni campioni. Incompenso sono state risparmiate notevoli risorse computazionali. Unsegnale simile è un segnale calcolato non a tasso audio (audio rate), ma atasso di controllo (control rate). come si vedrà, le UGen generano segnalinel momento in cui ricevano il messaggio .ar, o .kr: rispettivamenteil segnale risultante sarà aggiornato a tasso audio (audio rate) o a tassodi controllo ([k]ontrol rate). Si noti che si sta parlando di tasso di aggior-namento, e non di numero di campioni. SC genera un segnale audio in

6.4–160

tempo reale per forza a tasso audio: ma alcuni segnali che intervengononella sintesi sono aggiornati ad un tasso più basso di controllo.

Osservazione 9

I liquidi possono però essere stoccati in quantità limitate dentro appo-siti contenitori, da cui è possibile attingere quando serve. Questi liquididi per sé non scorrono, ma, attraverso dispositivi specializzati, possonoessere riversati in un liquido in scorrimento

Un buffer è una memoria temporanea che permette di conservare deidati audio richiesti da certi algoritmi di sintesi. Ad esempio, si considerila lettura di un file audio in funzione dell’elaborazione del suo contenu-to. Si potrebbe partire da un frammento di voce umana e costruire unsintetizzatore che intoni la voce sulle diverse altezze come rappresen-tate sui tasti di un pianoforte. Un simile sintetizzatore concettualmenteprevede due UGen.

• la prima elabora il frammento vocale rispetto all’altezza

• la seconda è responsabile della comunicazione con la scheda audio

Per poter elaborare il frammento, quest’ultimo deve essere disponibi-le alla prima UGen tutte le volte che questa ne ha bisogno: si pensi adeseguire una melodia a partire da quell’unico frammento trasformandoopportunamente l’altezza lungo la sequenza temporale delle note checompongono la melodia. Il contenuto del file audio deve allora essereletto dall’hard disk e viene conservato in memoria temporanea. In SCun buffer è appunto una simile memoria che il server allocata su richie-sta. Il segnale audio contenuto nel buffer di per sé è statico: e tuttavia vi

6.4–161

può essere una UGen che legge il contenuto del buffer in tempo reale elo invia alla scheda audio.

Osservazione 10

Tipicamente (anche se non necessariamente) un macchinario prevedeun dispositivo munito di un tubo che permette di far uscire il liquidoall’esterno. Altre volte può avere anche dispositivo con un tubo in en-trata da cui ricevere un liquido che proviene da altri macchinari

Il segnale numerico sintetizzato deve essere inviato alla scheda audioin modo tale che quest’ultima lo converta in segnale elettrico e lo in-vii 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 allascheda audio. Tipico esempio è la UGen Out. Evidentemente UGen diquesto tipo occupano l’ultimo posto nello UGen-Graph che rappresen-ta un synth. Se si omette una UGen di uscita il segnale viene calcolatosecondo quanto previsto dallae altre UGen nello UGen-Graph ma noninviato alla scheda audio (fatica mentale e computazionale sprecata). Sisupponga poi di collegare un dispositivo di entrata alla scheda audio, adesempio un microfono. Una UGen specializzata può rendere disponibi-le al synth tale segnale, in modo che possa essere elaborato (ad esempiosubire un qualche tipo di distorsione). A tal proposito SC prevede laUGen AudioIn.

Osservazione 11

La circolazione dei liquidi tra l’impianto e l’esterno (l’acqua dall’acquedottoin entrata, il prodotto sintetizzato in uscita) oppure tra i diversi mac-chinari nell’impianto avviene attraverso tubi speciali. I primi sono tubidi entrata/uscita, i secondi di collegamento. Attraverso quesi ultimi, i

6.4–162

liquidi possono così circolare nell’impianto e sono a disposizione de-gli altri macchinari. Questi tubi permettono perciò di connettere diversimacchinari

Si è già osservato come il server preveda una comunicazione con la sche-da audio, in entrata (“dal microfono”) e in uscita (“agli altoparlanti”).Questi canali di comunicazione prendono il nome di “bus”, secondo untermine che deriva dalla tecnologia dei mixer21. In effetti, rispetto a ”ca-nale” (che pure è il termine audio più vicino) il termine “tubo” può esse-re meno fuorviante oltre mnemotecnicamente efficace. Il sistema dei busnon deve essere pensato come un sistema di tubi che connettono statica-mente i macchinari, ma come un sistema di tubi disponibili a cui i mac-chinari si raccordano. Ad esempio, tutti i synth che intendono elaborareun segnale che provenga dall’esterno possono raccordarsi al bus che èdeputato alla lettura dell’entrata della scheda audio (al tubo che immet-te un liquido dall’esterno). Tutti i synth, per inviano i segnali in uscitaalla scheda audio, si raccordano ai bus che gestiscono la comunicazionecon quest’ultima: i segnali sui bus in uscita semplicemente si sommano.I bus finora citati sono specializzati per segnali audio (audio busses), maesistono anche bus specificamente dedicati ai segnali di controllo controlbusses. I bus sono indicati attraverso un numero progressivo, un indicea partire da 0. Per i bus di controllo, la numerazione è progressiva e nonci sono aspetti particolari da tener presenti. Per i bus audio, è invecenecessario ricordare che essi gestiscono la comunicazione con la sche-da audio. In particolare i primi bus audio (0, . . . , n) sono riservati alleuscite della scheda audio, i secondi alle entrate (n+ 1, . . . , o) (i tubi dientrata/uscita), seguono i bus ad uso interno (o+1, . . .) (i tubi di colle-gamento. Dunque, l’indicizzazione dipende dalla propria scheda audio.La configurazione standard con uscita stereo e ingresso microfono pre-vede i bus 0, 1 per i due canali stereo (i due altoparlanti) e il bus 2 per il

Il termine non sta per bus = mezzo di trasporto. Ed infatto pensare ad un bus audio21

come a un autobus è fuorviante.

6.5–163

microfono. Dal 3 in avanti, i bus possono essere utilizzati internamen-te. Per farci cosa? Per connettere diversi synth. Ad esempio, un synthinstrada il segnale in uscita sul bus 4 da dove altri synth possono pren-derlo. Ovvero, idraulicamente un macchinario si raccorda ad un tubo inun punto e vi immette del liquido: più avanti lungo il tubo un secondomacchinario si raccorda e preleva il liquido immesso.

Osservazione 12

I tubi di collegamento disperdono il loro liquido (che non è uscito attra-verso i tubi di uscita dell’impianto) negli scarichi dell’impianto. I liquidinon inquinano e la loro dispersione non è rilevante

Una volta immessi su un bus, i segnali sono disponbili. Se non si scri-ve su un bus connesso alla scheda audio semplicemente non si ha unrisultato percepibile. In altre parole, inviare un segnale su un bus nonrichiede di sapere cosa altri potranno fare di quel segnale, e neppure semai qualche altro synth lo utilizzerà. Che cosa succede al segnale sul busè irrilevanet. Questo assicura una comunicazione possibile tra synth, masenza che questa diventi obbligatoria o prevista in anticipo.

6.5 Appetizer:unesempiodisintesiecontrollointemporeale

L’esempio seguente serve per dare un’idea degli insieme degli elemen-ti fin qui citati. Senza scendere troppo nel dettaglio (scarso), ci si puòquantomeno fare un’idea generale di come funziona il tutto.

6.5–164

1 s = Server.local.boot ; // first boot the server

3 (4 // audio5 SynthDef.new("sineMe", { arg out = 0, amp = 0.25, kfreq = 5 ;6 Out.ar(out, SinOsc.ar(kfreq*50, mul: LFPulse.kr(kfreq, 0.25))*amp);7 }).send(s);8 )

10 (11 // four vars12 var aSynth, window, knob1, knob2, button;

14 aSynth = Synth.new("sineMe"); // the synth

16 // GUI: creation17 window = GUI.window.new("Knob", Rect(300,300,240,100));18 window.front;

20 knob1 = JKnob.new(window, Rect(30, 30, 50, 50));21 knob1.value = 0.25;

23 knob2 = JKnob.new(window, Rect(90, 30, 50, 50));24 knob2.value = 0.3;

26 button = GUI.button.new(window, Rect(150, 30, 50, 50)) ;27 button.states = [ // array of states28 [ "stop", Color.black ], ["start", Color.red]] ;

30 // GUI: controlling audio31 knob1.action_({arg v; aSynth.set("amp", v.value); });32 knob2.action_({arg v; aSynth.set("kfreq", v.value*15); });33 button.action = ({ arg button;34 var val = button.value.postln;35 if (val == 1, { aSynth.run(false) }, { aSynth.run })36 });

38 )

6.5–165

6.5.1 UnasynthDef

In primo luogo, è necessario costruire lo strumento che generi il segnale.Un esempio minimale di synthDef è quello riportato di seguito.

1 SynthDef.new("bee",2 { Out.ar(0, SinOsc.ar)}3 ).send(s);

• SynthDef: è l’oggetto che interessa• .new( … ): new è il metodo costruttore, che costruisce effettivamente

la synthDef (restituisce l’oggetto synthDef). Il metodo new prevedeun certo numero di argomenti. Qui ne vengono specificati due, pergli altri è opportuno lasciare quelli predefiniti.

• "bee": il primo argomento è una stringa che rappresenta il nome del-la synthDef: il nome verrà associato allo UGen-graph. I synth gene-rati a partire da questa synthDef saranno dei “bee”, cioè dei synthdel tipo “bee”. Qui “bee” è una stringa, ma potrebbe anche essere unsimbolo (\bee).

• { Out.ar(0, SinOsc.ar)}: lo UGen-graph è racchiuso tra paren-tesi graffe. Tutto ciò che è tra graffe in SC è una funzione. Dunque,lo UGen-graph è descritto da una funzione. Lo UGen-graph è costi-tuito da due UGen, Out e SinOsc: questo fatto è reso esplicito dalmessaggio .ar che le due UGen ricevono. In generale ciò che rispon-de al messaggio .ar o .kr è una UGen. E perché una UGen generi

code/server/simpleKnob.sc
code/server/synthDefMinimal.sc

6.5–166

in tempo reale un segnale deve ricevere i messaggi .ar o .kr. Perchéuna funzione per descrivere le relazioni tra UGen? Si ricordi che unafunzione è un oggetto che restituisce un valore ogni qualvolta glielosi chiede. Un synth è allora descritto da una funzione perché ad ogniistante che passa gli viene chiesto di restituire un valore di ampiez-za, il valore del campione audio. Ad ogni istante viene calcolato ilvalore della funzione descritta dalla UGen-Graph.

SinOsc Out

Synth: "bee"

Out bus: 0SERVER

Fig. 6.8 Schema di una synthdef minimale.

In Figura 6.8 è rappresentato, secondo le convenzioni precedenti, unsynth costruito a partire dalla synthDef “bee”. Come si vede il se-gnale risultante viene “raccordato” (inviato) sul bus 0. In manierapiù consueta lo UGen-Graph può essere descritto in forma di dia-gramma di flusso dal grafo di 6.922. Out è la UGen che si occupa diinviare alla scheda audio il segnale generato: senza Out non c’è co-municazione con la scheda audio, quindi non c’è suono udibile. Outprevede due argomenti. Il primo è l’indice del bus su cui inviare ilsegnale in uscita, il secondo è il segnale stesso. Out riceve un segnalee lo spedisce al bus audio 0, che indica, come già osservato, un cana-le della scheda audio (si ricordi: nei bus audio, prima gli output, poigli input). Il segnale che spedisce gli viene fornito da SinOsc: è uncaso di patching, di innesto di una UGen (SinOsc in un’altra (Out).SinOsc genera una segnale sinusoidale: laddove non si specifichino

Generato utilizzando le classi dot di Rohann Drape.22

6.5–167

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

SinOsc 440 0

Out 0

Fig. 6.9 Rappresentazionedello UGen-graph.

• .send(s): la synthDef, di per sé, non serve a nulla se non è carica-ta sul server. Il server è convenzionalmente assegnato alla variabileglobale s. Dunque il codice .send(s) chiama il metodo .send defi-nito per la synthDef, e dice alla synthDef di “spedirsi” al server s.Attenzione: tutto ciò che sta in una synthDef è costituito da specifi-che istruzioni per la sintesi del segnale.

Oltre a send, vi sono molti metodi definiti per la classe SynthDef, chepermettono ad esempio di scrivere la definizione su file (si tratta delmetodo writeDeFile): le synthDef così memorizzate verrano caricatead ogni accensione del server, e saranno perciò subito disponibili.Tornando all’esempio, ora il server ha pronto il progetto “bee” per potercreare dei synth di tipo “bee”.

6.5.2 UGeneUGen-Graph

6.5–168

La definizione della synthDef dell’esempio originale è invece la seguen-te.

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

Si noti come in questo caso lo UGen-Graph preveda alcuni argomentiin entrata che ne permettono il controllo in tempo reale. Essi sono out,amp, kfreq, tutti dotati di valori predefiniti. Ogni synth di tipo “sineMe”metterà a disposizione dell’utente i tre controlli equivalenti in entrata.La riga 2 descrive il patching tra UGen (Out, SinOsc, LFPulse). Si os-servi come quest’ultima aggiorni i propri valori ricalcolando il valorein uscita a tasso di controllo, secondo quanto previsto dal messaggio krinviato a LFPulse.È opportuno ora soffermarsi di più su una UGen, in particolare su SinOsc.Per sapere come si comporta ci si può evidentemente rivolgere all’helpfile relativo. Un’altra opzione, utile in fase di studio almeno, consistenell’accedere alla definizione nel codice sorgente.

1 SinOsc : UGen {2 *ar {3 arg freq=440.0, phase=0.0, mul=1.0, add=0.0;4 ^this.multiNew('audio', freq, phase).madd(mul, add)5 }6 *kr {7 arg freq=440.0, phase=0.0, mul=1.0, add=0.0;8 ^this.multiNew('control', freq, phase).madd(mul, add)9 }

10 }

code/server/synthDef.sc

6.5–169

Si noti come SinOsc erediti da UGen, la sopraclasse generica di tutte leUGen. In più, definisce soltanto due metodi di classe, ar e kr. Lasciandoperdere l’ultima riga di ogni metodo, si nota come i metodi prevedanoun certo numero di argomenti a cui può essere passato un valore “dafuori”. Si noti anche come tutti gli argomenti tipicamente abbiano unvalore predefinito. 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 mag-gior parte delle UGen: mul è un moltiplicatore del segnale mentre addè un incremento (positivo o negativo) che viene sommato al segnale. Siconsiderino 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 UGentipicamente è normalizzato, la sua ampiezza oscilla in [−1, 1] (altrevolte è compreso in [0, 1]). L’argomento mul definisce un moltiplica-tore che opera sull’ampiezza così definita, mentra add è un incrementoche si aggiunge allo stesso segnale. Nella prima riga il segnale è moltipli-cato per 1 e sommato a 0. Il segnale è cioè immutato. Si noti che i valorispecificati sono quelli predefinti, quindi si potrebbe scrivere la riga suc-cessiva ed ottenre esattamente lo stesso risultato. A scanso di equivoci,

code/server/sinOscSource.sc

6.5–170

“moltiplicare” e “aggiungere” significa che ogni campione del segnaleè moltiplicato e sommato per i valori specificati 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].Infine nell’esempio seguente

{SinOsc.ar(220, mul: 0.5, add:0.5)}.scope ;

il segnale precedente viene sommato a 0.5: la sua ampiezza sarà com-presa in [−1.0, 1.0]× 0.5 + 0.5 = [−0.5, 0.5] + 0.5 = [0.0, 1.0].L’assegnazione mul del valore costante 0.5 indica che ogni nuovo cam-pione verrà moltiplicato per 0.5. Si potrebbe pensare allora che mul siaun segnale costante. A tal proposito si può prendere in considerazionela UGen Line. Come dice l’help file:

“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 unasequenza di valori che vanno da start a adur in dur secondi. Nel codiceseguente

{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è unaprogressione da 0.5 a 0.5). Il segnale in uscita dall’oscillatore SinOscviene moltiplicato per l’uscita di Line. Ad ogni istante di tempo il cam-pione calcolato dalla prima UGen viene moltiplicato per il campionecalcolato dalla seconda (che ha sempre valore 0.5). Si noti che il segnale

6.5–171

risultante è uguale a quello precedente. È chiaro che l’aspetto interessan-te nell’uso di Line sta proprio nel fatto che i valori che la UGen generanon sono costanti ma variano invece tipicamente secondo una progres-sione (lineare, appunto).Questo è un crescendo dal niente:

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

Il patching è appunto l’innesto di una UGen in un argomento di un’altrao il calcolo di un segnale a partire dal contributo offerto da più UGenin una qualche relazione reciproca (qui di moltiplicazione). L’esempiopermette di capire come gli argomenti possano essere descritti non dacostanti ma da variabili, cioè da alti segnali. In altre parole, i segnalipossono modificare qualsiasi aspetto controllabile di altri segnali.

out:0

Out bus channelsArray

amp:0.25

* a b

kfreq:5

* a b: 50

LFPulse freq iphase: 0.25 width: 0.5

SinOsc freq phase: 0

* a b

Fig. 6.10 Rappresentazione dello UGen-graph.

6.5–172

Nella figura 6.10 è rappresentato il diagramma di flusso della synthDef"sineMe". Gli elementi neri descrivono il flusso a tasso audio, tutti glialtri le informazioni a tasso diverso (controllo/evento). LFPulse lavoraa tasso di controllo, mentre kfreq, amp, out vengono modificate a tassodi evento (ogni qualvolta un utente modifica i parametri). I blocchi “* ab” indicano blocchi di moltiplicazione. I valori di argomenti nelle UGennon specificati sono indicati attraverso i valori predefiniti.Il segnale moltiplicatore è prodotto da LFPulse, un generatore di ondequadre, con frequenza "kfreq" (quindi collegata alla frequenza dellasinusoide). Il segnale in uscita da LFPulse è unipolare, cioè compresonell’intervallo [0, 1]. Si può capire meglio la natura del segnale generatoattraverso:

{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 dell’argomento mul), e oscilla da uno all’altro 100 volte al se-condo. Utilizzando un segnale simile come moltiplicatore di un altrosegnale, si ha che, quando l’ampiezza è 0.0, il segnale risultante ha am-piezza 0.0 (silenzio), quando l’ampiezza è 0.5, in uscita si ha il segnaledi partenza scalato per 0.5. In sostanza, si produce una intermittenza.Dunque, la frequenza della sinusoide è correlata alla frequenza di inter-mitteza (più è acuta la frequenza, più frequentemente è intermittente).

6.5.3 Unsynth

Le righe 10 e 11 dell’esempio in discussione creano e mettono in funzio-ne un synth. In particolare

aSynth = Synth.new("sineMe")

6.5–173

assegna alla variabile aSynth un oggetto di tipo Synth inviando il co-struttore new alla classe. Il costruttore prevede come argomento una strin-ga 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) maanche come un evento sonoro (una nota): a pensarlo così, diventa ovvioche la costruzione del synth equivale alla generazione di un evento so-noro.Il codice seguente assume che la synthDef precedente sia ancora dispo-nibile.

1 ~synth1 = Synth.new("sineMe", [\kfreq, 10]) ; // synth plays, array ofargs

3 ~synth2 = Synth.newPaused("sineMe") ; // another synth4 ~synth2.run(true) ; // start playing

6 ~synth1.set(\kfreq, 3) ; // set kfreq to 3

8 ~synth2.setn(\amp, [20, 0.1]) ; // set 2 args from amp

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

12 ~synth2.getn(\out, 3, { arg valArray ; valArray.postln }) ; // get 4args from out

14 ~synth1.run(false) ; // stop playing15 ~synth2.run(false) ; // stop playing

17 ~synth1.free ; // free the synth18 ~synth2.free ; // free the synth

code/server/synth.sc

6.5–174

La riga 1 crea un synth e lo associa alla variabile ~synth1. Dopo il no-me, viene specificato un array che contiene coppie composte da nomedell’argomento 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 as-sociato 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, passan-do nome e valore (6). Attraverso setn è possibile controllare più argo-menti, passando il nome del primo e un array che contiene i valori deglin argomenti successivi. Nell’esempio vengono impostati due argomentia partire da \amp (incluso), ovvero \amp e \kfreq (10). Simmetricamentei metodi get e getn consentono di recuperare i valori degli argomentidal synth. Il primo restituisce il valore di un singolo argomento, mentreil secondo un array din valori (3 nell’esempio) a partire dal primo indi-cato (\out, ovvero i valori di \out, \amp, \kfreq). I valori sono passaticome argomenti della funzione seguente, che, negli esempi di riga 10 e12, chiede semplicemente di stampare i valori stessi sulla post window.Una synth non più utilizzato può essere messo in pausa, ma in quel casocontinua ad essere residente in memoria, e dunque ad occupare inutil-mente risorse del calcolatore. Per eliminare dal server il synth. è suffi-ciente inviare all’oggetto synth il messaggio free (17, 18).

6.5.4 GUI econtroller

Sulla creazione di elementi GUI non c’è molto da dire. Si tratta di co-struire due manopole all’interno di una finestra, secondo una tecnicagià vista più volte. Di nuovo c’è la creazione di un pulsante a partire daGUI.button, secondo una sintassi del tutto analoga a quella di GUI.knob.

6.5–175

La riga successiva definisce quanti stati ha il pulsante attraverso un ar-ray 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 manopolee al pulsante. La riga

knob.action_(arg v; aSynth.set("amp", v.value);

definisce la connessione tra il controller GUI knob1 e sintesi audio. L’azioneè associata alla manopola (knob1.action_) è

aSynth.set("amp", v.value)

Ogni volta che cambia il valore della manopola viene chiamato sull’oggettoaSynth (il synth) il metodo set che assegna al parametro specificato("amp") un valore, qui v.value. Il parametro ("amp") viene allora aggior-nato dentro lo UGen-Graph. Esso indica il moltiplicatore che è posto infondo alla cascata delle UGen SinOSc e LFPulse: di fatto si comportacome un controllo del volume a cui viene attacata una manopola knob1.Quest’ultima genera valori nell’intervallo [0, 1], cioè nell’escursione d’ampiezzadel segnale audio in forma normalizzata, e il segnale dunque varierà tra0 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. L’escursione di "kfreq"sarà perciò compresa in [0, 15]. Infine anche a button viene assegnataun’azione secondo una sintassi analoga a JKnob: l’argomento button in-dica appunto il pulsante, per cui button.value permette di accedere allostato del pulsante (ovvero l’indice dello stato nell’array degli stati but-ton.states). Ad ogni pressione del pulsante viene richiamata l’azione.L’azione valuta il lo stato del pulsante attraverso il costrutto condizio-nale (34). Se il valore val del pulsante è 1 viene chiamato il metodo

6.5–176

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 ese-gue il codice, il synth è attivo e lo stato è 0. Alla prima pressione lo statodiventa 1, la GUI viene aggiornata e viene eseguito il ramo condizionaleche contiene aSynth.run(false).

7.1–177

7 Controlliecanali

7.1 Inviluppi

Si è già discusso degli inviluppi d’ampiezza a proposito del calcolo di se-gnali in tempo differito. In particolare si era avuto modo di notare comela classe Env fosse specializzata nella generazione di “segnali” d’inviluppo.In realtà un oggetto di tipo Env non è un segnale, ma una sorta di stam-po del profilo temporale dilatabile (o comprimibile) a piacere. Tant’è cheper ottenere un segnale in tempo differito era stato necessario inviareil messaggio asSignal. In tempo reale un oggetto Env è una “forma”temporale a disposizione di una UGen specializzata che, leggendo ilcontenuto di Env, genera un segnale: la UGen EnvGen è appunto spe-cializzata in questo compito. In effetti, l’argomento times del costrut-tore di Env, che richiede di specificare un array di durate, deve esse-re inteso non come una specificazione assoluta, quanto piuttosto pro-porzionale. Le durate specificate indicano cioè la proporzione tra partidell’inviluppo (si ricordi che le durate sono qui riferite agli intervalli tra

7.1–178

valori d’ampiezza), non necessariamente una indicazione cronometri-ca. Si consideri la definizione 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 specifica ulteriormente dall’help file relativo, l’argomento enve-lope è un’istanza di Env. Gli argomenti levelScale, levelBias, ti-meScale sono tutti operatori che trasformano l’inviluppo passato comeprimo argomento. Si prenda come esempio l’inviluppo

1 e = Env.new(2 levels:[0.0, 1.0, 0.5, 0.5, 0.0],3 times: [0.05, 0.1, 0.5, 0.35]4 ).jplot ;

• levelScale: moltiplica i valori d’ampiezza dell’inviluppo. Il valorepredefinito è 1.0, che lascia i valori di levels in e immutati

• levelBias: viene sommato ai valori d’ampiezza. Se questi sono com-presi in entrata nell’escursione [0.0, 1.0], in uscita essi saranno com-presi in [0 + levelBias, 1.0 + levelBias]. Anche qui il valore didefault (0.0) non modifica i valori di e

• timeScale: moltiplica i valori di durata. La durata del nuovo invi-luppo sarà perciò timeScale × times. Il totale degli intervalliin e è pari a 1, dunque la durata dell’inviluppo generato da EnvGensarà pari a 1 × timeScale. Come in precedenza, il valore prede-finito (1.0) non cambia i valori dell’inviluppo passato come primoargomento.

code/controlliCanali/minimalEnv.sc

7.1–179

Come si vede EnvGen può “stirare” la forma dell’inviluppo in lungo e inlargo. In effetti, conviene utilizzare come escursione sia per l’ampiezzache per la durata di un inviluppo Env l’intervallo normalizzato [0.0, 1.0].Si potrà poi agire sugli argomenti di EnvGen. Nell’esempio seguente vie-ne modificato un esempio tratto dall’help file. Qui l’inviluppo è il tipicoinviluppo percussivo senza sostegno, accessibile inviando a Env il mes-saggio perc

1 // explicit signal multiplication2 { EnvGen.kr(Env.perc, 1.0, doneAction: 0) * SinOsc.ar(mul: 0.1) }.play

;

4 // effect of timeScale5 { EnvGen.kr(Env.perc, 1.0, timeScale: 10, doneAction: 0) * SinOsc.ar(mul:

0.1) }.play ;

7 // using mulAmp8 { SinOsc.ar(mul: 0.1 * EnvGen.kr(Env.perc, 1.0, timeScale: 10, doneAction:

0) ) }.play ;

10 // gate = 011 { EnvGen.kr(Env.perc, 0.0, doneAction: 0) * SinOsc.ar(mul: 0.1) }.play

;

13 // controlling the gate14 { EnvGen.kr(Env.perc, SinOsc.kr(4), doneAction: 0) * SinOsc.ar(mul:

0.1) }.play ;

16 JMouseBase.makeGUI ; // SwingOSC17 { EnvGen.kr(Env.perc, GUI.mouseX.kr(-1,1), doneAction: 0) * SinOsc.ar(mul:

0.1) }.play ;

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

code/controlliCanali/envGen2.sc

7.1–180

L’esempio di riga 3 illustra un’implementazione tipica dell’inviluppo: idue segnali vengono moltiplicati tra di loro ed, essendo un unipolare el’altro bipolare, il risultato sarà un segnale bipolare (la sinusoide invi-luppata, come si vede sostituendo a play il metodo jplot). L’altra im-plementazione tipica in SC (che produce lo stesso risultato) è illustratanella riga successiva in cui il segnale di inviluppo generato da EnvGen èil valore dell’argomento mul.Gli altri due argomenti sono:

• gate: l’argomento gate specifica un trigger. Un trigger è un segnaledi attivazione, è “qualcosa che fa partire qualcosa”. Per la precisione,un trigger funziona come una fotocellula: ogniqualvolta registra unpassaggio, invia un segnale. Tipicamente in SC il passaggio che fascattare il trigger è il passaggio dallo stato di partenza, che ha valore0, ad uno stato con valore> 0. In sostanza, EnvGen genera al tassoprescelto un segnale d’ampiezza pari a 0 finché non riceve il trigger.A quel punto legge l’inviluppo. Nei tre esempi alle righe 1-7 il valoredi gate è 1.0 (che è anche quello di default): essendo superiore a 0 fascattare il trigger. L’attivazione del trigger dice alla UGen EnvGen dileggere l’inviluppo. Dunque, eseguendo il codice si ascolta il segnaleinviluppato. L’esempio successivo prevede come valore di gate 0, eil trigger non scatta. Se si sostituisce ad una costante un segnale iltrigger viene attivato tutte le volte che viene sorpassato il valore disoglia 0. Nella riga 12 è una sinusoide con frequenza 4Hz che con-trolla il trigger gate. Tutte le volte che supera l’asse delle ascisse (en-trando nel semipiano positivo sopra lo 0) il trigger viene attivato. Ciòavviene 4 volte al secondo. Infine, nell’esempio seguente gate è unsegnale generato daGUI.mouseX. L’asse orizzonatale della tavola gra-fica costruita da JMouseBase.makeGUI viene ripartito nell’intervallo[−1, 1]. Lo 0 è perciò a metà lungo l’asse. Tutte le volte che lo sisupera il trigger viene attivato.

7.1–181

1 { EnvGen.kr(Env.perc, 1.0, doneAction: 0) * SinOsc.ar(mul: 0.1) }.play;

3 // –> SC works for you

5 (6 SynthDef.new("sinePerc",7 { Out.ar(0, EnvGen.kr(Env.perc, 1.0, doneAction: 2)8 *9 SinOsc.ar(mul: 0.1))

10 }).send(s);11 )

13 Synth.new("sinePerc") ;

• doneAction: un suono percussivo quale quello generato inviluppan-do una sinusoide, ad esempio con Env.perc, genera il tipico invilup-po percussivo senza sostegno. Ma che succede quando l’inviluppoè terminato? È necessario ricordare che nell’esempio di partenza, ri-portato qui sopra nella riga 1, SC svolge un lavoro oscuro ma sostan-ziale, che è riportato dalla riga 6 in avanti:

− crea una synthDef− istanzia da questa un synth e lo mette in funzioneSe si considera questo secondo blocco di codice, si può meglio ap-prezzare il problema. Che fine fa il synth? Senza deallocazione espli-cita, resta attivo: EnvGen continua a generare un segnale d’ampiezzanulla, e un’altra UGen responsabile della sinusoide, SinOsc un altrosegnale. Spetta infatti all’utente rimuovere il synth un volta che que-sti ha generato il segnale percussivo richiesto. Una sequenza di 20suoni percussivi allocherebbe un numero analogo di synth, con in-tuibile spreco di RAM e di CPU. L’argomento doneAction permette

code/controlliCanali/functionImplicit.sc

7.2–182

di evitare il lavoro di deallocazione, poiché esso viene preso in ca-rico direttamente da scsynth. L’argomento doneAction permette dispecificare che cosa scsynth debba fare del synth in questione unavolta terminata la lettura dell’inviluppo. I valori possibili sono at-tualmente 14. Ad esempio con doneAction = 0 scsynth non fa nullae il synth resta allocato e funzionante. Il valore più utilizzato è do-neAction = 2, che dealloca il synth. In sostanzia quel synth non c’èpiù e non c’è da preocuparsi della sua esistenza. Ovviamente il synthnon è più disponbile. Se si volesse generare un’altra sinusoide per-cussive sarebbe necessario costruire un altro synth dalla stessa syn-thDef. Si consideri l’esempio seguente in cui il mouse funziona comeun trigger. Se doneAction = 0 il synth è residente e ad ogni passag-gio del mouse può essere attivato grazie al trigger. Se doneAction= 2 dopo la prima attivazione il synth è deallocato e il comporta-mento del mouse non può inviare alcun messaggio. Nell’interfacciagrafica del server è visibile il numero dei synth attivi: si nota alloral’incremento del numero quando è creato il synth (implicitamente)e il diverso comportamento (persitenza/decremento) in funzione didoneAction.

1 JMouseBase.makeGUI ; // SwingOSC

3 // after reading Env.perc synth is still resident4 { EnvGen.kr(Env.perc, GUI.mouseX.kr(-1,1), doneAction: 0) * SinOsc.ar(mul:

0.1) }.play ;

6 // after reading Env.perc synth is freed7 { EnvGen.kr(Env.perc, GUI.mouseX.kr(-1,1), doneAction: 2) * SinOsc.ar(mul:

0.1) }.play ;

code/controlliCanali/envGen3.sc

7.2–183

7.2 Generalizzazionedegliinviluppi

Sebbene l’uso degli inviluppi sia tipico (e sia stato originato) per l’ampiezza,va comunque ricordato che EnvGen è di fatto un lettore di tabelle. Unoggetto Env è in effetti del tutto analogo ad una cosiddetta tabella in cuisono tabulati punti che descrivono un profilo. EnvGen è allora un’unitàdi lettura di tabelle speciali (di tipo Env) il cui contenuto può essere uti-lizzato per controllare parametri di diverso tipo. Si consideri il codiceseguente:

7.2–184

1 (2 SynthDef.new("sinEnv",3 {4 // defining an env5 var levels, times, env ;6 levels = Array.fill(50, { arg x ; sin(x)*x }).normalize ;7 times = Array.fill(49, 1).normalizeSum ;8 env = Env.new(levels, times) ;9 // using it extensively

10 Out.ar(0,11 Pan2.ar(12 SinOsc.ar(13 freq: Latch.kr(14 EnvGen.kr(env, timeScale: 50,levelScale: 100, levelBias:30

).poll15 .midicps.poll,16 LFPulse.kr(17 EnvGen.kr(env, timeScale: 50, levelScale: 5, levelBias:

10))18 ),19 mul: LFPulse.kr(EnvGen.kr(env, timeScale: 50,levelScale: 10,

levelBias: 15 )) ),20 EnvGen.kr(env, timeScale: 50, levelScale: 2, levelBias: -1),21 0.422 )23 )24 }).send(s) ;25 )

27 Synth.new("sinEnv") ;

Alcune considerazioni sulla synthDef:

• InviluppoLe righe 5-9 definiscono un inviluppo env, costituito da 50 pun-ti d’ampiezza intervallati da 49 durate. In particolare levels è un

code/controlliCanali/envGenPatch.sc

7.2–185

array che contiene un frammento di sinusoide che cresce progressi-vamente in ampiezza. La progressività è data dal moltiplicatore *x,dove x è l’indice dell’elemento calcolato. Si provi:

Array.fill(50, { arg x ; sin(x)*x }).normalize.plot ;

Il metodo normalize scala i valori dell’array nell’intervallo [0.0, 1.0].

1 [1,2,3,4,5].normalize

3 [ 0, 0.25, 0.5, 0.75, 1 ]

Nell’esempio dalla post window l’intervallo in cui sono compresii valori del array sono riscalati tra [0.0, 1.0]. I punti d’ampiezzacalcolati per levels sono pensati come equispaziati. È per questoche la definizione di times produce semplicemente un array riem-pito dal valore 1. Il metodo normalizeSum restituisce un array ar-ray/array.sum, dove a sua volta il metodo sum restituisce la sommadegli elementi dell’array in questione.

1 [1,2,3,4,5].sum

3 15

5 [1,2,3,4,5].normalizeSum

7 [ 0.066666666666667, 0.13333333333333, 0.2, 0.26666666666667,0.33333333333333 ]

9 [1,2,3,4,5].normalizeSum.sum

11 1

7.2–186

Come si vede nell’esempio dalla post window, la somma degli ele-menti nell’array è 15 e dunque normalizeSum divide per 15 ognielemento dell’array. Poiché 5 dà un contributo pari a un terzo di 15,nell’array restituito da normalizeSum il valore relativo è pari ad unterzo di 1 (0.3 . . .). I due array levels e times sono allora nella for-ma normalizzata preferibile. L’idea alla base della synthDef è quel-la di utilizzare lo stesso inviluppo, opportunamente modificato, percontrollare aspetti diversi del segnale.Prima di addentrarsi nella descrizione delle relazioni tra le UGenconviene avere sottomano un diagramma di flusso che ne visualizzile relazioni, quale quello in Figura 7.1. Il diagramma è semplificatorispetto al codice, piché non vi sono rappresentati i valori numericidegli inviluppi (che altrimenti risulterebbero in un grafico illeggibi-le).

EnvGen gate: 1 levelScale: 100 levelBias: 30 timeScale: 50 doneAction: 0 : 1 : 1 : -99 : -99 : 0 : 1 : 1 : 0

MIDICPS a

Poll trig in label: -1 trigid: 12 : 85 : 71 : 101 : 110 : 40 : 69 : 110 : 118 : 71 : 101 : 110 : 41

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

Impulse freq: 10 phase: 0

Impulse freq: 10 phase: 0

EnvGen gate: 1 levelScale: 5 levelBias: 10 timeScale: 50 doneAction: 0 : 1 : 1 : -99 : -99 : 0 : 1 : 1 : 0

LFPulse freq iphase: 0 width: 0.5

SinOsc freq phase: 0

* a b

EnvGen gate: 1 levelScale: 2 levelBias: 10 timeScale: 50 doneAction: 0 : 1 : 1 : -99 : -99 : 0 : 1 : 1 : 0

MIDICPS a

LFPulse freq iphase: 0 width: 0.5

Pan2 in pos level: 0.4

EnvGen gate: 1 levelScale: 2 levelBias: -1 timeScale: 50 doneAction: 0 : 1 : 1 : -99 : -99 : 0 : 1 : 1 : 0

Out bus: 0 channelsArray nil

Fig. 7.1 Diagramma di flusso (semplificato).

• Out.ar(0, Pan2.ar(SinOsc.ar. . .Si consideri la Figura 7.2 che riporta l’ultima parte del diagramma diflusso. In nero è rappresentato il flusso del segnale audio.Iniziando dal fondo, la UGen finale è Out che invia al bus 0 quan-to previsto dall’argomento successivo, Pan2.ar. Quest’ultima UGengestisce il panning stereo, ovvero la distribuzione tra due anali delsegnale: il suo metodo *ar è così descritto nel codice sorgente:

*ar { arg in, pos = 0.0, level = 1.0;

7.2–187

EnvGen gate: 1 levelScale: 100 levelBias: 30 timeScale: 50 doneAction: 0 : 1 : 1 : -99 : -99 : 0 : 1 : 1 : 0

MIDICPS a

Poll trig in label: -1 trigid: 12 : 85 : 71 : 101 : 110 : 40 : 69 : 110 : 118 : 71 : 101 : 110 : 41

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

Impulse freq: 10 phase: 0

Impulse freq: 10 phase: 0

EnvGen gate: 1 levelScale: 5 levelBias: 10 timeScale: 50 doneAction: 0 : 1 : 1 : -99 : -99 : 0 : 1 : 1 : 0

LFPulse freq iphase: 0 width: 0.5

SinOsc freq phase: 0

* a b

EnvGen gate: 1 levelScale: 2 levelBias: 10 timeScale: 50 doneAction: 0 : 1 : 1 : -99 : -99 : 0 : 1 : 1 : 0

MIDICPS a

LFPulse freq iphase: 0 width: 0.5

Pan2 in pos level: 0.4

EnvGen gate: 1 levelScale: 2 levelBias: -1 timeScale: 50 doneAction: 0 : 1 : 1 : -99 : -99 : 0 : 1 : 1 : 0

Out bus: 0 channelsArray nil

Fig. 7.2 Diagramma diflusso (semplificato): estratto.

Pan2 riceve cioè in entrata un segnale in e lo posiziona tra i due ca-nali attraverso l’argomento 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 in-termedi sono possibili. L’argomento level permette di specifcare unmoltiplicare generale per cui vengono scalati entrambi i segnali pri-ma di andare in uscita. Dunque ad Out è richiesto attraverso Pan2 diinviare alla scheda audio una coppia di segnali ottenuta scalando inmodo “complementare” l’ampiezza del segnale in. Si noti come ven-ga specificato un unico bus (0), ma il segnale sia distribuito su duecanali (è stereo). Se ne discuterà più avanti, per ora si osservi come alpanner venga fornito in entrata un segnale generato da un oscillatoresinusoidale (opportunamente) scalato: ciò che avviene dunque è cheuna sinusoide (opportunamente) elaborata viene (opportunamente)distribuita dal panner sui due canali. Nella synthDef, mentre levelvale 0.4 (riga 22), il primo uso dell’inviluppo env è nel controllo delpanning, cioè come argomento di pos (riga 21, cfr. 7.2). Qui EnvGenlegge env espandendolo temporalmente di un fattore 50 (timeSca-le), espandendone l’escursione dei valori dall’originario [0.0, 1.0]a [0.0, 2.0] (levelScale), aggiungendovi−1 (levelBias), così chel’escursione finale sia [−1.0, 1.0], ovvero quella richiesta da Pan2.

7.2–188

Il panning seguirà l’inviluppo env a sinusoide crescente, oscillandosempre di più sbilanciato tra i due canali.

• SinOsc.ar. . .Il blocco compreso tra le righe 13 e 20 contiene la definizione delsegnale che viene distribuito dal panner: si tratta di un oscillatoreSinOsc. Se si comincia dalll’argomento mul si nota come sia occu-pato da un generatore di onde impulsie a bassa frequenza, secondoun esempio già discusso in un altro caso. L’unica variante è che lafrequenza degli impulsi che modulano la sinusoide varia seguendoenv (20). In modo analogo a quanto visto sopra, il segnale generatoda EnvGen in questo caso varierà tra [15.0, 25.0], sempre lungo 50secondi. Si noti che l’incremento della densità delle pulsazioni è cor-relato allo spostamento verso il canale destro (è lo stesso inviluppo).

• freq: Latch.kr. . .Se si osserva nuovamente la Figura 7.1 si nota come l’argomento freqdi SinOsc sia controllato da una UGen, Latch. Dunque, la frequenzavaria in funzione del segnale generato da Latch. Quest’ultima imple-menta in SC un algoritmo classico, detto sample and hold. Si osservila 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 alvalore campionato fino al prelievo successivo: di qui il nome tipicodi sample (campiona) and hold (tieni). Il campionamento dipende datrig: un segnale trigger che attiva il prelevamento ogni volta chesi ha passaggio da negativo a positivo. Si considerino i due esempiseguenti:

7.2–189

1 // line going from 1 to 0 in 20 secs2 { Line.kr(1, 0, 20) }.scope

4 // sampled & held by a latch5 { Latch.kr(Line.ar(1, 0, 20), SinOsc.ar(0.5)) }.scope

La prima funzione genera un segnale i cui valori diminuiscono li-nearmente da 1 a 0 nell’arco di 20 secondi. La seconda campionaattraverso Latch il primo segnale “pescando” ad una frequenza chedipende dal trigger definito da SinOsc. Si ha triggering tutte le vol-te che il segnale passa da negativo a positivo: poiché la sinusoidecon i valori predefinti oscilla tra [−1, 1] un simile passaggio si hatutte le volte che la sinusoide inizia il ciclo (con valore 0). Atten-zione, il passaggio da positivo a negativo a metà del ciclo non fun-ziona come trigger. La frequenza della sinusoide è pari ad un cicloogni due secondi. Dunque, il valore campionato da Latch verrà te-nuto per due secondi. Se si eseguono le righe di codice si nota comenella prima ci sia un decremento continuo nel tempo, mentre nellaseconda, che mostra l’output del Latch, si vede attraverso gli scattiche l’aggiornamento procede a frequenza 0.5Hz (una volta ognidue secondi, appunto). Nell’esempio di partenza Latch campionae mantiene il segnale in uscita da EnvGen (riga 15). Si noti time-Scale = 50 che dilata l’inviluppo env in un tempo di 50 secondi.Per quanto concerne i valori d’ampiezza 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 le-velBias. Tralasciando momentaneamente il messaggio poll si notiil messaggio successivo, midicps, che chiede di convertire i valori in

code/controlliCanali/latch.sc

7.2–190

uscita da valori midi a frequenze (cps, o Hz: è quantitativamen-te lo stesso). Il valore midi del la sopra il do centrale è 69, e la suafrequenza 440Hz. Dunque:

1 69.midicps

3 440

5 440.cpsmidi

7 69

In forma più complessa:

1 Array.series(7, 10,1).postln.midicps

3 [ 10, 11, 12, 13, 14, 15, 16 ]4 [ 14.56761754744, 15.433853164254, 16.351597831287, 17.323914436055,

18.354047994838, 19.44543648263, 20.601722307054 ]

Tornando all’escursione dell’esempio:

1 [30, 130].midicps2 [ 46.249302838954, 14917.240368579 ]

Quest’ultima è l’escursione inHz in cui si muove l’inviluppo. Que-st’inviluppo viene campionato a tasso variabile, poiché la frequenzadi campionamento di Latch è controllata da un generatore di impul-si, LFPulse, la cui frequena di pulsazione è a sua volta determinata da

7.2–191

un EnvGen a cui è fornito l’inviluppo env. Anche questo env è dilata-to in 50 secondi, mentre la sua escursione (la frequenza di LFPulse)si estende in [10, 15] (Hz).

• pollL’architettura client/server di SC pone un problema di rilievo chespesso risulta nascosto. Il processo di sintesi è controllato dal client,ma è realizzato dal server. Il cliente dice cosa fare al fornitore di ser-vizi il quale svolge la sua azione ottemperando alle richieste. Comeperò può sapere il client cosa succede dentro il server? Come si fa asapere se non si sono commessi errori di implementazione? Il solofeedback audio non è sufficiente (il fatto che il risultato sia interes-sante indipendentemente dalla correttezza dell’implementazione èsenz’altro positivo, ma la serendipità non aiuta il debugging . . .). Ilfeedback visivo (attraverso i metodi scope e plot ad esempio) non èanalitico, cioè non comunica direttamente cosa succede in termini dicampioni audio calcolati. Il metodo poll, definito sulle UGen, diceal server di inviare indietro al client il valore di un campione audio,ad un tasso impostabile come argomento del metodo, e di stampar-lo sulla post window. Questo permette di monitorare cosa succedea livello campione nel server in uscita da ogni UGen. In sostanza, ilmetodo può essere concatenato dopo i metodi ar e kr. Ad esempio:

7.3–192

1 {SinOsc.ar(Line.ar(50, 10000, 10).poll).poll}.play

3 Synth("temp__1198652111" : 1001)4 UGen(Line): 50.02265 UGen(SinOsc): 0.007123736 UGen(Line): 149.5237 UGen(SinOsc): -0.1424068 UGen(Line): 249.0239 UGen(SinOsc): -0.570459

10 UGen(Line): 348.52311 UGen(SinOsc): -0.98286312 UGen(Line): 448.02313 UGen(SinOsc): -0.61604214 UGen(Line): 547.52315 UGen(SinOsc): 0.676455

Per ogni UGen, poll stampa sullo schermo il valore del segnale inuscita. Si noti come SinOsc oscilli in [−1, 1] mentre Line inizi la pro-gressione lineare da 50 a 10000.La sequenza poll.midi.poll nell’esempio precedente stampa primail valore in uscita da EnvGen, quindi la sua conversione inHz.

7.3 Sinusoidi&sinusoidi

L’ipotesi alla base dell’utilizzo di un inviluppo d’ampiezza sta nel con-trollo di un segnale da parte di un altro segnale in modo da ottenere unrisultato più “complesso”, “naturale”, “interessante” etc. Un segnale diinviluppo è un segnale unipolare, ma è evidentemente possibile utiliz-zare segnali bipolari. In particolare la sinusoide non è soltanto la formad’onda che produce lo spettro più semplice ma è anche la forma tipicache assume una variazione regolare intorno ad un punto di equilibrio.

7.3–193

Dunque, una sinusoide descrive opportunamente un fenomeno di oscil-lazione intorno ad un valore medio. Si considerino due casi:

1 // minimal tremolo2 { SinOsc.ar(mul: 0.5+SinOsc.kr(5, mul: 0.1)) }.play

4 // minimal vibrato5 { SinOsc.ar(freq: 440+SinOsc.kr(5, mul: 5)) }.play

7 // with mouse control

9 JMouseBase.makeGUI ; // SwingOSC

11 // tremolo12 { SinOsc.ar(mul: 0.5 + SinOsc.kr(13 freq: GUI.mouseX.kr(0, 10),14 mul: GUI.mouseY.kr(0.0, 0.5))) }.play

16 // vibrato17 { SinOsc.ar(freq: 440 + SinOsc.kr(18 freq: GUI.mouseX.kr(0, 10),19 mul: GUI.mouseY.kr(0, 10))) }.play

• tremolo: in musica un tremolo è una variazione periodica della dina-mica, ovvero dell’ampiezza come dimensione percettiva. L’implementazionedi un tremolo è evidentemente semplice. È sufficiente sommare all’ampiezzadel primo oscillatore il segnale prodotto da un oscillatore di control-lo: l’incremento varierà periodicamente con frequenza pari a quelladell’oscillatore di controllo da 0 fino al massimo (l’ampiezza dellostesso oscillatore), discenderà a 0 e al massimo negativo per ritor-nare infine a 0. Nell’esempio l’argomento mul contiene una costante(0.5) a cui vine sommato il segnale in uscita da un oscillatore che

code/controlliCanali/tremoloVibrato.sc

7.3–194

varia 5 volte al secondo nell’escursione [−0.1, 0.1]. Dunque, conla stessa frequenza l’ampiezza dell’oscillatore portante (audio) va-rierà nell’intervallo [0.4, 0.6]. Il contributo dell’oscillatore di con-trollo è allora una variazione periodica dell’ampiezza del segnalecontrollato, variazione il cui periodo è specificato dalla frequenzadell’oscillatore di controllo. Il suono sintetico acquista così una ca-ratteristica tipica dell’esecuzione strumentale (per esempio, dei fiati).Nell’esempio alle righe 11-14 viene utilizzato il mouse per controlla-re i due parametri del tremolo.

[Lavori in corso: introducendo il mouse ricordare che l'originesulla tavola è in alto a sx]

• vibrato: se si applica il ragionamento svolto per il tremolo questa vol-ta non all’ampiezza ma alla frequenza, si ottiene un vibrato. Un oscil-latore di controllo controlla un incremento (minimo) della frequen-za dell’oscillatore principale. Supponendo chef1, amp1, f2, amp2siano frequenza e ampiezza rispettivamente dell’oscillatore audio edi quello di controllo, la frequenza dell’oscillatore audio (f1, finoracostante) dopo l’incremento varia periodicamente (secondo la fre-quenza dell’oscillatore f2 di controllo) tra f1–amp2 e f1 + amp2.Si ricordi che l’uscita del segnale di controllo è infatti sempre una va-riazione d’ampiezza±amp2: questa variazione si somma in questocaso alla frequenza dell’oscillatore controllato f1. Nell’esempio (5),la variazione di frequenza è data dalla somma alla costante 440 delsegnale di un oscillatore che 5 volte al secondo oscilla tra [−5, 5] (siveda mul): dunque per 5 volte al secondo la frequenza audio varieràtra [435, 445]. Analogamente a quanto visto per il tremolo, è for-nito anche un esempio in cui frequenza e ampiezza del vibrato sonocontrollabili via mouse. Il risultato musicale di una simile variazioneperiodica dell’altezza di una nota viene definito vibrato. Il periododel vibrato dipende dal periodo dell’oscillatore di controllo: si pensi

7.3–195

a un violinista che sposta di poco ma continuamente il suo dito at-torno alla posizione della nota prescelta. Ancora: nella tipica tecnicadel canto operistico si mettono in atto simultaneamente tremolo evibrato.

Un esempio riassuntivo è il seguente:

1 (2 SynthDef("tremVibr",3 { arg freq = 440, mul = 0.15,4 tremoloFreq = 5 , tremoloMulPercent = 5,5 vibratoFreq = 10, vibratoMulPercent = 5 ;6 var tremoloMul = mul*tremoloMulPercent*0.01 ;7 var vibratoMul = freq*vibratoMulPercent*0.01 ;8 var tremolo = SinOsc.kr(tremoloFreq, 0, tremoloMul) ;9 var vibrato = SinOsc.kr(vibratoFreq, 0, vibratoMul) ;

10 var sinOsc = SinOsc.ar(freq+vibrato, 0, mul+tremolo) ;11 Out.ar(0, sinOsc) ;

13 }).send(s) ;14 )

16 (17 // pure sinusoid18 var aSynth ;19 aSynth = Synth.new("tremVibr", [ "tremoloMulPercent", 0, "vibratoMulPercent",

0]) ;20 )

La synthDef "tremVibr"prevede argomenti per il controllo dell’oscillatoreaudio e di tremolo e vibrato (si noti come tutti abbiano valori predefi-niti). Sia di tremolo che di vibrato è possibile controllare la frequenzae l’incidenza. Le prima è in entrambi i casi descritta in termini assoluti,cioè attraverso i cicli al secondo (insomma, inHz). Come si vede alle ri-ghe 8 e 9, tremolo e vibrato sono due segnali in uscita da due oscillatori

code/controlliCanali/tremoloVibratoPatch1.sc

7.3–196

sinusoidali che hanno appunto come freq rispettivamente tremoloFreqe vibratoFreq. Più interessante la descrizione dell’incidenza dei due pa-rametri (ovvero di quanto variano il segnale audio). In entrambi i casil’incidenza è relativa alla stato del segnale da controllare. in altre parole,l’incidenza è proporzionale ed è descritta percentualmente. Si consideri

var tremoloMul = mul*tremoloMulPercent*0.01 ;

tremoloMul è calcolato assumendo che tremoloMulPercent, il parame-tro controllabile dal synth, corrisponda ad una percentuale dell’ampiezzadel segnale. Se mul = 0.5 e tremoloMulPercent = 10 alloratremoloMul sarà pari al 10% dimul, cioè a 0.05. Dunque, il segna-le assegnato a tremolo sarà compreso nell’escursione [−0.05, 0.05]e l’ampiezza del segnale audio assegnato alla variabile sinOsc oscille-rà nell’intorno [0.45, 0.55]. Si ricordi che sinOsc è appunto una va-riabile, da non confondere con la UGen SinOsc): attraverso la variabi-le SinOsc il segnale viene passato come argomento ad Out. Un discor-so analogo al calcolo del tremolo vale per il vibrato con il calcolo divibratoMul. Nell’esempio, viene quindi creato un synth aSynth in cuiviene annullato in fase di creazione il contributo di tremolo e vibrato as-segnando un valore pari a 0 ai due argomenti "tremoloMulPercent" e"vibratoMulPercent" (non c’è incidenza di tremolo e vibrato poiché idue segnali di controllo hanno ampiezza nulla).A partire da una simile synthDef è possibile costruire una interfacciagrafica per controllare gli argomenti di un synth. L’idea di partenzaè avere per ogni parametro un cursore e di visualizzarne il nome e ilvalore. Dato il numero esiguo di parametri si potrebbe semplicementecostruire l’interfaccia “artigianalmente”, definendo cioè singolarmentetutti i componenti GUI necessari. Vale la pena però sperimentare un al-tro approccio, più automatizzato, secondo quanto già visto nella primainterfaccia grafica realizzata, quella “inutile”. Le classi di elementi ne-cessarie sono due:

7.3–197

1. GUI.slider: permette di costruire un cursore (in inglese, uno slider):la sintassi del costruttore (22) è piuttosto ovvia, giacché richiede unafinestra di riferimento (qui window23) e un rettagolo in cui viene dise-gnato il cursore.

2. GUI.staticText: è un campo di testo per la visualizzazione, che perònon prevede riconoscimento dell’input (scrive testo sullo schermoma non serve per immetterlo). La sintassi del costruttore è quellaconsueta, e definisce un riquadro ove verrà stampato sullo schermoil testo. In più (17 e 28) viene chiamato sull’istanza così ottenuta ilmetodo string_ che definisce il testo da stampare.

Si tratta di definire un array che contenga i nomi degli argomenti, e apartire da questo di

1. generare gli oggetti grafici2. definire le azioni associate a questi ultimi

L’unica difficoltà sta nel fatto che nel connettere valori in uscita dal cur-sore e argomenti del synth è necessario tenere in conto di un mappingdisomogeneo. Ogni cursore ha un’estensione compresa in [0, 1]. La fre-quenza freqdell’oscillatore audio può avere un’escursione 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 regi-stro sub-audio, [0, 15], mentre le due incidenze devono essere espres-se percentualmente, dunque in [0, 100]. È perciò necessario un lavo-ro di scalatura dei valori espressi da ogni cursore in quanto relativoad un preciso parametro. Non è opportuno passare i valori non scala-ti al synth includendo dentro la synthDef le operazioni necessarie alloscaling: in questo modo si verrebbe infatti a definire una dipendenza

Si noti come window sia resa più alta del richiesto aggiungendovi 30 × 2 pixel in al-23

tezza.

7.3–198

della synthDef dall’interfaccia grafica. Nel lavoro con l einterfacce gra-fiche è invece bene assumere che il modello e i dati siano indipenden-ti dall’interfaccia di controllo/display. In questo modo, si può buttarevia l’interfaccia senza dover modificare il modello (qui, la synthDef).D’altra parte, nella definizione di un algoritmo di sintesi quale previ-sto dalla synthDef è del tutto fuori luogo prevedere elementi che in-vece concernono la GUI. La soluzione proposta dell’esempio prevedel’uso di un IdentityDictionary, ovvero di una struttura dati che as-socia in maniera univoca ad una chiave un valore, secondo il modellodel dizionario che prevede per ogni lemma (la chiave) una definizio-ne (il valore): controlDict associa ad ogni stringa-argomento un arrayche ne definisce l’escursione (secondo il modello [minVal, maxVal]). Laparte di codice compresa tra 13 e 30 definisce tre array che contengo-no un numero pari alla dimensione di controlDict rispettivamente diblocchi di testo per la visualizzazione del nome del parametro, cursori eblocchi di testo deputati alla visualizzazione del valore degli argomenti.Quindi viene istanziato un synth. Infine si tratta di definire la funziona-lità della GUI attraverso un ciclo su ognuno degli elementi contenuti incontrolDict. Nel ciclo value indica l’elemento di destra (l’escursione),ed a partire da questa si ottiene la chiave name attraverso l’invocazionedel metodo controlDict.findKeyForValue(value). Ad esempio, se va-lue è [50, 1000], la chiave associata a name sarà "freq". Quindi vienecalcolata un’escursione range tra [0, 1] come differenza tra gli estremi(nell’esempio, 1000− 50 = 950) e il minimo viene considerato comescarto di partenza (offset).

labelArr[index].string_(name)

assegna all’elemento di testo index in labelArr la stringa name che virisulta visibile (nell’esempio, "freq"). Infine

slidArr[index].action = . . .

7.3–199

assegna all’elemento index di slidArr un’azione attraverso il consue-to meccanismo di una funzione che viene valutata ogni qualvolta si re-gistra una variazione nello stato del cursore (un movimento della levagrafica). Nella funzione l’argomento (qui: theSlid) è come di consue-to l’istanza dell’elemento grafico su cui è definita la funzione. L’azionerealizza quattro comportamenti, ognuno descritto da un’espressione al-le righe 39-42.

1. dichiarando la variabile paramValue se ne calcola il valore. Nell’esempioprecedentemente relativo a "freq", poiché theSlid.value è semprecompreso in [0, 1] ed essendo range = 50, l’escursione varia ap-punto tra [0, 950] + 50 = [50, 1000]

2. la seconda azione consiste nello stampare sullo schermo il nome delparametro ed il suo valore

3. si tratta di controllare il synth impostando per l’argomento name ilvalore paramValue (ovvero: aSynth.set(”freq”, 123.4567890)

4. infine il valore paramValue viene scritto nell’elemento di testo indexin valueArr.

7.3–200

1 (2 var aSynth = Synth.new("tremVibr") ;

4 var controlDict = IdentityDictionary[5 "freq" -> [50, 1000],6 "mul" -> [0,1],7 "tremoloFreq" -> [0, 15],8 "tremoloMulPercent" -> [0, 50],9 "vibratoFreq" -> [0, 15],

10 "vibratoMulPercent" -> [0, 50]11 ];

13 var window = GUI.window.new("Control Panel",14 Rect(30,30, 900, controlDict.size+2*30)) ;

16 var labelArr = Array.fill(controlDict.size, { arg index ;17 GUI.staticText.new( window, Rect( 20, index+1*30, 200, 30 ))18 .string_( 0 ) ;19 }) ;

21 var slidArr = Array.fill(controlDict.size,22 { arg index ;23 var slid = GUI.slider.new( window, Rect( 240, index+1*30, 340,

30 ));24 slid ;25 }) ;

27 var valueArr = Array.fill(controlDict.size, { arg index ;28 GUI.staticText.new( window, Rect( 600, index+1*30, 200, 30 ))29 .string_( 0 );30 }) ;

32 controlDict.do({ arg value, index ;33 var name = controlDict.findKeyForValue(value) ;34 var range = value[1]-value[0] ;35 var offset = value[0] ;36 [value, index].postln;37 labelArr[index].string_(name) ;38 slidArr[index].action = { arg theSlid ;39 var paramValue = theSlid.value*range + offset ;40 [name, paramValue].postln ;41 aSynth.set(name, paramValue) ;42 valueArr[index].string_(paramValue.trunc(0.001) ) ;43 }44 }) ;

46 window.front ;47 )

7.3–201

Si noti come l’interfaccia dipenda dal dizionario che connette la GUIal modello (la synthDef) definendo per ogni argomento il mapping op-portuno. Aggiornando la struttura dati per una nuova synthDef la GUIcambia di conseguenza. Nell’esempio che segue è stata definita una nuo-va SynthDef: per costruire un pannello di controllo è stato necessarioistanziare un synth con il nome della synthDef e aggiornare il control-Dict. Il resto del codice è stato omesso perché uguale a precedente.

1 (2 SynthDef("resonantDust", { arg rlpfFreq = 300, dustFreq = 100,3 rq = 0.01, mul = 10,4 delay = 0, decay = 0 ;5 Out.ar(0, CombL.ar(RLPF.ar(Dust.ar(dustFreq), rlpfFreq, rq, mul),6 1, delay, decay))7 }8 ).send(s) ;9 )

11 (12 var aSynth = Synth.new("resonantDust") ;

14 var controlDict = IdentityDictionary[15 "rlpfFreq" -> [50, 1000],16 "mul" -> [0,20],17 "dustFreq" -> [0, 300],18 "rq" -> [0, 0.1],19 "delay" -> [0, 10],20 "decay" -> [0, 10]21 ];

23 […]

25 )

code/controlliCanali/tremoloVibratoPatch2.sc
code/controlliCanali/sameGUI2.sc

7.3–202

Un certo numero di informazioni potrebbero poi essere desunte dallastessa synthDef se assegnata a variabile. Nell’esempio seguente la syn-thDef "resonantDust" è assegnata ad a: se si invia il messaggio name ada quest’ultima restituisce il suo nome.

1 a = SynthDef("resonantDust", { arg rlpfFreq = 300, dustFreq = 100,2 rq = 0.01, mul

= 10,3 delay = 0,

decay = 0 ;4 Out.ar(0, CombL.ar(RLPF.ar(Dust.ar(dustFreq), rlpfFreq, rq,

mul),5 1, delay, decay))6 }7 ).send(s) ;

9 a.name ;

11 resonantDust

Si potrebbero allora desumere in automatico dalla synthDef i nomdi de-gli argomenti che costituiscono le chiavi di controlDict. In particolaregli argomenti di una UGen-Graph function prendono il nome di controlnames.

1 a.allControlNames

3 [ ControlName P 0 rlpfFreq control 300, ControlName P 1 dustFreqcontrol 100, ControlName P 2 rq control 0.01, ControlName P 3 mulcontrol 10, ControlName P 4 delay control 0, ControlName P 5 decaycontrol 0 ]

Dall’array restituito da allControlNames, che prevede una sintassi spe-cifica, non è allora difficile ottenere le stringhe necessarie per le chiavidi controlDict

7.3–203

1 a.allControlNames.do({ arg item ;2 item.postln ;3 item.asString.split($ )[4].postln ;4 "\n".postln5 })6 ControlName P 0 rlpfFreq control 3007 rlpfFreq

9 ControlName P 1 dustFreq control 10010 dustFreq

12 ControlName P 2 rq control 0.0113 rq

15 ControlName P 3 mul control 1016 mul

18 ControlName P 4 delay control 019 delay

21 ControlName P 5 decay control 022 decay

Nel codice prima viene stampato item, cioè il nome intero. Nella rigasuccessiva, lo stesso è esplicitamente convertito in stringa (asString),quindi in suddiviso blocchi separati da un carattere di spaziatura ” ”: ladefinizione del carattere prevede l’uso del prefisso $, e il metodo resti-tuisce un array che contiene i singoli blocchi ottenuti come elementi. Adesempio:

1 "to be or not to be".split($ )

3 [ to, be, or, not, to, be ]

7.3–204

Dell’array così ottenuto si prende l’elemento numero 4, ovvero il nomedel parametro, che viene scritto sulla post window. L’unico problema diun approccio simile è che il mapping tra domini numerici (tra escursionedi ogni cursore e escursione del parametro correlato) non può essereautomatizzato, ma deve essere definito caso per caso. Dunque, l’uso diun dizionario, oltre ad esemplificare la struttura dati, si rivela una sceltainteressante. L’unica controindicazione sta nel fatto che un dizionarionon è una struttura “ordinata”: nel ciclo, l’ordine in cui vengono lette lechiavi è infatti deciso internamente da SC (e, tra l’altro, varia per ogniesecuzione del codice). In effetti, modificando il codice, sarebbe forsepiù opportuno utilizzare un array di array così:

1 [2 ["rlpfFreq", 50, 1000],3 ["mul", 0,20],4 ["dustFreq", 0, 300],5 ["rq", 0, 0.1],6 ["delay", 0, 10],7 ["decay", 0, 10]8 ]

Infine, si può ristrutturare il problema. Ciò che è richiesto è una GUIautomatica a partire dalla synthDef che prevede un controllo per ogniargomento, la cui escursione deve essere impostabile dall’utente. Per in-tanto la synthDef può essere assegnata ad una variabile globale ~aDef,così da essere richiamata in seguito.

7.3–205

1 (2 ~aDef = SynthDef("tremVibr",3 { arg freq = 440, mul = 0.15,4 tremoloFreq = 0 , tremoloMulPercent = 0,5 vibratoFreq = 0, vibratoMulPercent = 0 ;6 var tremoloMul = mul*tremoloMulPercent*0.01 ;7 var vibratoMul = freq*vibratoMulPercent*0.01 ;8 var tremolo = SinOsc.kr(tremoloFreq, 0, tremoloMul) ;9 var vibrato = SinOsc.kr(vibratoFreq, 0, vibratoMul) ;

10 var sinOsc = SinOsc.ar(freq+vibrato, 0, mul+tremolo) ;11 Out.ar(0, sinOsc) ;

13 }).send(s) ;14 )

A questo punto (esempio successivo) è possibile istanziare subito unsynth a partire dal nome della synthDef (~aDef.name) (2). Recuperandoil codice già discusso in precendenza, a partire dalla lista dei controlNa-mes si definisce un controlArr: un array che contiene per ogni argomen-to un array [nome dell'argomento, valore minimo, valore massimo].In fase di costruzione massimi e minimi sono impostati rispettivamen-te a 1 e a 0 (11). Viene quindi costruito un secondo array guiArr i cuielementi sono correlati uno ad uno con controlArr: è l’array che con-tiene gli oggetti GUI relativi a ciascun argomento della synthDef. Perogni elemento in controlArr viene creato un elemento in guiArr. Traquesti ci sono anche due campi numerici che accettano in entrata un va-lore impostabile dall’utente, due istanze di GUI.numberBox. La sintassi èpiuttosto intuitiva: si noti che viene loro assegnato il valore di massimoe minimo contenuto nel corrispettivo elemento di controlArr, attraver-so value_(item[. . .]) (22 3 24). Per ogni argomento c’è un elementonell’array controlArr che contiene

code/controlliCanali/tremoloVibratoPatch3Def.sc

7.3–206

1. il nome2. il minimo3. il massimo

e c’è un elemento in guiArr che contiene (19-27)

1. un campo di testo dove scrivere il nome2. un campo numerico dove inserire il minimo3. un campo numerico dove inserire il massimo4. un cursore di controllo5. un campo di testo dove scrivere il valore attuale dell’argomento

Creata l’interfaccia grafica, si tratta a questo punto di associare agli ele-menti le azioni opportune, secondo quanto avviene nelle righe 31-49.Per ognuno degli elementi di controlArr si recupera un elemento diguiArr (32-33). Questo elemento è un array che contiene tutti gli ogget-ti relativi all’argomento che è elemento di controlArr. La definizionedelle azioni concerne soltanto tre degli elementi grafici, quelli che devo-no rispondere all’interazine con l’utente. Questi deve poter muovere ilcursore dopo aver determinato massimi e minimi di ciascun argomento.

• L’azione collegata ai campi numerici (guiElement[1] e guiElement[2])assegna il valore immesso (value) dall’utente ai rispettivi campi dimassimo e minimo dell’elemento correlato in controlArr. Se dun-que l’utente imposta nei campi numerici relativi a "freq"] i valori20 e 10000, l’elemento relativo in controlArr sarà ["freq", 20,10000]

• Infine guiElement[3] è il cursore che controlla gli argomenti ipostan-doli sul synth. Il codice è uguale all’esempio precedente con un’unicadifferenza. Le variabili sono dichiarate all’interno della funzione checontrolla l’azione. Questo passaggio si rende necessario perché ran-ge e offset dipendo dai massimi e dai minimi, e questi possono es-sere variati dall’utente. È necessario perciò che ad ogni movimento

7.3–207

del cursore si aggiornino i valori di massimo e minimo leggendolidall’array controlArr.

7.3–208

1 (2 var aSynth = Synth.new(~aDef.name) ;

4 var controlArr = [], guiArr = [] ;

6 var window ;

8 ~aDef.allControlNames.do({ arg item ;9 var name = item.asString.split($ )[4] ;

10 controlArr = controlArr.add([name, 0.0, 1.0])11 }) ;

13 window = GUI.window.new(~aDef.name++" Control Panel",14 Rect(30,30, 900, controlArr.size+2*30)).front ;

16 // GUI creation17 controlArr.do({ arg item, ind ;18 var index = ind + 1; // add some space19 var guiElement = [20 GUI.staticText.new( window, Rect( 20, 30*index, 200, 30 ))21 .string_( item[0] ),22 GUI.numberBox.new( window, Rect( 240, 30*index, 50, 30 ))23 .value_(item[1]),24 GUI.numberBox.new( window, Rect( 300, 30*index, 50, 30 ))25 .value_(item[2]),26 GUI.slider.new( window, Rect( 370, 30*index, 340, 30 )),27 GUI.staticText.new( window, Rect( 720, 30*index, 200, 30 ))28 .string_( 0.0 ) ] ;29 guiArr = guiArr.add(guiElement) ;30 }) ;

32 // GUI action definition33 controlArr.do({ arg item, index ;34 var guiElement = guiArr[index] ;35 guiElement[1].action = { arg minBox ;36 item[1] = minBox.value ;37 } ;38 guiElement[2].action = { arg maxBox ;39 item[2] = maxBox.value ;40 } ;41 guiElement[3].action = { arg slider ;42 var name = item[0] ;43 var range = item[2].value - item[1].value ;44 var offset = item[1].value ;45 var paramValue = slider.value*range + offset ;46 [name, paramValue].postln ;47 aSynth.set(name, paramValue) ;48 guiElement[4].string_(paramValue.trunc(0.0001) ) ;49 }50 }) ;51 )

7.4–209

7.4 Segnalipseudo-casuali

La discussione precedente in relazione al controllo di un oscillatore daparte di un altro oscillatore può essere intuibilemente estesa alla più ge-nerale prospettive del controllo di una UGen da parte di un altra UGen.I segnali di controllo precedenti erano sinusoidi: in particolare la formad’onda sinusoidale era appunto il profilo temporale della variazione in-dotta nel segnale. Un profilo di variazione può evidentemente assumereforme diverse. Negli esempi seguenti le prime funzioni contengono unsegnale di controllo che verrà impiegato per controllare il vibrato di unoscillatore (con frequenza sub-audio e a tasso di controllo) nelle seconde.Attraverso il metodo jscope è possibile visualizzre il profilo dei primisegnali di controllo (per comodità di visualizzazione la frequenza è sta-ta modificata a 1000 Hz). In generale le UGen che iniziano con LF sonospecializzate nella generazioni di segnali di controllo.

1 { SinOsc.ar(1000) }.scope ;2 { SinOsc.ar(freq: 440+SinOsc.kr(2, mul: 50), mul: 0.5) }.play ;

4 { LFSaw.ar(1000) }.scope ;5 { SinOsc.ar(freq: 440 + LFSaw.kr(2, mul: 50), mul: 0.5) }.play ;

7 { LFNoise0.ar(1000) }.scope ;8 { SinOsc.ar(freq: 440 + LFNoise0.kr(2, mul: 50),mul: 0.5) }.play ;

10 { LFNoise1.ar(1000) }.scope ;11 { SinOsc.ar(freq: 440 + LFNoise1.kr(2, mul: 50), mul: 0.5) }.play ;

code/controlliCanali/tremoloVibratoPatch3.sc

7.4–210

Si nota immediatamente la differenza tra i due segnali periodici, la si-nuoside e l’onda a dente di quadra, proprio in relazione al profilo udi-bile della variazione. Le due Altre UGen impiegate sono specializzatenella generazione di segnali pseudo-casuali. In particolare LFNoise0 ge-nerata ad una certa frequenza valori d’ampiezza che tiene per la duratadi ogni periodo (cioè, fino quando non calcola un un nuovo valore): sesi osserva la finestra attivata quando jscope è invocato sulla UGen sinota l’andamento a gradoni. Come LFNoise0, LFNoise1 genera segnalipseudo-casali alla frequenza impostata, ma interpola tra gli estremi suc-cessivi. In altre parole, non salta da un valore al successivo, ma vi arrivagradualmente. L’esempio seguente è minimale ed il comportamento deidue generatori ne risulta chiaramente illustrato.

1 {SinOsc.ar(LFNoise1.ar(10, mul:200, add: 400))}.play

3 {SinOsc.ar(LFNoise0.ar(10, mul:200, add: 400))}.play

In entrambi i casi la frequenza dell’oscillatore è controllata da un gene-ratore di numeri casuali che aggiorna il suo stato 10 volte al secondo.In considerazione degli argomenti mul e add il segnale di controllo dellafrequenza varia casualmente allora nell’escursione [−1, 1] × 200 +400 = [200, 600]. Con LFNoise0 si ha un gradatum (di tipo scala-re), con LFNoise1 un continuum (un glissando). Si potrebbe pensare aLFNoise0 come ad una versione di LFNoise1 sampled and held: si pren-de un campione di un segnale che interpola tra valori pseudo-casualiquale quello generato da LFNoise1 ad un ogni intervallo di tempo pre-stabilito e lo si tiene fino al prelevamento successivo. Come si è visto,un simile comportamento è realizzato da Latch. L’esempio seguente èuna variazione di un esempio dall’help file di Latch. In entrambi i casi il

code/controlliCanali/controlSources.sc
code/controlliCanali/LFNoise.sc

7.4–211

segnale in uscita varia la sua frequenza ad un tasso di 9 volte al secondoscegliendo casualmente nell’intervallo [100, 900].

1 { SinOsc.ar(LFNoise0.kr(9, 400, 500), 4, 0.2)}.play

3 // the same but less efficient

5 { 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 compo-nente di un sequencer, cioè come generatore di sequenze discrete di va-lori. Due variazioni sul tema sono presentate nell’esempio seguente.

1 SynthDef("chromaticImproviser", { arg freq = 10 ;2 Out.ar(0, SinOsc.ar(3 freq: LFNoise0.kr(freq, mul:15, add: 60).round.midicps,4 mul: EnvGen.kr(Env.perc(0.05), gate: Impulse.kr(freq), doneAction:2)

)5 )6 }).play

8 SynthDef("modalImproviser", { arg freq = 10;9 var scale = [0, 0, 0, 0, 3, 3, 4, 5, 5, 6, 6, 7, 7, 7, 10]+60 ;

10 var mode = scale.addAll(scale+12).midicps ;11 var range = (mode.size*0.5).asInteger ;12 Out.ar(0, SinOsc.ar(13 freq: Select.kr(LFNoise0.kr(freq,14 mul: range,15 add: range).round, mode),16 mul: EnvGen.kr(Env.perc(0.05), gate: Impulse.kr(freq), doneAction:2)

)17 )18 }).play

code/controlliCanali/LFNoiseLatch.sc

7.4–212

I due “improvvisatori” generano sequenze casuali di altezze ad un tassopredefinito di 10 note al secondo. In entrambi i casi il generatore audioeè un oscillatore sinusoidale con un inviluppo percussivo.Il primo, vagamente hard bop, genera una sequenze di altezze cromatichecomprese tra in midi tra [45, 75]. Nel midi sono numerate linearmen-te le altezze, come se si trattasse di assegnare un indice ai tasti (bian-chi e neri) di un pianoforte. In notazione midi il do centrale è 60, dun-que le altezze variano tra il quindiciesimo tasto che lo precede, corri-spondente al la al di sotto dell’ottava inferiore, e il quindicesimo ta-sto seguente, il mi bemolle al di sopra dell’ottava superiore. Infatti la fre-quenza dell’oscillatore audio è data dalla sequenza di valori in uscita daLFNoise0, moltiplicati per 15 e sommati a 60 (secondo quanto previstoda mul, add). Per rappresentare i tasti del pianoforte sono utilizzati inmidi i numeri interi, mentre i valori intermedi corrispondono a frazionidi semitono. Siccome qui interessano le altezze temperate (trenta tastidi pianoforte), il segnale viene arrotondato all’intero tramite il metodoround (3).Il secondo gestisce le altezze in maniera diversa. In primo luogo scaledefinisce una scala che comprende le altezze indicate nell’array. Si notiche all’array viene aggiunto 60, dunque le altezze (in notazione midi)saranno:

1 [0, 0, 0, 0, 3, 3, 4, 5, 5, 6, 6, 7, 7, 7, 10]+602 [ 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 è de-finito come scale a cui viene aggiunta una copia di se stesso traspostadi un’ottava. Si ricordi che in midi il salto di ottava, pari ad un raddop-pio della frequenza, equivale ad un incremento di 12 (ovvero: 12 tastisul pianoforte). L’array mode viene quindi convertito in Hz attraverso

code/controlliCanali/LFNoise0.sc

7.4–213

midicps (10). La scelta della frequenza da generare è determinata dallaUGen Select, la sui sintassi prevede

*ar { arg which, array;

In sostanza, dato un array, Select ne restituisce l’elemento which. Ascanso di equivoci, si ricordi che, essendo una UGen, Select restitui-sce un segnale, cioè una sequenza, a tasso audio o di controllo, com-posta dall’elemento which di array. L’idea è quella di far selezionarecasualmente a Select una frequenza dall’array mode. Per fare ciò vieneutilizzato LFNoise0. Quest’ultimo, a frequenza freq, genera un nume-ro compreso in [−range,+range] + range = [0, 2range], dovela variabile range è definita come la metà intera di della dimensione dimode (11). Quindi, se mode.size = 26, allora range = 13, e LFNoise oscil-lerà in [0, 26]. Essendo i valori in uscita da LFNoise0 gli indici dell’arrayda cui Select legge, devono essere interi: di qui l’invocazione del me-todo round. Si è notato che mode contiene più volte gli stessi valori: èun modo semplice (un po’ primitivo, ma efficace. . .) per fare in modoche certe altezze vengano scelte con frequenze fino a quattro volte su-periori 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. Infine, un’ultima annotazione sull’inviluppod’ampiezza. Il segnale in uscita dall’oscillatore è continuo e deve essereinviluppato ogni volta che si produce un cambiamento di altezza, ov-vero alla stessa frequenza di LFNoise0, in modo tale da sincronizzarealtezze e ampiezza in un evento notale: per questo l’argomento gate ri-ceve come valore il segnale in usicta da Impulse, ovvero una sequenza diimpulsi che attivano l’inviluppo alla stessa frequenza freq di LFNoise0.Nell’esempio precedente l’utilizza di EnvGen richiede di specificare perl’argomento gate un trigger che operi alla stessa frequenza di LFNoise0:a tal fine è stata impiegata la UGen Impulse. Un segnale pseudo-casualespesso utilizzato come trigger è quello generato da Dust: quest’ultima

7.4–214

produce una sequenza di impulsi, nell’escursione [0, 1], che è distri-buita irregolarmente (stocasticamente) nel tempo, alla densità media alsecondo definita 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 inal-terto il resto del codice, si ottien che gli inviluppi verrano generati in unnumero medio pari a freq al secondo, manon in una serie cronometrica-mente regolare. Nell’esempio seguente le ascisse del mouse controllanola densità, che varierà nell’escursione [1, 500].

1 JMouseBase.makeGUI ; // SwingOSC2 { Dust.ar(GUI.mouseX.kr(1, 500)) }.play

L’utilizzo di Dust come trigger è piuttosto diffuso in SC. L’esempio se-guente è tratto dall’help file di TRand, un altra UGen interessante: TRandgenera un valore pseudo-casuale compreso nell’escursione descritta daidue primi arogmenti:

*ar { arg lo = 0.0, hi = 1.0, trig = 0.0;

La generazione avviene ogni qualvolta il trigger cambia stato, passandoda negativo a positivo. Nell’esempio seguente la prima parte prevedeuna come trigger Impulse: ad ogni nuovo impulso un Trand genera unnuovo valore utile alla frequenza di SinOsc. Si noti che è un altro modoper ottenere l’effetto sample & hold implementato con Latch. Se si sosti-tuisce Impulse con Dust, si ottiene una sequenza di impulsi (e dunquedi inviluppi) in una quantità media pari a freq.

code/controlliCanali/Dust1.sc

7.5–215

1 (2 {3 var trig = Impulse.kr(9);4 SinOsc.ar(5 TRand.kr(100, 900, trig)6 ) * 0.17 }.play;8 )

10 (11 {12 var trig = Dust.kr(9);13 SinOsc.ar(14 TRand.kr(100, 900, trig)15 ) * 0.116 }.play;17 )

7.5 Espansioneeriduzionemulticanale

Nell’esempio relativo alla generalizzazione degli inviluppi si è fatto ri-ferimento a Pan2, che riceveva in ingresso un segnale mono e fornivacome output una coppia di segnali risultanti da una varia distribuzionedell’ampiezza del segnale. Un segnale stereo, come quello in uscita daPan2, è in effetti una coppia di segnali che devono essere eseguiti in pa-rallello, tipicamente su due (gruppi di) altoparlanti. Tuttavia non è statonecessario specificare nella sintassi di Out una coppia di bus, bensì sol-tanto l’indice 0. Nella fattispecie, SC distribuisce automaticamente i due

code/controlliCanali/TRand.sc

7.5–216

segnali sul necessario numero di bus contigui (in questo caso 0 e 1). Que-st’operazione prende il nome di multichannel expansion. Più in generale,è importante sapere che SC opera una espansione multicanale tutte levolte che un argomento di una UGen è costituito da un array. Così nel-la prima synthdef dell’esempio seguente (”multi1”) l’argomento freq ècostituito non da un segnale o da un numero, ma da un array: dunqueSC genererà una coppia di sinuosodi con frequenza 100 e 1000 cheverrano inviate rispettivamente sui bus 0 e 1. In altre parole la synth-Def è identica nei risultati a quella successiva, ”multi2”, in cui esplicita-mente vengono inviati due segnali diversi ai due bus 0 e 1. Ma anche ilterzo esempio, ”multi3”, è equivalente: in esso Out riceve un array co-me valore dell’argomento channelArray, ed esegue di conseguenza unaespansione multicanale, secondo quanto esplicitamente prescritto nellasynthDef ”multi2”.

1 SynthDef( "multi1", {2 Out.ar(0, SinOsc.ar([100, 1000]))3 }).play ;

5 SynthDef( "multi2", {6 Out.ar(0, SinOsc.ar(100))7 + Out.ar(1, SinOsc.ar(1000))8 }).play ;

10 SynthDef( "multi3", {11 Out.ar(0,12 [SinOsc.ar(100), SinOsc.ar(1000)])13 }).play ;

. L’espansione multicanale avviene soltanto ed esclusivamente quandoagli argomenti di una UGen viene passato un oggetto di tipo , Array,non per le sue sopraclassi né per le sue sottoclassi. A pensarci, questo

code/controlliCanali/multi.sc

7.5–217

tipo di costrutto sintattico è estremamente potente. Si consideri quantoavviene nell’esempio seguente:

1 SynthDef("multi16", {2 var bFreq = 100 ; // base freq3 var sigArr = Array.fill(16,4 { arg ind ;5 var index = ind+1 ;6 SinOsc.ar(7 freq: bFreq*index+(LFNoise1.kr(8 freq: index,9 mul: 0.5,

10 add: 0.5)*bFreq*index*0.02)11 )12 }) ;13 Out.ar(0, sigArr)14 }15 ).play ;

Il diagramma di flusso è fornito in Figura 7.3, mentre un dettaglio è ri-prodotto 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 channeslsArrayl’array sigArr, costituito da 16 segnali.

Out.ar(0, sigArr)

Quest’ultimo è ottenuto chiamando il metodo costruttore fill a cui vie-ne indicato un numero (16, il numero di posti dell’array) e una funzione.Ogni chiamata della funzione restituisce un oggetto SinOsc.ar, ovveroun segnale sinusoidale, in cui il valore dell’argomento freq è correlatoall’indice progressivo dell’array. Nell’esempio seguente sono aggiunticome commento i valori risultanti dai primi due cicli dell’array. Come si

code/controlliCanali/multi16.sc

7.5–218

LF

Nois

e1:

1

MulA

dd

nil

: 0.5

: 0.5

*a

b:

100

MulA

dd

nil

: 0.0

2:

100

Sin

Osc

freq

phas

e: 0

Out

bus:

0ch

annel

sArr

aynil

nil

nil

nil

nil

nil

nil

nil

nil

nil

nil

nil

nil

nil

nil

LF

Nois

e1:

2

MulA

dd

nil

: 0.5

: 0.5

*a

b:

100

*a

b:

2

MulA

dd

nil

: 0.0

2:

200

Sin

Osc

freq

phas

e: 0

LF

Nois

e1:

3

MulA

dd

nil

: 0.5

: 0.5

*a

b:

100

*a

b:

3

MulA

dd

nil

: 0.0

2:

300

Sin

Osc

freq

phas

e: 0

LF

Nois

e1:

4

MulA

dd

nil

: 0.5

: 0.5

*a

b:

100

*a

b:

4

MulA

dd

nil

: 0.0

2:

400

Sin

Osc

freq

phas

e: 0

LF

Nois

e1:

5

MulA

dd

nil

: 0.5

: 0.5

*a

b:

100

*a

b:

5

MulA

dd

nil

: 0.0

2:

500

Sin

Osc

freq

phas

e: 0

LF

Nois

e1:

6

MulA

dd

nil

: 0.5

: 0.5

*a

b:

100

*a

b:

6

MulA

dd

nil

: 0.0

2:

600

Sin

Osc

freq

phas

e: 0

LF

Nois

e1:

7

MulA

dd

nil

: 0.5

: 0.5

*a

b:

100

*a

b:

7

MulA

dd

nil

: 0.0

2:

700

Sin

Osc

freq

phas

e: 0

LF

Nois

e1:

8

MulA

dd

nil

: 0.5

: 0.5

*a

b:

100

*a

b:

8

MulA

dd

nil

: 0.0

2:

800

Sin

Osc

freq

phas

e: 0

LF

Nois

e1:

9

MulA

dd

nil

: 0.5

: 0.5

*a

b:

100

*a

b:

9

MulA

dd

nil

: 0.0

2:

900

Sin

Osc

freq

phas

e: 0

LF

Nois

e1:

10

MulA

dd

nil

: 0.5

: 0.5

*a

b:

100

*a

b:

10

MulA

dd

nil

: 0.0

2:

1000

Sin

Osc

freq

phas

e: 0

LF

Nois

e1:

11

MulA

dd

nil

: 0.5

: 0.5

*a

b:

100

*a

b:

11

MulA

dd

nil

: 0.0

2:

1100

Sin

Osc

freq

phas

e: 0

LF

Nois

e1:

12

MulA

dd

nil

: 0.5

: 0.5

*a

b:

100

*a

b:

12

MulA

dd

nil

: 0.0

2:

1200

Sin

Osc

freq

phas

e: 0

LF

Nois

e1:

13

MulA

dd

nil

: 0.5

: 0.5

*a

b:

100

*a

b:

13

MulA

dd

nil

: 0.0

2:

1300

Sin

Osc

freq

phas

e: 0

LF

Nois

e1:

14

MulA

dd

nil

: 0.5

: 0.5

*a

b:

100

*a

b:

14

MulA

dd

nil

: 0.0

2:

1400

Sin

Osc

freq

phas

e: 0

LF

Nois

e1:

15

MulA

dd

nil

: 0.5

: 0.5

*a

b:

100

*a

b:

15

MulA

dd

nil

: 0.0

2:

1500

Sin

Osc

freq

phas

e: 0

LF

Nois

e1:

16

MulA

dd

nil

: 0.5

: 0.5

*a

b:

100

*a

b:

16

MulA

dd

nil

: 0.0

2:

1600

Sin

Osc

freq

phas

e: 0

Fig. 7.3 Espansione multicanale: dia-gramma di flusso.

7.5–219

LFNoise1 : 1

MulAdd nil : 0.5 : 0.5

* a b: 100

MulAdd nil : 0.02 : 100

SinOsc freq phase: 0

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

LFNoise1 : 2

MulAdd nil : 0.5 : 0.5

* a b: 100

* a b: 2

MulAdd nil : 0.02 : 200

SinOsc freq phase: 0

LFNoise1 : 3

MulAdd nil : 0.5 : 0.5

* a b: 100

* a b: 3

MulAdd nil : 0.02 : 300

SinOsc freq phase: 0

LFNoise1 : 4

MulAdd nil : 0.5 : 0.5

* a b: 100

* a b: 4

MulAdd nil : 0.02 : 400

SinOsc freq phase: 0

LFNoise1 : 5

MulAdd nil : 0.5 : 0.5

* a b: 100

* a b: 5

MulAdd nil : 0.02 : 500

SinOsc freq phase: 0

LFNoise1 : 6

MulAdd nil : 0.5 : 0.5

* a b: 100

* a b: 6

MulAdd nil : 0.02 : 600

SinOsc freq phase: 0

LFNoise1 : 7

MulAdd nil : 0.5 : 0.5

* a b: 100

* a b: 7

MulAdd nil : 0.02 : 700

SinOsc freq phase: 0

LFNoise1 : 8

MulAdd nil : 0.5 : 0.5

* a b: 100

* a b: 8

MulAdd nil : 0.02 : 800

SinOsc freq phase: 0

LFNoise1 : 9

MulAdd nil : 0.5 : 0.5

* a b: 100

* a b: 9

MulAdd nil : 0.02 : 900

SinOsc freq phase: 0

LFNoise1 : 10

MulAdd nil : 0.5 : 0.5

* a b: 100

* a b: 10

MulAdd nil : 0.02 : 1000

SinOsc freq phase: 0

LFNoise1 : 11

MulAdd nil : 0.5 : 0.5

* a b: 100

* a b: 11

MulAdd nil : 0.02 : 1100

SinOsc freq phase: 0

LFNoise1 : 12

MulAdd nil : 0.5 : 0.5

* a b: 100

* a b: 12

MulAdd nil : 0.02 : 1200

SinOsc freq phase: 0

LFNoise1 : 13

MulAdd nil : 0.5 : 0.5

* a b: 100

* a b: 13

MulAdd nil : 0.02 : 1300

SinOsc freq phase: 0

LFNoise1 : 14

MulAdd nil : 0.5 : 0.5

* a b: 100

* a b: 14

MulAdd nil : 0.02 : 1400

SinOsc freq phase: 0

LFNoise1 : 15

MulAdd nil : 0.5 : 0.5

* a b: 100

* a b: 15

MulAdd nil : 0.02 : 1500

SinOsc freq phase: 0

LFNoise1 : 16

MulAdd nil : 0.5 : 0.5

* a b: 100

* a b: 16

MulAdd nil : 0.02 : 1600

SinOsc freq phase: 0

Fig. 7.4 Espansione multicanale: diagramma di flusso (dettaglio).

vede l’argomento bFreq è una frequenza di base che viene moltiplicataprogressivamente per un intero da ind + 1 (in modo tale da evitare lamoltiplicazione per 0, che produrrebbe un segnale d’ampiezza nulla).A questa fondamentale (riga 7) viene aggiunta una variazione (un vi-brato) pseudo-casuale la cui frequenza è proporzionale all’indice (riga9), e la cui ampiezza è proporzionale al 5% della frequenza fondamen-tale per l’indice (righe 12-13). Nel primo caso, l’oscillatore ha frequen-za bFreq × 1 = 100Hz a cui si aggiunge una variazione del 2%,ovvero [−2, 2]Hz ad un tasso pari a 1Hz. Per ottenere una variazio-ne più continua è stato qui impiegato LFNoise1. Se si vuole semplifica-re l’esempio eliminando la variazione pseudo-casuale basta eliminaredall’esempio commentato le righe 8-13.

7.5–220

1 SynthDef("multi16commented", {2 var bFreq = 100 ; // base freq3 var sigArr = Array.fill(16,4 { arg ind ; // 0 15 var index = ind+1 ; // 1 26 SinOsc.ar(7 freq: bFreq*index // 100 2008 +(LFNoise1.kr( // +9 freq: index, // freq: 1

10 mul: 0.5,11 add: 0.5) // amp: 1 1 (fixed)12 *bFreq // bFreq: 100 (fixed)13 *index*0.02) // *1*0.02 = 2 *2*0.05 = 414 )15 }) ;16 Out.ar(0, sigArr)17 }18 ).play ;

Dunque, ogni elemento di sigArr è un oggetto SinOSc.ar, un segnalegenerato da una UGen SinOsc. Poiché Out riceve un array, alloca un nu-mero di bus pari alla dimensione dell’array, a partire da quello indicato,cioè da 0. Le 16 sinusoidi (dotate di diversi parametri) sono distribui-te 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à inviatauna delle sinusoidi. Senza l’espansione multicanale, sarebbe necessariodescrivere esplicitamente attraverso il codice la struttura riprodotta daldiagramma di flusso di Figura 7.3, un lavoro oneroso che invece SC faper l’utente, espandendo ogni elemento dell’array in un opportuno ra-mo del diagramma come meglio si vede in Figura 7.4. I rami hanno lastessa struttura, ma parametri differenti.Se si ascolta su un normale dispositivo stereo (quale quello di un pc) il ri-sultato della valutazione del codice precedente, saranno udibili soltanto

code/controlliCanali/multi16b.sc

7.5–221

le due prime sinusoidi, quelle con frequenza fondamentale pari a 100 e200Hz. A tal proposito SC mette a disposizione dell’utente una UGenspecializzata nella riduzione multicanale, ovvero nel missaggio di piùsegnali in un unico segnale: Mix. Quest’ultima semplicemente prevedecome argomento un array di segnali che restituisce missati in un unicosegnale. La versione seguente modifica l’esempio in discussione fornen-do ad Out un unico segnale, grazie all’intervento di Mix (riga 14). SI notiche quest’ultima UGen semplicemente somma l’output dei segnali checompongono l’array che riceve come argomento.

1 SynthDef("multi16mix", {2 var bFreq = 100 ; // base freq3 var sigArr = Array.fill(16,4 { arg ind ;5 var index = ind+1 ;6 SinOsc.ar(7 freq: bFreq*index+(LFNoise1.kr(8 freq: index,9 mul: 0.5,

10 add: 0.5)*bFreq*index*0.02) ,11 mul: 1/16 ;12 )13 }) ;14 Out.ar(0, Mix.new(sigArr))15 }16 ).play ;

Se tutte le sinusoidi hanno ampiezza normalizzata (come nel caso indiscussione), allora l’escursione del segnale in uscita da Mix sarà com-presa in [−1, 1] × 16 = [−16, 16], dunque abbondantemente oltrel’escursione rappresentabile dell’ampiezza. È perciò opportuno scalarel’ampiezza di ognuno dei segnali sinusoidali per un fattore pari ad 1/n

code/controlliCanali/multi16mix.sc

7.5–222

(riga 11), dove n è il numero dei segnali (la dimensione dell’array), inmodo che l’ampiezza complessiva sia 1/n× n = 1.Attraverso Mix si è persa la spazializzazione ottenuta attraverso la distri-buzione nei 16 canali (ancorché inudibili), ottenendo un unico segnale(mono, per così dire). Si tratta ora di utilizzare le possibilità dell’espansionemulticanale per una distribuzione stereo. Nell’esempio seguente vienegenerato un array di 16 segnali come negli esempi precedenti. Si notiperò che SinOsc.ar è diventato il segnale in entrata di una UGen Pan2.Come si è visto, Pan2 distribuisce un segnale in entrata su un fronte ste-reo in funzione dell’argomento pos. Per ogni sinusoide, pos varia casua-lemente in [−1, 1] con una frequenza pari a index (riga 15). Dunque,più elevato è l’indice, più elevata è la frequenza dell’oscillatore, più ra-pidamente varia la distribuzione spaziale. Si noti che è stata utilizzatala UGen LFNoise1, che interpola tra valori successivi, per evitare salti diposizione, simulando invece un progressivo movimento tra i valori as-sunti dalla distribuzione. In sostanza, l’array sigArr ora contiene 16 se-gnali generati da Pan2, cioè sinusoidi la cui frequenza e la cui frequenzadi spostamento sul fronte stereo incrementa progressivamente in fun-zione dell’indice dell’array. Ma Pan2 restituisce in uscita una coppia disegnali. Ogni segnale è infatti stereo, cioè è costituito da una coppia disegnali la cui ampiezza varia in funzione di pos. Si può verificare quantoosservato aggiungendo un riga

sigArr.postln ;

prima di riga 16 nella synthDef "multi16commented". Si otterà stampatosulla post window:

1 [ a SinOsc, a SinOsc, a SinOsc, a SinOsc, a SinOsc, a SinOsc, aSinOsc, a SinOsc, a SinOsc, a SinOsc, a SinOsc, a SinOsc, a SinOsc,a SinOsc, a SinOsc, a SinOsc ]

7.5–223

Se si fa lo stesso con "multi16mixPan", togliendo il marcatore di com-mento 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 O…etc…

La denominazione OutputProxy è il modo con cui SC chiama l’outputdi alcune UGen che hanno uscite multiple (come Pan2). A parte que-sto aspetto, qui irrilevante, è interessante osservare che ogni elementodell’array è a sua volta un array composto di due elementi, uno per ca-nale. Si consideri il prossimo esempio:

1 { Out.ar(0, Pan2.ar(SinOsc.ar, Line.kr(-1, 1, 10), 0.5)) }.play;

3 // only left channel (non multichannel expansion)4 { Out.ar(0, Pan2.ar(SinOsc.ar, Line.kr(-1, 1, 10), 0.5)[0]) }.play;

6 // only right channel (non multichannel expansion)7 { 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 sinistroa quello destro: pos è un segnale generato dalla UGen Line che linear-mente muove nell’arco di 10 secondi tra −1 e 1. Questo movimentosi traduce sul fronte stereo in decremento dell’ampiezza del segnale sulcanale sinistro e in un incremento della stessa sul canale destro. Le righe4 e 6 inviano ad Out soltano uno dei due canali, rispettivamente sinitro

code/controlliCanali/panArr.sc

7.5–224

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 sull’array restituito da Pan2. Lo spostamento sul fronte stereoè analiticamente scomposto in un crescendo del segnale a destra ed inun 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 nuovoarray con questa struttura

[[sig0sx, sig1sx, sig2, sx, sig3sx, . . ., sig15sx], [sig0dx, sig1dx,

sig2, dx, sig3dx, . . ., sig15dx]]

che è composta di due array, uno per canale, contenenti in contributidi ognuno dei segnali sinusoidali a quel canale. È allora possibile mis-sare ognuno dei due array ottenendo due singoli segnali, per il cana-le sinistro e per quello destro (righe 20 e 21), assegnati alle variabilileft, right. Inviando a Out l’array [left, right] si produce di nuovoun’espansione multicanale, per cui il primo (left) verrà inviato, attra-verso il bus 0, all’altoparlante sinistro, il secondo (right), attraverso ilbus 1, all’altoparlante destro.

7.5–225

1 SynthDef("multi16mixPan", {2 var bFreq = 100 ; // base freq3 var left, right ;4 var sigArr = Array.fill(16,5 { arg ind ;6 var index = ind+1 ;7 Pan2.ar(8 in: SinOsc.ar(9 freq: bFreq*index+(LFNoise1.kr(

10 freq: index,11 mul: 0.5,12 add: 0.5)*bFreq*index*0.02) ,13 mul: 1/16 ;14 ),15 pos: LFNoise1.kr(freq: index)16 )17 }) ;18 // sigArr.postln ;19 sigArr = sigArr.flop ;20 left = Mix.new(sigArr[0]) ;21 right = Mix.new(sigArr[1]) ;22 Out.ar(0, [left, right])23 }24 ).play ;

code/controlliCanali/multi16mixPan.sc

8.1–226

8 Sintesi,II:tecnichedigenerazionedelsegna-leaudio

8.1 Oscillatorietabelle

Discutendo di algoritmi di sintesi del segnali non in tempo reale, piùvolte si è discusso della generazione del segnale a partire dal calcolo diuna funzione, tipicamente sin(x). Sebbene concettualmente elegante,il computo diretto della funzione si rivela assai poco efficiente dal pun-to di vista computazionale: costringe infatti l’elaboratore a calcolare ilvalore della funzione per un numero di volte al secondo pari al tassodi campionamento prescelta (cioè 44.100 nel caso di qualità CD, il tas-so di campionamento predefinto in SC). È possibile però impiegare unaltro metodo, che ha una lunga tradizione in computer music: si trattacioè di costruire un oscillatore digitale. Finora il termine è stato impiegatosenza fornirene una definizione: si trtta ora di vedere più in dettaglioche cosa esso indichi. L’oscillatore digitale è un algoritmo fondamenta-le nella computer music, poiché non è soltanto impiegato per generaredirettamente un segnale ma è anche un’unità componente di molti altri

8.1–227

generatori di suono. Tornando al problema della generazione di una si-nusoide, e ragionando diversamente, si può osservare come un suonosinusoidale (ma in generale la parte stazionaria di ogni suono periodi-co) sia caratterizzato da una grande prevedibilità: esso si ripete ugua-le ad ogni periodo. Dato un periodo del segnale, si può allora pensaredi prelevarne il valore in n punti equidistanti. Questi valori campiona-ti possono essere immessi in una tabella di n punti: alla lista dei punticorrisponde un’altra lista con i valori della funzione. Una tabella di que-sto tipo prende il nome di wavetable (tabella dell’onda o forma d’ondatabulata).

Time (s)0 16

–1

1

0

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

Fig. 8.1 Forma d’onda campionata

Ad esempio il campionanamento di una sinusoide in ampiezza norma-lizzata (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.1–228

[04] = 0.98078525 [12] = -0.98078525 [05] = 0.8314696 [13]= -0.8314696[06] = 0.55557024 [14] = -0.55557024 [07] = 0.19509032 [15]= -0.19509032

L’algoritmo dell’oscillatore digitale prevede fondamentalmente il sem-plice svolgimento di due operazioni:

“leggi i valori della tabella dall’area di memoria in cui questa è con-servata”;

1. “arrivato all’ultimo indirizzo (il 15 nell’esempio), riparti dal primo(0)”. Quest’ultima operazione si definisce wrappping around.

Il metodo di sintesi descritto (Table Look-Up Synthesis) è estremamenteefficiente: la lettura di un dato dalla memoria è infatti un processo assaipiù veloce che non il calcolo del valore di una funzione. È agevole os-servare come una simile operazione di stoccaggio di una “forma” e suosuccessivo reperimento sia tra l’altro lo stesso approccio preso in con-siderazione nel momento in cui si è discusso di una generalizzazionedegli inviluppi. La tabella costituisce infatti un modello (statico e cari-cato in fase di inizializzzione) della forma d’onda: sta all’utente deci-dere ampiezza e frequenza del segnale sintetizzato dall’oscillatore. Perquanto concerne l’ampiezza l’operazione è ormai ovvia. Ad esempio,se il segnale tabulato ha ampiezza compresa in [−1, 1], moltiplicandonell’oscillatore tutti i valori della tabella per 0.5, il segnale in uscita avràun’ampiezza dimezzata, che oscillerà nell’escursione [–0.5,+0.5]. Co-me ormai noto, moltiplicare un segnale per un numero k produce comerisultato un segnale in cui ognuno dei valori risulta moltiplicato per k.Quindi, ognuno dei valori del segnale in entrata (la forma d’onda tabu-lata) viene scalato per 0.5 (Figura 8.2).

8.1–229

Time (s)0 16

–1

1

0

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

0.5

–0.5

Fig. 8.2 Scalatura dell’ampiezza

Passando all’implementazione in SC, si noti che, se il problema fossela generazione di una sinusoide, in SC sarebbe sufficiente utilizzare leUGen specializzate SinOsc e FSinOsc:

• SinOsc implementa l’algoritmo di table look-up, ma non richiededi specificare una tabella poiché la prevede internamente (di 8192punti)

• FSinOsc implementa un approccio diverso al problema della genera-zione di una sinusoide, in modo molto efficiente (di qui la F di Fast)seppur con qualche limitazione d’uso.

Tuttavia l’utilizo di tabelle non si limita evidentemente alla tabulazio-ne di una sinusoide e vale perciò la pena di soffermacisivi. Una delleUGen che si occupano esplicitamente di leggere da tabelle è Osc, il cuiprimo argomento è una tabella, la cui dimensione deve essere un mul-tiplo di 2 (per ragioni di ottimizzazione), e il secondo la frequenza allaquale l’oscillatore legge la tabella. Se la tabella rappresenta il periodo diun segnale, la frequenza indicherà la frequenza del segnale risultante.La tabella può essere fornita a Osc attraverso un buffer. Come si è visto

8.1–230

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 interounivoco attraverso il quale vi si può far riferimento. SC implementa unaclasse Buffer che si occupa della gestione dei buffer. In altre parole conun buffer si dice al server di riservare un certo spazio per certi dati. Laclasse Buffer prevede alcuni metodi per allocare buffer, per leggervidentro un file audio, ma anche per riempire direttamente un buffer conspecifici tipi di segnali, di larga utilità.

1 (2 var buf ;

4 buf = Buffer.alloc(s, 2.pow(10)) ;5 buf.sine1([1]) ;

7 {Osc.ar(buf.bufnum, 440)}.play8 )

10 (11 var buf ;

13 buf = Buffer.alloc(s, 2.pow(10)) ;14 buf.sine1( Array.fill(20, { 1.0.rand }) ) ;

16 buf.plot ;

18 {Osc.ar(buf.bufnum, 440, mul: 0.6)}.jscope19 )

Il codice precedente contiene due esempi. Nel primo alla variabile bufviene assegnato un oggetto Buffer ottenuto chiamando sulla classe ilcostruttore alloc. Il metodo dice al server s24 di allocare un buffer di una

code/sintesi/buffer.sc

8.1–231

certa dimensione. La dimensione è di per sé arbitraria: tuttavia nell’implementazionedegli oscillatori viene tipicamente richiesto (non solo in SC) per questio-ni di ottimizzazione che la dimensione della tabella sia una potenza di 2(2.pow(10)). Quindi viene inviato a buf il messaggio sine1 che riempieil buffer con una somma di sinusoidi armonicamente relate. La sintas-si è analoga al metodo waveFill della classe Array: l’array contiene leampiezze della armoniche. Nella fattispecie c’è solo una sinusoide (lafondamentale) con ampiezza unitaria. Osc richiede come primo argo-mento un indicativo del buffer da leggere (la tabella, appunto). Questoindicativo non può essere il nome della variabile perche è sconosciutaal server, risiedendo sul lato di sclang: deve invece essere l’indice delbuffer assegnato dal server. Per ottenerlo è sufficiente inviare a buf ilmessaggio bufnum, che restituisce l’indice del buffer (7): in questo modol’utente non si deve preoccupare di ricordare esplicitamente quale sial’indicativo assegnato ad ogni buffer. La tabella memorizzata nel bufferviene quindi letta con una frequenza pari a 440 volte al secondo. Nel se-condo esempio, l’array di ampiezze è generato attraverso il metodo fillinvocato su Array, che restituisce un array di 20 numeri pseudo-casualicompresi in [0.0, 1.0]: essi rappresentano le ampiezze stocastiche delleprime 20 armoniche. Come si vede, è possibile visualizzare i dati conte-nuti nel buffer attraverso il metodo plot (16). È altresì possibile fare inmodo che Osc legga da una tabella che contiene un segnale audio resi-dente sul disco rigido. Nell’esempio seguente viene dapprima generatoun segnale attraverso la classe Signal, a cui viene inviato il messaggiosineFill, che genera la consueta somma di sinusoidi: si noti che la di-mensione dell’array (primo argomento di sineFill) è immediatamentestabilita in una potenza di 2, ovvero 216 = 65536 (riga 6). Inoltre,per poter essere letto da Osc, il segnale deve essere convertito in un for-mato apposito, quale quello previsto dalla classe Wavetable: di qui la

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

8.1–232

conversione sig = sig.asWavetable (riga 7). la synthDef seguente sem-plicemente ingloba in una UGen Out una UGen Osc, prevedendo comeargomento bufnum: questo indica l’identificativo (numerico) del buffer,che serve per potervi far riferimento (è il “nome” del buffer). Quindi lasynthDef viene spedita al server (22). Infine, dopo aver memorizzato ilsegnale sotto forma di file audio (10-14), è possibile caricarlo in un buf-fer: la riga 30 utilizza il metodo readche alloca un buffer sul server s evi legge il file indicato dal percorso. Il buffer viene assegnato alla varia-bile buf. La dimensione del buffer (quanta memoria occupa) è inferitadirettamente da SC a partire dalla dimensione del file. Così, il nuovoSynth che viene generato (33) utilizza il buffer buf, il cui indentificativosul server è buf.bufnum.

8.1–233

1 (2 var sig ;3 var soundFile ;

5 //–> generating a signal6 sig = Signal.sineFill(2.pow(16), [1]) ; // 655367 sig = sig.asWavetable ; // mandatory !

9 //–> writing an audiofile10 soundFile = SoundFile.new ;11 soundFile.headerFormat_("AIFF").sampleFormat_("int16").numChannels_(1)

;12 soundFile.openWrite("sounds/signalTest.aiff") ;13 soundFile.writeData(sig) ;14 soundFile.close ;15 )

17 (18 //–> writing a synthDef19 SynthDef("tableOsc",{ arg bufnum = 0, freq = 440, amp = 0.4 ;20 Out.ar(0,21 Osc.ar(bufnum, freq, mul: amp))22 }).send(s) ;23 )

25 (26 var freq = 440 ;27 var buf, aSynth;

29 //–> allocating a buffer and directly reading in an audio file30 buf = Buffer.read(s, "sounds/signalTest.aiff") ;

32 //–> a tableOsc synth readding the table from the buffer33 aSynth = Synth.new("tableOsc", ["bufnum", buf.bufnum]) ;34 )

code/sintesi/table.sc

8.1–234

La caratteristica precipua dell’oscillatore è soltanto quella di generarecampioni sulla base della lettura della tabella: la forma d’onda tabulatanon dev’essere necessariamente sinusoidale. È possible infatti tabularequalsiasi forma, come pure importare una forma d’onda da un qualsiasisegnale preesistente.Nell’esempio seguente la tabella è riempita con valori pseudo-casuali: inparticolare la riga 12 sceglie per ogni elemento dell’array (di tipo Signal)un valore-pseudocasuale tra [0.0, 2.0]− 1 = [−1.0, 1.0]: il risultatoviene quindi converito in una Wavetable. Il resto del codice assume lasynthDef precedente ( "tableOsc" e riprende quanto già visto nel casodella generazione di una sinusoide. Una variabile di rilievo è exp (11),l’esponente di 2 che determina la dimensione della tabella (riga 12). Unoscillatore che non si arresti dopo una lettura della tabella (nel qual casoil concetto di frequenza di lettura non avrebbe molto senso) produce perdefinizione un segnale periodico: infatti reitera la lettura della tabella econseguentementemente il segnale si ripete uguale a se stesso. Più la ta-bella è grande, maggiore è l’informazione sulla sua “forma” (il dettagliotemporale) più il segnale è rumoroso. Viceversa, tabelle di dimensioniridotte producono segnali il cui profilo temporale è più semplice (menovalori pseudo-casuali), e dunque maggiormente “intonati”. Si provi adesempio a variare exp nell’escursione [1, 20], e a visualizzare il conte-nuto del buffer così ottenuto con jplot.

8.1–235

1 /*2 Periodic/Aperiodic3 oscillator reading from a pseudo-random filled table4 */

6 (7 var sig, exp ;8 var soundFile;

10 // table generation11 exp = 6 ; // try changing it bewteen [1, 20]12 sig = Signal.fill(2.pow(exp), {2.0.rand-1}).asWavetable ;

14 //–> writing the audio file15 soundFile = SoundFile.new ;16 soundFile.headerFormat_("AIFF").sampleFormat_("int16").numChannels_(1)

;17 soundFile.openWrite("sounds/signalTest.aiff") ;18 soundFile.writeData(sig) ;19 soundFile.close ;20 )

22 (23 var buf, aSynth ;24 buf = Buffer.read(s, "sounds/signalTest.aiff") ;25 aSynth = Synth.new("tableOsc", ["bufnum", buf.bufnum, "amp", 0.1]) ;26 )

Una modulazione della frequenza dell’oscillatore indica allora una va-riazione della velocità di lettura della tabella. La synthDef seguente pre-vede una variazione pseudo-casuale della frequenza con LFNoise0, se-condo una modlaità abbondantemente descritta. In particolare, con unatabella sinusoidale la synthDef riproduce esempi già visti. Si noti invececome nel caso di una tabella che contenga valori pseudo-casuali la varia-zione di velocità produca una variazione dell’altezza percepita soltanto

code/sintesi/tableNoise.sc

8.2–236

entro certi limiti, approssimativamente entro exp = 10. Oltre a talesoglia, la variazione nella tabella non mantiene più l’identità del profilotemporale che la tabelle descrive.

1 (2 // modulating the freq3 SynthDef("tableOsc",{ arg bufnum = 0, freq = 440, amp = 0.4 ;4 Out.ar(0,5 Osc.ar(bufnum, LFNoise0.ar(10, mul: 400, add: 400), mul: amp))6 }).send(s) ;7 )

8.2 Campionamento

Il metodo di sintesi concettualmente più semplice è il campionamen-to. Fondamentalmente, con l’espressione  ”prelevare un campione” siindica l’ottenimento di un segnale di breve durata o attraverso la regi-strazione diretta, oppure attraverso un’operazione di editing da un fileaudio. In ogni caso, nella sintesi per campionamento l’idea centrale èquella del playback di materiale sonoro preesistente.

8.2.1 Campionamentosemplice

code/sintesi/oscDef.sc

8.2–237

Nel campionamento semplice si ripropone l’idea che abbiamo incontra-to discutendo dell’oscillatore: la lettura di una tabella. Se però in quelcaso la forma d’onda, oltre a essere estremamente semplice (ma sarebbestato possibile costruire una forma qualsiasi), era tipicamente ridotta aun unico periodo, nel campionamento invece la forma d’onda tabulatarappresenta un suono complesso dotato di inviluppo, la cui durata di-pende soltanto dalle limitazioni imposte dall’hardware. Come intuibile,l’origine del campione (per registrazione diretta o per estrazione da unfile preesistente) non fa alcuna differenza. Una volta messo in memoria,il campione può essere richiamato ogni qualvolta l’utente lo desideriattraverso un dispositivo di controllo. Nonostante la semplicità concet-tuale, è evidente la potenza di un simile approccio, poiché si può avereagevolmente a disposizione una ricca tavolozza di suoni preregistrati:ad esempio un intero set di percussioni, così come molte altre librerie dicampioni25.Sebbene possa essere senz’altro possibile utilizzare un oscillatore comeOsc per leggere un file audio preventivamente caricato in un buffer, laUGen specializzata nella lettura di un buffer è PlayBuf. Essa non incorrenel limite di avere una tabella la cui dimensione sia multiplo di 2, mapuò invece leggere un buffer di dimensioni qualsiasi.

Non è questo il luogo per una sitografia, ma vale la pena di segnalare una risorsa di25

grande interesse, The Freesound Project,http://freesound.iua.upf.edu/

che mette a disposizione con licenza Creative Commons decine di migliaia di file au-dio, tutti indicizzati, scaricabili o ascoltabili on-line.

8.2–238

1 (2 SynthDef("playBuf", { arg bufnum = 0, loop = 0;3 Out.ar(0,4 PlayBuf.ar(1, bufnum, loop: loop)5 )6 }).send(s)7 )

9 (10 var buf, aSynth ;11 buf = Buffer.read(s,"sounds/a11wlk01-44_1.aiff") ;

13 aSynth = Synth("playBuf", ["bufnum", buf.bufnum, "loop", -1])14 )

Nell’esempio precedente la synthDef “avvolge” semplicemente playBufdi cui dimostra tre argomenti, i primi due e l’ultimo.

• Il primo argomento indica il numero dei canali (tipicamente, monoo stereo): se il suo valore è 1, allora il buffer è mono

• il secondo specifica l’indice del buffer, qui controllabilem come ar-gomento bufnum della synthDef.

• l’argomento loop indica la modalità di lettura e può avere due valori:0 indica un’unica lettura del buffer, 1 una lettura ciclica.

Nel seguito viene allocato un buffer buf che legge dal disco fisso, nellacartella sound dell’installazione locale di SC, il file a11wlk01-44_1.aiff.Si noti che il file è stato campionato ad un tasso di campionamento paria 44100Hz (come dice anche il nome). SC lo legge alla sua frequen-za di campionamento, predefinita a 44100Hz: essendo le due uguali,non c’è problema. Se invece fossero diverse sarebbe necessario tenerneconto.

code/sintesi/playBuf1.sc

8.2–239

8.2.2 Resamplingeinterpolazione

La semplice lettura del buffer, secondo quanto descritto per PlayBuf,riproduce il segnale tale e quale vi è stato memorizzato. Ripensandoal caso di Osc si può tuttavia notare come fosse specificabile una fre-quenza: questa frequenza indicava il numero di letture al secondo dellatabella contenuta nel buffer da parte dell’oscillatore. Se la tabella fossedi dimensione fissa, si potrebbe leggere più o meno velocemente la ta-bella, variando opportunamente il tasso di campionamento di scsynth.Ma poiché un buffer ha una dimensione fissa ma in compenso anche lasample rate non è modificabile, per poter variare la frequenza del segna-le uscente è necessario lavorare sulla tabella in un altro modo. Questolavoro è comune anche a PlayBuf e permette così di”intonare” ancheun suono campionato in una tabella. Il procedimento più usato a talescopo è usualmente definito resampling, che equivale, come indica il no-me, a un vero e proprio ricampionamento. Si consideri nuovamente latabella di Figura ??, che rappresenta una forma d’onda. Se si leggonotutti i punti della tabella, si ottiene il segnale originale. Si supponga in-vece, allo stesso taso di campionamento, di leggere soltanto un puntoogni due. Si ottiene come risultato un segnale dalla frequenza doppiarispetto all’originale. Infatti, il segnale risultante risulta costituito di unnumero di campioni pari alla metà del segnale originale, e dunque ilsuo periodo è pari alla metà del periodo originale, T2 = T1/2. Data larelazione f = 1/T , ne consegue che f2 = 1/T2, cioè 1/T1/2: allorail rapporto R = f2/f1 è uguale al rapporto tra 1/T1/2 e 1/T1, ov-veroR = 2. Si supponga di avere una tabella di 16 punti quale quelladi Figura ?? e che il tasso di campionamento sia di 44100 campioni alsecondo. Supponendo una semplice lettura della tabella sia che il pe-riodo T (ovvero quanto dura un ciclo della forma d’onda tabulata) è

8.2–240

pari a T = 16/44100 = 0.00036281179138322sec. Ciò indicache 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 ta-bella la lettura procede come illustrato qui di seguito e produce il risul-tato 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

Time (s)0 0.000362812

–1

1

0

Time (s)0 0.000362812

–1

1

0

Time (s)0 0.000362812

–1

1

0

Time (s)0 0.000362812

–1

1

0

Time (s)0 0.000362812

–1

1

0

Time (s)0 0.000362812

–1

1

0

Time (s)0 0.000362812

–1

1

0

Fig. 8.3 Lettura conpasso = 2 della tabella.

In Figura 8.4(a) è rappresentato un segnale sinusoidale di 440Hz. Leg-gendo un punto ogni due, si ottiene il segnale di Figura 8.4(b-c): in (b) siosserva come nella stessa durata il numero di picchi e gole nel segnalerisultante raddoppi, mentre in (c) si può osservare come ogni ciclo del

8.2–241

segnale sottocampionato sia rappresentato da metà del numero di cam-pioni rispetto all’originale. Il segnale sottocampionato ha frequenza paria 880Hz e durata pari a metà del segnale originale, come si vede nelleFigure 8.4(d-e).

Time (s)0 0.02

–1

1

0

Time (s)0 0.01

–1

1

0

Time (s)0 0.02

–1

1

0

Time (s)0 1

–1

1

0

Time (s)0 1

–1

1

0

Fig. 8.4 Sinusoide a 440 Hz (a), segnale sottocampiona-to leggendo un campione ogni due (b-c), segnale originalee 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 tra-sposizione all’ottava superiore, mentre 4 : 1 indica una trasposizionepari a quattro ottave.L’operazione fin qui descritta si chiama downsampling. L’upsampling èil procedimento simmetrico attraverso il quale è possibile diminuire lafrequenza del campione originario: a ogni punto della tabella vengo-no fatti corrispondere non uno, ma due campioni (nel senso di punti dicampionamento) nel segnale derivato.

8.2–242

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 l’altro della tabella originalevenisse messo un nuovo campione. Rispetto alla tabella originale, ci saràtra ogni coppia di punti un punto intermedio in più, secondo quantoavviene in Figura 8.5.

Time (s)0 0.000362812

–1

1

0

Time (s)0 0.000362812

–1

1

0

Time (s)0 0.000362812

–1

1

0

Time (s)0 0.000362812

–1

1

0

Fig. 8.5 Lettura conpasso = 0.5 della tabella.

La frequenza risultante è la metà di quella originale, il che equivale mu-sicalmente a una trasposizione al grave di un’ottava. I valori dei campio-ni aggiunti non ci sono nella tabella originale e devono essere inferiti inqualche modo, intuibilmente a partire dai valori contigui. Tipicamenteciò avviene per per interpolazione tra i due valori originali che precedo-no 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.2–243

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

• x0, y0, x1, y1, x.

Il problema è trovarey. Si osserva come siano omogenei i triangoliABQeAPO, e dunque valga una relazione di omogeneità tra i lati.

• ABQ ∝ APO → PO/BQ = AO/AQSosituendo ai termini i valori delle variabili si ottiene:

• (y − y0)/(y1− y0) = (x− x0)/(x1− x0)ovvero

• y = (x−x0)/(y1−y0)(x1−x0) + y0

(x0, y0)

(x1, y1)

(x, y)

A

B

O

P

Q

Fig. 8.6 Interpolazione lineare comerelazione tra triangoli

Nel caso precedente, in cui un campione viene aggiunto “in mezzo” adue precedenti, rispetto alla figura si ha che i campiono noti sono rap-presentati dai puntiA eB, che costituiscono i campioni 0 e 2 del nuovosegnale. Si tratta di calcolare il valore del campione con indice 1, ovvero

8.2–244

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

• (x0, y0) = (0, 0)• (x1, y1) = (2, 1)• x = 1E dunque:

• y = (1−0)/(1−0)(2−0) + 0 = 1

2 + 0 = 0.5Questo tipo di interpolazione è detto “lineare” perché intuibilmente cer-ca sulla retta che connette i due punti notiA eB il valore ignoto corri-spondente alla ordinata della nuova ascissa26.L’utilizzo dell’interpolazione permette di determinare il valori di cam-pioni che avrebbero un indice frazionario: cioè, di poter prendere nonsolo un campioni ogni due, tre etc, ma anche ogni 0.9, 1.234 etc. Ciòsignifica poter trasporre il campione originale non solo all’ottava, ma atutte le altre frequenze corrispondenti alle altre note di una tastiera, peri quali è necessario considerare anche rapporti numerici non interi tra ilnumero dei punti iniziali e e quello finale. Poiché il rapporto tra due fre-quenze a distanza di semitono (sulla tastiera, tra due tasti consecutivi:ad esempio, tra do e do♯) è 12√2 ≈ 1.06, come si vede in SC calcolandoil rapporto tra le frequenze (si ricordi che in notazione midi 60 indica ildo e l’unità rappresenta il semitono):

Esistono altre forme di interpolazione, che qui non si discutono, ma che si basano sullo26

stesso principio geometrico di determinare una certa curva, non necessariamente unaretta, tra due punti e determinare il valore di una ordinata per una ascissa intermedianota.

8.2–245

1 61.midicps/60.midicps2 1.0594630943593

Per ottenere una trasposizione di semitono sarebbe necessario preleva-re un punto ogni≈ 1.06. Questa operazione è possibile calcolando perinterpolazione il nuovo valore. Infine ricordiamo che, poiché dal puntodi vista timbrico raddoppiare la frequenza è ben diverso da trasporrestrumentalmente all’ottava (vedi Capitolo 1), se si vuole fare della sin-tesi imitativa è opportuno campionare tutte le diverse regioni timbriche(i registri) dello strumento prescelto. In un oscillatore come Osc l’idea èquella di fornire un periodo di un segnale e di poter controllare la fre-quenza del segnale risultante. La frequenza viene espressa in terminiassoluti, poiché indica il numero di volte in cui la tabella viene lettaal secondo, e dunque il numeroi di cicli per secondo. PlayBuf è inve-ce un lettore di buffer, impiegato tipicamente quando il buffer contienenon un ciclo ma un segnale complesso, di cui infatti non è necessarioconoscere la dimensione: la frequenza di lettura viene allora più util-mente espressa in termini di proporzione tra i periodi. Il rapporto vieneespresso in termini della relazione T1

T2, dove T1 è il numero di campioni

contenuti nel buffer e T2 quello del segnale risultante. La stessa relazio-ne può essere espressa in temrini di frequenza con f2

f1, dove f1 indica

la frequenza del segnale di partenza e f2 quella del segnale risultante.PlayBuf permette di specificare un simile tasso attraverso l’argomentorate: se rate = 1 allora il buffer viene letto al tasso di campionamentodel 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 f2pari alla metà dell’originale, ovvero scende di un’ottava. Se rate è nega-tivo, allora il buffer viene letto in senso inverso, dall’ultimi campione alprimo. Nell’esempio seguente l’argomento rate è gestito da un segna-le che risulta dall’uscita di una UGen Line. Quest’ultima definisce un

8.2–246

segnale che decresce linearmente in [−1, 1]. Questa progressione de-termina una lettura del buffer inizialmente senza modificazioni rispettoall’originale (rate = 1), a cui segue un progressivo decremento chesi traduce in un abbassamento della frequenza (e, si noti, in un incre-mento della durata) fino a rate = 0. Quindi il valore cresce ma consegno inverso: così fa la frequenza, ma il buffer viene letto all’inverso(dalla fine all’inizio). Il valore finale del segnale generato da Line è−1,che equivale ad una “velocità di lettura” pari a quella normale, ma alcontrario.

1 (2 SynthDef("playBuf2", { arg bufnum = 0;3 Out.ar(0,4 PlayBuf.ar(1, bufnum, rate:5 Line.kr(1, -1, 100), loop: -1)6 )7 }).send(s) ;8 )

10 (11 var buf, aSynth ;12 buf = Buffer.read(s,"sounds/a11wlk01-44_1.aiff") ;

14 aSynth = Synth("playBuf2", ["bufnum", buf.bufnum]) ;15 )

L’esempio seguente dimostra la potenza espressiva di SC (oltre alla suaefficienza) e riprende molti aspetti di un esempio già visto (che non siridiscuteranno). L’array source contiene 100 segnali stereo che risulta-no dalla spazializzazione attraverso Pan2 di PlayBuf. In ognuna delleUGen PlayBuf l’argomento rate è controllato da una UGen LFNoise0,così che l’escursione possibile dello stesso è [−2.0, 2.0], ovvero fino

code/sintesi/playBuf2.sc

8.3–247

ad un’ottava sopra ed anche al contrario. I 100 Out leggono lo stessobuffer, ognuno variando pseudo-casualmente la velocità di lettura. I se-gnali stereo sono quindi raggruppati per canale (attraverso flop, riga13), missati (14-15) e inviati come array di due elementi a Out (16).

1 (2 SynthDef("playBuf3", { arg bufnum = 0;3 var left, right ;4 var num = 30 ;5 var source = Array.fill(num, { arg i ;6 Pan2.ar(7 in: PlayBuf.ar(1, bufnum, rate:8 LFNoise0.kr(1+i, mul: 2), loop: -1),9 pos: LFNoise0.kr(1+i),

10 level: 1/num) ;11 }) ;

13 source = source.flop ;14 left = Mix.new(source[0]) ;15 right = Mix.new(source[1]) ;16 Out.ar(0, [left, right])17 }).send(s) ;18 )

20 (21 var buf, aSynth ;22 buf = Buffer.read(s,"sounds/a11wlk01-44_1.aiff") ;

24 aSynth = Synth("playBuf3", ["bufnum", buf.bufnum]) ;25 )

code/sintesi/playBuf3.sc

8.3–248

8.3 Sintesiadditiva

Il teorema di Fourier stabilisce che ogni segnale periodico per quantocomplesso può essere rappresentato come la somma di semplici segnalisinusoidali. L’idea alla base della sintesi additiva è quello di procederein senso inverso, ottenendo un segnale complesso a partire dalla som-ma di semplici segnali sinusoidali, ognuno dei quali dotato di un suoinviluppo d’ampiezza. La Figura 8.7 illustra un banco di oscillatori chelavorano in parallelo: essi generano n segnali, che vengono sommati eche risultano in un segnale complessivo spettralmente più ricco. Nellasintesi additiva classica gli oscillatori sono “intonati” a partire da unafrequenza fondamentale: le loro frequenze (f2 . . . fn) sono cioè mul-tipli interi della frequenza fondamentale (f1).

f 1

f 2

f 3

f 4

Out

oscilsharmonics

f n

Fig. 8.7 Banco di oscillatori.

Si tratta di uno dei metodi di sintesi in uso da più antica data proprioperché le due operazioni (sintesi di segnali sinusoidali e loro somma) so-no di implementazione molto semplice. Se in teoria del segnale si parladi “somma di segnali”, più tipicamente in ambito audio ci si riferiscealla stessa cosa in termini di missaggio (mixing, miscelazione). Come

8.3–249

si è visto, la UGen specializzata nel missaggio in SC è Mix, che riceve inentrata un array si segnali, li somma e restituisce un nuovo segnale (mo-no). Il missaggio in digitale è propriamente e banalmente una somma.Si comporta così

segnale 1: 0 - 1 - 2 - 3 - 2 - 1 - 1segnale 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 a = Array.series(20).normalize*2-1 ; // ramp in [-1.0, 1.0]3 b = Array.fill(20, { rrand(-0.5, 0.5) }) ; // rand in [-0.5, 0.5]4 c = a + b ; // sum

6 // plot7 [a, b, c].flop.flat.plot(numChannels:3, discrete:true, minval: -1,

maxval:1)8 )

I due array a e b contengono 20 valori. Il primo viene riempito di conuna serie che progredisce da 0 a 20, viene normalizzato nell’escursione[0, 1], quindi moltiplicato per 2 (l’escursione è ora [0.0, 2.0]), infinesottraendo 1 i valori sono compresi in [−1.0, 1.0]. Il secondo contienevalori pseudo-casuali nell’intervallo [−0.5, 0.5]. Il terzo (c) contiene lasomma (valore per valore) dei primi due. Il risultato è visibile attraversola riga 7.Passando alla sintesi, l’esempio seguente –per quanto minimale– è istrut-tivo rispetto alla potenza espressiva di SC. Come si vede, la riga 3 in-troduce un oggetto Mix: esso deve riceve come argomento un array di

code/sintesi/mix.sc

8.3–250

segnali che sommera restituendo un segnale unico. A Mix viene passa-to un array che contiene 20 segnali (generati da SinOsc. Si noti che iè il contatore e viene utilizzato come moltiplicatore della frequenza de-gli oscillatori, che saranno perciò multipli di 200Hz. Poiché i è inizia-lizzata a 0 il primo oscillatore avrebbe una frequenza nulla (= assenzadi segnale), e dunque è necessario aggiungere 1 a i. Nel missaggio, lasomma dei segnali è lineare: ciò vuol dire che n segnali normalizzatinell’intervallo [−1.0, 1.0] verranno missati in un segnale risultante lacui ampiezza sarà compresa tra n× [−1.0, 1.0] = [−n, n]: intuibil-mente, ciò risulta in un abbondante clipping. Nell’esempio, il problemaè risolto attraverso mul:1/20: ogni segnale ha al massimo ampiezza paria 1/20, e dunque, pure nella circostanza in cui tutti i segnali sono simul-taneamente al massimo dell’ampiezza, il segnale missato non supera imassimi previsti.

1 (2 {3 Mix.new // mix4 ( Array.fill(20, { arg i ; // 20 partials5 SinOsc.ar(200*(i+1), mul: 1/20)}))6 }.scope ;7 )

Per verdere che cosa avviene a livello spettrale è sufficiente valutare:

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

Vista l’attitudine algoritmica di SC, il linguaggio mette a disposizione unmetodo fill definito direttamente sulla classe Mix. Il codice seguente èesattamente identico nei risultati al precedente.

code/sintesi/additive1.sc

8.3–251

1 (2 {3 // the same4 Mix.fill(20, { arg i ;5 SinOsc.ar(200*(i+1), mul: 1/20)})6 }.scope ;7 )

Negli approcci precedenti ogni sinusoide inizia il suo ciclo nello stessomomento. Questo determina un picco visibile nella forma d’onda, chesi traduce in una sorto di impulso. In generale, la fase è irrilevante nel-la parte stazionaria del segnale, mente assume importanza critica neitransitori e nelle discontinuità del segnale. Poiché quanto qui interessaè soltanto la relazione tra le componenti (in termini stazionari), è meglioevitare allineamenti di fase. Nell’esempio qui di seguito ogni oscillatorericeve una fase iniziale casuale attraverso 2pi.rand.

1 (2 {3 // avoiding phase sync4 Mix.fill(20, { arg i ;5 SinOsc.ar(200*(i+1), 2pi.rand, mul: 1/20)})6 }.scope ;7 )

Variando l’ampiezza delle armoniche componenti, si modifica eviden-temente il peso che queste hanno nello spettro. Qui di seguito, un se-gnale stereo che risulta dalla somma di 40 sinusoidi a partire da toHzviene modulata nell’ampiezza in modo differente sul canale destro e su

code/sintesi/additive2.sc
code/sintesi/additive3.sc

8.3–252

quello sinistro:nel primo caso l’ampiezza dei componenti varia casual-mente con una frequenza di 1 volta al secondo, nel secondo caso se-gue una oscillazione sinusoidale con frequenza casuale nell’intervallo[0.3, 0.5]Hz. Gli argomenti mul e add rendono il segnale unipolare(compreso nell’intervallo [0.0, 1.0]). L’ampiezza è divisa per 20: se-condo quanto vista prima, dovrebbe essere divisa per 40, ma, a causadelle variazioni delle ampiezze dei segnali, una divisione (empirica) per20 è sufficiente ad evitare il clipping.

1 (2 {3 // stereo spectral motion4 Mix.fill(40, { arg i ;5 var right = { LFNoise1.kr(1, 1/20) } ;6 var left = { SinOsc.kr(rrand(0.3, 0.5), 2pi.rand, mul: 0.5, add:

0.5) } ;7 SinOsc.ar(50*(i+1), [2pi.rand, 2pi.rand], mul: [left/20, right]) })8 }.scope ;9 )

L’esempio seguente scala l’ampiezza di ogni componente in proporzio-ne inversa al numeto di armonica: più il numero è grande, minore èl’ampiezza, secondo un comportamente fisicamente comune negli stru-menti musicali. La variabile arr contiene prima una sequenza di interida 1 a 20, il cui ordine viene invertito (attraverso il metodo reverse), ilcui valore viene normalizzato in modo che la somma di tutti i compo-nenti sia pari a 1. In altre parole, sommando tutti gli elementi dell’arrayarr si ottiene 1. QUesto vuol dire che se si usano gli elementi dell’arraycome moltiplicatori delle ampiezze non si incorre in fenomeni di clip-ping. La frequenza del’oscillatore è espressa in notazione midi (60 = docentrale), che viene quindi convertita in cicli al secondo (midicps).

code/sintesi/additive4.sc

8.3–253

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

Attraverso la sintesi additiva possono essere generate proceduralmentemolte forme d’onda di tipo armonico. Ad esempio, l’onda quadra puòessere descritta come una somma infinita di armoniche dispari la cuiampiezza è proporzionale all’inverso del numero d’armonica, secondola relazione f1 × 1, f3 × 1/3, f5 × 1/5, f7 × 1/7 . . .La Figura 8.8 confronta un’onda quadra (in alto) con la somma pesatadelle prime 3 armoniche (al centro) e delle prime 10 armoniche (in bas-so), mentre la Figura 8.9 dimostra i cambiamenti nella forma d’onda (asinistra) e nello spettro (a destra, nel sonogramma) in relazione all’incrementodelle componenti armoniche.Come si nota, la ripidità degli spigoli nella forma d’onda è proporzio-nale al numero dei componenti. Dunque, l’arrotondamento indica unbasso numero di componenti e, all’opposto, la presenza di transizioniripide un numero alto.La discussione sulla sintesi additiva ha introdotto non a caso la classeMix, perché in effetti la sintesi additiva può essere generalizzata nei ter-mini di un micro-missaggio/montaggio in cui un insieme arbitrario dicomponenti viene sommato per produrre un nuovo segnale. Si parla inquesto caso di “somma di parziali”, intendendo come parziale un com-ponente di frequenza arbitraria in uno spettro (e che perciò non deve es-sere relata armonicamente alla fondamentale, e che può includere anchesegnali “rumorosi”). Restando nell’ambito dei componenti sinusoidali,

code/sintesi/additive5.sc

8.3–254

Time (s)0 0.03

–1

1

0

Time (s)0 0.03

–1

1

0

Time (s)0 0.03

–1

1

0

Fig. 8.8 Onda quadra, prime 3 e prime 10 armoniche.

Time (s)0 1

–1

1

0

Time (s)0 1

–1

1

0

Time (s)0 1

–1

1

0

Time (s)0 1

–1

1

0

Time (s)0 1

–1

1

0

Time (s)0 1

–1

1

0

Time (s)0 6

0

2·104

Fre

quen

cy (

Hz)

Fig. 8.9 Progressione nelle componenti e approssimazione progressivaall’onda quadra

8.3–255

si può descrivere l’armonicità nei termini di un rapporto tra le frequen-ze delle componenti (ratio, in latino e in inglese). In caso di armonicità,si ha integer ratio tra fondamentale e armoniche (il rapporto è espres-so da un numero intero), nel caso di un parziale si ha non-integer ratio,poiché parziale/f 1 non è intero. Nell’esempio seguente l’unica novità è lariga 10, che introduce un incremento casuale (e proporzionale al nume-ro di armonica) nelle frequenze delle armoniche. Dunque lo spettro saràinarmonico. Tra l’altro, una componente ridotta di inarmonicità è tipi-ca di molti strumenti acustici ed è di solito auspicabile (si parla alloradi quasi-integer ratio). Si noti che ogni volta che il codice viene valuta-to, nuovi valori vengono generati con conseguente cambiamento dellospettro.

1 (2 // Varying envelopes with quasi-integer ratio3 {4 Mix.new( Array.fill(50,5 { arg k ;6 var incr = 1 ; // quasi-integer. Try to increase to 2…5..10 etc7 var env ;8 i = k+1 ;9 env = { LFNoise1.ar(LFNoise0.ar(10, add:1.75, mul:0.75), add:0.5,

mul:0.5) };10 SinOsc.ar(50*i11 +(i*incr).rand,12 mul: 0.02/i.asFloat.rand)*env })13 )}.scope14 )

Uno spettro di parziali può essere ottenuto per addizione di componentisinusoidali, in numero di 80 nell’esempio qui di seguito.

code/sintesi/quasiInteger.sc

8.3–256

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

}) );6 }.scope7 )

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

1 (2 // Sweeping around 500 Hz3 {4 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) }) );5 }.scope6 )

Infine, in relazione alla generalizzazione della tecnica in termini di mon-taggio/missaggio, nell’esempio seguente quattro segnali ottenuti contecniche di sintesi diverse (e diverse sono infatti le UGen coinvolte: SinOsc,Blip, HPF, Dust, Formant) sono correlate attraverso la stessa frequenza(f): l’ampiezza di ognuno di essi è controllata da un generatore pseudo-casuale (le variabili a, b, c, d. Il segnale è poi distributio sul frontestereo attraverso Pan2.

code/sintesi/partials1.sc
code/sintesi/partials2.sc

8.3–257

1 // Additive in a broad sense: 4 UGens tuned around a frequency andrandomly mixed down

2 // (and randomly panned around)3 (4 a = { LFNoise1.ar(1, add:0.15, mul:0.15) };5 b = { LFNoise1.ar(1, add:0.15, mul:0.15) };6 c = { LFNoise1.ar(1, add:0.15, mul:0.15) };7 d = { LFNoise1.ar(1, add:0.15, mul:0.15) };

9 f = { LFNoise0.ar(LFNoise0.ar(0.5, add:0.95, mul: 0.95), add: 50, mul:50)};

11 m = { Pan2.ar(12 Mix.new([13 { SinOsc.ar(f, mul:a) },14 { Blip.ar(f, mul:b) },15 { HPF.ar(Dust.ar(f*0.2), f, mul:c) },16 { Formant.ar(f,mul:c) },17 ])18 , LFNoise1.ar(0.2, mul:1)19 ) };20 m.scope;21 )

La sintesi additiva si rivela particolarmente adatta per i suoni ad altezzadeterminata, in cui è sufficiente la definizione di un numero limitato diarmoniche particolarmente evidenti. In effetti, per costruire uno spigolo(come quello di un’onda quadra, o di un onda triangolare o a dente disega) sono necessarie teoricamente infinite componenti sinusoidali. Atal proposito, SC prevede specifici generatori, ad esempio Pulse gene-ra onde impulseive (tra cui l’onda quadra) e Saw onde a dente di sega.Questi tipi di segnali sono spesso utilizzati come segnali di controllo edesistono perciò versioni LF di queste UGen, cioè versioni che sono spe-cializzate nel lavorare a tasso di controllo LFPulse, LFSaw27. Una somma

code/sintesi/partials3.sc

8.3–258

di sinusoidi, per quanto numerose esse possano essere, produce un se-gnale aperiodico, ma non “rumoroso”, ed è il risultato tipico è un effettodi “liquidità”.

1 (2 // Filling the spectrum with sinewaves: frac of semitones3 m = {4 var comp = 500; // number of components. Increase as much as your CPU

can…5 var res = 0.1;6 var base = 30 ; // 30 = rumble7 Mix.new( Array.fill(comp,8 { arg i;9 SinOsc.ar(freq: (base+(i*res)).midicps // resolution: eg. interval

of 0.5 Hz10 // Note that the resulting wave h as a pulse freq of reso-

lution11 , phase: 2pi.rand, mul: 1.0/comp )}) // random phase12 )}.scope

14 )

L’esempio crea 500 sinusoidi in parallelo (è un bel numero. . .): il nume-ro degli stessi è controllato da comp. La strategia di riempimento gestiscele frequenze attraverso la notazione midi. Questo assicura la possibilitàdi equispaziare in altezza (percepita) le componenti (e non in frequen-za). La variabile res controlla la frazione di semitono che incrementa infunzione del contatore i a partire dalla altezza di base, base. Si noti co-me storicamente il principale problema della sintesi additiva sia stata lacomplessità di calcolo richiesta. Infatti un segnale complesso richiede ladefinizione di almeno 20 armoniche componenti, di ognuna delle quali

Ancora, Blip.ar(freq, numharm, mul, add) genera un numero numharm controllabile27

di componenti armoniche di un fondamentale freq, dotate tutte della stessa ampiezza.

code/sintesi/filling.sc

8.4–259

è necessario descrivere l’inviluppo in forma di spezzata. Come si vede,il problema computazionale è ormai di scarso rilievo, anche perché SC èparticolarmente sufficiente. La sintesi di suoni percussivi, nello spettrodei quali si evidenzia una preponderante componente di rumore (cioèdi parziali non legate alla serie armonica di Fourier), richiederebbe (alfine di ottenere un risultato discreto) un numero di componenti elevatis-simo. In realtà, è necessario un altro approccio, quale ad esempio quellodella sintesi sottrattiva (cfr. infra).

8.4 Sintesigranulare

La sintesi granulare muove dal presupposto che il suono possa esserepensato non solo in termini ondulatori ma anche in termini corpusco-lari. È usuale il paragone con i procedimenti pittorici del pointillisme:la somma di frammenti sonori di dimensione microscopica viene perce-pita come un un suono continuo, come la somma di minuscoli punti dicolore puro origina la percezione di tutti gli altri colori. Si tratta alloradi definire grani sonori la cui durata vari tra 1 e 100ms, ognuno deiquali caratterizzato da un particolare inviluppo d’ampiezza, che influi-sce notevolmente sul risultato sonoro. Come intuibile, se per sintetizzareun secondo di suono sono necessari ad esempio 100 grani, non è pen-sabile la definizione deterministica delle proprietà di ognuno di essi. Èperciò necessario ricorrere a un controllo statistico, che ne definisca i va-lori medi per i diversi parametri, ad esempio durata, frequenza, formad’onda, inviluppo: più complessivamente deve essere stabilita la densi-tà media dei grani nel tempo. La necessità di un controllo d’alto livellorispetto al basso livello rappresentato dai grani richiede così la definzio-ne di un modello di composizione: non a caso la sintesi granulare è unadelle tecniche che ha maggiormente stimolato un approccio algoritmico

8.4–260

alla composizione. Ad esempio si può organizzare la massa dei graniseguendo la legge di distribuzione dei gas perfetti (Xenakis), oppureattraverso una tipologia degli ammassi che deriva da quella meteoro-logica delle nuvole (Roads), o infine immaginare le sequenze di granicome dei flussi di particelle di cui determinare le proprietà globali tra-mite “maschere di tendenza” (Truax). In generale, va notato che per lasintesi granulare è richiesto al calcolatore un notevole lavoro computa-zionale per controllare i valori dei parametri relativi alla sintesi di ognisingolo grano. Esistono molti modi differenti di realizzare la sintesi gra-nulare, che rispondono a obiettivi anche assai diversi tra loro: in effetti,seppur con qualche approssimazione, può essere descritta come “sin-tesi granulare” ogni tecnica di sintesi che si basi sull’utilizzo di segnaliimpulsivi.

1 (2 // sync granular synthesis3 var baseFreq = { GUI.mouseX.kr(100, 5000, 1) };4 var disp = { GUI.mouseY.kr } ;5 var strata = 10 ;6 var minDur = 0.05, maxDur = 0.1 ;7 var minRate = 10, maxRate = 50 ;

9 JMouseBase.makeGUI ; // if you use SwingOSC

11 {12 Mix.fill(strata,13 { SinOsc.ar( baseFreq + LFNoise0.kr(rrand(minRate, maxRate),14 mul: baseFreq*disp*0.5), mul: 1/strata) }15 //* LFPulse.kr(1/rrand(minDur, maxDur))16 * SinOsc.ar(1/rrand(minDur, maxDur)*0.5).squared17 )18 }.freqscope19 )

code/sintesi/syncGrain.sc

8.4–261

Nell’esempio precedente è implementato un approccio in sincrono allasintesi granulare. In sostanza, nell’algoritmo di sintesi una sinusoide èinviluppata in ampiezza da un’onda quadra, generata da LFPulse: poi-ché LFPulse genera un segnale unipolare (la cui ampiezza è inclusa in[0.0, 1.0]), quando l’ampiezza dell’onda quadra è > 0, lascia passa-re il segnale, altrimenti lo riduce ad ampiezza 0. Il risultato è una “fi-nestrazione” del segnale. La durata di ogni finestra dipende dal ciclodell’onda quadra, ed è gestito da minDur e maxDur (le durate specificanoil periodo della finestra, dunque la frequenza di LFPulse sarà 1

T , cioé1/rrand(minDur, maxDur)). In questo caso, l’inviluppo del grano è ap-punto dato da un impulso (il ciclo positivo dell’onda quadrata). La ri-ga 16 (commentata) propone un altro inviluppo, ottenuto elevando alquadrato una sinusoide (cfr. supra). L’inviluppo avrà il doppio dei pic-chi nell’unita di tempo (attraverso il rovesciamento della parte negativadella sinusoide) e dunque la frequenza viene dimezzata attraverso *0.5.Si noterà che l’inviluppo a campana, meno ripido della sinusoide alquadrato riduce l’ampiezza delle componenti più elevate dello spettro.Come si vede, attraverso Mix viene missato insieme un numero stra-ta di segnali: in ognuno, baseFreq gestisce la frequenza di base a cuiviene aggiunto un valore pseudo-casuale disp, generato da LFNoise0,che rapprenta una percentuale normalizzata della frequenza di base (sebaseFreq = 100 e disp = 0.5, allora la frequenza dell’oscillatorevarierà nell’intervallo [100− 50, 100 + 50]). Il tasso di questa varia-zione è gestito da minRate e maxRate.L’approccio precedente è implementabile in tempo reale, e parte dall’assuntodi convertire, per così dire, segnali continui, in segnali impulsivi a parti-re da una operazione di finestrazione. Un altro approccio possibile pre-vederebbe di pensare ad ogni singolo grano come ad un evento di taglia“compositiva”: lo si vedrà discutendo dello scheduling. Un uso tipicodelle tecniche granulari sfrutta la granularità non tanto per la sintesiab nihilo quanto piuttosto per l’elaborazione: tipicamente, l’operazioneè chiamata “granulazione”, e come intuibile consiste nel sottoporre a

8.4–262

scomposizione in grani e successiva ricombinazione un segnale preesi-stente. In qualche misura, l’operazione è intrinsecamente in tempo dif-ferito, perché è necessario avere a disposizione una parte di segnale perpoterla scomporre e ricomporre. Ciò vuol dire che, in tempo reale, ènecessario quantomeno riempire un buffer (una locazione di memoriatemporanea) per poterlo granulare.

1 (2 b = Buffer.read(s, "sounds/a11wlk01-44_1.aiff");

4 SynthDef(\grainBuf, { arg sndbuf;5 Out.ar(0,6 GrainBuf.ar(2, Impulse.kr(10), 0.1, sndbuf,7 LFNoise1.kr.range(0.5, 2),8 LFNoise2.kr(0.1).range(0, 1),9 2,

10 LFNoise1.kr(3)11 ))12 }).send(s);13 )

15 x = Synth(\grainBuf, [\sndbuf, b])

L’esempio precedente è una semplificazione di quello proposto nel filedi help della UGen GrainBuf. Nel buffer b viene memorizzato il segnale"sounds/a11wlk01-44_1.aiff", sul quale GrainBuf opera. La sintassi diGrainBuf è la seguente:

GrainBuf.ar(numChannels, trigger, dur, sndbuf, rate, pos, interp,

pan, envbufnum, mul, add)

Il primo argomento specifica evidentemente il numero dei canali. Il se-condo il segnale di triggering che innesca un nuovo grano: nell’esempio

code/sintesi/grainBuf.sc

8.5–263

è una sequenza di impulsi con frequenza di 10Hz. Si provi a sostituirein Impulse.kr(10) il valore 10 con GUI.mouseX(1, 300) per verificarel’effetto della densità dei grani28. L’argomento dur determinata la dura-ta di ogni grano (costante a 0.1), il successivo il buffer (nello specifico,sarà b). Gli argomenti rate, pos, interp, pan, envbufnum controlla-no rispettivamente il tasso di lettura del campione (come in PlayBuf), laposizione di lettura del buffer (normalizzata tra [0.0, 1.0], il metodo diinterpolazione per il pitchshifting dei grani (qui, 2, ad indicare interpo-lazione lineare), la posizione sul fronte stereo (in caso di segnali stereo,come in Pan2), un buffer (opzionale) in cui è contenuto un inviluppoda utilizzare per inviluppare i grani. Alcuni argomenti sono controlli dageneratori LowFrequency. Alcune parole solo su ciò che non si è visto. Ilmetodo range permette di scalare un segnale all’interno dell’intervallospecificato dai due valori passati: è decisamente più intelligibile del la-voro (istruttivo. . .) con mul eadd29 La UGen LFNoise2 produce un ru-more a bassa frequenza con interpolazione quadratica tra un valore e ilsuccessivo, a differenza che nel caso di LFNoise1 in cui l’interpolazioneè lineare. Il risultato è un segnale molto “arrotondato”. Si provi ad ascol-tare e a guardare i seguenti segnali:

1 {LFNoise0.ar(200)}.scope

3 {LFNoise1.ar(200)}.scope

5 {LFNoise2.ar(200)}.scope

Senza dimenticare JMouseBase.makeGUI ; in caso si usi SwingOSC.28

Il metodo assume che il segnale sia nell’escursione normalizzata, quindi non funziona29

se gli argomenti mul e add vengono specificati.

8.5–264

8.5 Sintesisottrattiva

Nella sintesi sottrattiva il segnale in input è generalmente un segnalecomplesso, dotato di uno spettro molto ricco. Questo segnale subisce unprocesso di filtraggio in cui si attenuano le frequenze indesiderate, enfa-tizzando soltanto alcune regioni (più o meno estese) dello spettro. Più ingenerale, un filtro è un dispositivo che altera lo spettro di un segnale inentrata. Un filtro agisce cioè enfatizzando o attenuando determinate fre-quenze del segnale: una modifica dello spettro determina a livello per-cettivo un cambiamento nel timbro del suono. I parametri fondamentalidi un filtro sono: il tipo, la frequenza di taglio/centrale, l’ordine. Si rico-noscono usualmente quattro tipi di filtri: passa-basso (lowpass), passa-alto (highpass), passa-banda (bandpass) e elimina-banda (bandreject, onotch). I quattro tipi sono schematizzati in Figura 8.10.

Fig. 8.10 Tipi di filtro. L’area grigia rappresenta l’intervallo di fre-quenze che il filtro lascia passare.

8.5–265

In un filtro passa-basso o passa-alto ideali, data una frequenza di taglio,tutte le frequenze rispettivamente superiori o inferiori a questa dovreb-bero essere attenuate a 0. Allo stesso modo, in un filtro passa-banda oelimina-banda ideali, data una banda di frequenze, tutte le frequenzerispettivamente esterne o interne alla banda dovrebbero essere attenua-te a 0. La frequenza di taglio è perciò quella frequenza a partire dallaquale viene effettuato il filtraggio. Nei filtri passa- o elimina-banda sidefiniscono sia la larghezza di banda (bandwidth) che la frequenza cen-trale: data una regione dello spettro, la prima ne misura la larghezza, laseconda la frequenza al centro. Ad esempio, in un filtro che passa tut-te le frequenze nell’intervallo [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 filtro passabasso (acronimo di “Low Pass Filter”), efreq indica la frequenza di taglio. Poiché la sorgente è un rumore bian-co (si noti il patching), il risultato visibile dovrebbe assomigliare allaFigura 8.10, passabasso. In realtà, l’attenuazione è sempre progressiva,e proprio la ripidità della curva è un indicatore della bontà del filtro(intuibilmente, più l’inviluppo spettrale è ripido dopo la frequenza ditaglio, meglio è). Poiché i filtri che rispondano ai requisiti dell’idealitànon esistono, si considera come frequenza di taglio quella a cui il filtroattenua di 3 dB il livello d’ampiezza massimo. Se perciò il passaggio trala regione inalterata e quella attenuata dal filtro è graduale, un ultimoparametro diventa rilevante: la pendenza della curva. Quest’ultima, mi-surata in dB per ottava, definisce l’ordine del filtro. Ad esempio, un filtrodel I ordine presenta una attenuazione di 6 dB per ottava, uno del II di12 dB, del III di 18 dB e così via.

8.5–266

Se si pensa al caso discusso del rumore bianco, si nota come una par-te dell’energia (quella relativa alle frequenze “tagliate”) sia attenuata(idealmente a zero). Questa diminuzione riduzione dell’energia com-plessiva di un segnale risulta, nel dominio del tempo, in una modificadell’ampiezza del segnale, con una conseguente diminuzione del “vo-lume” del suono risultante. Tipicamente, più il filtraggio è consisten-te, maggiore è il decremento dell’energia. Ragion per cui spesso è ne-cessario incrementare in uscita l’ampiezza del segnale, compensandol’energia persa (un processo tipicamente detto balancing).Un altro parametro di rilievo in un filtro passabanda è il cosiddetto Q.Il Q intuitivamente, rappresenta il grado di risonanza di un filtro. Piùprecisamente:

Q = fcentralelarghezzaBanda

Q è cioè il rapporto tra la frequenza centrale e la larghezza di banda.Esprimere il funzionamento del filtro attraverso Q permette di tene-re in conto il problema della percezione della frequenza. Mantenere Qcostante lungo tutto lo spettro vuol dire infatti adeguare la larghezzadi banda all’altezza percepita (all’intervallo musicale). Ad esempio: sefcentrale = 105; facuta = 110Hz; fgrave = 100Hz alloraQ =

105110−100 = 10, 5. Se manteniamo Q costante, e incrementiamo la fre-quenza centrale del nostro filtro a 10.500Hz, otteniamo come estremi11.000Hz e 10.000Hz. La larghezza di banda è passata da 10Hz a1000Hz, conformente con la nostra percezione dell’altezza. Dunque,Q ∝ risonanza, perché se Q è elevato, la banda è (percettivamen-te) stretta e diventa progressivamente percepibile un’altezza precisa. Ilparametro Q indica allora la “selettività” del filtro, la sua risonanza.Nella circostanza, usuale nella sintesi sottrattiva, in cui si impieghinopiù filtri, il filtraggio può avvenire in parallelo o in serie: nel primo casopiù filtri operano sullo stesso segnale simultaneamente (analogamentea quanto avveniva per il banco di oscillatori); nel secondo caso, i filtri

8.5–267

sono invece collegati in cascata, e il segnale in entrata passa attraversopiù filtri successivi. Seppur in termini non del tutto esatti, la sintesi sot-trattiva può essere pensata come un procedimento simmetrico a quellaadditiva: se in quest’ultima si parte generalmente da forme d’onda sem-plici (sinusoidi) per ottenere segnali complessi, nella sintesi sottrattivasi parte invece da un segnale particolarmente ricco per arrivare a unsegnale spettralmente meno denso. Come sorgenti possono essere uti-lizzati 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 enere-gia lungo tutto lo spettro e ben si prestano ad essere punti di partenzaper una tecnica sottrattiva. In Figura 8.11 la larghezza di banda di quat-tro filtri (con frequenze centrali 400, 800, 1200, 1600 Hz) che operano inparallelo su un rumore bianco viene diminuita da 1000, a 10, fino a 1 Hz,trasformando il rumore in un suono armonico.

Time (s)0 10

0

5000

Fre

quen

cy (

Hz)

Fig. 8.11 Filtraggio: da rumorebianco a spettro armonico

Per questioni di efficienza computazionale, le UGen specializzate nelfiltraggio 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 filtraggio (maggiore la risonanza). L’esempio seguen-te implementa la stessa situazione di filtraggio armonico in SC: a è un

8.5–268

array che contiene 10 valori da utilizzare come frequenze centrali per ilfiltro passabanda BPF. Poiché l’array viene passato come argomento aBPF ne consegue una espansione multicanale: ne risultano 10 segnali. Diessi, soltanto due sono udibili (nel caso la scheda audio sia stereo), quelliche risultano dal filtraggio a 100 e 200Hz.Attraverso scope si possonoosservare tutti i segnali e notare come il rumore bianco in entrata facciarisuonare ogni filtro intorno alla sua frequenza centrale. L’esempio suc-cessivo è uguale, ma invia i segnali ad un oggetto Mix: il segnale com-plessivo, che risulta dalla somma dei dieci filtraggi sul rumore biancole cui frequenze sono armoniche di 100Hz è udibile, e visibile comeforma d’onda e spettro (scope e freqscope).

8.5–269

1 // Filtering results in a harmonic spectrum2 (3 var sound = {4 var rq;5 i = 10; rq = 0.01; f = 100; // rq = reciprocal of Q –> bw/cutoff6 w = WhiteNoise.ar; // source7 a = Array.series(i, f, f); // array: [ 100, 200, 300, 400, 500,

600, 700, 800, 900, 1000 ]8 m = BPF.ar(w, a, rq, i*0.5); // mul is scaled for i9 m;

10 } ;11 sound.scope(10) ; // see 10 channels of audio, listen to first

212 )

14 (15 var sound = {16 var rq;17 i = 10; rq = 0.01;f = 100; // rq = reciprocal of Q –> bw/cutoff18 w = WhiteNoise.ar; // source19 a = Array.series(i, f, f); // array: [ 100, 200, 300, 400, 500,

600, 700, 800, 900, 1000 ]20 n = BPF.ar(w, a, rq, i*0.5); // mul is scaled for i21 m = Mix.ar(n); // mixDown22 m;23 } ;24 sound.scope ; // mixdown: waveform25 sound.freqscope ; // mixdown: spectrum26 )

Un aspetto particolarmente interessante della sintesi sottrattiva sta nellasomiglianza che presenta con il funzionamento di molti strumenti acu-stici, nei quali una fonte di eccitazione subisce più filtraggi successivi.Ad esempio, la cassa armonica di una chitarra si comporta come un ri-sonatore: vale a dire, opera un filtraggio selettivo attenuando certe fre-quenze ed enfatizzandone altre. Un discorso analogo vale per la voce

code/sintesi/filtri.sc

8.6–270

umana, nella quale l’eccitazione glottidale passa attraverso il filtraggiosuccessivo di tutti i componenti del cavo orale: proprio per questo lasintesi sottrattiva è il metodo alla base della tecnica standard di simu-lazione artificiale della voce, la cosiddetta codifica a predizione lineare(Linear Predictive Coding). Nell’esempio successivo, è possibile confron-tare alcune sorgenti e verificare il risultato del filtraggio già discussoselezionando quella che interessa.

1 // Here we change sources

3 // sources4 { w = Pulse.ar(100, 0.1, mul: 0.1) }.play5 { w = Dust2.ar(100, mul: 1) }.play6 { w = LFNoise0.ar(100, 0.1, mul: 1) }.play7 { w = PinkNoise.ar(mul: 0.1) }.play8 { w = WhiteNoise.ar(mul: 0.1) }.play

10 (11 {12 i = 10; q = 0.01;f = 100;13 //w = Pulse.ar(100, 0.1, mul: 0.1);14 //w = Dust2.ar(100, mul: 1);15 //w = LFNoise0.ar(100, 0.1, mul: 1);16 //w = PinkNoise.ar;17 w = WhiteNoise.ar;18 a = Array.series(i, f, f);19 n = BPF.ar(w, a, q, i);20 m = Mix.ar(n)*0.2;21 z = n.add(w).add(m);22 m;23 }.scope // try .plot too24 )

code/sintesi/sources.sc

8.6–271

8.6 Analisierisintesi

Come si è visto, il momento fondamentale sia nella sintesi additiva chenella sintesi sottrattiva è il controllo dei parametri che determinano ri-spettivamente l’inviluppo delle componenti armoniche e le caratteristi-che dei filtri impiegati. Nelle tecniche di sintesi basate su analisi e risin-tesi sono proprio i dati di controllo che vengono derivati dall’analisi diun segnale preesistente ad essere utilizzati nella fase di sintesi. Il pro-cesso viene usualmente scomposto in tre fasi:

1. creazione di una struttura dati contenente i dati ricavati dall’analisi;2. modifica dei dati d’analisi;3. risintesi a partire dai dati d’analisi modificati

Se questa è l’architettura generale, molte sono comunque le possibilitàdi realizzazione. Il caso del Phase Vocoder (PV) è particolarmente in-teressante e permette di introdurne l’implementazione in SC. Nel PV,il segnale viene tipicamente analizzato attraverso una STFT (Short Ti-me Fourier Transform). In un analisi STFT il segnale viene suddivisoin frame (finestre, esattamente nell’accezione di prima in relazione allasintesi granulare), ognuno dei quali passa attraverso un banco di filtripassabanda equispaziati in parallelo tra 0 e sr (la frequenza di campio-namento). Il risultato complessivo dell’analisi di tutti i frame è, per ognifiltro, l’andamento di una componente sinusoidale di frequenza pari allafrequenza centrale del filtro stesso. La struttura di un frame può esseredescritta come segue:

FRAME 1 2 3 . . .bin: 1 freqval 1 freqval 2 freqval 3 . . .

8.6–272

ampval 1 ampval 2 ampval 3 . . .bin: 2 freqval 1 freqval 2 freqval 3 . . .

ampval 1 ampval 2 ampval 3 . . .. . . . . . . . . . . . . . .

bin: 512 freqval 1 freqval 2 freqval 3 . . .ampval 1 ampval 2 ampval 3 . . .

In sostanza:

1. si scompone ogni frame del segnale originale in un insieme di com-ponenti di cui si determinano i valori di ampiezza e frequenza;

2. da queste componenti si costruiscono gli inviluppi relativi ad am-piezza e frequenza per tutti i segnali sinusoidali:  l’ampiezza, la fasee la frequenza istantanee di ogni componente sinusoidale sono cal-colate interpolando i valori dei frame successivi

Gli inviluppi superano perciò i limiti del singolo frame. I due inviluppicosì ottenuti possono essere impiegati per controllare un banco d’oscillatoriche riproducono per sintesi additiva il segnale originale. Se il file d’analisinon viene modificato, la sintesi basata su FFT riproduce in maniera teo-ricamente identica il segnale originale, anche se in realtà si verifica nelprocesso una certa perdita di dati. Il PV sfrutta l’aspetto più interessantedell’analisi FFT: la dissociazione tra tempo e frequenza. È così possibi-le modificare uno dei due parametri senza per questo alterare l’altro.Più in generale è possibile modificare autonomamente i parametri dicontrollo di tutte le componenti. Un’altra possibilità di grande rilievo èl’estrazione degli inviluppi di soltanto alcune delle componenti.Essendo il passo di analisi necessariamente antecedente alla sintesi (ecostoso computazionalmente), le implementazioni classiche prevedo-no la scrittura su file dei dati d’analisi e la successiva importazion edelaborazioni dei dati per la risintesi. L’utilizzo in tempo reale richiedenell’implementazione in SC di allocare un buffer in cui vengono scritti

8.6–273

progressivamente i dati d’analisi. In particolare, la dimensione del buf-fer (che deve essere una potenza di 2) corrisponde alla dimensione dellafinestra d’analisi (il frame). In sostanza, ogni finestra prelevata sul se-gnale viene memorizzata nel buffer: ogni nuova finestra sostituisce laprecedente. I dati memorizzati nel buffer sono il risultato della conver-sione dal dominio del tempo al dominio della frequenza effettuata sullafinestra (detto un po’ approssimartivamente, lo spettro istantaneo dellafinestra, che conta come un’unica unità di tempo). Questa operazione èsvolta dalla UGen FFT, che effettua appunto una Fast Fourier Transformsulla finestra. I dati conservati nel buffer possono essere a quel puntomanipolati opportunamemte attraverso un insieme di UGen estrema-mente potenti, il cui prefisso è PV_ (Phase Vocoder). Tipicamente la ma-nipolazione lavora sullo stesso buffer, sostituendo i dati precedenti. Ma-nipolazti o meno, il buffer contiene comunque ancora dati che rappre-sentano il segnale (la finestra prelevata) nel dominio della frequenza. Peressere inviati in uscita, i dati devono perciò essere convertiti dal domi-nio della frequenza a quello del tempo (dallo spettro alla forma d’onda):l’operazione è svolta dalla UGen IFFT, ovvero Inverse Fast Fourier Tran-sform. In sostanza, l’intero processo prende la forma seguente:

segnale in entrata→ FFT→ PV_. . .→ IFFT→ segnale in uscita

L’elemento PV_ è in teoria opzionale, nel senso che i dati possono es-sere risintetizzati senza che siano stati rielaborati. Evidentemente unasimile situazione non è particolarmente utile, ma permette di spiegareil processo:

8.6–274

1 a = Buffer.alloc(s, 2048, 1) ;2 b = Buffer.read(s,"sounds/a11wlk01.wav") ;

4 (5 SynthDef("noOperation", { arg fftBuf, soundBuf ;6 var in, chain;7 in = PlayBuf.ar(1, a.bufnum, loop:1) ;8 chain = FFT(bufnum, in) ; // time –> freq9 Out.ar(out,

10 IFFT(chain) // freq –> time11 );12 }).play(s,[\fftBuf, a, \soundBuf, b]) ;13 )

Nell’esempio, il segnale in viene trasformato nel dominio della frequen-za dalla UGen FFT (si noti il nome tipico assegnato alla variabile, chain,ad indicare che si tratta di un concatenamento di finestre in successio-ne). I dati d’analisi vengono memorizzati nel buffer a (che è passatoall’argomento fftBuf), la cui dimensione è una potenza di 2, 2048 =211. Quindi in uscita a Out viene semplicemente inviato il segnale risul-tante dalla trasformazione al contrario realizzata da IFFT. In un mondoideale, non ci sarebbe perdita di dati tra analisi e risintesi ed il segnale inuscita sarebbe una ricostruzione perfetta di quello in entrata: nel mondoreale si assite invece ad alcune trasformazioni del segnale indotte dallacatena di elaborazioni, tipicamente ridotte, ma potenzialmente impor-tanti, soprattutto nel caso di segnali molto rumorosi.L’insieme delle UGen PV_ è molto esteso, e molto potente. Qui di se-guito verranno soltanto presentate un paio di applicazioni. Si consideril’esempio seguente: la synthDef realizza un noise gate, cioè lascia passa-re il segnale al di sopra di una cert ampiezza selezionabile con il mouse.

code/sintesi/FFTnoOp.sc

8.6–275

1 c = Buffer.read(s, "sounds/a11wlk01-44_1.aiff") ;

3 SynthDef("noiseGate", { arg soundBufnum ;4 var sig;5 sig = PlayBuf.ar(1, soundBufnum, loop:1);6 sig = sig.abs.thresh(GUI.mouseX.kr(0,1)) * sig.sign;7 Out.ar(0, sig);8 }).play(s, [\soundBufnum, c.bufnum]) ;

10 JMouseBase.makeGUI ;

12 // see what's going on13 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 cam-pioni la cui ampiezza è superiore alla soglia (gestita da mouseX), mentrerestituisce il valore 0.0 se il campione è al di sotto della soglia (cioè: az-zera il segnale al di sotto di una certa soglia). In questo modo, tutta laparte di segnale inferiore ad un certo valore di soglia viene eliminata.Tipicamente ciò permette di eliminare i rumori di fondo, che si presen-tano come segnali con ampiezza ridotta e costante. Tuttavia, il segnale,che ora è unipolare, suono più o meno all’ottava superiore a causa delribaltamento. Il metodo sign restituisce −1 se il campione ha valorenegativo e 1 se positivo. Moltiplicando ogni campione per il segno delcampione originale la parte di segnale negativa ribaltata ritorna ad ave-re segno negativo. Vale la pena di osservare come, secondo prassi usualein SC, lo stesso risultato può essere ottenuto attraverso differenti imple-mentazioni30. Ad esempio:

Un suggerimento per la prima implementazione proviene da Nathaniel Virgo, la se-30

conda è stata proposta da Stephan Wittwer, entrambi attraverso la mailing list.

code/sintesi/noiseGate.sc

8.6–276

1 c = Buffer.read(s, "sounds/a11wlk01-44_1.aiff") ;

3 SynthDef("noiseGate2", { arg soundBufnum = 0;4 var pb, ir, mx;5 mx = GUI.mouseX.kr;6 pb = PlayBuf.ar(1, soundBufnum, loop: 1);7 ir = InRange.ar(pb.abs, mx, 1);8 Out.ar(0, pb * ir)9 }).play(s, [\soundBufnum, c.bufnum]);

11 JMouseBase.makeGUI ;

13 // see what's going on14 s.scope(1) ;

La UGen InRange.kr(in, lo, hi) restituisce 1 se il campione ha valoreincluso nell’intervallo [lo, hi], 0 altrimenti. Nell’esempio, lo è gestitoda mouseX, mentre hi è pari a 1. Dunque, il segnale in uscita ir è unasequenza di 0 e 1 in funzione del valore assoluto del campione: se ilvalore assoluto (abs) è superiore a low–qui assegnata alla variabile mx–,allora l’output è 1. Il segnale sarà necessariamente uguale o inferiore a1, che è il massimo nella forma normalizzata. Il segnale originale pb vie-ne moltiplicato per ir, che lo azzera se l’ampiezza è inferiore alla sogliamx (poiché lo moltiplica per 0) e lo lascia inalterato se è superiore allasoglia (poiché lo moltiplica per 1).Si noterà che, seppur efficace, il noise gate produce importanti “buchi”nel segnale. L’esempio seguente sfrutta un altro approccio e realizzaquello che si potrebbe definire un (noise) gate spettrale. La UGen PV_MagAboveopera sui dati d’analisi assegnati alla variabile chain: lascia invariati ivalori d’ampiezza dei bin la cui ampiezza è superiore ad un valore disoglia (qui controllato dal mouseX), mentre azzera quelli la cui ampiezza

code/sintesi/noiseGate2.sc

8.6–277

è inferiore. Come si nota, è possibile in questo modo eliminare comple-tamente lo sfondo rumoroso che le cui componenti si situano ad un am-piezza ridotta rispetto ad altri elemtni (la voce) in primo piano. Comesi nota, l’operazione è molto potente, ma non indolore, perché le stessecomponenti sono rimosse da tutto lo spettro (anche dalla figure in primopiano, per così dire).

1 (2 // Allocate two buffers, b for FFT, c for soundfile3 a = Buffer.alloc(s, 2048, 1);4 b = Buffer.read(s, "sounds/a11wlk01-44_1.aiff");5 "done".postln;6 )

8 // A spectral gate9 (

10 // Control with mouse spectral treshold11 SynthDef("magAbove", { arg bufnum, soundBufnum ;12 var in, chain;13 in = PlayBuf.ar(1, soundBufnum, loop: 1);14 chain = FFT(bufnum, in);15 chain = PV_MagAbove(chain, JMouseX.kr(0, 100, 0));16 Out.ar(0, 0.5 * IFFT(chain));17 }).play(s,[\out, 0, \bufnum, a.bufnum, \soundBufnum, b.bufnum]);

19 JMouseBase.makeGUI ;// for SwingOSC20 )

Un’altra applicazione che ha un suo corrispettivo nel dominio della fre-quenza è il filtraggio. Intuibilmente, se il segnale finestrato è scompo-sto in frequenza, è allora possibile eliminare i bin relativi alle frequen-ze che si vogliono filtrare (ad esempio, tutti quelli sopra una certa so-glia: filtro passa-basso, tutti quelli sotto: filtro passa-alto). Nell’esempio

code/sintesi/fft1.sc

8.6–278

seguente il mouse controlla l’argomento wipe di PV_BrickWall. Se wi-pe è pari a 0, non c’è effetto, se è < 0 la UGen lavora come un filtropassa-basso, se > 0 come un passa-alto. L’escursione possibile è com-presa nell’intervallo [−1.0, 1.0] (il che richiede di determinare empiri-camente il valore pari alla frequenza desiderata). Il filtro spettrale pren-de giustamente il nome di “brickwall” perché la ripidità in questo casoè verticale.

1 (2 // Allocate two buffers, b for FFT, c for soundfile3 a = Buffer.alloc(s, 2048, 1);4 b = Buffer.read(s, "sounds/a11wlk01-44_1.aiff");5 )

7 (8 // An FFT filter9 SynthDef("brickWall", { arg bufnum, soundBufnum ;

10 var in, chain;11 in = PlayBuf.ar(1, soundBufnum, loop: 1);12 chain = FFT(bufnum, in);13 chain = PV_BrickWall(chain, JMouseX.kr(-1.0,1.0, 0));14 // -1.0 –> 0.0: LoPass ; 0.0 –> 1.0: HiPass15 Out.ar(0, IFFT(chain)*10);16 }).play(s,[\bufnum, a.bufnum, \soundBufnum, b.bufnum]);

18 JMouseBase.makeGUI ; // for SwingOSC19 )

Una delle applicazioni più tipiche del PV consiste nell’elaborazione del-la frequenza indipendentemente dal tempo. Tra le molte UGen dispo-nibili, l’esempio seguente sfrutta PV_BinShift. PV_BinShift(buffer,

code/sintesi/fft2.sc

8.6–279

stretch, shift) permette di traslare e di scalare tutti i bin, rispetti-vamente din un fattore shift e stretch. La traslazione (shift corri-sponde ad uno spostamento nello spettro: ad esempio, dato un spet-tro di tre componenti [100, 340, 450], una traslazione +30 risulta in[130, 370, 480]. Nel codice seguente, l’argomento stretch riceve va-lore pari a 1 (nessuna trasformazione), mentre shift varia in funzio-ne di mouseX nell’intervallo [−128, 128]. Osservando lo spettro e –inparticolare– muovendo il mouse verso destra, si nota come l’intero spet-tro si sposti lungo l’asse delle frequenze.

1 (2 a = Buffer.alloc(s,2048,1);3 b = Buffer.read(s,"sounds/a11wlk01.wav");4 )

6 (7 SynthDef("fftShift", { arg bufnum, soundBufnum ;8 var in, chain;9 in = PlayBuf.ar(1, soundBufnum, loop: 1);

10 chain = FFT(bufnum, in);11 chain = PV_BinShift(chain, 1, MouseX.kr(-128, 128) );12 Out.ar(0, 0.5 * IFFT(chain).dup);13 }).play(s,[\bufnum, a, \soundBufnum, b]);

15 JMouseBase.makeGUI ; // for SwingOSC

17 s.scope ;18 )

In maniera analoga, l’argomento stretch permette di moltiplicare il va-lore dei bin: come si vede osservando la visualizzazione dello spettronell’esempio seguente (in particolare muovendo progressivamente il mou-se da sinistra a destra), dalla variazione di scalenell’intervallo [0.25, 4]

code/sintesi/fft3.sc

8.6–280

(con incremento esponenziale) consegue una progressiva espansione spet-trale.

1 (2 a = Buffer.alloc(s,2048,1);3 b = Buffer.read(s,"sounds/a11wlk01.wav");4 )

6 (7 SynthDef("fftStretch", { arg bufnum, soundBufnum ;8 var in, chain;9 in = PlayBuf.ar(1, soundBufnum, loop: 1);

10 chain = FFT(bufnum, in);11 chain = PV_BinShift(chain, MouseX.kr(0.25, 4, \exponential) );12 Out.ar(0, 0.5 * IFFT(chain).dup);13 }).play(s,[\bufnum, a, \soundBufnum, b]);

15 JMouseBase.makeGUI ; // for SwingOSC

17 s.scope ;18 )

Due note per concludere.Il metodo dup(n), definito su Object, resituisce un array che contienen copie dell’oggetto stesso. Il valore di default di n è 2. Negli esempiprecedenti, dup inviato a IFFT restituisce un array [IFFT, IFFT], chedunque richiede una espansione multicanale: essendo il valore predefi-nito di n = 2, il segnale in uscita è stereo.Infine, si sarà notato come i valori di controllo per le UGen PV non sia-no espressi in Herz, ma alcune volte in forma normalizzata (è il caso diPV_BrickWall, altre in escursioni che dipendono dall’implementazionedelle UGen stesse (si pensi a [−128, 128] nel caso di PV_BinShift).In effetti, la situazione, per cui i valori dipendono fondamentalmente

code/sintesi/fft4.sc

8.7–281

dall’implementazione,non è chiarissima e deve tipicamente essere risol-ta in maniera empirica.

8.7 Modulazione

Si ha modulazione quando un aspetto di un oscillatore (ampiezza, fre-quenza, fase) varia continuamente in relazione a un altro segnale. Il se-gnale che viene trasformato si definisce “portante” (carrier), quello checontrolla la trasformazione “modulante” (modulator). Un oscillatore èdefinito da un’ampiezza e da una frequenza fisse: nella modulazionesi sostituisce uno dei due parametri con l’output di un altro oscillatore.Tremolo e vibrato costituiscono di fatto due esempi di modulazione, ri-spettivamente dell’ampiezza e della frequenza: due segnali di controllo(modulanti) che modificano periodicamente i segnali originali (portan-ti). Le implementazioni più semplici delle modulazioni di ampiezza efrequenza sono perciò analoghe a quelle del vibrato e del tremolo. Neicasi di tremolo e vibrato però la modulazione avviene al di sotto del-la soglia di udibilità (subaudio range), il segnale modulante ha cioè unafrequenza inferiore ai 20 Hz, e la modulazione viene percepita non co-me trasformazione timbrica, ma espressiva. Se invece la frequenza dellamodulante cade all’interno del campo uditivo, il risultato della modu-lazione è un cambiamento spettrale che si manifesta percettivamentecome cambiamento qualitativo del timbro.In generale, il risultato di una modulazione è un nuovo segnale le cuicaratteristiche dipendono dalla combinazione dei due parametri fon-damentali: frequenza e ampiezza di portante e modulante. La modula-zione è una tecnica di larghissimo impiego perché è estremamente eco-nonomica in termini computazionali: contrariamente a quanto avviene

8.7–282

Time (s)0 15.343

0

5000

Fre

quen

cy (

Hz)

Time (s)0 13.7314

0

5000

Fre

quen

cy (

Hz)

Time (s)1 3

–1

1

0

Time (s)1 1.1

–1

1

0

Fig. 8.12 Incremento della frequenza di vibrato (destra) e di tremolo (sini-stra), sonogramma (alto) e forma d’onda (basso): dall’espressività al timbro.

per la sintesi additiva, che richiede la definizione di moltissimi parame-tri di controllo, attraverso la modulazione, anche impiegando soltantodue oscillatori come portante e modulante, è possibile creare spettri digrande complessità. Proseguendo nell’analogia con temolo e vibrato, lamodulazione può avvenire nell’ampiezza e nella frequenza.

8.7.1 Modulazioneadanelloed’ampiezza

8.7–283

Quando il segnale modulante (M) controlla l’ampiezza della portante(C), si possono avere due tipi di modulazione d’ampiezza, che dipen-dono dalle caratteristiche della modulante. Si ricordi che un segnale bi-polare si estende tra un massimo positivo e uno negativo, mentre unsegnale unipolare è invece compreso completamente tra valori positivi.

1 {SinOsc.ar}.scope ; // bipolar2 {SinOsc.ar(mul: 0.5, add:0.5)}.scope ; // unipolar

Un segnale audio è tipicamente bipolare (in ampiezza normalizzata oscil-la nell’intervallo [−1.0, 1.0]). Come si vede, per trasformare un segna-le bipolare in uno unipolar è sufficiente moltiplicarlo e aggiungervi unoffset (nell’esempio: [−1.0, 1.0]→ [−0.5, 0.5]→ [0.0, 1.0]). Se ilsegnale modulante è bipolare si ha una modulazione ad anello (Ring Mo-dulation, RM), se è unipolare si ha quella che viene definita per sineddo-che modulazione d’ampiezza (Amplitude Modulation, AM). Supponen-do che portante e modulate siano due sinusoidi di frequenza rispettiva-menteC eM , il segnale risultante da una modulazione ad anello ha unospettro complesso formato da due frequenze, pari aC −M eC +M ,chiamate bande laterali (side-bands). SeC = 440Hz eM = 110Hz, ilrisultato della modulazione ad anello è un segnale il cui spettro è dotatodi due componenti armoniche, 330 e 550Hz. Se invece la modulanteè unipolare, e si ha una modulazione d’ampiezza, lo spettro del segna-le risultante conserva anche la componente frequenziale della portante,oltre a somma e differenza di portante e modulante. Mantenendo tut-ti i parametri identici, ma realizzando una AM attraverso l’impiego diun segnale unipolare, si avranno perciò C −M ,C ,C +M : tre com-ponenti pari a 330, 440, 550Hz. Nel caso in cui la differenza risulti disegno negativo si ha semplicemente un’inversione di fase: il che equiva-le a dire che la frequenza in questione comunque si “ribalta” al positivo:ad esempio, un risultante di −200Hz sarà presente come frequenzadi 200Hz. L’esempio seguente mostra due possibili implementazioni,

8.7–284

del tutto identiche nei risultati, ma utili come esempio di patching. Nel-le prime implementazioni (3, 7) la modulante controlla l’argomento muldella portante. Nelle seconod, si ha moltiplicazione diretta dei segna-li.L’argomento mul definisce il valore per cui ogni campione deve esseremoltiplicato: il moltiplicatore è fornito dal valore del segnale modulan-te. La moltiplicazione di segnali fa la stessa cosa: ogni nuovo campionedella portante viene moltiplicato per un nuovo campione della modu-lante.

1 // better to select log display for frequencies2 // RM3 { SinOsc.ar(440, mul: SinOsc.ar(110))}.freqscope ;4 { SinOsc.ar(440)*SinOsc.ar(110) }.freqscope ; // the same

6 // AM7 { SinOsc.ar(440, mul: SinOsc.ar(110, mul: 0.5, add:0.5))}.freqscope ;8 { SinOsc.ar(440)*SinOsc.ar(110, mul:0.5, add:0.5) }.freqscope ;

8.7.2 Modulazioneadanellocometecnicadielabora-zione

La sintesi per modulazione d’ampiezza (AM) nasce come tecnica percostruire spettri più complessi a partire da sinusoidi. La modulazionead anello ha invece un’origine e un lungo passato analogico31 non tantocome tecnica di generazione ab nihilo ma come tecnica di elaborazione

Lo stesso nome deriva dalla configurazione “ad anello” dei diodi usata per approssi-31

mare la moltiplicazione nelle implementazioni in tecnologia analogica.

code/sintesi/rm1.sc

8.7–285

di segnali complessi32. In primo si può pensare di modulare un segnalecomplesso (ad esempio di origine strumentale) con un segnale sinusoi-dale. Un segnale complesso avrà uno spettro composto di un insieme dicomponenti “portanti”Cn:

C = C1, C2, C3 . . . , Cn

Data una sinusoideM , una modulazione ad anelloC×M risulta alloranella somma e differenza di tra ogni componente C eM . Se il segnalemodulante è unipolare, anche le componenti C saranno presenti nellospettro:

C1 −M, (C1), C1 +M ;C2 −M, (C2), C2 +M, . . . Cn −M, (Cn), Cn +MSeM è ridotto, allora Cn −M e Cn +M saranno vicini a Cn: adesempio, con uno spettro inarmonico Cn = 100, 140, 350, 470, etce M = 10, si avrà 90, 110, 130, 150, 340, 360, 460, 480, etc −M, etc + M . Sostanzialmente l’inviluppo spettrale non cambia, maraddopia di densità. Quando inveceM è elevato, allora si realizza unaespansione spettrale. Nell’esempio precedente, seM = 2000 allora lospettro risultante sarà 1900, 2100, 1860, 2140, 1650, 2350, 1730, 2470, etc−M, etc+M . La situazione è rappresentata in Figura 8.13 (tratta da Puc-kette cit.).Nell’esempio seguente un file audio è modulato da una sinusoide la cuifrequenza incrementa esponenzialmente tra 0 e 10000Hz nell’arco di30 secondi. Si noti all’inizio l’effetto di “raddopio” intorno alle frequen-ze dello spettro e la progressiva espansione spettrale, che porta, nel fina-le, all’emergenza della simmetria spettrale intorno alle frequenze dellasinusoide.

Alcuni riferimenti e figure da Miller Puckette, Theory and Techniques of Electronic Music,32

http://www-crca.ucsd.edu/~msp/techniques.htm

8.7–286

5.2. MULTIPLYING AUDIO SIGNALS 125

frequency

amplitude

(a)

(b)

(c)

Figure 5.4: Result of ring modulation of a complex signal by a pure sinusoid:(a) the original signal’s spectrum and spectral envelope; (b) modulated by arelatively low modulating frequency (1/3 of the fundamental); (c) modulatedby a higher frequency, 10/3 of the fundamental.

Multiplying by the signal of frequency β gives partials at frequencies equal to:

α1 + β,α1 − β, . . . ,αk + β,αk − β.

As before if any frequency is negative we take its absolute value.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 spectralenvelope and the component frequencies of the result transform by relativelysimple rules.

The resulting spectrum is essentially the original spectrum combined withits reflection about the vertical axis. This combined spectrum is then shifted tothe right by the modulating frequency. Finally, if any components of the shiftedspectrum are still left of the vertical axis, they are reflected about it to makepositive frequencies again.

In part (b) of the figure, the modulating frequency (the frequency of thesinusoid) is below the fundamental frequency of the complex signal. In this case

Fig. 8.13 Influenza diM sull’inviluppo spet-trale: segnale originale, addensamento edespansion (da Puckette cit.).

1 (2 b = Buffer.read(s,"sounds/a11wlk01.wav"); // buffering the file

4 { PlayBuf.ar(1, b.bufnum, BufRateScale.kr(b.bufnum), loop: 1)5 *6 SinOsc.ar(XLine.kr(1, 10000, 30))7 }.freqscope8 )

Lo stesso approccio può essere esteso ai casi in cuiM è complesso: èquanto avveniva tipicamente nell’uso analogico dell’RM, in cui spessodue segnali strumentali venivano modulati tra loro33. Nel caso allora in

Ad esempio, in alcuni importanti lavori di K.H.Stockhausen (dal vivo): Kontakte, Hym-33

nen, Mikrophonie, Prozession

code/sintesi/rm2.sc

8.7–287

cui C eM siano complessi, si ha addizione/sottrazione reciproca ditutte le componenti, così che, se C eM hanno rispettivamente i e kcomponenti spettrali, C ×M avrà i × k componenti, un risultato fi-nale evidentemente di grande complessità. L’esempio seguente utilizzalo stesso file audio predefinito in SC. Il primo segnale modulante è unacopia del medesimo file, la cui velocità di lettura è modificata attraversoun generatore di segnale esponenziale da 0.1 a 10.0 (si può ascoltarevalutando le righe 5-9). Come si vede, il segnale risultante dalla RM (ri-ghe 11-19) presenta un inviluppo spettrale molto complesso, in cui pureresta traccia percettiva dei due segnali di partenza.

1 (2 b = Buffer.read(s,"sounds/a11wlk01.wav"); // buffering the file3 )

5 ( // the modulating signal6 { PlayBuf.ar(1, b.bufnum,7 XLine.kr(0.1, 10.0, 30, doneAction:0),8 loop: 1) }.play ;9 )

11 (12 { PlayBuf.ar(1, b.bufnum, BufRateScale.kr(b.bufnum), loop: 1)13 *14 PlayBuf.ar(1, b.bufnum,15 XLine.kr(0.1, 10.0, 30, doneAction:0),16 loop: 1)17 *518 }.freqscope19 )

Nel secondo esempio, lo stesso segnale dal file audio è moltiplicato perun onda quadra la cui frequenza incrementa in 30secda 1 a 10.000Hz.

code/sintesi/rm3.sc

8.7–288

Si noti che finché la frequenza è nel registro subaudio (freq <≈ 20), ilsegnale modulante opera propriamente come un inviluppo d’ampiezza.

1 // modulating signal2 { Pulse.ar(XLine.kr(1, 10000, 30))}.play ;

4 (5 b = Buffer.read(s,"sounds/a11wlk01.wav"); // buffering the file

7 { PlayBuf.ar(1, b.bufnum, BufRateScale.kr(b.bufnum), loop: 1)8 *9 Pulse.ar(XLine.kr(1, 10000, 30))

10 }.freqscope11 )

Un altro esempio interessante di applicazione della RM si ha quando ilsegnale portante è un segnale complesso armonico con frequenza fonda-mentale C , mentre la modulante è una sinusoide con frequenzaM =C2 . Lo spettro risultante è armonico con frequenza M=C

2 , e compren-de soltanto armoniche dispari. In sostanza, si tratta dello spettro delsegnale portante abbassato di un’ottava, secondo un’elaborazione cheprende il nome in ambito pop/rock di octaver. Ad esempio, seC1−n =100, 200, 300, . . . n eM = 50, allora il segnale modulato avrà que-ste componenti: 100 − 50, 100 + 50, 200 − 50, 200 + 50, 300 −50, 300 + 50 = 50, 150, 150, 250, 250, 350, 350. Si tratta delle ar-moniche dispari (raddoppiate) di C2 . I rapporti Tra frequenze risultantisono infatti 1, 3, 5, 7, . . .. Per recuperare le armoniche pari (altrimentiil segnale è più “vuoto” rispetto all’originale), è sufficiente sommare ilsegnale originale.

code/sintesi/rm4.sc

8.7–289

1 //RM octaver2 (3 SynthDef.new("RmOctaver", {4 var in, freq ;5 in = SoundIn.ar(0) ; // audio from soundcard6 Pitch.kr(in)[0].poll ; // the retrieved freq7 Pitch.kr(in)[1].poll ; // is the in sig pitched?8 freq = Pitch.kr(in)[0] ; // the pitch freq9 Out.ar(0, SinOsc.ar(freq: freq*0.5)*in+in);

10 // RM freq/2 + source11 }).send(s) ;12 )

14 Synth.new("RmOctaver") ;

La synthDef "RmOctaver" riceve in input il segnale dall’ingresso dellascheda audio (il microfono, tipicamente), attraverso la Ugen SoundIn.Nella UGen, il primo argomento indica il bus richiesto: per convenien-za l’indice 0 rappresenta sempre il primo ingresso. La UGen Pitch in-vece è una UGen di analisi, che estrae un segnale di controllo che rap-presenta la frequenza fondamentale (pitch in inglese) del segnale ana-lizzato. Pitch fornisce in uscita un array che comprende due segnali. Ilprimo è costituito dai valori di frequenza rilevati, il secondo da un se-gnale che può avere valore binario [0, 1], ad indicare rispettivamenteassenza/presenza di frequenza fondamentale rilevabile. Le righe 6 e 7chiamano sui due segnali che compongono l’array in uscita da Pitch ilmetodo poll che stampa sullo schermo il valore del segnale. La frequen-za freq ÷2 viene utilizzata per controllare un oscillatore34 che moltipli-ca il segnale in, infine in viene sommato al segnale risultante.In modo analogo è poi possibile utilizzare una modulante con M =

Si moltiplica per 0.5 per questioni di efficienza computazionale. Le divisioni costano34

al calcolatore molto di più delle moltiplicazioni.

code/sintesi/octaver.sc

8.7–290

n× C . In questo caso si ha un incremento delle componenti superioridello spettro del segnale portante. SeM = n × C si ha un “ribatti-mento” delle armoniche. Si consideri:C = 100, 200, 300, n× 100 eM = 200(n×2). Lo spettro risultante sarà: 100−200 = 100, 100+200 = 300, 200− 200 = 0, 200 + 200 = 400, . . ..

8.7.3 Modulazionedifrequenza

Nella modulazione di frequenza (FM) è la frequenza della portante cheviene modificata dalla modulante. Si coinsiderino due oscillatori cheproducano due sinusoidi di frequenzaC eM . Alla frequenzaC dell’oscillatoreportante viene sommato l’output dell’oscillatore modulante. In questomodo la frequenzaC subisce una serie di variazioni che la fanno deviareverso l’acuto (quando l’output diM è positivo,C viene incrementata)e verso il grave (inversamente, quando l’output diM è negativo,C de-crementa). Con il termine frequenza di deviazione di picco (peak frequen-cy deviation) – o semplicemente deviazione (D) – si indica l’escursionemassima, misurata inHz, subita dalla frequenza della portante, che di-pende dall’ampiezza della modulante.La differenza fondamentale tra FM da una parte e RM e AM dall’altrasta nel fatto che con la FM non si producono solo 2 o 3 bande laterali, mauna serie teoricamente infinita data da tutte le frequenzeC±n×M . SeC = 220 eM = 110, si ha conn = 1: 330 e 110; conn = 2: 440 e 0;conn = 3: 550 e 110 (–110 invertito di fase), e così via. Valgono a pro-posito le considerazioni sugli spettri già svolte per la AM. Con la FM siapre allora la possibilità di creare spettri anche molto complessi attraver-so una tecnica di grande semplicità computazionale. Proprio per questomotivo la FM è diventata la prima tecnica di sintesi digitale di gran-dissimo successo commerciale, grazie a una serie, fortunatissima negli

8.7–291

anni ‘80, di sintetizzatori della Yamaha (soprattutto il celebre DX7). LaFM è stata usata dall’inizio ‘900 nelle telecomunicazioni (come noto, è diuso comune la locuzione “radio in modulazione di frequenza”): sul fini-re degli anni ’60, all’università di Stanford, John Chowning sperimentainizialmente con rapidissimi vibrato, implementa quindi una versionedigitale, e ne esamina a fondo le possibilità (1973).Turenas (1972) è il pri-mo brano ad usare estensivamente la FM, anche se il primo tentativo inassoluto è Sabelithe del 1966, completato nel 1988. Chowning ottiene condue oscillatori risultati timbrici pari all’uso di 50 oscillatori in sintesi ad-ditiva. 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à, del-le infinite bande teoricamente presenti, poche sono quelle significative:per determinarne il numero approssimativo è utile il cosiddetto indicedi modulazione I . La relazione che lega deviazione, frequenza modu-lante e indice di modulazione è la seguente: I = D

M . In questo sen-so, D rappresenta la “profodità” della modulazione. Il valore dato daI + 1 viene considerato un buon indicatore del numero approssimativodi bande laterali  “significative”. L’utilità della formula sta in questo: sesi considerano D e M costanti (poiché dipendono da modulante e por-tante), l’indice fornisce una misura della ricchezza in bande laterali dellospettro. Dalla relazione precedente discende che l’ampiezza della modu-lante è data dalla relazione:D = I ×M . Se I = 0, non c’è deviazio-ne, poiché l’incremento fornito dalla modulante alla portante (la devia-zione) è nullo, e il segnale portante non viene modulato. L’incrementodell’indice di modulazione indica un incremento della ricchezza dellospettro.Riassumendo: nella FM, la natura dello spettro generato (cioè, la posi-zione delle bande laterali) è determinata dalla relazione tra la portante ela modulante, mentre la ricchezza dello spettro (cioè, il numero di bandelaterali) è proporzionale all’ampiezza del segnale modulante.

8.7–292

Frequency (Hz)0 22050

Sou

nd p

ress

ure

leve

l (dB

/H

z)

20

40

60

Frequency (Hz)0 22050

Sou

nd p

ress

ure

leve

l (dB

/H

z)

20

40

60

Frequency (Hz)0 22050

Sou

nd p

ress

ure

leve

l (dB

/H

z)

20

40

60

Fig. 8.14 I = 1, 3, 7Nell’esempio seguente, il mouse controlla frequenza e ampiezza del se-gnale modulanteM , mentre C , ch è costante, è pari a 1000Hz. Posi-zionando il mouse nell’orgine (si ricordi che è l’angolo in alto a sinistra),il contributo diM è pari a 0 (l’ampiezza del segnale è nulla): dunque, siascolta soltanto la sinusoide C a 1000Hz. Spostando leggermente inbasso il mouse (incrementando l’ampiezza della modulante) e mouven-dosi verso destra si ascolta e si osserva nello spettro il contributo datodaM la cui frequenza incrementa da 0 a 1000. Come si nota aumentala distanza tra le bande laterali in relazione alla formula C ±M . Spo-stando verso il basso il mouse, aumenta invece l’energia associata allebande laterali (i picchi diventano più prominenti, lo spetto più ricco).

1 JMouseBase.makeGUI // required on SwingOSC

3 (4 { SinOsc.ar(5 1000 // C freq6 + SinOsc.ar( // modulator7 freq: GUI.mouseX.kr(0, 1200), // M freq8 mul: GUI.mouseY.kr(0, 10000) // M amp9 ),

10 mul: 0.311 )}.freqscope12 )

code/sintesi/fm.sc

8.7–293

8.7.4 C:M ratio

Oltre all’ampiezza della modulante, l’altro fattore che va tenuto in con-siderazione nello studio delle caratteristiche spettrali del segnale risul-tante da una modulazione è il rapporto tra le frequenze di portante emodulante. Tale fattore è di particolare rilievo per la modulazione difrequenza o nella modulazione ad anello di segnali complessi: in en-trambi casi, a differenza di quanto avviene nella AM o RM in cui C eM siano sinusoidali, il segnale in uscita risulta costituito da numerosecomponenti, e non soltanto da due (RM) o tre (AM). Il rapporto tra lefrequenze dei due segnali viene usualmente definito come C:M ratio (diqui in poi: cmr). Poiché le frequenze componenti lo spettro del segna-le comprendono somma e differenza di C eM , se cmr è un intero lospettro sarà armonico, comprendendo due multipli del massimo comundivisore tra C eM . Ad esempio, se C = 5000Hz eM = 2500Hz(cmr = 2 : 1), si avranno (in AM) 2500, 5000 e 7500Hz: uno spettroarmonico formato da fondamentale (2500) e dalle prime due armoni-che (i multipli 5000 e 7500Hz). Due casi interessanti in RM e in FMsi hanno quando cmr = 1 : 2 e C : Mratio = 1 : 1. Nel pri-mo caso sono presenti soltanto le armoniche dispari: con C = 1000e M = 2000, si ha in RM (−)1000, 3000, a cui si aggiungono inFM: (−)3000, 5000, (−)5000, 7000, ecc. Nel secondo caso, tutte learmoniche 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 nell’esempio precedente), al-lora le frequenze ottenute sono multipli della modulante, che diven-ta fondamentale del segnale generato. Se il denominatore è maggio-re di 1, è il massimo comun divisore tra C e M a diventare la fon-damentale del segnale risultante dalla modulazione: con C = 3000

8.7–294

e M = 2000 (cmr = 3 : 2) la nuova fondamentale è il massi-mo comun divisore, ovvero 1000Hz, lo spettro essendo composto da1000 Hz (3000 − 2000), cui si aggungono (in AM) 3000Hz (C), e5000Hz (3000 + 2000). A differenza di questo caso, la fondamentalepuò anche essere apparente. Ad esempio, se C = 5000 eM = 2000,(cmr = 5 : 2) la nuova fondamentale è sempre 1000Hz (il MCD tra5000 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 dall’orecchio, pur non essen-do presente fisicamente nel segnale, come la fondamentale di cui so-no presenti le armoniche III, V e VII. La fondamentale viene ricostruitadall’orecchio se cade nel campo di udibilità e se sono presenti un nu-mero sufficiente di armoniche. Quest’ultima considerazione vale per lamodulazione di frequenza, poiché in RM e AM semplici (dove C eMsono segnali sinusoidali) le bande laterali sono rispettivamente sem-pre solo 2 e 3. Nell’esempio precedente, sempre con C = 5000Hzma con M = 2000, si avranno (in AM) frequenze risultanti pari a3000, 5000, 7000Hz: terzo, quinto, settimo armonico di 1000Hz.La C:M ratio è perciò un’indicatore della “armonicità” dello spettro: piùè semplice la frazione (cioè minore è il prodotto C × M ), più vicinisono 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 comearmonico, ma che risulta invece meno artificiale proprio perché simulale inarmonicità presenti negli strumenti acustici.In generale, si ha che ratio pari a N : 1, 1 : N riproducono lo stessospettro. Il numero dei parziali può essere calcolato a partire dai compo-nenti 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 inarmo-niche (o “fondamentale mancante”). Ad esempio, se cmr = 2 : 5,lo spettro risultante sarà 2, 3, 7, 8, 12, 13 . . .: come si vede, manca lafondamentale (la componente 1) e molte armoniche. In più, lo spettrosi allontana all’acuto.Si consideri cmr = 5 : 7: si produce uno spettro

8.7–295

distintamente inarmonico: 2, 5, 9, 12, 16, 19, 23, 26, . . ..La synthDef seguente permette di controllare una modulazione di fre-quenza utilizando, oltre la frequenza di base freq, ovveroC , i parametriderivati dalla cmr, ovvero c, m, a. I primi due indicano numeratore edenominatore della cmr, il terzo l’ampiezza della modulante.

1 (2 SynthDef("cm", { arg f = 440, c = 1, m = 1, a = 100, amp = 0.3 ;3 Out.ar(0,4 SinOsc.ar(5 f // base freq6 + SinOsc.ar( // modulator7 freq: f * m / c, // M freq8 mul: a // M amp9 ),

10 mul: amp)11 )12 }).send(Server.local) ;13 )

L’interfaccia grafica seguente permette di controllare i parametri f, c,m, a attraverso cursori e campi d’inserimento numerico.

code/sintesi/cmDef.sc

8.7–296

1 (2 var w = GUI.window.new("C:M player", Rect(100, 100, 250, 420)).front ;3 //sliders4 var sl1 = GUI.slider.new(w, Rect(10, 10, 50, 350)) ;5 var sl2 = GUI.slider.new(w, Rect(60, 10, 50, 350)) ;6 var sl3 = GUI.slider.new(w, Rect(110, 10, 50, 350)) ;7 var sl4 = GUI.slider.new(w, Rect(160, 10, 50, 350)) ;8 // numberboxes9 var nb1 = GUI.numberBox.new(w, Rect(10, 360, 40, 20)) ;

10 var nb2 = GUI.numberBox.new(w, Rect(60, 360, 40, 20)) ;11 var nb3 = GUI.numberBox.new(w, Rect(110, 360, 40, 20)) ;12 var nb4 = GUI.numberBox.new(w, Rect(160, 360, 40, 20)) ;13 // labels14 var l1 = GUI.staticText.new(w, Rect(10, 390, 40, 20)).string_("freq")

;15 var l2 = GUI.staticText.new(w, Rect(60, 390, 40, 20)).string_("c") ;16 var l3 = GUI.staticText.new(w, Rect(110, 390, 40, 20)).string_("m") ;17 var l4 = GUI.staticText.new(w, Rect(160, 390, 40, 20)).string_("a") ;

19 /* to be continued… */

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

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

Infine, la parte di codice che segue è piuttosto lunga (come tipico con leinterfacce grafiche), ma cioò che fa è semplicemente associare ad ognielemento grafico l’azione di controllo sul synth (cmsynth.set) e aggior-nare l’elemento correlato (cambiando il valore del cursore, cambia anche

code/sintesi/cm2.sc
code/sintesi/cm2b.sc

8.7–297

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

code/sintesi/cm.sc

8.7–298

1 /* …here*/2 // base freq3 sl1.action = { arg sl ;4 var val = sl.value*freq ;5 cmsynth.set("f", val) ;6 nb1.value = val ;7 } ;

9 nb1.action = { arg nb ;10 var val = nb.value ; // 0-1000 Hz11 cmsynth.set("f", val) ;12 sl1.value = val/freq ;13 } ;

15 // C16 sl2.action = { arg sl ;17 var val = (sl.value*(num-1)).asInteger+1 ;18 cmsynth.set("c", val) ;19 nb2.value = val ;20 } ;

22 nb2.action = { arg nb ;23 var val = nb.value.asInteger ;24 cmsynth.set("c", val) ;25 sl2.value = val/num ;26 } ;

28 // M29 sl3.action = { arg sl ;30 var val = (sl.value*(num-1)).asInteger+1 ;31 cmsynth.set("m", val) ;32 nb3.value = val ;33 } ;

35 nb3.action = { arg nb ;36 var val = nb.value.asInteger ;37 cmsynth.set("m", val) ;38 sl3.value = val/num ;39 } ;

41 // a42 sl4.action = { arg sl ;43 var val = sl.value*10000 ;44 cmsynth.set("a", val) ;45 nb4.value = val ;46 } ;

48 nb4.action = { arg nb ;49 var val = nb.value ;50 cmsynth.set("a", val) ;51 sl4.value = val/10000 ;52 } ;

54 s.freqscope ; // let's see what's going on55 )

8.7–299

In conclusione, le tecniche per modulazione hanno dalla loro parte la ca-pacità di creare spettri di grande complessità unitamente ad un’estremaeconomicità in termini computazionali: si pensi alla complessità spet-trale ottenuta ad esempio in FM a partire da due sinusoidi. D’altra par-te, non è ovvia la relazione tra controllo e risultato. In più, le tecnicheper modulazione scontano una inapplicabilità analitica: è estremamen-te difficile l’estrazione funzionale dei parametri controllo dall’analisi dimateriale sonoro preesistente.

code/sintesi/cm3.sc

9.1–300

9 Suonoorganizzato:(minimal)schedulinginSC

9.1 Serverside:attraversoleUGen

Il controllo dell’informazione e dei suoi processi, attraverso il linguag-gio SC, e la sintesi del segnale, attraverso la gestione delle UGen nellesynthDef, sono due momenti fondamentali che devono essere integrati:mutuando un’espressione di Edgar Varèse, si tratta di arrivare al “suo-no organizzato”. La locuzione è interessante perché unisce il lavoro sul-la materia sonora alla sua organizzazione temporale: ne consegue unadefinizione in fondo molto generale di musica e composizione, comeun insieme di eventi sonori. In termini molto generali, lo scheduling èappunto l’assegnazione di risorse per la realizzazione di un evento inun certo momento35. Come usuale, in SC esistono potenzialmente modidiversi di realizzare lo scheduling. Una opzione, che potrebbe derivare

Ad esempio, http://en.wikipedia.org/wiki/Scheduling. Un termine italiano possi-35

bile è “programmazione”, nel senso di gestione nel tempo delle risorse, ma in generaleil termine italiano è molto meno specifico.

9.1–301

da sintetizzatori analogi, è quella di gestire il sequencing (la messa insequenza) degli eventi attraverso segnali. L’esempio seguente, pur nel-la sua banalità, dimostra un aspetto che si è già discusso: un inviluppoapplicato ad un segnale continuo lo può trasformare in un insieme dieventi discreti.

1 (2 SynthDef.new("schedEnv", {3 Out.ar(0,4 Pan2.ar( // usual panner5 InRange.ar(LFNoise2.ar(10), 0.35, 1).sign6 *7 Formant.ar(LFNoise0.kr(1, mul: 60, add: 30).midicps, mul:0.5),8 LFNoise1.kr(3, mul:1) ) // random panning9 )

10 }).send(s) ;11 )

13 Synth.new("schedEnv") ;

In questo caso, alla UGen Formant (si veda il file di help) genera un se-gnale che viene inviluppato dal segnale descritto dal codice di riga 4.Si è già visto in precedenza il funzionamento di InRange, che restitui-sce un segnale il cui valore dipende dall’inclusione o meno del valoredel campione in entrata nell’intervallo specificato (qui [0.35, 1]). Se ilvalore è incluso, InRange restituisce il valore stesso, altrimenti restitui-sce 0. Il metod sign restituisce 1 se il segnale è positivo, 0 se è pari a0. Dunque, il segnale in uscita sarà composto da 1 o 0 in funzione ri-spettivamente del superamento o meno della soglia di 0.35. Un similesegnale d’inviluppo “buca” il segnale audio tutte le volte che vale 0, difatto trasformando un segnale continuo in un insieme di eventi. Si valutiquanto segue per capire cosa succed in 10 secondi.

code/scheduling/bySig.sc

9.1–302

{InRange.ar(LFNoise2.ar(10), 0.35, 1).sign}.plot(10)

L’effetto di generazione di eventi è sottolineato nel caso di istanziazionemultipla di più synth36. L’esempio, già conosciuto, chiarisce un puntogenerale in relazione allo scheduling: il parallelismo (voci, strati, comeli voglia chiarire) è gestito da SC semplicemente istanziando più synth,esattamente come quando si valuta il codice interattivamente.

1 (2 SynthDef.new("schedEnv", {3 Out.ar(0,4 Pan2.ar( // usual panner5 InRange.ar(LFNoise2.ar(10), 0.35, 1).sign6 *7 Formant.ar(LFNoise0.kr(1, mul: 60, add: 30).midicps, mul:0.5),8 LFNoise1.kr(3, mul:1) ) // random panning9 )

10 }).send(s) ;11 )

13 (14 100.do({15 Synth.new("schedEnv") ;16 })17 )

Si è già discussa la UGen Select.kr(which, array), che implementaa livello server una funzione tipica del sequencing: a tasso di control-lo, ogni volta che calcola un nuovo valore selezione un elemento whichdell’array array. Vale la pena di riprendere la discussione. Il codice se-guente è tratto dal file di help.

Il clicking, che produce una solta di polvere di sfondo rispetto agli altri oggetti sonori36

quasi vocali, dipende da un’applicazione dell’inviluppo che non si cura delle transi-zioni. Nel singolo synth non era particolarmente evidente.

code/scheduling/bySig2.sc

9.2–303

1 (2 {3 var a, cycle;4 a = Array.fill(32, { rrand(30,80) }).postln.midicps;5 cycle = a.size * 0.5;6 Saw.ar(7 Select.kr(8 LFSaw.kr(1.0, 0.0, cycle, cycle),9 a

10 ),11 0.212 );13 }.play;14 )

L’array a contiene 32 frequenze, a caso tra le altezze 30 e 80 in notazioneMIDI (riga 4). La riga 7 specifica che il segnale in uscita sarà un onda adente di sega, la cui ampiezza sarà pari a 0.2 (riga 12). La frequenza ègestita invece da Select. All’argomento array è passato a,cioè le altez-ze prescelte proverrano dall’array precedentemente creato. La parte piùinteressante è la riga 9, che determina quale sarà l’indice dell’elementodi a da prelevare. La riga 6 ha assegnato a cycle valore pari a metà del-la dimensione di a, ovvero 16. La UGen LFSaw genera un’onda a dentedi sega, con frequenza 1 nell’esempio: in sostanza, una volta al secon-do la forma d’onda incrementa linearmente tra −1.0 e 1.0. Senonchél’argomenti mul e add valgono cycle. Dunque, il segnale è una retta cheincrementa tra 0 e 31, e il valore del campione (convertito internamentein intero) è utilizzato da Select come indice dell’elemento da seleziona-re in a37. L’utilizzo di segnali simili (ramp, “a rampa”) è tipico nei synth

Un’altra UGen che permette di generare eventi discreti in maniera analoga è Stepper.37

code/scheduling/select.sc

9.2–304

analogici38, in cui spesso lo scheduling è gestito attraverso segnali con-tinui opportunamente generati.

9.2 Serverside:leUGenDemand

Un approccio allo scheduling molto peculiare è implementato nelle UGendi tipo Demand, o “demand rate”, secondo una locuzione –come si vedrà–significativa. La UGen Demand.ar(trig, reset, [..ugens..]) operain relazione ad un trigger (trig). Ogni qualvolta un segnale di trigger èricevuto39, la UGen richiede un valore ad ognuna delle altre UGen inclu-se nell’array [..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. Nell’esempio seguente, il segnale di trigger perDemand è generato dalla UGen Impulse. Ogni impulso prevede una tran-sizione tra minimo (0) e massimo (qui 1), e dunque un trigger. Inutilediscutere 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 valo-re nell’array demand a. Quest’ultimo è riempito da Dseq (riga 4), cheproduce una sequenza di valori quale quella prevista nell’array fornitocome primo argomento ([1,3,2,7,8]), ripetuta –in questo caso– per 3volte. Ascoltando il risultato, ci si accorge come la sequenza a sia ap-punto costituita dalla ripetizione per 3 volte di un segmento: quando ilsegmento è concluso, Demand restituisce l’ultimo valore della sequenza.

L’implementazione più usuale prende il nome di “phasor”: si veda la UGen omonima38

in SC.Si ricordi che si ha un evento di triggering ogni qualvolta si verifica una transizione39

da 0 ad valore positivo.

9.2–305

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

9 }.play;10 )

Come si vede, in sostanza le UGen di tipo Demand sono generatori divalori a richiesta, e si differenziano tra loro per i pattern che possonoprodurre. Ad esempio, come si è visto Dseq genera sequenze di valo-ri costruite iterando n volte un array. Invece, una UGen come Drandriceve due argomenti: il primo è un array di valori, il secondo un un nu-mero che rappresenta il numero di valori pescati a caso nell’array for-nito. Nell’esempio, l’array è lo stesso del caso precedente, mentre freqè una sequenza di durata inf di valori prelevati pseudo-causalmentesullo stesso array a40.

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

9 }.play;10 )

Inoltre la frequenza di triggering è stata incrementata da 4 a 10.40

code/scheduling/demand1.sc

9.2–306

La potenza espressiva delle UGen di tipo Demand sta nella possibilità diinnesto ricorsivo. L’esempio seguente è del tutto analogo al primo casoqui discusso. senonché uno degli elementi dell’array su cui opera Dseqè Drand.

1 (2 {3 var freq, trig, reset, seq;4 trig = Impulse.kr(10);5 seq = Dseq(6 [42, 45, 49, 50,7 Dxrand([78, 81, 85, 86], LFNoise0.kr(4).unipolar*4)8 ], inf).midicps;9 freq = Demand.kr(trig, 0, seq);

10 RLPF.ar(Pulse.ar(freq + [0,0.7])* 0.1, 1500, 0.01);11 }.play;12 )

Ciò che avviene è che seq è una sequenza che ripete infinite volte (inf)un pattern costituito dai numeri 42, 45, 49, 50, e da un quinto ele-mento definito da Dxrand: analogamente a Drand, quest’ultima UGenpesca a caso nell’array fornito ([78, 81, 85, 86]), ma la sequenzain uscita non prevede ripetizioni dello stesso elemento. La dimensio-ne della sequenza in uscita è controllata da LFNoise0: in sostanza, variapseudo-casualmente nell’escursione [0, 4]. Nel primo caso, il contribu-to di Dxrand è nullo, nel secondo consiste in tutti e quattro i valori, inordine pseudo-casuale.

code/scheduling/demand2.sc
code/scheduling/demand3.sc

9.2–307

1 (2 SynthDef("randMelody",3 { arg base = 40, trigFreq = 10;4 var freq, trig, reset, seq;5 var structure = base+[0, 2, 3, 5, 6, 8] ;6 trig = Impulse.kr(trigFreq);7 seq = Dseq(8 structure.add(9 Dxrand(structure+12, LFNoise0.kr(6).unipolar*6))

10 , inf).midicps;11 freq = Demand.kr(trig, 0, seq);12 Out.ar(0, LPF.ar(13 Mix.fill(5, {Saw.ar(freq +0.1.rand + [0,0.7])* 0.1}), 1500));14 }).send(s);15 )

17 (18 15.do({ arg i ;19 Synth.new("randMelody",20 [\base, 20+(i*[3, 5, 7].choose), \trigFreq, 7+(i/10) ])21 })22 )

Nell’esempio precedente una synthDef costruita in maniera analoga alcaso precedente prevede come argomenti base e trigFreq: il primo rap-prsenta la frequenza di base in notazione MIDI, il secondo la frequenzadi triggering. Il ciclo successivo sovrappone dieci voci: in ognuna la fre-quenza 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.Quest’ultimo aspetto permette di realizzare un progresivo dephasing: itempi degli strati sono cioè lievemente differenti, e il sincronismo ini-ziale si disperde progressivamente.L’idea alla base delle UGen che si basano sul meccanismo Demand è

code/scheduling/demand4.sc

9.3–308

quella di fornire la possibilità di annidare all’interno delle synthDef aspet-ti tipicamente di più alto livello. In più, la possibilità dell’incassamentodi una UGen nell’altra è estremamente potente. Ma in realtà forse è con-cettualmente più lineare separare due aspetti, che lavorano tipicamentea tassi di aggironamento diverso: la sintesi (ad audio rate) e lo schedu-ling (a event rate). Non a caso, le UGen Demand sono strettamente rela-te con i cosidetti “Pattern”, strutture per il controllo compositivi di altolivello sul lato del linguaggio. In qualche modo, le UGen Demand co-stituiscono una sorta di versione sul lato server dei Pattern. Al di là deiPattern, su lato del linguaggio lo scheduling si basa fondamentalmentesulle Routine.

9.3 Langside:Routines

In effetti, il modo più usuale (e forse più sensato) per effettuare lo sche-duling degli eventi consiste nel gestirlo dal lato del linguaggio. La strut-tura di controllo fondamentale in proposito in SC è la routine. Di per se,le routine sono strutture di controllo che estendono il funzionamentodelle funzioni, e il loro uso non è limitato allo scheduling. In realtà, loscheduling è soltanto una delle applicazioni possibili delle routine, an-che se la più tipica. L’esempio seguente mostra una routine minimale.Come si vede, la routine riceve come argomento una funzione. La fun-zione contiene un ciclo che per 10 volte stampa “rallentando”, quindiassegna a time valore apri a i*0.1. L’unico 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 routiner definisce cioè una programmazione che deve essere eseguita, in cui ilmessaggio wait ricevuto da un numero indica un tempo di attesa (pa-ri al ricevente del messaggio) prima di proseguire nell’esecuzione della

9.3–309

espressione successiva. La gestione del compito nel tempo è affidata adun orologio, nella circostanza SystemClock, cioè ad una classe specializ-zata nel “tenere il tempo” alla massima precisione possible. Al’orologioè chiesto di eseguire (play) una routine (r). L’orologio interpreta gli og-getti che ricevono il messaggio waitco,q quantitaà temporali in cui so-spendere l’esecuzione della sequenza di esperessioni. Quando questa ri-prende, le espressioni successivi sono valutate il più in fretta possibile.Nella circostanza, la routine r attende un tempo time che cresce pro-gressivamente. Realizzato il ciclo, r prescivere di attendere 1 secondo edi scrivere ”fi”, poi un altro secondo e di scrivere ”n”.

1 (2 // Minimal routine3 r = Routine.new(4 { var time ;5 10.do ({ arg i ;6 "rallentando".postln ;7 time = (i*0.1).postln ;8 time.wait ;9 }) ;

10 1.wait ;11 "fi".postln ;12 1.wait ;13 "ne".postln ;14 }15 ) ;16 )

18 SystemClock.play(r)

Si noti che la routine “ricorda” il suo stato interno: se si valuta nuova-mente SystemClock.play(r) si ottiene di ritorno l’oggetto SystemClock,poiché la routine è ormai terminata.

code/scheduling/minimal.sc

9.4–310

1 SystemClock.play(r)

3 SystemClock

Per riportare lo stato interno della routine alla condizione iniziale è ne-cessario inviarle il messaggio reset.

1 r.reset

3 a Routine

A questo punto è possible rieseguire da capo la routine. Una sintassi al-ternativa è fornita dal metodo play definito direttamente per le routine,che riceve come argomento un orologio.

1 // the same:2 r.play(SystemClock)

4 rallentando5 06 [etc]

È chiaro come una routine costituisca la base per la gestione di processitemporali. Semplificando, quando si vogliono eseguire espressioni se-condo una certa progressione temporale è sufficiente avviluparle in unaroutine ed inframezzare opportunamente espressioni del tipo n.wait.

9.4 Orologi

9.4–311

Il prossimo esempio discute la generazione di una GUI che funziona dasemplice cronometro, e permette di introdurre alcuni elementi in piùnella discussione sullo scheduling. Una volta eseguito il codice, si apreuna piccola finestra che visualizza l’avanzamento del tempo a partire da0: il cronometro parte e viene interrotto nel momento in cui la finestraviene chiusa.

1 (2 var w, x = 10, y = 120, title =�"Tempus fugit" ; // GUI var3 var clockField ;4 var r, startTime = thisThread.seconds ; // scheduling

6 w = GUI.window.new(title, Rect(x, y, 200, 60)) ;7 clockField = GUI.staticText.new(w, Rect(5,5, 190, 30))8 .align_(\center)9 .stringColor_(Color(1.0, 0.0, 0.0))

10 .background_(Color(0,0,0))11 .font_(GUI.font.new("Century Gothic", 24));12 r = Routine.new({13 loop({14 clockField.string_((thisThread.seconds-startTime)15 .asInteger.asTimeString) ;16 1.wait }) // a clock refreshing once a second17 }).play(AppClock) ;18 w.front ;19 w.onClose_({ r.stop }) ;20 )

Le prime righe dichiarano le variabili. Come si vede, fondamentalmen-te il codice prevede due tipi di elementi: elementi GUI (righe 2, 3) e ele-menti che si occupano di gestire l’aggiornamento dei valori temporali. Inparticolare, la riga 4 assegna a startTime il valore di thisThread .se-conds: thisThread è un oggetto particolare, una pseudo-variabile checontiene una istanza della classe Thread, che tiene il conto di di quanto

code/scheduling/chrono.sc

9.4–312

tempo è passato dall’inizio della sessione dell’interprete. Se si valuta unpaio di volte la riga seguente si noterà come il valore restituito dal meto-do seconds incrementi di conseguenza (e corrisponda ai secondi passatida quando si è avviata l’applicazione).

1 thisThread.seconds

Dunque, startTime contiene un valore che rappresenta il tempo in cuiil codice viene valutato (il momento 0 del cronometro). Inutile dilun-garsi analiticamente sulla costruzione dell’interfaccia grafica che nonha nulla di particolare. Si noti soltanto lo stile tipico di programmazio-ne nelle GUI che prevede il concatenamento dei messaggi (righe 8-11)per impostare le diverse proprietà grafiche. Le righe 12-17 definisconoinvece la routine che si occupa ogni secondo di calcolare il nuovo valo-re del cronometro e di aggiornare il campo dell’elemento GUI. La fun-zione contenuta nella routine contiene un ciclo infinito (si ricordi cheloop è un sinonimo di inf.do) che esegue due espressioni: la primaaggiorna il campo dell’ora impostando la proprietà string di clock-Field (righe 13-14), la seconda richiede di attendere un secondo pri-ma di ripetere il ciclo (1.wait, riga 16). Il nuovo valore del tempo davisualizzare vience calcolato in tre passi. In primo luogo viene chiestoall’interprete il valore attuale del tempo passato attraverso una chiama-ta di thisThread.seconds: ad esso viene sottratto il tempo di partenzastartTime per ottenere la differenza. Quindi il risultato viene converti-to in numeri interi (qui non interessano i valori decimali): dunque nelnumero intero di secondi pasasati da quando il cronometro è esistito.Infine, il metodo, definito per la classe SimpleNumber, asTimeString re-stituisce una stringa nella forma ore:minuti:secondi. Ad esempio:

9.4–313

1 20345.asTimeString

3 5:39:05

Il metodo play riceve come argomento un oggetto di tipo Clock, ma nonè SystemClock: infatti, quest’ultimo non può essere utilizzato nel casodi GUI. AL suo posto deve essere utilizzato AppClock. La soluzione èobbligata perché dipende dalla gestione delle priorità: nel caso di sche-duling, i messaggi al server audio hanno la priorità su altre funzioanlità,GUI inclusa. Questo non permetterebbe di gestire da una stessa routi-ne il controllo di un synth e di un elemento GUI: una simile operazioneè possibile utilizzando all’interno della routine il metodo defer imple-mentato dalle funzioni. In sostanza, nella routine le espressioni che con-cernono la GUI vengono raccolte all’interno di una funzione (racchiusetra parentesi graffe) a cui viene inviato il messaggio defer che permettedi differire il risultato della loro valutazione nel momento in cui vi sianorisorse computazionali disponibili (ovvero: senza sottrarne alla compu-tazione audio). Infine, la riga 19 definisce una proprietà della finestra w:onClose prevede come valore una funzione che viene eseguita nel mo-mento in cui w è chiusa. La funzione contiene r.stop: la routine r vieneciò arrestata alla chiusura della finestra. Il codice seguente è una variantedel precedente che dimostra le ultime considerazioni. L’aggiornamentodella GUI è racchiuso in una funzione a cui è inviato defer (riga 14-17):dunque, è possibile utilizzare SystemClock (riga 18). È stata poi rimossal’assegnazione della funzione onClose e un messaggio postln permet-te di stampare l’aggiornamento del tempo sulla post window. Se ora sichiude la finestra, la routine continua ad essere operante (all’infinito),come si vede dalla post window. Inoltre, la routine r chiede di impo-stare la stringa che contiene l’ora nel campo grafico: poiché questo nonesiste più (la GUI è stata chiusa), la post window segnala un errore.

9.5–314

1 (2 var w, x = 10, y = 120, title =�"Tempus fugit" ; // GUI var3 var clockField ;4 var r, startTime = thisThread.seconds ; // scheduling

6 w = GUI.window.new(title, Rect(x, y, 200, 60)) ;7 clockField = GUI.staticText.new(w, Rect(5,5, 190, 30))8 .align_(\center)9 .stringColor_(Color(1.0, 0.0, 0.0))

10 .background_(Color(0,0,0))11 .font_(GUI.font.new("Century Gothic", 24));12 r = Routine.new({13 loop({14 {15 clockField.string_((thisThread.seconds-startTime)16 .asInteger.asTimeString.postln) ;17 }.defer ;18 1.wait }) // a clock refreshing once a second19 }).play(SystemClock) ;20 w.front ;21 )

9.5 Sintetizzatori/eventi

L’approccio precedente permette a tutta evidenza di essere esteso all’audio.Se si eccettua il problema della priorità, non c’è infatti niente di peculia-re alle interfacce grafiche nella gestione dello scheduling discussa pre-cedente. La synthDef seguente si basa su una sequenza di impulsi didensità density (riga 7), che vengono filtrati da un filtro risonante pas-sabasso RLPF (righe 6-9), riverberate attraverso la UGen FreeVerb (ri-ghe 5-10), e infine distributie sul fonte streo attraverso Pan2 (righe 4-12).

code/scheduling/chrono2.sc

9.5–315

Prevede come argomenti la densità degli impulsi (density), la frequen-za di taglio del filtro (ffreq), il reciproco di Q (rq) e um moltiplicatored’ampiezza (amp).

1 (2 SynthDef("filtImp", { arg density = 30, ffreq = 200, rq = 0.1, amp =

1;3 Out.ar(0,4 Pan2.ar(5 FreeVerb.ar(6 RLPF.ar(7 Impulse.ar(density),8 ffreq,9 rq

10 ), room:111 )*amp*5,12 LFNoise1.ar(2))13 )14 }).send(s)15 )

Una volta costruito il synth (synth), lo scheduling è gestito attraversouna routine infinita (loop), che ad ogni iterazione imposta (synth.set)i valori degli argomenti del synth. La routine sfrutta abbondantementevalori pseudo-casuali: ad esempio, la densità degli impulsi che mettonoin risonanza il filtro varia nell’escursion [5, 20]. Più interessante il cal-colo di ffreq, che seleziona un numero casuale in [70, 80] e lo moltipli-ca per un fattore pari a 0.75, quindi converte da valori midi a frequen-za. Ogni valore midi prescelto sarà perciò un multiplidi 0.75 semitoni,ovvero lungo una scala di 3

8 di tono. Anche il tempo in secondi che inter-corre tra una iterazione e la successiva è generato pseudo-casualmentenell’intervallo [0.1, 1.0].

code/scheduling/sched0.sc

9.5–316

1 (2 var synth = Synth(\filtImp) ;

4 Routine.new({5 loop ({6 synth.set(7 \density, rrand(5, 20),8 \ffreq, (rrand(70, 80)*0.75).postln.midicps,9 \rq, rrand(0.001, 0.005),

10 \amp, 0.5.rand + 0.511 ) ;12 rrand(0.1, 1).wait ;13 }) ;14 }15 ).play(SystemClock)16 )

Nell’esempio precedente l’idea fondamentale è quella di costruire unsyntt (uno strumento) e controllarlo attraverso una routine. La synth-Def seguente permette di introdurre un secondo approccio. Essa preve-de una semplice sinusoide a cui è aggiunto un vibrato ed un inviluppod’ampiezza. I parametri di entrambi sono controllabili dall’esterno: a,b, c rappresentano punti dell’inviluppo, vibrato e vibratoFreq i dueparametri del vibrato.

code/scheduling/sched1.sc

9.5–317

1 (2 SynthDef("sineMe1",{ arg out = 0, freq = 440, dur = 1.0, mul = 0.5,

pan = 0,3 a, b, c,4 vibrato, vibratoFreq;

6 var env;7 env = Env.new([0, a, b, c, 0],[dur*0.05, dur*0.3,dur*0.15,dur*0.5],

'welch');8 Out.ar(out,9 Pan2.ar(

10 SinOsc.ar(11 freq: freq+SinOsc.kr(mul:vibrato, freq: vibratoFreq),12 mul:mul13 ) * EnvGen.kr(env, doneAction:2)14 ), pan)

16 }).send(s);17 )

L’inviluppo d’ampiezza è utilizzato da una UGen EnvGen, il cui argo-mento doneAction riceve un valore 2. Ciò significa che, una volta con-cluso l’inviluppo, il synth viene deallocato sul server. Questo implicache il synth non esiste più: esso si comporta perciò non tanto come unostrumento ma come un evento. Si osservi cosa avviene nello routine:

code/scheduling/microInstallationDef.sc

9.5–318

1 (2 var r = Routine.new({3 inf.do({ arg i ;4 var env, dur = 0.5, freq, end, mul, pan, vibrato, vibratoFreq ;5 a = 1.0.rand ;6 b = 0.7.rand ;7 c = 0.5.rand ;8 pan = 2.0.rand-1 ;9 freq = ([0, 2, 3, 5, 6, 8, 10, 12, 13, 15, 16, 18].choose+70).midicps;

10 // 13 pitches on a non-8ving modal fragment11 dur = rrand(0.015, 0.5);12 mul = rrand(0.05, 0.8);13 vibrato = (dur-0.015)*100;14 vibratoFreq = dur*10;15 Synth.new("sineMe1", [16 "vibrato", vibrato,17 "vibratoFreq", vibratoFreq,18 "a", a,19 "b", b,20 "c", c,21 "freq", freq,22 "dur", dur,23 "mul", mul,24 "env", env]25 ) ;26 end = 0.15.rand;27 (dur+end).wait;28 });29 });

31 SystemClock.play(r);32 )

Ad ogni iterazione del ciclo viene istanziato un synth che si dealloca nelmomento in cui l’inviluppo è concluso (dunque non è neppure neces-sario chiamare il metodo free). In questo secondo esempio, l’oggetto

code/scheduling/microInstallation.sc

9.6–319

synth non è trattato come uno strumento, cioè come un dispostivo per-sistente (una tromba, un basso, un fagotto) il cui comportamento va con-trollato in funzione del presentarsi di nuovi eventi. Piuttosto, qui il synthdiventa un evento sonoro, l’equivalente di una nota musicale.Nuovamente, la synthDef fa largo uso di valori pseudo-casuali, che of-frono alla sinusoide una qualità tipicamente “fischiata”. L’unico aspettodi rilievo è il controllo della frequenza (riga 9). L’array definisce una se-quenza 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 attraversoil messaggio reset e interrotte attraverso il messaggio stop.

1 (2 r = Routine({3 inf.do({ arg i ; i.post; ": going on".postln; 2.wait})4 }).play5 )

7 // wait a bit8 r.reset

10 // had enough11 r.stop

13 // start again14 // first reset15 r.reset16 //then17 r.play

9.6–320

Tuttavia, nel momento in cui ricevono il messaggio stop, per poter es-sere di nuovo attivate devono prima ricevere il messaggio reset che leriporta nella condizione di partenza. Questo aspetto costituisce una li-mitazione potenzialmente importante allo uso musicale. La classe Taskimplementa questo comportamento: è un processo “che può essere mes-so in pausa” (pauseable process).

1 (2 t = Task({3 inf.do({ arg i ; i.post; ": going on".postln; 2.wait})4 }) ;5 )

7 // start8 t.play

10 // pause: internal state is stored11 t.pause

13 // start from last state14 t.resume

16 // reset to 017 t.reset

19 // stop: the same as pause20 t.stop

22 // the same as resume23 t.play

Come si vede, i metodi stop/pause, e play/resume si comportano esat-tamente nello stesso modo. L’esempio seguente mette in luce le diffe-renze tra task e routine. La synthDef "bink" utilizza un filtro risonanteResonz, sollecitato da un rumore bianco. Il tutto è inviluppato da un in-viluppo percussivo e distribuito sul fronte stereo attraverso Pan2.

9.6–321

1 (2 SynthDef("bink", { arg freq, pan = 1;3 Out.ar(0,4 Pan2.ar(5 EnvGen.kr(Env.perc, doneAction:2)*6 Resonz.ar(WhiteNoise.ar, freq, 0.001)),7 LFNoise1.kr(pan)8 )9 }).send(s) ;

10 )

12 (13 var pitch ;14 var arr = Array.fill(8, { arg i ;15 Task({16 inf.do({ arg k ;17 pitch = (i*[3, 5, 7].choose+(k*0.5)+40).clip2(200) ;18 Synth(\bink, [\freq, pitch.midicps,19 \pan, pitch/50 ]) ;20 ((9-i)/8).wait ;21 })22 })23 }) ;

25 var isPlaying ;

27 arr.do({ |t| t.play}) ; // play all

29 // first time evaluate until up here

31 8.do({ arg ind ;32 isPlaying = true ;33 Routine({34 inf.do({35 if (isPlaying)36 { arr[ind].pause; isPlaying = false }37 { arr[ind].play; isPlaying = true } ;38 (Array.series(6, 0, 1/6).choose).wait39 })40 }).play

42 })

44 )

9.7–322

Alla variabile arr è assegnato un array che contien 8 task. Ognuno diessi permette di generare uno voce in cui le altezze dipendono dal con-tatore (in sostanza le voci tendono a differenziarsi per registro). Il tasksceglie un valore tra [3, 5, 7], lo moliplica per l’indice (il registro dellavoce, per così dire) e incrementa progressivamente di un quarto di to-no l’altezza. Il punto di partenza è la nota 40, e la progressione vienebloccata al valore 200. Ogni strato ha un suo tasso di sviluppo autono-mo: il più grave si muove più lentamente, il più acuto più velocemente,nell’intervallo di [0.125, 1] secondi. Si noti che si tratta di una quantiz-zazione ai trentaduesimi con tempo pari a 60. La progressione procedeinesorabilmente se si valuta il codice fino alla riga 27. Fino a qui l’utilizzodei task è in fondo accessorio: sarebbe stato perfettamente possibile usa-re routine. La riga 31 introduce un nuovo processo di scheduling. Il cicloseleziona progressivamente gli indici dei task contenuti in arr. Quindiesegue una routine che modifica lo stato del task selezionato valutandola variabile isPlaying (la quale è reinizializzata per ogni task, riga 32). Insostanza, la routine verifica se il task è in esecuzione: se così è, lo arresta(e aggiorna isPlaying), se è fermo, lo fa ripartire (e aggiorna isPlay-ing). Infine attende per una durata scelta a caso tra le frazioni multipledi un sesto di secondo (appositamente: in modo da essere asimmetricorispetto alla suddivisione binaria precedente).

9.7 Micro/macro

L’utilizzo delle routine e dei task permette di chiarire meglio l’utilità del-la classi di tipo Demand. In effetti, rispetto allo scheduling, esse non sonoalternative alle classi appena discusse. Piuttosto permettono di gestire

code/scheduling/task.sc

9.7–323

quello che potrebbe essere definito come micro-scheduling: uno sche-duling interno al synth. L’opzione è particolarmente interessante se unsimile micro-scheduling viene incassato in uno scheduling, si potrebbedire macro-, gestito da una routine o da un task. La synthDef seguen-te è una variazione di un esempio già discusso. Qui density gestisce lafrequenza di un segnale impulsivo che fa da trigger per una Demand (ri-ghe 9-10). Queste ultime ciclano su dFreq a cui è assegnata una UGenDrand, la quale, a sua volta, genera una sequenza infinita di elementiselezionati casualmente dall’array fornito. Quest’ultimo utilizza un in-sieme di intervalli a quarti di tono che vengono trasposti in funzionedell’argomento midinote. Gli stessi valori, convertiti in frequenze, con-trollano sia la frequenza del generatore di impulsi (riga 9) che la frequen-za di taglio del filtro (riga 10). In assenza della riga 13, un synth di tipofiltImp2 genera una sequenza continua: ma la presenza di EnvGen e deisui argomenti Env.perc e doneAction:2, in seguito all’applicazione diun inviluppo percussivo, libera il synth. Ne consegue un oggetto sono-ro composito, che prevede una variabilità interna, in questo caso unavariazione del profilo melodico, ma che resta un singolo oggetto.

9.7–324

1 (2 SynthDef("filtImp2", { arg density = 5, midinote = 90, rq = 0.1, amp

= 1;3 var trig = Impulse.kr(density) ;4 var dFreq = Drand([0, 2, 3.5, 5, 6, 6.5, 11.5, 12]+midinote, inf) ;5 Out.ar(0,6 Pan2.ar(7 FreeVerb.ar(8 RLPF.ar(9 Impulse.ar(Demand.kr(trig, 0, dFreq.midicps)),

10 Demand.kr(trig, 0, dFreq.midicps),11 rq12 ), room:113 ) * EnvGen.kr(Env.perc, doneAction:2) * amp,14 LFNoise1.ar(density*0.25))15 )16 }).send(s)17 )

L’esempio seguente utilizza una routine per gestire lo scheduling di og-getti filtImp2. L’esempio è anche interessante perché utilizza l’argomentocontatore i della funzione associata alla routine per definire una tra-sformazione 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 dipendonosempre da i, j, k.

code/scheduling/sched0b.sc

9.7–325

1 (2 var j, k ;

4 Routine.new({5 inf.do({ arg i;6 k = i.clip2(40) ;7 j = i.clip2(60) ;8 [i, k, j].postln ;9 Synth(\filtImp2, [

10 \density, rrand(5, 15)+(k/4),11 \midinote, rrand(40, 50)+j,12 \rq, rrand(0.001, 0.005)-(k/20000),13 \amp, 0.1.rand + (((60-j)/60)*0.5)14 ]15 ) ;16 (rrand(0.1, 0.3+(60-j*0.05))).wait ;17 }) ;18 }19 ).play(SystemClock)20 )

code/scheduling/sched2.sc

Recommended