Programmation à la volée

Notes de travail pour une application en chantier.
17 novembre 2018

Je suis en train de fabriquer une application Node.js qui me permettra de faire de la programmation improvisée. L’expression anglaise “live coding” est plus souvent utilisée. Wikipedia la traduit par « programmation à la volée » ou « programmation juste-à-temps ». Je ne suis pas enchanté par ces traductions mais faute de mieux, ça fera. Elle contient deux champs de texte CodeMirror, la première qui contrôle un serveur SuperCollider, et la seconde qui controle un sketch p5.js.

Pour créer cette application, j’ai utilisé une bonne quantité de code écrit pour Prynth, un logiciel créé par Ivan Franco, Harish Venkatesan, Ajin Tom et Antoine Maiorca. Prynth est distribué avec une licence Creative Commons Cc By-Nc-Sa 4.0. J’ai également repris un peu de code écrit par Olivia Jack pour son logiciel Hydra, qui est distribué avec une licence Gnu Affero Gpl v3.0.

Mon application s’appelle Les environs. Elle peut être téléchargée sur GitHub.

Fonctionnement général

Je dois avoir une machine à laquelle je peux envoyer n’importe quel sketch p5.js. Je ne dois pas utiliser une nouvelle copie de la machine à chaque nouveau sketch, ça n’aurait aucun sens. Ainsi, il me faudrait pouvoir définir un sketch comme étant un ensemble de fichiers JavaScript et SuperCollider. Je définirais ce sketch dans un fichier json.

{
    "root": "../../Graph-Sequencers/",
    "javascript-files": [
        "libraries/p5.min.js",
        "libraries/p5.dom.min.js",
        "libraries/p5.sound.min.js",
        "libraries/socket.io.js",
        "frame-export.js",
        "graph-prototype.js",
        "vertex-prototype.js",
        "edge-prototype.js",
        "walker-prototype.js",
        "song-prototype.js",
        "sketch.js"
    ],
    "default-javascript-file": "sketch.js",
    "supercollider-files": [
        "supercollider/pink-noise-synth.scd",
        "supercollider/triangle-wav-synth.scd",
        "supercollider/osc-receive.scd"
    ],
    "default-supercollider-file": "supercollider/osc-receive.scd"
}

Ensuite, il me suffirait, lorsque je lance le serveur Node.js, de lui envoyer en argument le nom du sketch à servir, comme ceci :

node server("graph-sequencers").js

Les valeurs "default-javascript-file" et "default-supercollider-file" permettront de déterminer quels fichiers textes seront chargés dans les deux zones de CodeMirror. Ces valeurs pourraient aussi être vides.

La barre de commandes

Le logiciel contiendra une barre de commande à partir de laquelle la gestion des fichiers pourra être faite, entièrement avec des commandes textuelles, qui n’interrompront jamais l’animation en cours ou la musique.

Méthodes de contrôle modulaire

Plusieurs idées intéressantes pour contrôler les flots d’informations en direct pourraient être pigées dans la boîte à outils de Max/msp. Par exemple, si j’ai une variable globale n = 16 qui est utilisée dans ma boucle draw(), je peux évidemment la changer en évaluant une ligne de code telle que n = 20;, mais il serait intéressant de pouvoir déclancher différentes fonctions qui feraient des interpolations dans le temps de la variable n, dont la valeur pourrait ainsi changer de façon progressive au lieu de changer brusquement, d’un seul coup.

J’aimerais beaucoup avoir une syntaxe du genre :

line(n, 20, 70);

Dans ce cas-ci, la fonction line() Serait-il aussi intéressant d’avoir une variante de Line() nommée XLine(), qui ferait une interpolation exponentielle ? Comment cela fonctionnerait-il ? Quels usages pourrais-je en faire ?créerait un nouvel objet de la classe Line, qui exécuterait l’interpolation. Cet objet Line prendrait la variable n et lui assignerait la valeur 20 tout au long des prochaines 70 images d’animation.

Mais ceci est, je crois, plutôt impossible à réaliser. Parce qu’à l’intérieur de la fonction line(), n n’est plus la variable globale n mais bien la valeur de cette variable au moment de l’exécution de line(). Je ne crois pas qu’il soit possible d’assigner une variable globale à une fonction de cette façon.

La solution serait peut-être de ne pas utiliser de simples variables pour contrôler le flot de l’information, mais utiliser plutôt un objet construit spécialement pour cet usage. Et cet objet aurait des méthodes particulières, tout comme l’objet p5.Vector() contient une panoplie de méthodes qui permet de changer ses valeurs. Par exemple, je pourrais avoir un prototype d’objet nommé SingleValue, qui contiendrait une propriété value à laquelle des fonctions extérieures comme draw() pourraient accéder.

// Would I need a singleValues array to check on all the instances
// of SingleValue that are currently modified by inner functions?
let singleValues = [];

let SingleValue = function(v) {
    this.value = v;
    this.currentModifiers = [];
};
SingleValue.prototype.line = function() {
    // Some code here would apply an interpolation line 
    // to this.value, changing it smoothly over time.
};

Ainsi, en contexte de programmation improvisée, je pourrais écrire et évaluer ce genre de lignes :

// Add an interpolation line, changing the value of n
// to 20 over the course of 70 animation frames.
n.addLine(20, 70);
// Add a cosine fade (a smooth fade that has the shape
// of the cosine function) to n.
n.addCosFade(20, 70);

