···3535 fill: luma(240),
3636 radius: 4pt,
3737 width: 100%,
3838- // Figure itself is already non breakable, afaik
3838+ // Figure itself is already non breakable, AFAIK
3939 breakable: caption != "",
4040 if type(content) == str {
4141 raw(
···108108 ],
109109)
110110111111-Avec cette idée dans la tête, je me mets à gribouiller une ébauche d'"alphabet des formes", qui, naïvement, chercher à énumérer toutes les formes construisibles à partir de formes simples, que l'on peut superposer, pivoter et translater.
111111+Avec cette idée dans la tête, je me mets à gribouiller une ébauche d'"alphabet des formes", qui, naïvement, chercher à énumérer toutes les formes constructibles à partir de formes simples, que l'on peut superposer, pivoter et translater.
112112113113#grid(
114114 columns: (1fr, 1fr),
···119119120120Principalement par simple intérêt esthétique, je vectorise cette page via Illustrator. Vectoriser signifie convertir une image bitmap, représentée par des pixels, en une image vectorielle, qui est décrite par une série d'instructions permettant de tracer des vecteurs (d'où le nom), leur ajouter des attributs comme des couleurs, des règles de remplissage (Even-Odd, Non-Zero, etc.), des effets de dégradés, etc.
121121122122-Un aspect intéréssant est que, parmi les différents formats d'image vectorielles existant, le _SVG_, pour _Scalable Vector Graphics_, est indéniablement le plus populaire, et est un standard ouvert décrivant un format texte.
122122+Un aspect intéressant est que, parmi les différents formats d'image vectorielles existant, le _SVG_, pour _Scalable Vector Graphics_, est indéniablement le plus populaire, et est un standard ouvert décrivant un format texte.
123123124124Il est donc très facile de programmatiquement générer des images vectorielles à travers ce format.
125125···140140 "reflections",
141141 "spline-optimisation",
142142 "weaving",
143143- ).map(artwork => grid.cell(
144144- image("../examples/gallery/" + artwork + ".svg", width: 100%),
145145- ))
143143+ ).map(artwork => grid.cell(image("../examples/gallery/" + artwork + ".svg", width: 100%)))
146144 ),
147145)
148146149149-L'étape prochaine dans cette démarche était évidemment donc de générer procéduralement ces formes. Afin d'avoir des résultats intéréssants, et devant l'évidente absurdité d'un projet d'énumération _complète_ de _toutes les formes_, on préfèrera des générations procédurales dites "semi-aléatoires", dans le sens où certains aspects du résultat final sont laissés à l'aléatoire, comme le placement des formes élémentaires, tandis que de d'autres, comme la palette de couleurs, sont des décisions de l'artiste.
147147+L'étape prochaine dans cette démarche était évidemment donc de générer procéduralement ces formes. Afin d'avoir des résultats intéressants, et devant l'évidente absurdité d'un projet d'énumération _complète_ de _toutes les formes_, on préférera des générations procédurales dites "semi-aléatoires", dans le sens où certains aspects du résultat final sont laissés à l'aléatoire, comme le placement des formes élémentaires, tandis que de d'autres, comme la palette de couleurs, sont des décisions de l'artiste.
150148151149Le modèle initialement choisi dans les premières ébauches de Shapemaker est le suivant:
152150···187185188186=== Interprétation collective
189187190190-Avec 30 œuvres abstraites sans nom, je me suis posé la question de comment les nommer. J'aurais pu les nommer au gré de ma propre imagination, mais j'ai trouvé intéréssant le faire de laisser cette décision au grand public, qui tomberait né à né avec ces manifestations de pseudo-hasard virtuel.
188188+Avec 30 œuvres abstraites sans nom, je me suis posé la question de comment les nommer. J'aurais pu les nommer au gré de ma propre imagination, mais j'ai trouvé intéressant le faire de laisser cette décision au grand public, qui tomberait né à né avec ces manifestations de pseudo-hasard virtuel.
191189192190Le choix du nom d'une œuvre, en particulier quand elle est aussi abstraite et dénuée de contexte explicite, peut se faire parmi une potentielle infinité de titres, du littéral, au descriptiviste au poétique.
193191194192Les œuvres possèdent toutes un QR code amenant sur une page web qui permet de (re)nommer l'œuvre, en y apposant optionnellement son nom, en l'adoptant jusqu'à ce que lea prochain·e n'en prenne la garde.
195193196196-J'ai donc laissé le public trouver ces œuvres, cachées à travers la ville, dans l'esprit des fameux _Spaces Invaders_ de Paris @spaceinvadersparis (qui d'ailleurs étendent leur colonisation bien au-délà de Paris, allant même jusqu'à l'ISS @spaceinvadersiss).
194194+J'ai donc laissé le public trouver ces œuvres, cachées à travers la ville, dans l'esprit des fameux _Spaces Invaders_ de Paris @spaceinvadersparis (qui d'ailleurs étendent leur colonisation bien au-delà de Paris, allant même jusqu'à l'ISS @spaceinvadersiss).
197195198196199197#let work = (slug, caption, with-context: false, screenshot: true) => figure(
···217215)
218216219217220220-#work("paramount", ["Paramount"])
221218#work("reflets-citadins", ["Reflets Citadins", nommée par _Enide_])
222222-#work(
223223- "lenvolée-du-cerf-volant",
224224- ["l'envolée du Cerf-Volant", nommée par _Nicolas C._],
225225-)
219219+#work("paramount", ["Paramount"])
220220+// #work(
221221+// "lenvolée-du-cerf-volant",
222222+// ["l'envolée du Cerf-Volant", nommée par _Nicolas C._],
223223+// )
226224227225Certaines ont été souvent renommées, beaucoup ont été volées, et certaines restent encore inconquises.
228226···289287Les concepts de transformations et de filtres sont également très proche de ce qu'on peut retrouver dans des logiciels de création d'images raster, comme Photoshop.
290288291289290290+== Découpage en modules
291291+292292+Pour render la bibliothèque plus claire, et éventuellement pouvoir facilement séparer la crate en plusieurs sous-crates pour améliorer la vitesse de compilation @rustcompileunits, la crate est découpée en plusieurs modules:
292293293294#grid(
294295 columns: (1fr, 1fr),
295295- gutter: 1em,
296296+ gutter: 2em,
296297 [
297297- La bibliothèque fournit une grande quantité de fonctions utiles pour redimensionner des régions, en prendre le milieu.
298298-299299- La partie purement géométrique de la bibliothèque, définissans `Point`, `Region` et leurs opérations utiles associées (itérer les points d'une région, calculer le milieu d'une région, etc.), sont regroupées dans `shapemaker::geometry`.
300300-301301- Les définitions des objets et de tout leurs aspects visuels (`Fill`, `Transform`, `Filter`, `Color`, `Object`, `ColoredObject`) sont regroupées dans `shapemaker::objects`.
302302-303303- Il y a également `shapemaker::random` qui regroupe des fonctions de génération aléatoire, permettant d'introduire facilement et de manière plus ou moins granulaire, une part d'aléatoire dans le processus de génération: `Region.random_point()`, `Color::random()`, etc.
304304-305305- Enfin, `shapemaker::rendering` implémente le rendu d'un canvas et de tout ce qu'il contient en SVG
298298+ / geometry: partie purement géométrique de la bibliothèque, définissant `Point`, `Region` et leurs opérations utiles associées
299299+ / graphics: définitions des objets et tout leurs aspects visuels (`Fill`, `Transform`, `Filter`, `Color`, `Object`, `ColoredObject`)
300300+ / random: fonctions de génération aléatoire, permettant d'introduire facilement et de manière plus ou moins granulaire, une part d'aléatoire dans le processus de génération: `Region.random_point()`, `Color::random()`, etc.
301301+ / rendering: implémentation du rendu en SVG, et conversion en PNG
302302+ / video: cf #ref(<crate::video>)
303303+ / synchronization: cf #ref(<crate::synchronization>)
304304+ / vst: cf #ref(<crate::vst>)
305305+ / wasm: cf #ref(<crate::wasm>)
306306 ],
307307 diagram(
308308 caption: [Dépendances entre les modules de la bibliothèque],
···318318 ),
319319)
320320321321-322322-323321= Rendu en images
324322325323Maintenant que l'on a cette structure, il est bien évidemment essentiel de pouvoir la rendre en un fichier image exploitable, en PNG par exemple.
326324327327-L'idée est d'exploiter le standard SVG et tout l'écosystème existant autour pour éviter d'avoir à ré-implémenter un moteur de rasterisation à la main: SVG possède déjà énormément de fonctionnalités, et faire ainsi nous permet de fournir un "escape hatch" et de fournir à Shapemaker des fragments de code SVG pour des cas spécifiques que la bibliothèque ne couvrirait pas, à travers `Object::RawSVG`, qui prend en argument un arbre SVG brut.
325325+L'idée est d'exploiter le standard SVG et tout l'écosystème existant autour pour éviter d'avoir à ré-implémenter un moteur de rastérisation à la main: SVG possède déjà énormément de fonctionnalités, et faire ainsi nous permet de fournir un "escape hatch" et de fournir à Shapemaker des fragments de code SVG pour des cas spécifiques que la bibliothèque ne couvrirait pas, à travers `Object::RawSVG`, qui prend en argument un arbre SVG brut.
328326329327Ce processus de rendu est réalisé via l'implémentation d'un trait, une sorte d'équivalent des interfaces dans les langages orientés objet @rusttraits:
330328···339337340338Ce _trait_ est ensuite implémenté par la plupart des structures de `shapemaker::graphics`:
341339342342-/ Canvas: rendu de toutes ses `Layer`, en prenant garde à les ordonner correctement pour que les premières couches soit déssinées par dessus les dernières
340340+/ Canvas: rendu de toutes ses `Layer`, en prenant garde à les ordonner correctement pour que les premières couches soit dessinées par dessus les dernières
343341/ Layer: rendu de l'ensemble des `ColoredObject` qu'elle contient, en les regroupant dans un groupe SVG #raw(lang: "svg", "<g>")
344342/ ColoredObject: rendu de l'`Object` qu'il contient, en appliquant les transformations et filtres
345343/ Object: dépend de la variante: `Object::Rectangle` est rendu comme un #raw(lang: "svg", "<rect>"), `Object::Circle` est rendu comme un #raw(lang: "svg", "<circle>"), etc.
···393391 ),
394392)
395393396396-En suite, pour convertir en PNG, on utilise une autre bibliothèque, _resvg_, qui implémente presque complétement la spécification SVG 1.1, et l'implémente même mieux que Firefox, Safari et Chrome @resvg. L'arbre SVG que l'on a construit est sérialisé en string, puis parsé par _resvg_, qui le transforme en un arbre de rendu, qui est ensuite rasterisé en une pixmap#footnote[Matrice plate de pixels RGBA], qui est finalement écrit dans un fichier PNG.
394394+En suite, pour convertir en PNG, on utilise une autre bibliothèque, _resvg_, qui implémente presque complètement la spécification SVG 1.1, et l'implémente même mieux que Firefox, Safari et Chrome @resvg. L'arbre SVG que l'on a construit est sérialisé en string, puis parsé par _resvg_, qui le transforme en un arbre de rendu, qui est ensuite rasterisé en une pixmap#footnote[Matrice plate de pixels RGBA], qui est finalement écrit dans un fichier PNG.
397395398396#diagram(
399397 caption: [Rendu d'un canvas SVG en PNG],
···412410Le passage par une string svg est évidemment une perte de performance, qui est discutée #ref(<perf-svgstring>, form: "page")
413411414412415415-= Render loop et hooks
413413+= Render loop et hooks <crate::video>
416414417417-On peut maintenant rasteriser un canvas. Passer à l'étape vidéo donc à réaliser cette opération sur chaque _frame_ de la vidéo finale. Cependant, la vidéo devant se synchroniser au son, la tâche est rendu plus difficile: en effet, il ne suffit pas d'exposer à l'artiste une fonction `render_frame`, qui prendrait en argument le numéro de frame actuel et permettrait de définir le canvas pour chaque frame: on a besoin de moyen de _réagir_ à des moments clés de la musique.
415415+On peut maintenant rastériser un canvas. Passer à l'étape vidéo donc à réaliser cette opération sur chaque _frame_ de la vidéo finale. Cependant, la vidéo devant se synchroniser au son, la tâche est rendu plus difficile: en effet, il ne suffit pas d'exposer à l'artiste une fonction `render_frame`, qui prendrait en argument le numéro de frame actuel et permettrait de définir le canvas pour chaque frame: on a besoin de moyen de _réagir_ à des moments clés de la musique.
418416419419-Pour donner les moyens à l'artiste d'exprimer cela, on utilise un concept assez commun en programmation, les _hooks_, nommés ainsi car, essentiellement, ils permettent à du code utilisateur de s'imiscer dans certains moments de l'exécution d'une bibliothèque @hooks.
417417+Pour donner les moyens à l'artiste d'exprimer cela, on utilise un concept assez commun en programmation, les _hooks_, nommés ainsi car, essentiellement, ils permettent à du code utilisateur de s’immiscer dans certains moments de l'exécution d'une bibliothèque @hooks.
420418421419Dans notre cas, on va donner les hooks suivants:
422420···450448451449Un hook reçoit notamment une référence mutable au Canvas #raw(lang: "rust", "&mut Canvas") car il _modifie le canvas de la frame en cours_. Le moteur de rendu vidéo ne possède en fait qu'un seul canvas, qui est successivement modifié au long de la vidéo.
452450453453-Le générique #raw(lang: "rust", "<C>") existe car l'artiste peut définir des données additionelles à stocker dans le contexte, pratique pour stocker des données à travers la vidéo, au delà de l'exécution d'un unique hook#footnote[Par exemple, "quelle a été la dernière ligne de parole affichée? il faut passer à la prochaine"]
451451+Le générique #raw(lang: "rust", "<C>") existe car l'artiste peut définir des données additionnelles à stocker dans le contexte, pratique pour stocker des données à travers la vidéo, au delà de l'exécution d'un unique hook#footnote[Par exemple, "quelle a été la dernière ligne de parole affichée? il faut passer à la prochaine"]
454452455453On met également à disposition une méthode `with_hook`, qui rajoute un hook à la liste, permettant de facilement les définir:
456454···462460 lang: "rust",
463461 is_method: true,
464462 transform: it => (
465465- "impl Video<C> {\n ...\n"
466466- + it.replace("<AdditionalContext>", "<C>")
467467- + "\n}"
463463+ "impl Video<C> {\n ...\n" + it.replace("<AdditionalContext>", "<C>") + "\n}"
468464 ),
469465 ),
470466)
···479475 lang: "rust",
480476 is_method: true,
481477 transform: it => (
482482- "impl Video<C> {\n ...\n"
483483- + it.replace("<AdditionalContext>", "<C>")
484484- + "\n}"
478478+ "impl Video<C> {\n ...\n" + it.replace("<AdditionalContext>", "<C>") + "\n}"
485479 ),
486480 ),
487481)
488482489489-Le moteur de rendu vidéo est donc une boucle qui, à chaque frame, regarde dans l'ensemble des _hooks_ enregistrés, lesquels doivent être exécutés, les exécute, puis rasterise le canvas en une frame qui est ensuite donnée à l'encodeur vidéo:
483483+Le moteur de rendu vidéo est donc une boucle qui, à chaque frame, regarde dans l'ensemble des _hooks_ enregistrés, lesquels doivent être exécutés, les exécute, puis rastérise le canvas en une frame qui est ensuite donnée à l'encodeur vidéo:
490484491485#diagram(
492486 caption: [Pipeline],
···530524 ```,
531525)
532526533533-La boucle de rendu en elle-même itère sur *les instants, ms par ms, et non pas les frames*. C'est important pour garder la vidéo en synchronisation avec le son. J'avais initialiement fait la boucle sur les frames, et la vidéo se décalait progressivement.
527527+La boucle de rendu en elle-même itère sur *les instants, ms par ms, et non pas les frames*. C'est important pour garder la vidéo en synchronisation avec le son. J'avais initialement fait la boucle sur les frames, et la vidéo se décalait progressivement.
534528535529#codesnippet(```rust
536530let render_ms_range = self.start_rendering_at..self.duration_ms();
···547541#codesnippet(
548542 dedent(
549543 cut-around(
550550- it => it
551551- .trim()
552552- .starts-with("if context.frame != previous_rendered_frame"),
544544+ it => it.trim().starts-with("if context.frame != previous_rendered_frame"),
553545 it => it.trim().ends-with("}"),
554546 read("../src/video/encoding.rs"),
555547 ),
···559551La rastérisation est l'encodage sont réalisés après la fin de la boucle de rendu pour pouvoir paralléliser la rastérisation, voir #ref(<perf-parallelrasterize>).
560552561553562562-= Sources de synchronisation
554554+= Sources de synchronisation <crate::synchronization>
563555564556On a pu voir dans les exemples de code précédents que les hooks reçoivent deux arguments essentiels dans leur fonctions: le _canvas_, discuté précédemment, et un _contexte_.
565557···578570Dans chacun de ces cas, l'objectif est de pouvoir inférer depuis ces ressources les informations suivantes:
579571580572- Le BPM#footnote[Beats per minute, aussi appelé tempo] du morceau, avec éventuellement des évolutions au cours du morceau
581581-- D'éventuels marqueurs temporels permettant de réagir à des changements de phrases musicales (par exemple, la classique construction _build-up_ / _drop_ / _break_ en EDM#footnote[Electronic Dance Music]), sans avoir à harcoder un timestamp dans le code de la vidéo: ces marqueurs sont placés dans le logiciel de production musicale (cf #ref(<flstudiomarkers>), #ref(<flstudiomarkers>, form: "page"))
573573+- D'éventuels marqueurs temporels permettant de réagir à des changements de phrases musicales (par exemple, la classique construction _build-up_ / _drop_ / _break_ en EDM#footnote[Electronic Dance Music]), sans avoir à coder en dur un timestamp dans le code de la vidéo: ces marqueurs sont placés dans le logiciel de production musicale (cf #ref(<flstudiomarkers>), #ref(<flstudiomarkers>, form: "page"))
582574- Pour chaque instrument, et à chaque instant:
583575 - Les notes jouées: pitch#footnote[hauteur] et vélocité#footnote[intensité avec laquelle la note a été jouée]
584576 - Des éventuelles évolutions de paramètres influant sur le timbre de l'instrument (ouverture d'un filtre passe bas pour un synthétiseur, pédale de sustain pour un piano, etc)
···641633642634…Sauf que les coordonnées temporelles MIDI sont en _deltas de ticks MIDI_. Les ticks sont indépendant du BPM, et les deltas sont des simples différences du nombre de ticks passés entre deux évènements.
643635644644-La durée d'un tick est aussi dépendante du _PPQ_, ou _Pulse per quarter_, qui correspond à la résolution temporellle d'un fichier MIDI, c'est l'équivalent des FPS en vidéos ou de la fréquence d'échantillonage en audio @midippq.
636636+La durée d'un tick est aussi dépendante du _PPQ_, ou _Pulse per quarter_, qui correspond à la résolution temporelle d'un fichier MIDI, c'est l'équivalent des FPS en vidéos ou de la fréquence d’échantillonnage en audio @midippq.
645637646638#codesnippet(
647639 include-function(
···699691#imagefigure(
700692 "./flstudiomidimacro.png",
701693 [
702702- Dialoge d'avertissement lors de l'utilisation de la macro "Prepare for MIDI export" dans FL Studio
694694+ Dialogue d'avertissement lors de l'utilisation de la macro "Prepare for MIDI export" dans FL Studio
703695 ],
704696)
705697···729721730722Étant donné l'aspect fastidieux de la solution précédente, il est intéressant de se pencher sur les fichiers de projet des logiciels de production musicale, afin de _remonter totalement à la source du morceau de musique_: le fichier qui est ouvert par l'artiste, celui sur lequel iel travaille.
731723732732-Malheureusement, les logiciel libres sont très loin derrière les standards de l'industrie en terme de production musicale, et il est ajourd'hui assez irréaliste de penser pouvoir produire de la musique avec des alternatives libres qui possède des formats de fichier de projet ouverts.
724724+Malheureusement, les logiciel libres sont très loin derrière les standards de l'industrie en terme de production musicale, et il est aujourd'hui assez irréaliste de penser pouvoir produire de la musique avec des alternatives libres qui possède des formats de fichier de projet ouverts.
733725734734-On doit donc se tourner vers de la rétro-ingénierie, et avoir une implémentation d'un "adapteur" pour chaque logiciel de production musicale que l'on souhaite supporter.
726726+On doit donc se tourner vers de la rétro-ingénierie, et avoir une implémentation d'un "adaptateur" pour chaque logiciel de production musicale que l'on souhaite supporter.
735727736728=== FL Studio
737729···746738 ),
747739)
748740749749-Cependant, l'auteur·ice de la bibliothèque n'a malheureusemnet plus le temps de la maintenir @pyflp3.12, et, étant donné l'évolution de FL Studio, le parser est voué à progressivement ne plus supporter les dernières versions du logiciel.
741741+Cependant, l'auteur·ice de la bibliothèque n'a malheureusement plus le temps de la maintenir @pyflp3.12, et, étant donné l'évolution de FL Studio, le parser est voué à progressivement ne plus supporter les dernières versions du logiciel.
750742751743Étant donné que je suis utilisatrice de FL Studio, je n'a pas cherché de potentielles solutions pour d'autres logiciels de MAO.
752744···754746755747Étant donné que l'adapter est en Python, l'intégrer proprement dans Shapemaker consisterai à éventuellement utiliser une solution de FFI#footnote[Foreign Function Interface, permettant d'appeler des fonctions écrites dans un autre langage de programmation] comme PyOxide @pyo3, ce qui demanderait également beaucoup de travail d'adaptation.
756748757757-== Dépôt de "sondes" dans le logiciel de MAO
749749+== Dépôt de "sondes" dans le logiciel de MAO <crate::vst>
758750759751#grid(
760752 columns: (3fr, 1fr),
···765757766758 L'avantage de cette approche est qu'elle est agnostique au logiciel de MAO: en effet, VST est _le_ standard de plugins audio, supporté par tout les logiciels.
767759768768- C'est via cette technologie que les artistes peuvent jouer des instruments virtuels, allant des pianos physiquement simulés @pianoteq, en passant par vocaloïdes#footnote[simuateurs de parole chantée, cas à application musicale de la synthèse vocale] (comme par exemple Hatsune Miku @mikudayooo), aux synthétiseurs additifs, soustractifs, à wavetables (dont un exemple très populaire est Serum @serum).
760760+ C'est via cette technologie que les artistes peuvent jouer des instruments virtuels, allant des pianos physiquement simulés @pianoteq, en passant par vocaloïdes#footnote[simulateurs de parole chantée, cas à application musicale de la synthèse vocale] (comme par exemple Hatsune Miku @mikudayooo), aux synthétiseurs additifs, soustractifs, à wavetables (dont un exemple très populaire est Serum @serum).
769761770762 C'est aussi cette technologie qui est utilisée pour appliquer des effets aux signaux audio créés par les instruments (on parle de VST _effets_, contrairement aux VST _générateurs_), allant des modélisations de pédales d'effets de guitare ou de compresseurs analogiques à tube, aux simulation de compression digitale de signaux ("bitcrushing"), aux égaliseurs fréquentiels.
771763···781773 Il est donc possible de recevoir du signal, *autant audio que MIDI*, en entrée d'un VST.
782774]
783775784784-Autre possibilité, qui s'avère utile parmis nos objectifs: les VSTs peuvent exposer à l'hôte (le logiciel de MAO) des paramètres changeables, ce qui permet de faire évoluer le timbre d'un instrument, l'intensité d'une réverbération, etc. Faire varier des paramètres au cours du temps est un aspect essentiel de la musique, en particulier électronique, qui contribue à "donner vie" à un morceau.
776776+Autre possibilité, qui s'avère utile parmi nos objectifs: les VSTs peuvent exposer à l'hôte (le logiciel de MAO) des paramètres changeables, ce qui permet de faire évoluer le timbre d'un instrument, l'intensité d'une réverbération, etc. Faire varier des paramètres au cours du temps est un aspect essentiel de la musique, en particulier électronique, qui contribue à "donner vie" à un morceau.
785777786778On peut donc également exposer des paramètres sur notre VST-sonde, qui peuvent servir à automatiser des changements de couleurs, de formes, etc, en suivant une évolution dans le timbre d'un instrument, par exemple, depuis la source directement (il suffit d'envoyer le signal d'automatisation au VST-sonde, en plus de l'instrument lui-même).
787779788788-On exfiltre ensuite ces données hors du logiciel vers un "beacon", via un simple API WebSocket, qui permet une communication instantanée beaucoup plus performante que des requêtes HTTP, et est plus approprié à l'envoie de potentiellement plusieurs miliers de points de données par secondes: en effet, le VST-sonde s'imiscant dans la chaîne de traitement audio, il ne doit pas la ralentir considérablement, sous peine de rendre le logiciel de MAO inutilisable
780780+On exfiltre ensuite ces données hors du logiciel vers un "beacon", via un simple API WebSocket, qui permet une communication instantanée beaucoup plus performante que des requêtes HTTP, et est plus approprié à l'envoie de potentiellement plusieurs milliers de points de données par secondes: en effet, le VST-sonde s’immisçant dans la chaîne de traitement audio, il ne doit pas la ralentir considérablement, sous peine de rendre le logiciel de MAO inutilisable
789781790782#codesnippet(
791783 caption: "Implémentation de la fonction permettant à une probe de se signaler auprès du beacon",
···808800809801#diagram(
810802 caption: [Exfiltration de données depuis la chaîne de traitement du logiciel de MAO],
811811- size: 80%,
803803+ size: 75%,
812804 ```dot
813805 digraph G {
814806 rankdir="LR";
807807+ // splines=ortho;
815808 compound=true;
816809 node[shape="record"];
817810818811 subgraph cluster_host {
819812 label = "Logiciel de MAO"
820813821821- subgraph cluster_track {
822822- label = "Pour chaque piste"
823823- midi -> instrument -> effects -> probe
824824- midi -> probe
825825- automation -> instrument
826826- automation -> probe
814814+ subgraph cluster_bass {
815815+ label = "Bass"
816816+ midi -> synth -> probe_1
817817+ midi -> probe_1
818818+ autom_in_bass [shape=point, label=""]
819819+ autom_in_bass -> probe_1
820820+ autom_in_bass -> synth
821821+822822+ probe_1[label="probe #1"]
823823+ }
824824+ subgraph cluster_drums {
825825+ label = "Drums"
826826+ midi_2 [label="midi"]
827827+ midi_2 -> drums -> probe_2
828828+ midi_2 -> probe_2
829829+ autom_in_drums [shape=plaintext, label=""]
830830+831831+ probe_2[label="probe #2"]
827832 }
833833+834834+ subgraph cluster_voice {
835835+ label = "Voice"
836836+ sampler -> effects -> probe_3
837837+ autom_in_voice [shape=point, label=""]
838838+ autom_in_voice -> probe_3
839839+ autom_in_voice -> effects
840840+841841+ probe_2[label="probe #3"]
842842+ }
843843+844844+ automation -> autom_in_bass [arrowhead=none]
845845+ automation -> autom_in_voice [arrowhead=none]
846846+ automation -> autom_in_drums [style=invis]
828847 }
829848830849 subgraph cluster_shapemaker {
831850 label = "Shapemaker"
832832- wip[label="(en développement)", shape="plaintext"]
851851+ wip[label="(en développement)", shape="plaintext"]
833852 beacon -> wip
834853 }
835854836836- probe -> beacon [label="ws://"]
855855+ probe_1 -> beacon [label="ws://"]
856856+ probe_2 -> beacon [label="ws://"]
857857+ probe_3 -> beacon [label="ws://"]
837858838859 }
839860 ```,
840861)
841862842863843843-== Temps réel: WASM et WebMIDI
864864+== Temps réel: WASM et WebMIDI <crate::wasm>
844865845845-Il est possible de réagir en temps réel à des pressions de touches sur des appareils conçus pour la production musicale assistée par ordinateur (MAO): des claviers, des potentiomères pour ajuster des réglages affectant le timbre d'un son, des pads pour déclencher des sons et, par exemple, jouer des percussions, etc.
866866+Il est possible de réagir en temps réel à des pressions de touches sur des appareils conçus pour la production musicale assistée par ordinateur (MAO): des claviers, des potentiomètres pour ajuster des réglages affectant le timbre d'un son, des pads pour déclencher des sons et, par exemple, jouer des percussions, etc.
846867847868Ces appareils sont appelés "contrôleurs MIDI", du protocole standard qui régit leur communication avec l'ordinateur.
848869849849-S'il est évidemment possible d'interagit avec ces contrôleurs depuis un programme natif (c'est après tout ce que font les logiciels de production musicale), j'ai préféré tenté l'approche Web, pour en faciliter l'accessibilité et en réduire le temps nécéssaire à la mise en place #footnote[
850850- Imaginez, votre ordinateur a un problème 5 minutes avant le début d'une installation live, et vous aviez prévu d'utiliser Shapemaker pour des visuels. En faisant du dispostif un site web, il suffit de brancher son contrôleur à l'ordinateur d'un·e ami·e, et c'est tout bon.
870870+S'il est évidemment possible d'interagit avec ces contrôleurs depuis un programme natif (c'est après tout ce que font les logiciels de production musicale), j'ai préféré tenté l'approche Web, pour en faciliter l'accessibilité et en réduire le temps nécessaire à la mise en place #footnote[
871871+ Imaginez, votre ordinateur a un problème 5 minutes avant le début d'une installation live, et vous aviez prévu d'utiliser Shapemaker pour des visuels. En faisant du dispositif un site web, il suffit de brancher son contrôleur à l'ordinateur d'un·e ami·e, et c'est tout bon.
851872].
852873853874Comme pour de nombreuses autres technologies existant à la frontière entre le matériel et le logiciel, les navigateurs mettent à disposition des sites web une technologie permettant de communiquer avec les périphériques MIDI connectés à la machine: c'est l'API WebMIDI @webmidi.
···856877857878Il existe cependant un moyen de "faire tourner du code Rust" dans un navigateur Web: la compilation vers WebAssembly (WASM), un langage assembleur pour le web @wasm, qui est une cible de compilation pour quelques des langages compilés plus modernes, comme Go @gowasm or Rust @rustwasm
858879859859-En exportant la _crate_ shapemaker en bibliothèque Javascript via wasm-bindgen @wasmbindgen, il est donc possible d'exoser à une balise #raw("<script>", lang: "html") les fonctions de la bibliothèque, et brancher donc celles-ci à des _callbacks_ donnés par l'API WebMIDI:
880880+En exportant la _crate_ shapemaker en bibliothèque Javascript via wasm-bindgen @wasmbindgen, il est donc possible d’exposer à une balise #raw("<script>", lang: "html") les fonctions de la bibliothèque, et brancher donc celles-ci à des _callbacks_ donnés par l'API WebMIDI:
860881861882#figure(
862883 caption: "Exposition de fonctions à WASM depuis Rust, et utilisation de celles-ci dans un script Javascript",
···906927 ),
907928)
908929909909-Au final, on peut arriver à une performance live interactive @pianowasmdemo intéréssante, et assez réactive pour ne pas avoir de latence (et donc de désynchronisation audio/vidéo) perceptible.
930930+Au final, on peut arriver à une performance live interactive @pianowasmdemo intéressante, et assez réactive pour ne pas avoir de latence (et donc de désynchronisation audio/vidéo) perceptible.
910931911911-Les navigateurs Web supportant nativement le format SVG, qui se décrit notamment comme incluable directement dans le code HTML d'une page web @svginhtml, il est possible de simplement générer le code SVG, et de laisser le navigateur faire le rendu, ce qui s'avère être une solution très performante.
932932+Les navigateurs Web supportant nativement le format SVG, qui se décrit notamment comme directement incluable dans le code HTML d'une page web @svginhtml, il est possible de simplement générer le code SVG, et de laisser le navigateur faire le rendu, ce qui s'avère être une solution très performante.
912933913934= Performance
914935···957978 ```,
958979)
959980960960-L'inconvénient est que, pour la partie encoding vidéo, il n'existe pas encore vraiment d'encodeur H.264#footnote[Codec vidéo, très souvent utilisé pour les fichiers MP4, par exemple] en pur Rust, la plupart des solutions étant des bindings#footnote[bibliothèque utilisant des FFIs pour donner un accès idiomatique à une bibloithèque provenant d'un autre langage de programmation] vers des bibliothèques C, notamment ffmpeg.
981981+L'inconvénient est que, pour la partie encoding vidéo, il n'existe pas encore vraiment d'encodeur H.264#footnote[Codec vidéo, très souvent utilisé pour les fichiers MP4, par exemple] en pur Rust, la plupart des solutions étant des bindings#footnote[bibliothèque utilisant des FFIs pour donner un accès idiomatique à une bibliothèque provenant d'un autre langage de programmation] vers des bibliothèques C, notamment ffmpeg.
961982962983Cela rend l'installation de la bibliothèque beaucoup plus complexe, notamment sur Windows (les logiciels de production musicale sont très rares à fonctionner correctement sur Linux, surtout quand on prend en compte que les VSTs doivent eux aussi fonctionner sur Linux):
963984964985#codesnippet(
965965- caption: "Erreur recontrée pendant la compilation des bindings Rust à libx264",
986986+ caption: "Erreur rencontrée pendant la compilation des bindings Rust à libx264",
966987 ```
967988 Compiling ffmpeg-sys-next v7.1.0
968989 error: failed to run custom build command for `ffmpeg-sys-next v7.1.0`
···98210039831004#diagram(
9841005 caption: [Détail de la boucle de rendu],
985985- scale(90%, reflow: true)[
10061006+ [
9861007 ```dot
9871008 digraph G {
9881009 compound=true;
989989- splines="ortho";
990990- node[shape="record"];
10101010+ // Either of these makes edge labels disappear...
10111011+ // splines="ortho";
10121012+ // node[shape="record"];
99110139921014 hooks -> canvas;
9931015 subgraph cluster_tosvg {
···9981020 render_to_svg -> stringify_svg [label="0.1ms"]
9991021 }
10001022 }
10231023+ stringify_svg -> "svg string" [label="0.1ms"]
10011024 subgraph cluster_rasterize {
10021025 label = "Encode frame [167ms]"
10031026 subgraph g_rasterize {
10041027 rank=same;
10051005- stringify_svg -> "svg string"
10061028 "svg string" -> "usvg tree" [label="48ms"]
10071029 "usvg tree" -> pixmap [label="11ms"]
10081030 pixmap -> "hwc frame" [label="108ms"]
10091031 }
10101032 }
1011103310121012- canvas -> "svg string" [weight=10, style=invis]
10341034+ canvas -> "svg string" [weight=10, style=invis]
10131035 }
10141036 ```
10151037 ],
···10631085 ```
10641086]
1065108710661066-Il est donc nécéssaire de convertir entre ces deux formats, ce qui est lent car demande de copier les données.
10881088+Il est donc nécessaire de convertir entre ces deux formats, ce qui est lent car demande de copier les données.
10891089+10901090+La solution initiale utilisait `video_rs::Frame::from_shape_fn`:
10911091+10921092+#codesnippet[
10931093+ ```rust
10941094+ Ok(video_rs::Frame::from_shape_fn(
10951095+ (pixmap.height() as usize, pixmap.width() as usize, 3),
10961096+ |(y, x, c)| {
10971097+ let pixel = pixmap
10981098+ .pixel(x as u32, y as u32)
10991099+ .expect(&format!("No pixel found at x, y = {x}, {y}"));
11001100+ match c {
11011101+ 0 => pixel.red(),
11021102+ 1 => pixel.green(),
11031103+ 2 => pixel.blue(),
11041104+ _ => unreachable!(),
11051105+ }
11061106+ },
11071107+ ))
11081108+ ```
11091109+]
11101110+11111111+Cependant, cette solution est très lente car _non parallélisée_, je l'ai donc réimplémentée avec de la parallélisation sur chaque pixel:
11121112+11131113+#codesnippet(
11141114+ include-function(
11151115+ "../src/video/encoding.rs",
11161116+ "pixmap_to_hwc_frame",
11171117+ lang: "rust",
11181118+ is_method: true,
11191119+ ),
11201120+)
1067112110681068-Une solution serait de passer à une bibiothèque plus bas niveau et voir s'il est possible de donner directement les données de pixmap à l'encodeur, sans conversion, ou tout du moins sans avoir à copier les données.
11221122+On effectue toujours de la copie, mais la conversion est nettement plus rapide ainsi.
1069112310701070-Une autre solution est de faire proposer une contribution à la bibiothèque de rendu utilisée par _resvg_, _tiny_skia_#footnote[Tiny-skia est notamment utilisé par Typst @typsttinyskia @typsttinyskiacargotoml, l'alternative moderne à LaTeX sur laquelle ce papier a été typeset], pour pouvoir instrumentaliser les lectures et écritures à sa pixmap, et ainsi écrire dans la représentation voulue par libx264 directement.
11241124+Une solution serait de passer à une bibliothèque plus bas niveau et voir s'il est possible de donner directement les données de pixmap à l'encodeur, sans conversion, ou tout du moins sans avoir à copier les données.
11251125+11261126+Une autre solution est de faire proposer une contribution à la bibliothèque de rendu utilisée par _resvg_, _tiny_skia_#footnote[Tiny-skia est notamment utilisé par Typst @typsttinyskia @typsttinyskiacargotoml, l'alternative moderne à LaTeX sur laquelle ce papier a été typeset], pour pouvoir instrumentaliser les lectures et écritures à sa pixmap, et ainsi écrire dans la représentation voulue par libx264 directement.
1071112710721128== SVG vers string vers SVG <perf-svgstring>
10731129···1075113110761132= Conclusion
1077113311341134+Malgré les multiples solutions de synchronisation audio-vidéo testées, avec certaines s’avérant infructueuses, l'approche par VST-sondes semble prometteuse, et permettrait de remplir presque tout les objectifs fixés au début du #ref(<crate::synchronization>).
11351135+11361136+L'approche WASM/WebMIDI explorée au #ref(<crate::wasm>) est une solution appropriée pour des installations live, qui mérite d'être d'avantage explorée, possiblement en vue de la création d'une solution de scripting pour VJing#footnote[Visual Jockeying, l'art de mixer des visuels en live, souvent en concert ou en boîte de nuit]
11371137+11381138+== Pistes d'améliorations
11391139+11401140+=== Feedback loop
11411141+11421142+Enfin, un des points les plus importants à améliorer reste la "feedback loop" _pendant la conception d'une procédure de génération_, qui reste extrêmement longue à cause de la lenteur de compilation de Rust, et du fait que, contrairement à un logiciel de montage vidéo, par exemple, on ne peut que re-rendre la vidéo en MP4 (même si l'on peut décider de rendre qu'une petite partie), ouvrir le fichier, et regarder le résultat.
11431143+11441144+Une idée serait de, là aussi, utiliser le backend WASM/WebMIDI pour fournir une sorte de preview du code en temps réel: une interface simple permet de placer une tête de lecture à un instant, et montre la frame à cet instant, et se rafraîchit quand le code change. Avec éventuellement la possibilité de faire "play".
11451145+11461146+Encore faut-il que la vitesse de recompilation de Rust le permette, même si ce serait à proiri possible tant que la crate utilisant Shapemaker (celle que l'artiste écrit) reste légère.
11471147+11481148+=== Un langage de scripting
11491149+11501150+Rust étant un des langages de programmation les plus difficiles à utiliser, on pourrait éventuellement exposer l'API de Shapemaker à un langage de scripting plus léger, comme Lua par exemple, ce qui permettrait également de rendre le projet plus accessible.
11511151+11521152+Cela permettrait éventuellement aussi d'améliorer la vitesse de compilation de la crate écrite par l'artiste, qui pourrait, si elle est trop faible, empêcher l'implémentation de la solution de feedback loop telle qu'évoquée plus tôt. Des projets comme Tauri embarque un système de HMR#footnote[Hot Module Replacement, permettant de recharger du code en temps réel sans recharger la page, technologie assez prévalente dans le développement web frontend], non pas pour leur bibliothèque Rust, mais pour les bindings JavaScript exposé aux utilisateur·ice·s de la bibliothèque @taurihmr.
11531153+11541154+On pourrait même envisager afficher cette _preview_ dans le logiciel de MAO, en tant qu'un 2e VST, "Shapemaker Preview". Ceci demande d'implémenter encore un backend de rendu, autre que H.264 ou WASM, mais serait certainement la meilleure solution en terme d'UX#footnote[expérience utilisateur·ice]
11551155+11561156+== Code source
11571157+11581158+Le code source du projet est disponible en ligne sur Github:
11591159+11601160+#align(center)[
11611161+ #link("https://github.com/gwennlbh/shapemaker")[gwennlbh/shapemaker]
11621162+]
11631163+11641164+Le répertoire `paper/` contient la source de ce papier, écrit en Typst
11651165+11661166+== Exemples
11671167+11681168+Le projet n'étant pas encore terminé, il n'a pas encore de clips musicaux publiés. Cependant, voici des liens vers quelques tests:
11691169+11701170+- #link("https://youtu.be/3lx6VAz_UKM")
11711171+- #link("https://instagram.com/p/C62JfogoUt9")
11721172+11731173+#bibliography("bibliography.yaml")
1078117410791175#show: arkheion-appendices
10801176= Marqueurs dans un logiciel de MAO
···10891185 ],
10901186) <flstudiomarkers>
1091118710921092-// Add bibliography and create Bibiliography section
10931093-// #bibliography("bibliography.yaml", style: "./ieee-with-locations.csl")
10941094-#bibliography("bibliography.yaml")
11881188+= Série "interprétation collective" 1
11891189+#grid(
11901190+ columns: 6,
11911191+ ..range(1, 31).map(it => image("./street/frames/" + str(it) + ".svg"))
11921192+)