Introduction au live coding sur SuperCollider (II)

Publié le : 2023-24-12

Auteur : Raphaël Maurice Forment

Introduction

Le labyrinthe SuperCollider


Si vous avez lu et suivi le premier article, tout est en place et vous devriez maintenant être prêt à jouer. L’une des qualités mais aussi l’un des défauts de SuperCollider est de ne pas vous astreindre à suivre une route particulière concernant la manière dont il vous faut approcher la création sonore lorsque vous débutez. Le logiciel est extrêmement générique et chaque utilisateur développe progressivement ses propres abstractions et une logique qui lui est propre. Il existe plusieurs dizaines de mécanismes distincts et la documentation tentaculaire du logiciel recommande souvent des approches contradictoires ou des solutions qui ne fonctionneront pas nécessairement avec votre méthode de travail. Il est donc nécessaire d’être toujours conscient des choix que vous avez opéré et ne pas hésiter à revenir en arrière si ces derniers ne vous conviennent pas. SuperCollider est un logiciel déjà ancien, et la librairie standard est si vaste qu’il est difficile de la comprendre entièrement et encore moins de la maîtriser. Il est toutefois possible de développer une certaine expertise pour utiliser SuperCollider d’une certaine manière et de construire progressivement ses connaissances en fonction des projets.


JITLib et le live coding


L’approche que nous suivons dans ce guide repose sur la librairie JITLib. Cette librairie développée par Julian Rohrhuber est devenue si populaire qu’elle est désormais intégrée par défaut à SuperCollider lors de l’installation. Les avantages offerts par cette dernière ne sont pas évidents à saisir lorsqu’on débute sur SuperCollider. Un grand nombre d’utilisateurs l’ignorent complètement dans leur travail. La raison qui nous pousse à l’utiliser est que cette dernière est prévue, par défaut, pour permettre le live coding. Même dans ce domaine déjà très spécifique, JITLib reste encore relativement générique. Le mécanisme que la librairie implémente est très simple : JITLib permet de remplacer à chaud, en temps réel, n’importe quelle donnée (audio, information) par une information de nature similaire. Voici la traduction du premier paragraphe de la documentation :


La programmation just in time (ou la programmation conversationnelle, le live coding, la programmation à la volée, la programmation interactive) est un paradigme qui inclut l’activité de programmation dans le déroulement du programme. Dans ce paradigme, le programme n’est pas considéré comme un outil qui est conçu avant d’être utilisé plus tard de manière productive. Plutôt, il s’agit de concevoir la conception dynamique des programmes comme une description et comme une conversation. Écrire du code devient une partie intégrante de l’expérience musicale ou de la pratique expérimentale.


JITLib intègre ce principe à SuperCollider de manière assez approfondie. On se retrouve rarement bloqué du fait d’une incompatibilité entre la librairie standard et JITLib. Par ailleurs, JITLib est si finement intégré qu’il est parfois délicat de savoir si telle ou telle fonctionnalité relève de cette librairie ou vient de l’installation standard de SuperCollider.


Conseils de lecture


Cette partie du guide est de loin la plus pénible. Elle introduit tout les concepts importants que nous manipulerons pour faire de la musique avec SuperCollider. Il s’agit en quelque sorte du solfège élémentaire pour un live coder utilisant SuperCollider. Je suis conscient que les premiers paragraphes sont particulièrement difficiles pour un utilisateur débutant. N’hésitez pas à continuer votre lecture si un détail vous échappe, vous finirez sans doute par comprendre ultérieurement par la pratique. Ne vous laissez pas arrêter par du vocabulaire ou des concepts peu familiers.

ProxySpace et Ndefs

Le principe de base de la librairie


Tout commence avec un peu de vocabulaire. JITLib introduit la notion de ProxySpace, un environnement de références vers des proxys : ”an environment of references on a server”. Une référence est un nom associé à un objet. C’est aussi simple que ~a = 2 ou ~a est un proxy et 2 un objet. Ce système fonctionne à l’aide de proxys, il nous faut donc comprendre ce qu’est un proxy. C’est une notion relativement simple, il s’agit d’un objet qui contient quelque chose.


