Θέματα SuperCollider [1]

Έξοδος | Out

Οι περισσότερες γεννήτριες ήχου έχουν μία μόνο έξοδο. Αυτός είναι ο λόγος που σε όλα τα προηγούμενα παραδείγματα ο ήχος είναι μονοφωνικός και ακούγεται από το αριστερό ηχείο. Βέβαια τις περισσότερες φορές δουλεύουμε σε στερεοφωνία ή ακόμη και σε περισσότερα από δύο ηχεία. Χρειαζόμαστε έτσι ένα αντικείμενο που να μας επιτρέπει να καθορίζουμε την έξοδο του σήματος που συνθέτουμε. Αυτή είναι η δουλειά του UGen Out.

Το Out δέχεται μόνο δύο ορίσματα, arguments όπως γνωρίζεις ότι ονομάζονται. Στη θέση του δεύτερου ορίσματος τοποθετούμε το σήμα, π.χ. μία γεννήτρια όπως την LFTri, και με το πρώτο όρισμα καθορίζουμε την έξοδο στην οποία θα σταλεί.

Out.ar(έξοδος, σήμα)

Η πρώτη έξοδος της κάρτας ήχου αντιστοιχεί στον αριθμό 0, που κανονικά είναι συνδεδεμένο το αριστερό ηχείο, ενώ η δεύτερη έξοδος αντιστοιχεί στο 1 που αντιστοιχεί στο δεξί ηχείο. Αυτό ίσως φαίνεται περίεργο αρχικά αλλά θα καταλάβεις αργότερα τον λόγο που αρχίζουμε να μετράμε από το 0 τις διαθέσιμες εξόδους και εισόδους στο SuperCollider.

Στον επόμενο κώδικα, η γεννήτρια LFTri, που δημιουργεί μία τριγωνική κυματομορφή στα 200 Hz με πλάτος 0.2, έχει τοποθετηθεί μέσα σε ένα Out. Θέτοντας την τιμή 0 στο πρώτο όρισμα, το σήμα στέλνεται στην αριστερή έξοδο της κάρτας ήχου.

{ Out.ar(0, LFTri.ar(200, mul: 0.2)) }.play; // send audio out from the left speaker

Για να στείλουμε το σήμα στο δεξί ηχείο δεν έχουμε παρά να αντικαταστήσουμε στον προηγούμενο κώδικα το 0 με το 1, αλλάζοντας με αυτόν τον τρόπο την έξοδο.

{ Out.ar(1, LFTri.ar(200, mul: 0.2)) }.play; // send audio out from the right speaker

Αν η κάρτα ήχου σου διαθέτει περισσότερες από δύο εξόδους και θέλεις να στείλεις το σήμα σε μία από αυτές, τότε απλώς μπορείς να θέσεις στο Out την αντίστοιχη τιμή: η 3η έξοδος είναι διαθέσιμη στο 2, η 4η έξοδος είναι διαθέσιμη στο 3 κ.ο.κ. Αυτό μπορεί να σου φανεί χρήσιμο αν δουλεύεις με πολυκάναλα ηχοσυστήματα, κάνοντας για παράδειγμα ηχητικό σχεδιασμό σε surround 5.1.

Για να στείλεις ένα σήμα σε πολλαπλές εξόδους ταυτόχρονα πρέπει να χρησιμοποιήσεις έναν πίνακα (Array), του οποίου τα στοιχεία θα αντιστοιχούν στις εξόδους του Out. Αν λοιπόν επιθυμούμε να στείλουμε ένα σήμα ταυτόχρονα στο δεξί και το αριστερό ηχείο μπορούμε να θέσουμε στο πρώτο όρισμα του Out τον πίνακα [0, 1].

{ Out.ar([0, 1], LFTri.ar(200, mul:0.2)) }.play; // the same signal comes out from left & right

Πολυκάναλη Επέκταση | Multichannel Expansion

