this repo has no description
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

✏️ Fix grammar issues

+54 -66
-1
examples/dna-analysis-machine/src/main.rs
··· 16 16 gray: "#81a0a8".into(), 17 17 cyan: "#4fecec".into(), 18 18 }); 19 - 20 19 canvas.set_grid_size(16, 9); 21 20 canvas.set_background(Color::Black); 22 21
+52 -63
paper/main.typ
··· 89 89 90 90 #text( 91 91 size: 0.88em, 92 - include-function( 93 - "../src/examples.rs", 94 - "dna_analysis_machine", 95 - lang: "rust", 96 - transform: it => "use shapemaker::*\n\n" + it, 97 - ), 98 - ) 92 + raw(lang: "rust", read("../examples/dna-analysis-machine/src/main.rs")), 93 + ) <demo-code> 99 94 100 95 #pagebreak() 101 96 ··· 148 143 "reflections", 149 144 "spline-optimisation", 150 145 "weaving", 151 - ).map(artwork => grid.cell( 152 - image("../examples/gallery/" + artwork + ".svg", width: 100%), 153 - )) 146 + ).map(artwork => grid.cell(image("../examples/gallery/" + artwork + ".svg", width: 100%))) 154 147 ), 155 148 ) 156 149 ··· 232 225 ), 233 226 ) 234 227 228 + Certaines ont été souvent renommées, beaucoup ont disparues, et certaines restent encore inconquises. 235 229 236 230 #work("reflets-citadins", ["Reflets Citadins", nommée par _Enide_]) 237 231 #work("paramount", ["Paramount"]) ··· 240 234 ["l'envolée du Cerf-Volant", nommée par _Nicolas C._], 241 235 ) 242 236 243 - Certaines ont été souvent renommées, beaucoup ont été volées, et certaines restent encore inconquises. 244 237 245 238 #work("danse-le-ciel", ["Danse le ciel"], with-context: true) 246 239 #work("bridging", [_Sans titre_], only-context: true) ··· 260 253 261 254 À 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_. 262 255 263 - 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. 256 + 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. 264 257 265 - 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_. 258 + 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_. 266 259 267 260 268 261 = Une _crate_ Rust avec un API sympathique 269 262 270 - 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. 263 + Pour implémenter cette génération, il faut donc donner un moyen à l'artiste de décrire son langage visuel. 271 264 272 - Ainsi, Shapemaker est une bibliothèque réutilisable, ou _crate_ dans l'écosystème Rust @rustcrates. 265 + 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"). 273 266 274 - 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")) 267 + 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) 275 268 276 269 277 270 #diagram( ··· 303 296 304 297 Ce 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. 305 298 306 - 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. 307 - 299 + 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. 308 300 309 301 == Découpage en modules 310 302 311 - 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: 303 + 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: 312 304 313 305 #grid( 314 306 columns: (1fr, 1fr), 315 307 gutter: 2em, 316 308 [ 317 - / geometry: partie purement géométrique de la bibliothèque, définissant `Point`, `Region` et leurs opérations utiles associées 309 + / geometry: partie purement géométrique, définissant `Point`, `Region` et leurs opérations associées 318 310 / graphics: définitions des objets et tout leurs aspects visuels (`Fill`, `Transform`, `Filter`, `Color`, `Object`, `ColoredObject`) 319 311 / 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. 320 - / rendering: implémentation du rendu en SVG, et conversion en PNG 312 + / rendering: implémentation du rendu en SVG et PNG 321 313 / video: cf #ref(<crate::video>) 322 314 / synchronization: cf #ref(<crate::synchronization>) 323 315 / vst: cf #ref(<crate::vst>) ··· 339 331 340 332 = Rendu en images 341 333 342 - 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. 334 + 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. 343 335 344 - 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. 336 + 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. 345 337 346 - 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: 338 + 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: 347 339 348 340 #codesnippet( 349 341 lang: "rust", ··· 354 346 ), 355 347 ) 356 348 357 - Ce _trait_ est ensuite implémenté par la plupart des structures de `shapemaker::graphics`: 349 + Ce _trait_ est ensuite implémenté par la plupart des structures de `shapemaker::graphics`, de la façon suivante: 358 350 359 351 / 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 360 - / Layer: rendu de l'ensemble des `ColoredObject` qu'elle contient, en les regroupant dans un groupe SVG #raw(lang: "svg", "<g>") 352 + / 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 361 353 / ColoredObject: rendu de l'`Object` qu'il contient, en appliquant les transformations et filtres 362 354 / 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. 363 355 / Fill: dépend de la variante: simple attribut SVG `fill` pour `Fill::Solid`, utilisation de #raw(lang: "svg", "<pattern>") pour `Fill::Hatches`, etc. 364 356 / Transform: attribut SVG `transform` 365 357 / Filter: définition d'un #raw(lang: "svg", "<filter>") avec les attributs correspondants 366 - / Color: utilise le `ColorMapping` donné pour réaliser sa variante en une valeur de couleur SVG (notation hexadécimale) 358 + / 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) 367 359 368 360 #diagram( 369 361 caption: [Objets rendables en SVG], ··· 397 389 columns: (1fr, 1fr), 398 390 gutter: 2em, 399 391 [ 400 - 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`. 392 + 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`. 401 393 ], 402 394 codesnippet( 395 + caption: [Définition du type de `ObjectSizes`], 403 396 lang: "rust", 404 397 size: 0.87em, 405 398 cut-around( ··· 410 403 ), 411 404 ) 412 405 413 - 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. 406 + 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. 414 407 415 408 #diagram( 416 409 caption: [Rendu d'un canvas SVG en PNG], ··· 426 419 ```, 427 420 ) 428 421 429 - Le passage par une string svg est évidemment une perte de performance, qui est discutée #ref(<perf-svgstring>, form: "page") 430 - 431 422 432 423 = Render loop et hooks <crate::video> 433 424 434 - 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. 425 + 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. 435 426 436 427 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. 437 428 438 429 Dans notre cas, on va donner les hooks suivants: 439 430 440 - / each_beat: Appelé sur chaque nouveau temps fort de la musique 431 + / each_beat: Appelé sur chaque battement de la musique 441 432 / on_note: Appelé à chaque début de note jouée, par un ou des instruments en particulier à préciser 442 433 / at_timestamp: Appelé une fois, à un instant précis de la vidéo 443 434 / ...: et pleins d'autres 444 435 445 - 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. 436 + 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. 446 437 447 438 #codesnippet( 448 439 size: 0.85em, ··· 465 456 ).replace("anyhow::Result", "Result"), 466 457 ) 467 458 468 - 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. 459 + 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. 469 460 470 - 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"] 461 + 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" 471 462 472 463 On met également à disposition une méthode `with_hook`, qui rajoute un hook à la liste, permettant de facilement les définir: 473 464 ··· 479 470 lang: "rust", 480 471 is_method: true, 481 472 transform: it => ( 482 - "impl Video<C> {\n ...\n" 483 - + it.replace("<AdditionalContext>", "<C>") 484 - + "\n}" 473 + "impl Video<C> {\n ...\n" + it.replace("<AdditionalContext>", "<C>") + "\n}" 485 474 ), 486 475 ), 487 476 ) ··· 496 485 lang: "rust", 497 486 is_method: true, 498 487 transform: it => ( 499 - "impl Video<C> {\n ...\n" 500 - + it.replace("<AdditionalContext>", "<C>") 501 - + "\n}" 488 + "impl Video<C> {\n ...\n" + it.replace("<AdditionalContext>", "<C>") + "\n}" 502 489 ), 503 490 ), 504 491 ) 505 492 506 - 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: 493 + 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: 507 494 508 495 #diagram( 509 496 caption: [Pipeline], ··· 520 507 color = "#f0f0f0" 521 508 522 509 // Set specific weights to encourage circular layout 523 - "next frame" -> hooks [weight=2, label="Trigger"]; 524 - hooks -> canvas [weight=2, label="Modify"]; 525 - canvas -> frame [weight=2, label="Render"]; 526 - frame -> "next frame" [weight=2]; 510 + "next frame" -> hooks // [label="Trigger"]; 511 + hooks -> canvas // [label="Modify"]; 512 + // is_fresh[shape=diamond, label="New frame?"] 513 + is_fresh[shape=point, label=""] 514 + canvas -> is_fresh [label="new frame?"]; 515 + is_fresh -> frame [label="Yes"]; 516 + is_fresh -> "next frame" [label="No"]; 517 + frame -> "next frame"; 527 518 } 528 519 529 520 syncdata[label="sync data"]; ··· 539 530 syncdata -> "next frame" 540 531 541 532 usercode[label="user code"]; 542 - usercode -> hooks [label="Specifies"] 533 + usercode -> hooks [style=dashed] // [label="Defines"] 543 534 544 535 frame -> video 545 536 syncdata -> audio -> video ··· 547 538 ```, 548 539 ) 549 540 550 - 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. 541 + 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]. 551 542 552 543 #codesnippet(```rust 553 544 let render_ms_range = self.start_rendering_at..self.duration_ms(); ··· 559 550 context.frame = self.fps * context.ms / 1000; 560 551 ```) 561 552 562 - 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: 553 + 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: 563 554 564 555 #codesnippet( 565 556 dedent( 566 557 cut-around( 567 - it => it 568 - .trim() 569 - .starts-with("if context.frame != previous_rendered_frame"), 558 + it => it.trim().starts-with("if context.frame != previous_rendered_frame"), 570 559 it => it.trim().ends-with("}"), 571 560 read("../src/video/encoding.rs"), 572 561 ), 573 562 ), 574 563 ) 575 564 576 - 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>). 565 + 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>)). 577 566 578 567 579 568 = Sources de synchronisation <crate::synchronization> ··· 582 571 583 572 Ce 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. 584 573 585 - 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? 574 + 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? 586 575 587 576 Les sous-sections suivantes traites des différentes approches explorées: 588 577 589 - / Amplitudes de _stems_: utilisation des signaux audio bruts depuis des exports piste par piste du morceau 590 - / Analyser de fichiers MIDI: utilisation d'un standard stockant des informations de notes jouées. 591 - / 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 592 - / 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. // 593 - / 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 578 + / Amplitudes _stems_-par-_stems_: utilisation des signaux audio bruts depuis des exports piste par piste du morceau 579 + / Analyse de fichiers MIDI: utilisation d'un standard stockant les notes jouées dans le temps. 580 + / 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. 581 + / 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. 582 + / 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 594 583 595 584 Dans chacun de ces cas, l'objectif est de pouvoir inférer depuis ces ressources les informations suivantes: 596 585 597 - - Le BPM#footnote[Beats per minute, aussi appelé tempo] du morceau, avec éventuellement des évolutions au cours du morceau 598 - - 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")) 586 + - Le BPM#footnote[Beats per minute, aussi appelé tempo], avec éventuellement des évolutions au cours du morceau 587 + - 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")) 599 588 - Pour chaque instrument, et à chaque instant: 600 589 - Les notes jouées: pitch#footnote[hauteur] et vélocité#footnote[intensité avec laquelle la note a été jouée] 601 590 - 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) 602 591 603 592 604 - == Amplitudes de _stems_ 593 + == Amplitudes _stems_-par-_stems_ 605 594 606 595 Cette 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. 607 596
+1 -1
paper/template.typ
··· 72 72 }, 73 73 number-align: center, 74 74 ) 75 - show raw: set text(size: 0.85em, font: "Martian Mono", weight: "bold") 75 + show raw: set text(size: 0.85em, font: ("Martian Mono", "MartianMono NF")) 76 76 set text(font: "New Computer Modern", lang: "fr") 77 77 set raw(theme: "snazzylight.tmTheme") 78 78 show math.equation: set text(weight: 400)
+1 -1
src/video/engine.rs
··· 243 243 when: Box::new(move |_, ctx, _, _| { 244 244 stems 245 245 .split(',') 246 - .map(|n| ctx.stem(n.trim())) 246 + .map(|stem_name| ctx.stem(stem_name.trim())) 247 247 .any(|stem| stem.notes.iter().any(|note| note.is_on())) 248 248 }), 249 249 render_function: Box::new(render_function),