Proxy: un proxy est un contenant pour quelque chose qui tourne sur le serveur, généralement un contrôle ou un algorithme audio. C’est un objet vide ou non-vide. Un proxy peut contenir le node d’un oscillateur (cf. la suite) mais il pourrait aussi contenir un filtre audio, un synthétiseur que nous venons de créer ou même un pattern algorithmique. Le proxy englobe ce qu’il contient et réalise à notre place tout un tas d’opérations qu’il est d’ordinaire nécessaire de gérer manuellement. Par exemple, on peut remplacer le contenu d’un proxy par un autre node sans interruption du signal, supprimer le contenant d’un proxy sans pour autant supprimer la boîte elle-même.


Un ProxySpace est un ensemble de clés et de valeurs, un grand sac dans lequel on manipule des proxy. Les clés sont des références vers des NodeProxy. Tout ceci est sans aucune importance tant que nous ne le manipulons pas pour générer du son.


Remplacer l’environnement par défaut par un ProxySpace


Dans le guide précédent se trouvait une ligne assez inhabituelle, celle qui nous servait pour démarrer le serveur :

p = ProxySpace.push(s.boot);

Sans entrer dans le détail, cette ligne réalise deux actions distinctes :


1) elle démarre le serveur audio de SuperCollider (s.boot)

2) elle pousse l’environnement par défaut dans un ProxySpace


Avec la méthode push, toutes les variables globales de l’environnement actuel deviennent mécaniquement des NodeProxy dans le ProxySpace. Pour s’en convaincre, il suffit de taper le nom d’une variable et d’observer la valeur de retour :


~bob; // -> NodeProxy.nil(localhost, nil)

Ce n’est pas très parlant si vous n’êtes oas habitués à SuperCollider ou à la programmation. Essayons maintenant de voir ce que cela signifie lorsque nous souhaitons manipuler du son, sans spécifiquement aborder tout les détails. Évaluez le code suivant ligne par ligne :

~osc = {SinOsc.ar(200) * 0.5}; // J'évalue, rien ne se passe. Associe une fonction audio à un NodeProxy
~osc.play;                     // On connecte l'audio à la sortie et on joue la fonction
~osc = {SinOsc.ar(400) * 0.5}; // On remplace la fonction précédente par une autre (sans interruption !)
~osc.stop(fadeTime: 4);        // On stoppe avec un joli fade-out
~osc.clear;                    // On libère la mémoire

Nous avons associé une fonction audio ({SinOsc.ar(200) * 0.5}) à une référence (~osc). C’est tout le principe de JITLib. Cette association peut être remplacée à tout moment sans interruption grâce au proxy. Le NodeProxy nommé ~osc accepte un grand nombre de manipulations différentes et son rôle est totalement redéfini par rapport au comportement habituel d’une variable en dehors de l’utilisation de JITLib.


Ce n’est plus une variable, c’est un Proxy dans notre ProxySpace. La ligne ~osc.stop(fadeTime: 4) démontre aussi que les NodeProxy intègrent de nombreuses méthodes destinées à gérer l’audio: fade in, fade out, contrôle du niveau des sources : etc.


Nous allons utiliser ce principe fondamental introduit par JITLib tout au long de ce guide pour contrôler tout ce que nous souhaitons / pouvons contrôler : algorithmes audio, patterns algorithmiques, effets sonores, etc. Il est essentiel de retenir ce principe du proxy car il nous permet de savoir réellement ce que nous sommes en train de manipuler à tout moment au cours du jeu : essentiellement des NodeProxy.


Sans en savoir beaucoup plus, il est déjà possible de faire un petit peu de musique en s’amusant à remplacer une source par un autre :

~osc = {SinOsc.ar([200, 100]) * 0.5}; // On associe une source à un NodeProxy, un double oscillateur
~osc.play(fadeTime: 4);               // On lance le NodeProxy avec un fade-in
~osc.fadeTime = 4;                    // On change le fadeTime général

~osc = {LPF.ar(SinOsc.ar([400, 100]), SinOsc.ar(1/4).range(200,2000)) * 0.5}; // On remplace la source
~osc = {LPF.ar(SinOsc.ar([800, 350]), SinOsc.ar(1/4).range(200,2000)) * 0.5}; // On remplace la source
~osc = {LPF.ar(SinOsc.ar([200, 150]), SinOsc.ar(1/4).range(200,2000)) * 0.5}; // On remplace la source

~osc.stop(fadeTime: 4);               // Fade-out
~osc.clear;                           // On libère la mémoire