Όταν ως είσοδο σε μια γεννήτρια ήχου τοποθετείται ένα Array τότε δημιουργούνται αντίγραφα αυτής της γεννήτριας, που το κάθε ένα έχει και μία διαφορετική τιμή του πίνακα. Υποθέτω ότι η προηγούμενη πρόταση είναι λίγο δυσνόητη. Παρατήρησε ότι στον επόμενο κώδικα στη θέση της συχνότητας του SinOsc δεν υπάρχει ένας σταθερός αριθμός, αλλά ένας πίνακας δύο θέσεων που περιέχει τα στοιχεία 200 και 250. Αυτό έχει ως αποτέλεσμα όταν τρέξουμε την εντολή, το πρόγραμμα να δημιουργήσει όχι έναν αλλά δύο ταλαντωτές όπου ο κάθε ένας δημιουργεί μία ξεχωριστή κυματομορφή. Σε αυτήν την περίπτωση το πρώτο σήμα (στα 200Hz) βγαίνει από την 1η έξοδο της κάρτας και το δεύτερο (στα 250Hz) από τη 2η έξοδο.

{ SinOsc.ar(freq: [200, 250], mul: 0.2) }.scope;

Όταν θέλεις να ελέγξεις ξεχωριστά την ένταση των 2 σημάτων τότε μπορείς να χρησιμοποιείς έναν πίνακα και για το όρισμα mul όπως φαίνεται αμέσως μετά.

{ SinOsc.ar(freq: [200, 250], mul: [0.2, 0.4]) }.scope;

Χωροθέτηση στο Στερεοφωνικό Πεδίο | Stereo Panning

Αν είχες την ευκαιρία να δουλέψεις με κάποιο λογισμικό ηχογράφησης ή κονσόλα ήχου, τότε μπορείς να καταλάβεις εμπειρικά πώς λειτουργεί η χωροθέτηση (panning). Με αυτόν τον όρο εννοούμε την κατανομή μιας (συχνά μονοφωνικής) ηχητικής πηγής μεταξύ δύο ή περισσότερων ηχείων. Μεταβάλλοντας την ισχύ του σήματος ανάμεσα στα ηχεία μπορούμε να δημιουργήσουμε την ψευδαίσθηση ότι ο ήχος προέρχεται από μια συγκεκριμένη κατεύθυνση στη στερεοφωνική εικόνα ή κινείται στον χώρο.

Στο SuperCollider το αντικείμενο Pan2 χρησιμοποιείται για να κάνουμε στερεοφωνική χωροθέτηση (panning), δηλαδή να τοποθετήσουμε τον ήχο μας κάπου μεταξύ δύο ηχείων. Δέχεται τρία ορίσματα: *ar (in, pos: 0, level: 1).

Στη θέση του πρώτου ορίσματος (in) τοποθετείται το σήμα που θέλουμε να κάνουμε panning. Το δεύτερο όρισμα (pos) καθορίζει πώς θα κατανεμηθεί το σήμα μεταξύ των ηχείων και λαμβάνει συνεχείς τιμές μεταξύ [-1, 1]. Με την τιμή 0 το σήμα κατανέμεται ισάξια και στα δύο ηχεία, στο -1 ο ήχος βγαίνει μόνο από το αριστερό ηχείο ενώ στο +1 μόνο από το δεξί. Οι ενδιάμεσες τιμές ανάμεσα στα δύο άκρα αντανακλούν τη σχετική ένταση των ηχείων. Το Pan2 λειτουργεί με τέτοιον τρόπο ώστε η αύξηση της έντασης του ενός ηχείου να αντισταθμίζεται από την αντίστοιχη μείωση του άλλου.

{ Out.ar(0, Pan2.ar(in: SinOsc.ar(freq: 200, mul: 0.2), pos: 0)) }.play ;

Σε αυτόν τον κώδικα ένας ημιτονοειδής ταλαντωτής, SinOsc.ar(freq: 200, mul: 0.2), λειτουργεί ως το εισερχόμενο σήμα για το panner. Δίνοντας την τιμή 0 στο όρισμα pos του Pan2 επιλέγουμε να στείλουμε το σήμα και στα δύο ηχεία, στο κέντρο δηλαδή της στερεοφωνικής εικόνας. Το Pan2 μαζί με τον φωλιασμένο κώδικα γίνεται με τη σειρά του το εισερχόμενο σήμα για το Out, που επιτρέπει να καθορίσουμε την πρώτη έξοδο που θα χρησιμοποιηθεί.

Nesting: Ένθεση του Pan2 εντός του Out.

