···89899090#text(
9191 size: 0.88em,
9292- include-function(
9393- "../src/examples.rs",
9494- "dna_analysis_machine",
9595- lang: "rust",
9696- transform: it => "use shapemaker::*\n\n" + it,
9797- ),
9898-)
9292+ raw(lang: "rust", read("../examples/dna-analysis-machine/src/main.rs")),
9393+) <demo-code>
999410095#pagebreak()
10196···148143 "reflections",
149144 "spline-optimisation",
150145 "weaving",
151151- ).map(artwork => grid.cell(
152152- image("../examples/gallery/" + artwork + ".svg", width: 100%),
153153- ))
146146+ ).map(artwork => grid.cell(image("../examples/gallery/" + artwork + ".svg", width: 100%)))
154147 ),
155148)
156149···232225 ),
233226)
234227228228+Certaines ont été souvent renommées, beaucoup ont disparues, et certaines restent encore inconquises.
235229236230#work("reflets-citadins", ["Reflets Citadins", nommée par _Enide_])
237231#work("paramount", ["Paramount"])
···240234 ["l'envolée du Cerf-Volant", nommée par _Nicolas C._],
241235)
242236243243-Certaines ont été souvent renommées, beaucoup ont été volées, et certaines restent encore inconquises.
244237245238#work("danse-le-ciel", ["Danse le ciel"], with-context: true)
246239#work("bridging", [_Sans titre_], only-context: true)
···260253261254À force de générer des centaines de petites images géométriques, il m'est venu à l'idée de les transformer en frames d'une _vidéo_.
262255263263-Afin d'évaluer à quoi pourrait ressembler une telle chose, j'ai commencé par simplement faire une boucle, écrasant un même fichier .png à un intervalle de temps régulier, fichier ouvert dans XnView @xnview, qui permet de se re-charger automatiquement quand le fichier affiché change.
256256+Afin d'évaluer à quoi pourrait ressembler une telle chose, j'ai commencé par simplement faire une boucle, écrasant un même fichier .png à un intervalle de temps régulier, fichier ouvert dans XnView @xnview, qui se recharge automatiquement quand le fichier affiché change.
264257265265-Bien évidemment, surtout s'il s'agit d'une vidéo synchronisée à sa bande son, il ne suffit pas de générer une frame aléatoire chaque seconde. Il faut pouvoir _réagit à des moments et rythmes clés du morceau_.
258258+Bien évidemment, surtout s'il s'agit d'une vidéo synchronisée à sa bande son, il ne suffit pas de générer une frame aléatoire chaque seconde. Il faut pouvoir _*réagir* à des moments et rythmes clés du morceau_.
266259267260268261= Une _crate_ Rust avec un API sympathique
269262270270-Pour implémenter cette génération, il faut donner donc un moyen à l'artiste de décrire sa procédure de génération.
263263+Pour implémenter cette génération, il faut donc donner un moyen à l'artiste de décrire son langage visuel.
271264272272-Ainsi, Shapemaker est une bibliothèque réutilisable, ou _crate_ dans l'écosystème Rust @rustcrates.
265265+Ainsi, Shapemaker est une bibliothèque, ou _crate_ dans l'écosystème Rust @rustcrates, dont l'on peut se servir pour créer son script, dont un exemple est montré #ref(<demo-code>, form: "page").
273266274274-La création d'un procédé de génération est conceptualisée par un canvas, composé de une ou plusieurs couches ou _layers_ d'objets. Ces objets sont _colorés_ (possèdent une information sur la manière dont il faut les remplir: bleu solide, hachures cyan, etc.), et peuvent également subir des filtres et transformations #footnote[Avec un peu de recul, le terme d'objet texturé est plus approprié, mais le code n'a pas encore changé]. Ils sont aussi _placés_ dans l'espace du canvas: le canvas possède une information de _région_, un intervalle 2D de points valables. Les objets se placent dans cette région, en stockant dans leur structure les coordonnées de _points_ marquant leur positionnement dans l'espace (coins pour un #raw(lang: "rust", "Object::Rectangle"))
267267+La procédure est conceptualisée par un canvas, composé de une ou plusieurs couches ou _layers_ d'objets. Ces objets sont _colorés_ (possèdent une information sur la manière dont il faut les remplir: bleu solide, hachures cyan, etc.), et peuvent également subir des filtres et transformations #footnote[Avec un peu de recul, le terme d'objet texturé est plus approprié, mais le code n'a pas encore changé]. Ils sont aussi _placés_ dans l'espace du canvas: le canvas possède une information de _région_, un intervalle 2D de points valables. Les objets se placent dans cette région, en stockant en leur sein les coordonnées de _points_ marquant leur positionnement dans l'espace (par exemple, #raw(lang: "rust", "Object::Rectangle") stocke deux `Point` pour définir ses coins)
275268276269277270#diagram(
···303296304297Ce modèle mental permet de travailler plus efficacement car il est bien plus proche de la manière dont on a tendance à penser l'art visuel: sur Illustrator par exemple, ce sont des objets, organisés en plusieurs couches, qui possèdent des attributs dictant leur remplissage.
305298306306-Les 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.
307307-299299+Les concepts de transformations et de filtres sont également très proche de ce qu'on peut retrouver dans des logiciels de traitement d'images raster, comme Photoshop.
308300309301== Découpage en modules
310302311311-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:
303303+Pour rendre la bibliothèque plus claire, et pouvoir éventuellement séparer la crate en plusieurs sous-crates et ainsi améliorer la vitesse de compilation @rustcompileunits dans le futur, la crate est découpée en plusieurs modules:
312304313305#grid(
314306 columns: (1fr, 1fr),
315307 gutter: 2em,
316308 [
317317- / geometry: partie purement géométrique de la bibliothèque, définissant `Point`, `Region` et leurs opérations utiles associées
309309+ / geometry: partie purement géométrique, définissant `Point`, `Region` et leurs opérations associées
318310 / graphics: définitions des objets et tout leurs aspects visuels (`Fill`, `Transform`, `Filter`, `Color`, `Object`, `ColoredObject`)
319311 / 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.
320320- / rendering: implémentation du rendu en SVG, et conversion en PNG
312312+ / rendering: implémentation du rendu en SVG et PNG
321313 / video: cf #ref(<crate::video>)
322314 / synchronization: cf #ref(<crate::synchronization>)
323315 / vst: cf #ref(<crate::vst>)
···339331340332= Rendu en images
341333342342-Maintenant que l'on a cette structure, il est bien évidemment essentiel de pouvoir la rendre en un fichier image exploitable, en PNG par exemple.
334334+Maintenant que l'on a cette structure, il est bien évidemment essentiel de pouvoir l'exporter en un fichier image exploitable, en PNG par exemple.
343335344344-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.
336336+L'idée est d'utiliser 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 également d'avoir 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.
345337346346-Ce 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:
338338+Ce processus de rendu est réalisé via l'implémentation d'un _trait_, une sorte d'équivalent en Rust des interfaces présentes dans les langages orientés objet @rusttraits:
347339348340#codesnippet(
349341 lang: "rust",
···354346 ),
355347)
356348357357-Ce _trait_ est ensuite implémenté par la plupart des structures de `shapemaker::graphics`:
349349+Ce _trait_ est ensuite implémenté par la plupart des structures de `shapemaker::graphics`, de la façon suivante:
358350359351/ 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
360360-/ Layer: rendu de l'ensemble des `ColoredObject` qu'elle contient, en les regroupant dans un groupe SVG #raw(lang: "svg", "<g>")
352352+/ Layer: rendu de l'ensemble des `ColoredObject` qu'elle contient, en les regroupant dans un groupe SVG #raw(lang: "svg", "<g>"), ce qui garanti l'ordre de superposition des objets qu'elle contient
361353/ ColoredObject: rendu de l'`Object` qu'il contient, en appliquant les transformations et filtres
362354/ 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.
363355/ Fill: dépend de la variante: simple attribut SVG `fill` pour `Fill::Solid`, utilisation de #raw(lang: "svg", "<pattern>") pour `Fill::Hatches`, etc.
364356/ Transform: attribut SVG `transform`
365357/ Filter: définition d'un #raw(lang: "svg", "<filter>") avec les attributs correspondants
366366-/ Color: utilise le `ColorMapping` donné pour réaliser sa variante en une valeur de couleur SVG (notation hexadécimale)
358358+/ Color: utilise le `ColorMapping` donné pour réifier sa variante#footnote["variante" dans le sens des _variantes d'un enum_, `Color` étant un enum de couleurs nommées, `Color::Black`, `Color::Pink`, etc.] en une valeur de couleur SVG (en notation hexadécimale)
367359368360#diagram(
369361 caption: [Objets rendables en SVG],
···397389 columns: (1fr, 1fr),
398390 gutter: 2em,
399391 [
400400- Les arguments `cell_size` et `object_sizes` permettent de réaliser en valeur concrètes (pixels) les valeurs de taille abstraites: la distance unitaire entre deux points est définie par `cell_size`, et les tailles des objets, qui, par choix, n'est pas contrôlable finement, sont définies par `object_sizes`.
392392+ Les arguments `cell_size` et `object_sizes` permettent de réaliser en valeur concrètes (pixels) les valeurs de taille abstraites: la distance unitaire entre deux points est définie par `cell_size`, et les tailles des objets, qui, par choix, ne sont pas finement contrôlables, sont définies par `object_sizes`.
401393 ],
402394 codesnippet(
395395+ caption: [Définition du type de `ObjectSizes`],
403396 lang: "rust",
404397 size: 0.87em,
405398 cut-around(
···410403 ),
411404)
412405413413-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.
406406+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_#footnote[Ce choix à première vue étonnant, qui consistue une perte de performance, est discuté au #ref(<perf-svgstring>), #ref(<perf-svgstring>, form: "page")], qui le transforme en un arbre de rendu, qui est ensuite rasterisé en une pixmap#footnote[Matrice plate de pixels RGBA], qui est finalement encodée en PNG puis écrite dans un fichier.
414407415408#diagram(
416409 caption: [Rendu d'un canvas SVG en PNG],
···426419 ```,
427420)
428421429429-Le passage par une string svg est évidemment une perte de performance, qui est discutée #ref(<perf-svgstring>, form: "page")
430430-431422432423= Render loop et hooks <crate::video>
433424434434-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.
425425+On peut maintenant rastériser un canvas. Passer à l'étape vidéo consiste 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 rendue 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 _réagir_ à des moments clés de la musique.
435426436427Pour 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.
437428438429Dans notre cas, on va donner les hooks suivants:
439430440440-/ each_beat: Appelé sur chaque nouveau temps fort de la musique
431431+/ each_beat: Appelé sur chaque battement de la musique
441432/ on_note: Appelé à chaque début de note jouée, par un ou des instruments en particulier à préciser
442433/ at_timestamp: Appelé une fois, à un instant précis de la vidéo
443434/ ...: et pleins d'autres
444435445445-Les hook stockent simplement deux fonctions: `when` pour savoir si le hook doit être exécuté à in instant précis, et `render_function` qui contient les actions à effectuer à cet instant.
436436+Un `Hook` est consistué de deux fonctions: `when` pour savoir si le hook doit être exécuté à un instant donné, et `render_function` qui décrit les modifications à effectuer sur le canvas.
446437447438#codesnippet(
448439 size: 0.85em,
···465456 ).replace("anyhow::Result", "Result"),
466457)
467458468468-Un 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.
459459+Un 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 effet qu'un seul canvas, qui est successivement modifié au cours de la vidéo.
469460470470-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"]
461461+Le paramètre générique #raw(lang: "rust", "<C>") existe car l'artiste peut définir des données additionnelles à stocker dans le contexte, ce dernier étant partagé entre les différentes exécutions des hooks. Par exemple: "quelle a été la dernière ligne de parole affichée? il faut passer à la prochaine"
471462472463On met également à disposition une méthode `with_hook`, qui rajoute un hook à la liste, permettant de facilement les définir:
473464···479470 lang: "rust",
480471 is_method: true,
481472 transform: it => (
482482- "impl Video<C> {\n ...\n"
483483- + it.replace("<AdditionalContext>", "<C>")
484484- + "\n}"
473473+ "impl Video<C> {\n ...\n" + it.replace("<AdditionalContext>", "<C>") + "\n}"
485474 ),
486475 ),
487476)
···496485 lang: "rust",
497486 is_method: true,
498487 transform: it => (
499499- "impl Video<C> {\n ...\n"
500500- + it.replace("<AdditionalContext>", "<C>")
501501- + "\n}"
488488+ "impl Video<C> {\n ...\n" + it.replace("<AdditionalContext>", "<C>") + "\n}"
502489 ),
503490 ),
504491)
505492506506-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:
493493+Le moteur de rendu vidéo est donc une boucle qui, à chaque itération, regarde dans l'ensemble des _hooks_ enregistrés, exécute ceux qui le demande, puis rastérise le canvas en une frame qui est ensuite donnée à l'encodeur vidéo:
507494508495#diagram(
509496 caption: [Pipeline],
···520507 color = "#f0f0f0"
521508522509 // Set specific weights to encourage circular layout
523523- "next frame" -> hooks [weight=2, label="Trigger"];
524524- hooks -> canvas [weight=2, label="Modify"];
525525- canvas -> frame [weight=2, label="Render"];
526526- frame -> "next frame" [weight=2];
510510+ "next frame" -> hooks // [label="Trigger"];
511511+ hooks -> canvas // [label="Modify"];
512512+ // is_fresh[shape=diamond, label="New frame?"]
513513+ is_fresh[shape=point, label=""]
514514+ canvas -> is_fresh [label="new frame?"];
515515+ is_fresh -> frame [label="Yes"];
516516+ is_fresh -> "next frame" [label="No"];
517517+ frame -> "next frame";
527518 }
528519529520 syncdata[label="sync data"];
···539530 syncdata -> "next frame"
540531541532 usercode[label="user code"];
542542- usercode -> hooks [label="Specifies"]
533533+ usercode -> hooks [style=dashed] // [label="Defines"]
543534544535 frame -> video
545536 syncdata -> audio -> video
···547538 ```,
548539)
549540550550-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.
541541+La boucle de rendu en elle-même itère sur *les instants de la vidéo, milliseconde par milliseconde, et non pas les frames*. C'est important pour garder la vidéo en synchronisation avec le son. J'avais initialement fait itérer la boucle sur les frames, et la vidéo se décalait progressivement de sa bande son#footnote[Ma théorie est qu'il faut itérer sur un sorte de dénominateur commun des deux pas temporels, sachant les informations de synchronisation de la musique ont un pas de temps bien plus court que le FPS de la vidéo].
551542552543#codesnippet(```rust
553544let render_ms_range = self.start_rendering_at..self.duration_ms();
···559550 context.frame = self.fps * context.ms / 1000;
560551```)
561552562562-On exécute bien les hooks à chaque itération de la boucle, mais par contre on ne rend une nouvelle frame que quand le numéro de frame change:
553553+On exécute bien les hooks à chaque itération de la boucle, mais par contre on ne rend une nouvelle frame uniquement si le numéro de frame change:
563554564555#codesnippet(
565556 dedent(
566557 cut-around(
567567- it => it
568568- .trim()
569569- .starts-with("if context.frame != previous_rendered_frame"),
558558+ it => it.trim().starts-with("if context.frame != previous_rendered_frame"),
570559 it => it.trim().ends-with("}"),
571560 read("../src/video/encoding.rs"),
572561 ),
573562 ),
574563)
575564576576-La 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>).
565565+La rastérisation et 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>)).
577566578567579568= Sources de synchronisation <crate::synchronization>
···582571583572Ce contexte, en plus de quelques informations déposées par la boucle de rendu (milliseconde actuelle, numéro de frame actuel, etc), contient surtout _des informations musicales sur l'instant présent_, comme les notes actuellement jouées, les amplitudes instantanées de chaque piste, etc.
584573585585-Afin d'obtenir ces information, il faut analyser quelque chose: la question est donc, de quels fichiers ou signaux tirer parti pour construire ces informations?
574574+Afin d'obtenir ces information, il faut bien analyser quelque chose: la question est donc: de quels fichiers ou signaux tirer parti pour construire ces informations de synchronisation?
586575587576Les sous-sections suivantes traites des différentes approches explorées:
588577589589-/ Amplitudes de _stems_: utilisation des signaux audio bruts depuis des exports piste par piste du morceau
590590-/ Analyser de fichiers MIDI: utilisation d'un standard stockant des informations de notes jouées.
591591-/ Analyse de fichiers .flp: utilisation des fichiers de projet de FL Studio, un logiciel de production musicale. C'est l'équivalent d'un fichier source en programmation
592592-/ Sondes dans le logiciel de MAO#footnote[MAO: Musique Assistée par Ordinateur]: utilisation de plugins VST pour envoyer des informations de synchronisation potentiellement arbitraire, directement depuis le logiciel de production musicale. //
593593-/ Temps réel: utilisation de signaux MIDI en "live", solution contournant le problème de la synchronisation et toute la partie rendu vidéo et rastérisation. Plutôt prévue pour un autre cas d'usage, les utilisations en concert et installations live
578578+/ Amplitudes _stems_-par-_stems_: utilisation des signaux audio bruts depuis des exports piste par piste du morceau
579579+/ Analyse de fichiers MIDI: utilisation d'un standard stockant les notes jouées dans le temps.
580580+/ Analyse de fichiers .flp: utilisation des fichiers de projet de FL Studio, un logiciel de production musicale. C'est l'équivalent d'un fichier source en programmation, là où l'export .mp3 serait l'équivalent d'un exécutable.
581581+/ Sondes dans le logiciel de MAO#footnote[MAO: Musique Assistée par Ordinateur]: utilisation de plugins VST pour envoyer des informations de synchronisation potentiellement arbitraire, directement depuis le logiciel de production musicale.
582582+/ Temps réel: utilisation de signaux MIDI en "live", solution contournant le problème de la synchronisation et toute la partie rendu vidéo et rastérisation. Plutôt prévue pour un autre cas d'usage, les concerts et installations live
594583595584Dans chacun de ces cas, l'objectif est de pouvoir inférer depuis ces ressources les informations suivantes:
596585597597-- Le BPM#footnote[Beats per minute, aussi appelé tempo] du morceau, avec éventuellement des évolutions au cours du morceau
598598-- 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"))
586586+- Le BPM#footnote[Beats per minute, aussi appelé tempo], avec éventuellement des évolutions au cours du morceau
587587+- Des 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"))
599588- Pour chaque instrument, et à chaque instant:
600589 - Les notes jouées: pitch#footnote[hauteur] et vélocité#footnote[intensité avec laquelle la note a été jouée]
601590 - 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)
602591603592604604-== Amplitudes de _stems_
593593+== Amplitudes _stems_-par-_stems_
605594606595Cette approche consiste à demander à l'artiste de fournir un fichier audio par piste du morceau de musique. On entend "piste" ici assez vaguement, plus le nombre de fichiers est grand, plus il est possible de réagir à des changements d'amplitudes individuels. En général, une piste correspond un-à-un à un instrument.
607596
+1-1
paper/template.typ
···7272 },
7373 number-align: center,
7474 )
7575- show raw: set text(size: 0.85em, font: "Martian Mono", weight: "bold")
7575+ show raw: set text(size: 0.85em, font: ("Martian Mono", "MartianMono NF"))
7676 set text(font: "New Computer Modern", lang: "fr")
7777 set raw(theme: "snazzylight.tmTheme")
7878 show math.equation: set text(weight: 400)