Même si tout reste assez primitif pour le moment, on peut déjà faire beaucoup de choses en suivant ce principe. Il est possible d’utiliser n’importe quel algorithme audio et de le mettre à jour graduellement tout au long d’une performance. Ce type de live coding centré autour de la musique à jour d’un générateur sonore se prête plutôt bien à de la musique électro-acoustique, ambient, noise, etc.


Les Ndefs : une autre manière de faire la même chose


La technique que nous utilisons avec ProxySpace.push(s.boot) dissimule l’utilisation que nous faisons des NodeProxy. Cette fonctionnalité a été intégrée car elle permet de gagner du temps de frappe mais elle a pour désavantage de rendre plus difficilement perceptible ce que nous sommes réellement en train de faire. À première vue, il semble que ~a = 2 soit juste une assignation de variable comme dans un langage de programmation classique. Pourtant, il s’agit d’une opération qui crée/modifie un NodeProxy.


Nous occultons le fait que les variables globales de SuperCollider sont maintenant des NodeProxy. Il est possible de se passer entièrement de Proxyspace.push et de cet avantage/désagrément en utilisant les Ndefs. Il s’agit d’une préférence personnelle, presque d’ordre stylistique.


Pour être plus précis, ProxySpace.push(...) transforme le scope global en un ProxySpace. Seule les variables de a à z sont épargnées.

Le terme de NDef est un raccourci pour Node Proxy Definition. On retrouve du vocabulaire familier. C’est une autre manière de désigner exactement le même type d’objet que ce que nous manipulons depuis le début ! Seule la syntaxe diffère. Profitons-en quand même pour évoquer rapidement ce qu’est un node :


Node: un node est un objet défini en interne par le serveur audio de SuperCollider. Un synthétiseur est un node, beaucoup d’objets présents sur le serveur sont des nodes. Il s’agit d’un objet générique utilisé pour une opération audio : contrôle ou synthétiseur. C’est un objet abstrait, qu’on ne manipule jamais directement. Les fonctions audio que nous venons d’utiliser dans l’exemple précédent sont des nodes que l’on associe à un proxy. Un node tire ce nom du fait que ce sont des noeuds dans un graphe audio, des objets qui ont une position dans un graphe de traitement du signal.


Les Ndefs ont pour avantage de ne pas se propager dans l’environnement local. Elles rendent tout un petit peu plus clair. Réécrivons l’exemple précédent en utilisant uniquement des Ndefs :

Ndef(osc, {SinOsc.ar([200, 100]) * 0.5});  // On associe une source à un NodeProxy, un double oscillateur
Ndef(osc).play(fadeTime: 4);               // On lance le NodeProxy avec un fade-in
Ndef(osc).fadeTime = 4;                    // On change le fadeTime général

Ndef(osc, {LPF.ar(SinOsc.ar([400, 100]), SinOsc.ar(1/4).range(200,2000)) * 0.5}); // On remplace la source
Ndef(osc, {LPF.ar(SinOsc.ar([800, 350]), SinOsc.ar(1/4).range(200,2000)) * 0.5}); // On remplace la source
Ndef(osc, {LPF.ar(SinOsc.ar([200, 150]), SinOsc.ar(1/4).range(200,2000)) * 0.5}); // On remplace la source

Ndef(osc).stop(fadeTime: 4);               // Fade-out
Ndef(osc).clear;                           // On libère la mémoire

C’est à vous de choisir quelle est la syntaxe que vous préférez.

Gestion des NodeProxy

fadeTime

Nous avons déjà utilisé la capacité des NodeProxy à opérer des fade-ins et des fade-outs. C’est une fonctionnalité très pratique, surtout lorsque vous manipulez des sources audio dynamiques et que vous souhaitez faire des transitions souples de l’une à l’autre. Il existe trois types de fade :

  • fade-in à l’entrée : c’est un argument de la méthode .play : ~osc.play(fadeTime: 4) ou Ndef(\osc).play(fadeTime: 4)
  • fade-in en sortie : c’est un argument de la méthode .stop ou .clear : ~osc.stop(fadeTime: 4) ou Ndef(\osc).stop(fadeTime: 4)
  • fade général : c’est un attribut du NodeProxy que l’on contrôle avec la syntaxe ~osc.fadeTime = 4;
~osc = {SinOsc.ar(200) * 0.5}; // On crée une source audio

~osc.play(fadeTime: 1);        // On fait entrer doucement

~osc.fadeTime = 12;            // Transition très longue