Αντιλαμβάνεσαι ότι με αυτόν τον τρόπο, φωλιάζοντας δηλαδή το ένα αντικείμενο μέσα στο άλλο, μπορούμε να κατασκευάσουμε αρκετά πολύπλοκες ηχητικές δομές. Το πρόβλημα είναι ότι όσα περισσότερα αντικείμενα συνδυάζονται, τόσο πιο δυσανάγνωστος γίνεται ο κώδικας. Ήδη με τρία αντικείμενα η τελευταία εντολή έχει αρχίσει να γίνεται δύσκολη στο μάτι. Φαντάσου πώς θα έμοιαζε αν χρησιμοποιούσαμε π.χ. 20 αντικείμενα. Don't panic, υπάρχει λύση. Για να μπορέσεις να γράψεις με ευκολία πολύπλοκο κώδικα θα πρέπει να χρησιμοποιήσεις μεταβλητές. Αυτό είναι το θέμα της επόμενης παραγράφου.

Μεταβλητές | Variables

Οι μεταβλητές είναι όμορφα πράγματα. Χρησιμοποιούνται για να αποθηκεύσουμε αντικείμενα (π.χ. μία τιμή, μία γεννήτρια ήχου, κτλ.) τα οποία μπορούν να χρησιμοποιηθούν σε διαφορετικά μέρη του προγράμματος. Φαντάσου ένα κομμάτι μνήμης του υπολογιστή στο οποίο αποθηκεύουμε κάτι, και για να μπορέσουμε να το ανακαλέσουμε του δίνουμε ένα συμβολικό όνομα. Οι μεταβλητές δηλώνονται στην αρχή κάθε συνάρτησης με το πρόθεμα var. Θυμήσου ότι τα άγκιστρα { } ορίζουν στην ουσία μία συνάρτηση. Η ανάθεση ενός αντικειμένου σε μία μεταβλητή γίνεται χρησιμοποιώντας το σύμβολο ίσον =.

Παρατήρησε τώρα τον επόμενο κώδικα.

(
var myVar1, myVar2, sum;
myVar1 = 4;
myVar2 = 7;
sum = myVar1 + myVar2;
)

Το πρόθεμα var ορίζει τρεις μεταβλητές, myVar1, myVar2 και sum. Οι λεξούλες αυτές, οι οποίες είναι αυθαίρετες και μπορείς αν θέλεις να τις αντικαταστήσεις με κάτι δικό σου, λειτουργούν ως «δοχεία» που γεμίζουν με δεδομένα στη συνέχεια. Στη myVar1 ανατίθεται η τιμή 4, στη myVar2 η τιμή 7, ενώ στη μεταβλητή sum ανατίθεται το άθροισμα των δύο. Επιλέγοντας και τρέχοντας όλες τις γραμμές μαζί, θα δεις να τυπώνεται στο post window ο αριθμός 11 που αντιστοιχεί στην τελευταία γραμμή του προγράμματος. Θυμήσου ότι μπορείς να επιλέξεις πολλές γραμμές ταυτόχρονα όταν είναι τοποθετημένες εντός παρενθέσεων κάνοντας διπλό κλικ πάνω σε κάποια από τις ακριανές παρενθέσεις.

Αν δοκιμάσεις να κάνεις πρώτα την ανάθεση των μεταβλητών και στη συνέχεια να τις ορίσεις, τότε το πρόγραμμα θα παραπονεθεί τυπώνοντας ένα μήνυμα λάθους.

// ERROR: syntax error, unexpected VAR.
(
sum = myVar1 + myVar2;
var myVar1, myVar2, sum;
myVar1 = 4;
myVar2 = 7;
)

Αυτό συμβαίνει διότι το SuperCollider εκτελεί τον κώδικα από πάνω προς τα κάτω, και αυτό που προσπαθεί να υπολογίσει πρώτα είναι η εντολή sum = myVar1 + myVar2 χωρίς όμως ακόμη να γνωρίζει τι είναι το καθένα από αυτά, αφού ορίζονται λανθασμένα μετέπειτα. Από αυτό το παράδειγμα καταλαβαίνεις ότι αυτού του είδους οι μεταβλητές εξαφανίζονται από τη μνήμη του υπολογιστή μόλις η διαδικασία που τις χρησιμοποιεί τελειώσει. Τα myVar1, myVar2 και sum είναι ενεργά τη στιγμή που τρέχουμε τον κώδικα και διαγράφονται από τη μνήμη στο τέλος. Αν δοκιμάσεις να γράψεις myVar1 σε μία νέα γραμμή κώδικα έξω από την παρένθεση και να την τρέξεις, τότε θα λάβεις το μήνυμα λάθους "ERROR: Variable 'myVar1' not defined", που σημαίνει ότι το πρόγραμμα δε γνωρίζει να υπάρχει κάποια μεταβλητή με το όνομα myVar1, παρόλο που προηγουμένως είχε χρησιμοποιηθεί.