Et ma fonction draw() pourrait alors ressembler à ceci :

// a.v is a reference to the "value" of the a object,
// which can be changed dynamically with lines,
// cosine fades, and oscillators.
p.draw = function() {
    p.background(255, 255);
    let v = p.sin(j * 0.1);
    v = p.map(v, -1, 1, 8, -8);
    for (var i = 0; i < 1200; i+= 1) {
        let x = -30 + p.cos(j) * i * a.v + (p.width * 0.5);
        let y = -30 + p.sin(j) * i * a.v + (p.height * 0.5);
        p.ellipse(x, y, 4, 4);
    }
    j += 0.005;
}

Dans cet exemple, si j était un objet de type SingleValue au lieu d’être une simple variable, j’écrirais j.add(0.005); au lieu de j += 0.005;, exactement comme les méthodes mathématiques du prototype p5.Vector. Si la valeur à incrémenter était aussi un objet SingleValue, j’aurais une ligne de code semblable à celle-ci : Cette ligne de code est malheureusement bien moins limpide et transparente que la ligne qu’elle remplace. Mais bien entendu que des structures logiques plus complexes font disparaître une part de simplicité.

j.add(increment.v);

Il serait aussi très intéressant de pouvoir créer des lignes à multiples segments comme on en trouve dans SuperCollider. Serait-il aussi intéressant de pouvoir créer des lignes qui se répéteraient une certaine quantité de fois (ou même une quantité infinie de fois) ? Rendu là, une ligne deviendrait aussi une sorte d’oscillateur, par exemple une ligne pourrait devenir un signal triangulaire ou un signal en dents de scie. Je ne sais pas. Il faudrait réfléchir à la chose et surtout, en essayer des applications. Si une ligne devenait infinie, il deviendrait intéressant de la traiter comme un oscillateur, donc de l’appliquer avec une enveloppe. On verra.

// The first array is a group of values to reach,
// the second array is the number of animation frames
// it takes to reach those values.
n.addLine([10, 20, 30, 10], [30, 60, 30]);

Une pile d’oscillateurs

Une autre façon très versatile de contrôler des flots d’informations serait de pouvoir faire osciller des valeurs. Je pourrais ainsi avoir la méthode suivante dans le prototype SingleValue :

// The first two values are the minimum and maximum delta,
// the third value is the frequency of the oscillator,
// or how many frames a full cycle takes.

// An optional fourth value could be the attack
// envelope of the oscillator: how many frames does
// it takes for the oscillator to reach full amplitude.

n.addOscillator(-0.1, 0.1, 60, 120);

Cette méthode ajouterait un oscillateur à la valeur value de l’objet n. C’est-à-dire que value serait recalculé à chaque image d’animation : on y additionnerait ou soustrairait Il serait aussi intéressant de pouvoir créer des oscillateurs qui multiplieraient leurs valeurs avec celle du nombre initial, au lieu d’additionner et de soustraire. Ce serait un peu comme improviser l’écriture de polynômes. une certaine quantité déterminée par une fonction sinusoïdale. L’ajout d’un nouvel oscillateur pourrait même être fait de façon douce, c’est-à-dire qu’il s’appliquerait progressivement jusqu’à ce qu’il soit exprimé au complet (on parlerait ici de l’amplitude de l’oscillateur).

Ensuite, une méthode releaseOscillator permettrait de diminuer progressivement l’amplitude d’un oscillateur et d’ensuite le « détacher » de l’objet n sur lequel il était appliqué.

// The first argument is the index of the oscillator
// to release, the second argument is the release time.
n.releaseOscillator(0, 120);

L’écriture de graphes

Ces amas de nombres se modifiant les uns les autres peuvent être imaginés comme étant des graphes, tout comme les graphes d’opérations qui sont à la base de Max/Msp et SuperCollider. Par exemple, un objet N pourrait contenir une référence à un autre objet N. Ça devient rapidement tout un casse-tête.

Il serait aussi tout à fait possible d’avoir un système servant à visualiser ces graphes dans l’application. Ces visualisations pourraient servir lors de la programmation à la volée mais aussi lors de l’écriture et du débogage du système.

Zone de désactivation des éditeurs

Comme il sera intéressant de pouvoir contrôler une œuvre en mouvement avec le clavier (comme il possible et très utile de le faire avec Max/msp), j’aurai besoin d’une zone dans l’écran où je peux simplement déplacer le curseur afin de désactiver les éditeurs CodeMirror.

let controlZone = document.getElementById("control");
controlZone.addEventListener("mouseenter", function( event ) {   
    // Here, we'll disable the input of the CodeMirror instances.
    console.log("Enter the zone!");
}, false);
controlZone.addEventListener("mouseleave", function( event ) {   
    // Here, we'll reactivate the input of the CodeMirror instances.
    console.log("Leave the zone!");
}, false);

Écriture improvisée de “shaders” Glsl

Beaucoup de complexité s’ajoutera lorsque je voudrai pouvoir écrire des “shaders” improvisés dans ma machine. Je dois commencer par regarder le travail de Patricio Gonzalez Vivo pour son Glsl live editor (licence Mit) et Rémi Papillié pour sa machine à coder du raymarching (aussi en licence Mit).

Contexte

Cette note de blog fait partie de mon projet de recherche Vers un cinéma algorithmique, démarré en avril 2018. Je vous invite à consulter la toute première note du projet pour en apprendre davantage.