~osc = {SinOsc.ar(2000) * 0.5}; // Transition lente vers la fréquence voulue

~osc.clear(fadeTime: 4);       // On s'arrête

.stop / .clear

Les méthodes .stop et .clear ne réalisent pas la même opération :

  • .stop : déconnecte le NodeProxy du reste de la chaîne audio. Il continue à tourner (et à consommer des ressources) mais en silence ! Vous pourrez le reconnecter plus tard.
  • .clear : détruit le NodeProxy. Vous pouvez réaliser la même opération en tapant : ~osc = nil. Notez toutefois que .clear permet de spécifier un fade-out avant la destruction.

Si vous souhaitez vous débarrasser de tout les NodeProxy actifs, il existe cette commande :

currentEnvironment.free;

Elle applique la fonction free à tout ce qui compose l’environnement global. Puisque ce dernier est un ProxySpace, on libère tout les NodeProxy.

Modifier un/des paramètres

Vous n’êtes pas obligés de réévaluer l’algorithme dans son intégralité pour modifier une valeur sur un NodeProxy. Parfois, il est préférable de mettre à jour un paramètre sans que le fade-in et le fade-out ne s’appliquent. Ce problème sera particulièrement sensible lorsque vous utiliserez des effets tels qu’un délai ou une réverbération.


Si vous changez uniquement de valeur en réévaluant l’algorithme en entier, cela causera des problèmes avec l’amplitude générale du signal. Cela peut aussi causer un effet de brouillon lié à la superposition de plusieurs algorithmes en cours de fade-in / fade-out. Deux méthodes existent pour mettre à jour un paramètre : .set (instantané) et .xset (progressif).

.set

La méthode .set met immédiatement à jour un paramètre immédiatement, dès que possible :

~osc = { arg freq=200; SinOsc.ar(freq) * 0.5};
~osc.play(fadeTime: 2);
~osc.set(req, 800);
~osc.set(req, 400);
~osc.clear(2);

.xset

La méthode .xset met immédiatement à jour un paramètre progressivement, suivant le fadeTime :

~osc = { arg freq=200; SinOsc.ar(freq) * 0.5};
~osc.play(fadeTime: 2);
~osc.fadeTime = 8; // On change le fadeTime pour .xset
~osc.xset(req, 800);
~osc.xset(req, 400);
~osc.clear(2);

Contrôler plusieurs paramètres


Il est possible de contrôler plusieurs paramètres en une seule commande si besoin est :

~osc.xset(req, 800, amp, 0.2);

Tout dépend de ce dont vous avez besoin. Réévaluer la fonction entière peut aussi être une stratégie intéressante dans certains cas.

Communication entre NodeProxies

On peut associer plusieurs NodeProxies pour former des algorithmes audio plus complexes et modulaires. Chaque NodeProxy peut être imaginé comme un module remplissant une fonction particulière dans un synthétiseur modulaire plus imposant. Pensez au patching dans un environnement comme Max/MSP ou Pure Data ou au patching analogique d’un synthétiseur physique.


Il est possible de définir un NodeProxy oscillateur puis un contrôle (de type LFO) pour moduler la fréquence de cet oscillateur. Voici la méthode la plus simple que vous puissiez employer :

~source = {arg freq=400; SinOsc.ar(freq) * 0.5}; // Une source que l'on souhaite moduler
~source.play;
~source.set(req, 300);              // On peut utiliser set pour une valeur statique
~freq = { SinOsc.ar(1/2) * 400 };   // Voici un LFO (Low Frequency Oscillator)
~source.map(\freq, ~freq);          // Utilisation de la fonction map

.map possède une fonction alternative, nommée .xmap. Elle fonctionne tout comme .set et .xset.

Conclusion

Dans cette section du guide, nous avons appris :

  • Ce qu’est un NodeProxy et un ProxySpace, l’outil de base offert par JITLib
  • La différence entre ProxySpace.push et l’utilisation explicite des NDefs
  • Comment démarrer, stopper et arrêter un NodeProxy
  • Comment contrôler le fade-in et le fade-out et la transition entre algorithmes

Je ne fais ici qu’effleurer les différentes commandes que possèdent les NodeProxy. Si vous souhaitez en apprendre plus, allez voir la documentation. Nous utiliserons un nombre limité de méthodes au fur et à mesure, lorsque nous en aurons besoin.

Revenir à l'index