Αν θέλεις να παίξεις με μεταβλητές που να μην έχουν αυτού του είδους τον περιορισμό και να είναι διαθέσιμες σε πιο ευρύ μέρος του προγράμματος τότε μπορείς να χρησιμοποιήσεις Environment Variables που δηλώνονται χρησιμοποιώντας την περισπωμένη ~ πριν από το όνομα. Μία τέτοια μεταβλητή δεν είναι ανάγκη να οριστεί. Η ανάθεση γίνεται απευθείας και είναι άμεσα διαθέσιμη όπως στον επόμενο κώδικα .

~koukou = 32; // this is an environment variable
// the language remembers that ~koukou is a variable holding the value 32
~koukou; 

Καλό είναι τα ονόματα που δίνεις στις μεταβλητές να αντανακλούν τη χρήση τους ώστε να είναι πιο εύκολη η ανάγνωση του κώδικα καθώς οι γραμμές εντολών πληθαίνουν. Σου προτείνω δηλαδή να μη χρησιμοποιείς ονόματα όπως το ~koukou, που δε σημαίνει τίποτα εκτός και έχεις καταφέρει να φτιάξεις έναν αλγόριθμο που μιμείται τον κούκο...

Ας δούμε πώς μπορεί να διατυπωθεί ο κώδικας που παρουσιάστηκε στην παράγραφο της χωροθέτησης, χρησιμοποιώντας απλές μεταβλητές ώστε να γίνει πιο ευέλικτος και ευανάγνωστος.

(
{
    var signal, pan; // declare variables
    signal = SinOsc.ar(freq: 200, mul: 0.2); // define what the variable signal holds
    pan = Pan2.ar(in: signal, pos: 0); // define what the variable panner holds
    Out.ar(0, pan)
}.play
)

Με τον συγκεκριμένο τρόπο παρουσίασης οι γραμμές εντολών που απαιτούνται είναι περισσότερες, αλλά μπορούμε πιο εύκολα να κατανοήσουμε τι κάνει το κάθε μέρος του προγράμματος. Παρατήρησε ότι στο τέλος της τελευταίας γραμμής Out.ar(0, pan) δεν είναι υποχρεωτικό να τοποθετήσουμε το ελληνικό ερωτηματικό ; καθώς δεν ακολουθεί άλλη εντολή. Δε θα ήταν λάθος όμως αν το είχαμε βάλει.

Υπάρχει ακόμη κάτι που θα ήθελα να σου πω σχετικά με τις μεταβλητές διότι θα το συναντήσεις συχνά στα αρχεία βοήθειας. Τα μικρά γράμματα από το a έως το z μπορούν να χρησιμοποιηθούν ως μεταβλητές1 χωρίς να χρειάζεται να οριστούν όπως και στην περίπτωση των μεταβλητών περιβάλλοντος που είδαμε.

a = 4; // assign 4 to a
b = 8; // assign 8 to b
c = (a * b); // assign (a * b) to c
c; // find out what c is

Μπορείς να μάθεις το περιεχόμενο αυτών των 26 μεταβλητών τρέχοντας την εντολή this.dump που θα τυπώσει ένα κατεβατό σαν το επόμενο.

[...]
a : Integer 4
b : Integer 8
c : Integer 32
d : nil
e : nil
f : nil
g : nil
h : nil
i : nil
j : nil
k : nil
l : nil
m : nil
n : nil
o : nil
p : nil
q : nil
r : nil
s : instance of Server (0x10dbc36c8, size=47, set=6)
t : nil
u : nil
v : nil
w : nil
x : nil
y : nil
z : nil
[...]

Βλέπεις ότι με εξαίρεση τα a, b, c, στα οποία προηγουμένως έγινε ανάθεση, όλα τα γράμματα είναι κενά (nil), πλην του s το οποίο δεσμεύεται για τον server. Χρησιμοποιώντας τη συγκεκριμένη μεταβλητή, που είναι ένα «αντίτυπο» του server, μπορούμε να καλέσουμε διάφορες χρήσιμες μεθόδους όπως αυτές που φαίνονται στη συνέχεια. Αν θέλεις να μάθεις περισσότερα ρίξε μία ματιά στο τεράστιο help-file του.

Με λίγα λόγια, όλα τα μικρά γράμματα πλην του s μπορούν να χρησιμοποιηθούν ως μεταβλητές. Το s μας δίνει εύκολη πρόσβαση στον server και επιτρέπει, καλώντας τις αντίστοιχες μεθόδους, να ελέγξουμε τη λειτουργία του. Θυμήσου ιδιαίτερα τις μεθόδους record και stopRecording που θα τις χρειαστείς όταν θελήσεις να ηχογραφήσεις καθώς και την killAll που μπορείς να καλέσεις στην περίπτωση που κάτι πάει στραβά και θελήσεις να σταματήσεις βίαια όλες τις ηχητικές διαδικασίες.

Μηνύματα Λάθους | Error Messages

Είναι συνηθισμένο όταν γράφουμε κώδικα να κάνουμε λάθη. Σε αυτήν την περίπτωση το πρόγραμμα παραπονιέται τυπώνοντας στο post-window ένα μήνυμα, που παρότι αρχικά φαίνεται ακαταλαβίστικο, μας βοηθάει να βρούμε πού ακριβώς έχει κολλήσει ο κώδικας. Με τον καιρό θα μάθεις να τα κατανοείς. Θα σου δείξω δύο αρκετά συνηθισμένα λάθη που προκύπτουν.

(
{
    var signal, pan;
    signal = SinOsc.ar(freq: 200, mul: 0.2) // <- ERROR: ; is missing       pan = Pan2.ar(in: signal , pos: 0);
    Out.ar(0, pan)
}.play
)

Αν προσπαθήσεις να τρέξεις τις προηγούμενες γραμμές δε θα ακούσεις τίποτα. Το SuperCollider θα παραπονεθεί με ένα μήνυμα λάθους σαν το επόμενο:

ERROR: syntax error, unexpected NAME, expecting '}'
  in file 'selected text'
  line 5 char 4:

    pan = Pan2.ar(in: signal, pos: 0);
   ^^^
    Out.ar(0, pan)
-----------------------------------

Διαβάζουμε ότι έχει γίνει λάθος στη σύνταξη (syntax error) και ότι το πρόγραμμα σταμάτησε να καταλαβαίνει τι πρέπει να κάνει στη γραμμή 5. Σε τέτοιες περιπτώσεις το λάθος βρίσκεται συνήθως λίγο πριν την ένδειξη. Σ' αυτόν τον κώδικα λείπει απλώς ένα ερωτηματικό στο τέλος της 4ης γραμμής.

Δες και αυτό:

(
{
    var signal, pan; 
    signal = SinOsc.ar(freq: 200, mul: 0.2); 
    pan = Pan@.ar(in: signal, pos: 0); // <- ERROR: there's no object Pan@
    Out.ar(0, pan)
}.play
)

ERROR: Class not defined.
  in file 'selected text'
  line 5 char 10:

    pan = Pan@.ar(in: signal, pos: 0);

    Out.ar(0, pan)
-----------------------------------

Εδώ το μήνυμα μας πληροφορεί ότι το πρόγραμμα δεν αναγνωρίζει μία κλάση2 στη γραμμή 5 (class not defined). Μπορείς να θυμάσαι πως οτιδήποτε αρχίζει με κεφαλαίο γράμμα στο SuperCollider είναι ένα Class, όπως π.χ. το SinOsc ή το Out. Συνήθως λοιπόν πρόκειται για τυπογραφικό λάθος σε κάποιο αντικείμενο, όπως εδώ στο Pan2 που έχει εκ παραδρομής πληκτρολογηθεί ως Pan@. Διόρθωσέ το αν θέλεις και δοκίμασε να τρέξεις τον κώδικα εκ νέου.



  1. Οι 26 μεταβλητές που αντιστοιχούν στα μικρά γράμματα από το a έως το z ονομάζονται interpreter variables. 

  2. Το SuperCollider είναι μία γλώσσα που υιοθετεί τα χαρακτηριστικά του αντικειμενοστραφούς προγραμματισμού (object-oriented programming). Όλες οι οντότητες είναι αντικείμενα (objects). Μία κλάση (class) περιγράφει τη δομή και τις ιδιότητες που είναι κοινές σε μία ομάδα αντικειμένων. Φαντάσου την κλάση σαν ένα καλούπι από το οποίο μπορούμε να φτιάξουμε αντικείμενα και η οποία καθορίζει τις μεθόδους όλων των αντικειμένων που ανήκουν σε αυτή.