this repo has no description
3
fork

Configure Feed

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

:memo: Almost finished, missing end of performance and conclusion

+798 -202
+158 -6
paper/bibliography.yaml
··· 79 79 type: artwork 80 80 author: Victor Vasarely 81 81 date: 1964 82 - publisher: 82 + publisher: 83 83 name: Fondation Vasarely 84 84 location: Alvéole 5, 1 avenue Marcel Pagnol, 13090 Aix-en-Provence, France 85 85 # IEEE sadly doesn't care about those ··· 99 99 title: Space Invaders 100 100 type: article 101 101 author: The European Space Agency 102 - url: 102 + url: 103 103 value: https://www.esa.int/Space_in_Member_States/France/Highlights/Space_Invaders 104 104 date: 2025-03-23 105 105 date: 2015-03-14 ··· 121 121 rustcrates: 122 122 title: The Rust Programming Language § 7.1 Packages and Crates 123 123 type: book 124 - author: 124 + author: 125 125 - Steve Klabnik 126 126 - Carol Nichols 127 127 - Chris Krycho 128 128 url: 129 129 value: https://doc.rust-lang.org/book/ch07-01-packages-and-crates.html 130 130 date: 2025-03-23 131 - 131 + 132 132 rusttraits: 133 133 title: "The Rust Programming Language § 10.2 Traits: Defining Shared Behavior" 134 134 type: book 135 - author: 135 + author: 136 136 - Steve Klabnik 137 137 - Carol Nichols 138 138 - Chris Krycho ··· 145 145 type: repository 146 146 author: 147 147 - Yevhenii Reizner 148 - publisher: 148 + publisher: 149 149 name: Linebender 150 150 url: 151 151 value: https://github.com/linebender/resvg 152 152 date: 2025-03-23 153 + 154 + hooks: 155 + title: What are hooks? 156 + type: web 157 + author: Jorge Israel Peña 158 + url: 159 + value: https://softwareengineering.stackexchange.com/a/13748 160 + date: 2025-03-24 161 + 162 + remixconteststems: 163 + title: Will Clarke & Armada Music Launch 'Midnight Mass' Remix Contest 164 + type: web 165 + author: [LabelRadar, Armada Music] 166 + publisher: Beatportal 167 + abstract: DOWNLOAD THE STEMS HERE! 168 + date: 2025-03-12 169 + url: 170 + value: https://www.beatportal.com/articles/866279-will-clarke-armada-music-launch-midnight-mass-remix-contest 171 + date: 2025-03-24 172 + 173 + sourcesep: 174 + title: "Wave-U-Net: A Multi-Scale Neural Network for End-to-End Audio Source Separation" 175 + type: article 176 + author: [Daniel Stoller, Sebastian Ewert, Simon Dixon] 177 + date: 2018-09-14 178 + publisher: 179 + name: 19th International Society for Music Information Retrieval Conference (ISMIR 2018) 180 + serial-number: 181 + arxiv: "1806.03185" 182 + doi: "10.48550/arXiv.1806.03185" 183 + url: 184 + value: https://arxiv.org/abs/1806.03185 185 + date: 2025-03-24 186 + 187 + onset: 188 + title: Onset (Album Version) 189 + type: artwork 190 + author: [Postamble] 191 + date: 2024-07-01 192 + url: 193 + value: https://gwen.works/emoti*ns 194 + date: 2025-03-24 195 + 196 + midispec: 197 + title: Summary of MIDI 1.0 Messages 198 + type: reference 199 + author: [MIDI Association] 200 + url: 201 + value: https://midi.org/summary-of-midi-1-0-messages 202 + date: 2025-03-24 203 + 204 + midippq: 205 + title: MIDI for Musicians 206 + type: book 207 + author: [Craig Anderton] 208 + date: 1986 209 + publisher: Amsco Publications 210 + page-range: 8-10 211 + archive: Internet Archive 212 + serial-number: 213 + isbn: 978-0-8256-1050-9 214 + url: 215 + value: https://archive.org/details/midiformusicians0000ande 216 + date: 2025-03-24 217 + 218 + pyflp: 219 + title: pyflp 2.2.1 220 + type: repository 221 + author: 222 + demberto 223 + # alias: demberto 224 + publisher: PyPI 225 + date: 2023-06-05 226 + url: 227 + value: https://pypi.org/project/pyflp/ 228 + date: 2025-03-24 229 + 230 + pyflp3.12: 231 + title: 🐞 Python 3.12 Enum issue 232 + type: article 233 + author: 234 + kingkongjames 235 + # alias: kingkongjames 236 + publisher: GitHub 237 + date: 2023-11-07 238 + url: 239 + value: https://github.com/demberto/PyFLP/issues/183 240 + date: 2025-03-24 241 + 242 + pyo3: 243 + title: PyO3 244 + type: repository 245 + author: 246 + - David Hewitt 247 + - Yuji Kanagawa 248 + - Nikolay Kim 249 + - knostin 250 + - Daniel Grunwald 251 + - Bruno Kolenbrander 252 + - Icxolu 253 + publisher: GitHub 254 + url: 255 + value: https://github.com/PyO3/pyo3 256 + date: 2025-03-24 257 + 258 + pianoteq: 259 + title: Pianoteq Overview 260 + type: web 261 + author: Modartt 262 + url: 263 + value: https://www.modartt.com/pianoteq_overview 264 + date: 2025-03-24 265 + 266 + serum: 267 + title: "Serum: Advanced Wavetable Synthesizer" 268 + type: web 269 + author: Xfer Records 270 + url: 271 + value: https://xferrecords.com/products/serum 272 + date: 2025-03-24 273 + 274 + vst: 275 + title: VST 3 Interfaces Documentation 276 + type: reference 277 + author: Steinberg 278 + url: 279 + value: https://steinbergmedia.github.io/vst3_doc/vstinterfaces/index.html 280 + date: 2025-03-24 281 + 282 + mikudayooo: 283 + title: About HATSUNE MIKU 284 + type: web 285 + author: Crypton Future Media 286 + url: 287 + value: https://ec.crypton.co.jp/pages/prod/virtualsinger/cv01_us 288 + date: 2025-03-24 289 + 290 + nihplug: 291 + title: nih_plug 292 + type: reference 293 + author: Robbert van der Helm 294 + url: 295 + value: https://nih-plug.robbertvanderhelm.nl/nih_plug/ 296 + date: 2025-03-24 297 + 298 + vcpkg: 299 + title: vcpkg - Open source C/C++ dependency manager 300 + type: web 301 + author: Microsoft 302 + url: 303 + value: https://vcpkg.io/en/ 304 + date: 2025-03-24
+27 -27
paper/colorshed.svg
··· 4 4 <g data-object="--orange_bg" style="fill: red;transform-box: fill-box;" transform-origin="25 75"> 5 5 <rect height="50" width="50" x="0" y="50"/> 6 6 </g> 7 + <g data-object="--cyan_bg" style="fill: yellow;transform-box: fill-box;" transform-origin="75 25"> 8 + <rect height="50" width="50" x="50" y="0"/> 9 + </g> 10 + <g data-object="--red_bg" style="fill: brown;transform-box: fill-box;" transform-origin="75 75"> 11 + <rect height="50" width="50" x="50" y="50"/> 12 + </g> 7 13 <g data-object="--brown_bg" style="fill: purple;transform-box: fill-box;" transform-origin="125 75"> 8 14 <rect height="50" width="50" x="100" y="50"/> 9 15 </g> 10 16 <g data-object="--yellow_bg" style="fill: orange;transform-box: fill-box;" transform-origin="125 25"> 11 17 <rect height="50" width="50" x="100" y="0"/> 12 18 </g> 19 + <g data-object="--purple_bg" style="fill: pink;transform-box: fill-box;" transform-origin="25 125"> 20 + <rect height="50" width="50" x="0" y="100"/> 21 + </g> 13 22 <g data-object="--pink_bg" style="fill: green;transform-box: fill-box;" transform-origin="75 125"> 14 23 <rect height="50" width="50" x="50" y="100"/> 15 24 </g> 16 - <g data-object="--blue_bg" style="fill: cyan;transform-box: fill-box;" transform-origin="25 25"> 17 - <rect height="50" width="50" x="0" y="0"/> 18 - </g> 19 - <g data-object="--cyan_bg" style="fill: yellow;transform-box: fill-box;" transform-origin="75 25"> 20 - <rect height="50" width="50" x="50" y="0"/> 21 - </g> 22 25 <g data-object="--green_bg" style="fill: blue;transform-box: fill-box;" transform-origin="125 125"> 23 26 <rect height="50" width="50" x="100" y="100"/> 24 27 </g> 25 - <g data-object="--purple_bg" style="fill: pink;transform-box: fill-box;" transform-origin="25 125"> 26 - <rect height="50" width="50" x="0" y="100"/> 27 - </g> 28 - <g data-object="--red_bg" style="fill: brown;transform-box: fill-box;" transform-origin="75 75"> 29 - <rect height="50" width="50" x="50" y="50"/> 28 + <g data-object="--blue_bg" style="fill: cyan;transform-box: fill-box;" transform-origin="25 25"> 29 + <rect height="50" width="50" x="0" y="0"/> 30 30 </g> 31 31 </g> 32 32 <g class="layer" data-layer="circles"> 33 - <g data-object="--blue" style="fill: blue;transform-box: fill-box;" transform-origin="25 25"> 34 - <circle cx="25" cy="25" r="25"/> 35 - </g> 36 - <g data-object="--cyan" style="fill: cyan;transform-box: fill-box;" transform-origin="75 25"> 37 - <circle cx="75" cy="25" r="25"/> 38 - </g> 39 - <g data-object="--yellow" style="fill: yellow;transform-box: fill-box;" transform-origin="125 25"> 40 - <circle cx="125" cy="25" r="25"/> 41 - </g> 42 - <g data-object="--red" style="fill: red;transform-box: fill-box;" transform-origin="75 75"> 43 - <circle cx="75" cy="75" r="25"/> 44 - </g> 45 33 <g data-object="--pink" style="fill: pink;transform-box: fill-box;" transform-origin="75 125"> 46 34 <circle cx="75" cy="125" r="25"/> 47 35 </g> 48 36 <g data-object="--green" style="fill: green;transform-box: fill-box;" transform-origin="125 125"> 49 37 <circle cx="125" cy="125" r="25"/> 50 38 </g> 51 - <g data-object="--purple" style="fill: purple;transform-box: fill-box;" transform-origin="25 125"> 52 - <circle cx="25" cy="125" r="25"/> 39 + <g data-object="--orange" style="fill: orange;transform-box: fill-box;" transform-origin="25 75"> 40 + <circle cx="25" cy="75" r="25"/> 41 + </g> 42 + <g data-object="--red" style="fill: red;transform-box: fill-box;" transform-origin="75 75"> 43 + <circle cx="75" cy="75" r="25"/> 44 + </g> 45 + <g data-object="--cyan" style="fill: cyan;transform-box: fill-box;" transform-origin="75 25"> 46 + <circle cx="75" cy="25" r="25"/> 53 47 </g> 54 48 <g data-object="--brown" style="fill: brown;transform-box: fill-box;" transform-origin="125 75"> 55 49 <circle cx="125" cy="75" r="25"/> 56 50 </g> 57 - <g data-object="--orange" style="fill: orange;transform-box: fill-box;" transform-origin="25 75"> 58 - <circle cx="25" cy="75" r="25"/> 51 + <g data-object="--blue" style="fill: blue;transform-box: fill-box;" transform-origin="25 25"> 52 + <circle cx="25" cy="25" r="25"/> 53 + </g> 54 + <g data-object="--yellow" style="fill: yellow;transform-box: fill-box;" transform-origin="125 25"> 55 + <circle cx="125" cy="25" r="25"/> 56 + </g> 57 + <g data-object="--purple" style="fill: purple;transform-box: fill-box;" transform-origin="25 125"> 58 + <circle cx="25" cy="125" r="25"/> 59 59 </g> 60 60 </g> 61 61 <defs/>
paper/dna-analysis-machine.png

This is a binary file and will not be displayed.

paper/flstudiomarkers.png

This is a binary file and will not be displayed.

paper/flstudiomidimacro.png

This is a binary file and will not be displayed.

paper/flstudioprobe.png

This is a binary file and will not be displayed.

+14 -14
paper/grid.svg
··· 1 1 <svg height="170" viewBox="-10 -10 170 170" width="170" xmlns="http://www.w3.org/2000/svg"> 2 2 <rect fill="white" height="170" width="170" x="-10" y="-10"/> 3 3 <g class="layer" data-layer="root"> 4 - <g data-object="--(1, 0)" style="fill: black;transform-box: fill-box;" transform-origin="75 25"> 5 - <circle cx="50" cy="0" r="2"/> 6 - </g> 7 - <g data-object="--(2, 0)" style="fill: black;transform-box: fill-box;" transform-origin="125 25"> 8 - <circle cx="100" cy="0" r="2"/> 9 - </g> 10 - <g data-object="--(0, 1)" style="fill: black;transform-box: fill-box;" transform-origin="25 75"> 11 - <circle cx="0" cy="50" r="2"/> 4 + <g data-object="--(2, 1)" style="fill: black;transform-box: fill-box;" transform-origin="125 75"> 5 + <circle cx="100" cy="50" r="2"/> 12 6 </g> 13 7 <g data-object="--(0, 2)" style="fill: black;transform-box: fill-box;" transform-origin="25 125"> 14 8 <circle cx="0" cy="100" r="2"/> ··· 16 10 <g data-object="--(0, 0)" style="fill: black;transform-box: fill-box;" transform-origin="25 25"> 17 11 <circle cx="0" cy="0" r="2"/> 18 12 </g> 19 - <g data-object="--(1, 1)" style="fill: black;transform-box: fill-box;" transform-origin="75 75"> 20 - <circle cx="50" cy="50" r="2"/> 13 + <g data-object="--(0, 1)" style="fill: black;transform-box: fill-box;" transform-origin="25 75"> 14 + <circle cx="0" cy="50" r="2"/> 21 15 </g> 22 - <g data-object="--(2, 2)" style="fill: black;transform-box: fill-box;" transform-origin="125 125"> 23 - <circle cx="100" cy="100" r="2"/> 16 + <g data-object="--(2, 0)" style="fill: black;transform-box: fill-box;" transform-origin="125 25"> 17 + <circle cx="100" cy="0" r="2"/> 24 18 </g> 25 19 <g data-object="--(1, 2)" style="fill: black;transform-box: fill-box;" transform-origin="75 125"> 26 20 <circle cx="50" cy="100" r="2"/> 27 21 </g> 28 - <g data-object="--(2, 1)" style="fill: black;transform-box: fill-box;" transform-origin="125 75"> 29 - <circle cx="100" cy="50" r="2"/> 22 + <g data-object="--(1, 0)" style="fill: black;transform-box: fill-box;" transform-origin="75 25"> 23 + <circle cx="50" cy="0" r="2"/> 24 + </g> 25 + <g data-object="--(2, 2)" style="fill: black;transform-box: fill-box;" transform-origin="125 125"> 26 + <circle cx="100" cy="100" r="2"/> 27 + </g> 28 + <g data-object="--(1, 1)" style="fill: black;transform-box: fill-box;" transform-origin="75 75"> 29 + <circle cx="50" cy="50" r="2"/> 30 30 </g> 31 31 </g> 32 32 <defs/>
paper/hwccorrect.png

This is a binary file and will not be displayed.

paper/hwcwrong.png

This is a binary file and will not be displayed.

paper/main.pdf

This is a binary file and will not be displayed.

+574 -136
paper/main.typ
··· 1 - #import "template.typ": arkheion, arkheion-appendices 2 - #import "utils.typ": include-function, cut-around, cut-between 1 + #import "template.typ": arkheion, arkheion-appendices, monospace 2 + #import "utils.typ": include-function, cut-around, cut-between, dedent 3 3 4 4 #import "@preview/diagraph:0.3.2" 5 5 #show raw.where(lang: "dot"): it => diagraph.render(it.text) ··· 19 19 scale(size, content, reflow: true), 20 20 ) 21 21 22 - #let codesnippet(caption: "", content, lang: "rust", size: 1em) = text(size: size, block( 23 - inset: 1.5em, 24 - fill: luma(230), 22 + #let breakout(content) = block( 23 + inset: 1em, 24 + fill: luma(240), 25 25 radius: 4pt, 26 26 width: 100%, 27 - breakable: false, 28 - raw( 29 - lang: lang, 30 - content, 31 - ), 32 - )) 27 + pad(x: 1em, align(center, text(size: 1.1em, content))), 28 + ) 29 + 30 + #let codesnippet(caption: "", content, lang: "rust", size: 1em) = { 31 + let snip = text( 32 + size: size, 33 + block( 34 + inset: 1.5em, 35 + fill: luma(240), 36 + radius: 4pt, 37 + width: 100%, 38 + // Figure itself is already non breakable, afaik 39 + breakable: caption != "", 40 + if type(content) == str { 41 + raw( 42 + lang: lang, 43 + content, 44 + ) 45 + } else { 46 + content 47 + }, 48 + ), 49 + ) 50 + 51 + if caption != "" { 52 + figure(caption: caption, align(left, snip)) 53 + } else { 54 + snip 55 + } 56 + } 33 57 34 58 #show link: underline 35 59 ··· 43 67 ), 44 68 ), 45 69 date: [#datetime.today().day() Mars 2025], 46 - keywords: ( 47 - "audiovisuel", 48 - "procédural", 49 - "SVG", 50 - "Rust", 51 - "WASM", 52 - "WebMIDI", 53 - "VST", 54 - ), 70 + // keywords: ( 71 + // "audiovisuel", 72 + // "procédural", 73 + // "DSP", 74 + // "SVG", 75 + // "Rust", 76 + // "WASM", 77 + // "MIDI", 78 + // "VST", 79 + // ), 55 80 ) 56 81 57 82 ··· 71 96 = Introduction 72 97 73 98 == À la recherche d'une impossible énumération des formes 74 - 75 - 76 99 77 100 #grid( 78 101 columns: (1fr, 1.5fr), ··· 378 401 digraph { 379 402 rankdir="LR"; 380 403 node [shape="record"]; 381 - "svg tree" -> "svg string" 382 - "svg string" -> "usvg tree" 383 - "usvg tree" -> "pixmap" 384 - pixmap -> "png file" 404 + "svg tree" -> "svg string" 405 + "svg string" -> "usvg tree" 406 + "usvg tree" -> "pixmap" 407 + pixmap -> "png file" 385 408 } 386 - ``` 409 + ```, 387 410 ) 388 411 389 - Le passage par une string svg est évidemment une perte de performance, qui est discutée #ref(<perf-svgstring>, form: "page") 412 + Le passage par une string svg est évidemment une perte de performance, qui est discutée #ref(<perf-svgstring>, form: "page") 390 413 391 414 392 415 = Render loop et hooks 393 416 417 + 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. 418 + 419 + 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. 420 + 421 + Dans notre cas, on va donner les hooks suivants: 422 + 423 + / each_beat: Appelé sur chaque nouveau temps fort de la musique 424 + / on_note: Appelé à chaque début de note jouée, par un ou des instruments en particulier à préciser 425 + / at_timestamp: Appelé une fois, à un instant précis de la vidéo 426 + / ...: et pleins d'autres 427 + 428 + 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. 429 + 430 + #codesnippet( 431 + size: 0.85em, 432 + cut-around( 433 + it => it.trim().starts-with("pub struct Hook"), 434 + it => it == "}", 435 + read("../src/video/engine.rs"), 436 + ) 437 + + "\n\n" 438 + + cut-around( 439 + it => it.trim().starts-with("pub type HookCondition"), 440 + it => it.trim().ends-with(";"), 441 + read("../src/video/engine.rs"), 442 + ) 443 + + "\n\n" 444 + + cut-around( 445 + it => it.trim().starts-with("pub type RenderFunction"), 446 + it => it.trim().ends-with(";"), 447 + read("../src/video/engine.rs"), 448 + ).replace("anyhow::Result", "Result"), 449 + ) 450 + 451 + 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. 452 + 453 + 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"] 454 + 455 + On met également à disposition une méthode `with_hook`, qui rajoute un hook à la liste, permettant de facilement les définir: 456 + 457 + 458 + #codesnippet( 459 + include-function( 460 + "../src/video/engine.rs", 461 + "with_hook", 462 + lang: "rust", 463 + is_method: true, 464 + transform: it => ( 465 + "impl Video<C> {\n ...\n" 466 + + it.replace("<AdditionalContext>", "<C>") 467 + + "\n}" 468 + ), 469 + ), 470 + ) 471 + 472 + Voici par exemple la définition du hook `on_note`: 473 + 474 + #codesnippet( 475 + size: 0.9em, 476 + include-function( 477 + "../src/video/engine.rs", 478 + "on_note", 479 + lang: "rust", 480 + is_method: true, 481 + transform: it => ( 482 + "impl Video<C> {\n ...\n" 483 + + it.replace("<AdditionalContext>", "<C>") 484 + + "\n}" 485 + ), 486 + ), 487 + ) 488 + 489 + 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: 490 + 394 491 #diagram( 395 492 caption: [Pipeline], 396 493 size: 60%, ··· 433 530 ```, 434 531 ) 435 532 533 + 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. 534 + 535 + #codesnippet(```rust 536 + let render_ms_range = self.start_rendering_at..self.duration_ms(); 537 + 538 + let mut frames_to_encode: Vec<(Time, String)> = vec![]; 539 + 540 + for _ in render_ms_range.into_iter() { 541 + context.ms += 1_usize; 542 + context.frame = self.fps * context.ms / 1000; 543 + ```) 544 + 545 + 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: 546 + 547 + #codesnippet( 548 + dedent( 549 + cut-around( 550 + it => it 551 + .trim() 552 + .starts-with("if context.frame != previous_rendered_frame"), 553 + it => it.trim().ends-with("}"), 554 + read("../src/video/encoding.rs"), 555 + ), 556 + ), 557 + ) 558 + 559 + 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>). 436 560 437 561 438 562 = Sources de synchronisation 439 563 564 + On 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_. 565 + 566 + 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. 567 + 568 + 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? 569 + 570 + Les sous-sections suivantes traites des différentes approches explorées: 571 + 572 + / Amplitudes de _stems_: utilisation des signaux audio bruts depuis des exports piste par piste du morceau 573 + / Analyser de fichiers MIDI: utilisation d'un standard stockant des informations de notes jouées. 574 + / 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 575 + / 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. // 576 + / 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 577 + 578 + Dans chacun de ces cas, l'objectif est de pouvoir inférer depuis ces ressources les informations suivantes: 579 + 580 + - Le BPM#footnote[Beats per minute, aussi appelé tempo] du morceau, avec éventuellement des évolutions au cours du morceau 581 + - 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")) 582 + - Pour chaque instrument, et à chaque instant: 583 + - Les notes jouées: pitch#footnote[hauteur] et vélocité#footnote[intensité avec laquelle la note a été jouée] 584 + - 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) 585 + 586 + 587 + == Amplitudes de _stems_ 588 + 589 + 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. 590 + 591 + === Accessibilité 592 + 593 + Exporter un projet en fichiers audios piste-par-piste, des _stems_, est une pratique plutôt courante, par exemple lors de concours de remix @remixconteststems, pour fournir aux participant·e·s les éléments du morceau séparés et ainsi faciliter la création d'un remix. 594 + 595 + On pourrait faciliter encore plus l'usage en, par exemple, proposant de faire de la séparation de source par réseaux neuronaux si l'artiste ne peut pas ou ne souhaite pas faire un export en stems @sourcesep. Cette approche serait d'autant plus utile car l'on n'a pas le besoin ici d'une qualité sonore sur les pistes séparées, étant donné que l'on ne s'en sert qu'à des fins d'analyse pour de la synchronisation. 596 + 597 + 598 + 599 + === Performance 600 + 601 + Néanmoins, ce processus de lire dans une structure de donnée les amplitudes à chaque instant reste assez coûteux, que ce soit en temps de calcul ou en mémoire. 602 + 603 + === Faisabilité 604 + 605 + De plus, la correspondance signal $mapsto$ note jouée est beaucoup moins évidente qu'elle n'en paraît. Un signal peut être décomposé en amplitude et fréquence, mais une note possède deux caractéristiques bien plus utiles aux musicien·ne·s: 606 + 607 + / Vélocité $cancel(arrow.l.bar)$ amplitude: Les amplitudes d'un signal sont très variables, et il est difficile de déterminer un seuil de déclenchement efficace, en prenant en compte la présence d'effets (en particulier l'echo ou la réverbération). 608 + / Pitch $arrow.l.bar$ fréquence: Pour obtenir le pitch d'une note, il faut effectuer une analyse fréquentielle du signal. Ceci pourrait à priori ne pas être trop complexe, mais n'a pas été tenté étant donné les difficultés soulevées par le point précédent. Il est en plus très difficile de séparer plusieurs notes d'un accord. 609 + 610 + 611 + == Export MIDI 612 + 613 + Cette méthode consiste d'une certaine manière à prendre le problème "à l'envers" par rapport à la méthode précédente: on part d'information _sur les notes jouées_, desquelles on peut dériver les amplitudes, depuis la vélocité. 614 + 615 + === Faisabilité 616 + 617 + Le format MIDI @midispec permet de spécifier: 618 + 619 + - Pour chaque piste: les notes jouées (pitch et vélocité) 620 + - Pour le morceau dans sa globalité, le BPM 621 + 622 + Bien que l'on puisse assez facilement inférer une sorte d'amplitude simulée à partir des vélocités, le problème inverse se pose: si l'on veut animer un objet en prenant en compte les échos, par exemple, MIDI ne peut pas nous aider. 623 + 624 + Mais pour de nombreux usages, le résultat final paraît beaucoup plus "en réaction avec la musique" qu'avec une approche par amplitudes réelles, certainement grâce à la précision apportée par le fait d'utiliser les évènements de notes jouées "à la source". 625 + 626 + ==== Ticks MIDI 627 + 628 + Pour l'implémentation, rien de bien compliqué, on rajoute les notes une à une dans notre structure de données en partant des évènements MIDI: 629 + 630 + #codesnippet( 631 + lang: "rust", 632 + dedent( 633 + cut-around( 634 + it => it.trim().starts-with("match message"), 635 + it => it == " }", 636 + read("../src/synchronization/midi.rs"), 637 + ), 638 + ), 639 + ) 640 + 641 + 642 + …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. 643 + 644 + 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. 645 + 646 + #codesnippet( 647 + include-function( 648 + "../src/synchronization/midi.rs", 649 + "midi_tick_to_ms", 650 + lang: "rust", 651 + ), 652 + ) 653 + 654 + Pour passer de ticks à des millisecondes réelles, il faut réifier ces ticks en lisant le BPM, *qui peut changer au cours du morceau*. Les changements de BPM sont des évènements MIDI parmi le stream plat du fichier. 655 + 656 + #codesnippet[ 657 + ```rust 658 + // Convert deltas to absolute ticks 659 + let mut track_no = 0; 660 + for track in midifile.tracks.iter() { 661 + track_no += 1; 662 + let mut absolute_tick = 0; 663 + for event in track { 664 + absolute_tick += event.delta.as_int(); 665 + timeline 666 + .entry(absolute_tick) 667 + .or_default() 668 + .insert(track_names[&track_no].clone(), *event); 669 + } 670 + } 671 + 672 + // Convert ticks to ms 673 + let mut absolute_tick_to_ms = HashMap::<u32, usize>::new(); 674 + let mut last_tick = 0; 675 + for (tick, tracks) in timeline.iter().sorted_by_key(|(tick, _)| *tick) { 676 + for event in tracks.values() { 677 + if let TrackEventKind::Meta(MetaMessage::Tempo(tempo)) = event.kind { 678 + now.tempo = tempo.as_int() as usize; 679 + } 680 + } 681 + let delta = tick - last_tick; 682 + last_tick = *tick; 683 + now.ms += midi_tick_to_ms(delta, now.tempo, now.ticks_per_beat as usize); 684 + absolute_tick_to_ms.insert(*tick, now.ms); 685 + } 686 + ``` 687 + ] 688 + 689 + 690 + 691 + === Performance 692 + 693 + L'inférence d'amplitudes à partir des vélocités est assez coûteuse. La raison de ce coût n'a pas encore été étudiée. 694 + 695 + === Accessibilité 696 + 697 + Malheureusement, là où l'export d'un projet musical en stems se résume à un simple clic dans un menu, l'export en MIDI est souvent plus complexe. Par exemple, sur FL Studio, il demande à créer _une copie du projet, avec toutes les pistes converties en "instruments MIDI"_, ce qui est fastidieux: 698 + 699 + #imagefigure( 700 + "./flstudiomidimacro.png", 701 + [ 702 + Dialoge d'avertissement lors de l'utilisation de la macro "Prepare for MIDI export" dans FL Studio 703 + ], 704 + ) 705 + 706 + === Conclusion 707 + 708 + Cette méthode, malgré l'aspect fastidieux de sa mise en place, est une amélioration nette par rapport à l'approche par amplitude: 709 + 710 + #codesnippet[ 711 + #monospace[ 712 + Commit #link("https://github.com/gwennlbh/shapemaker/commit/7ae7a14a90f16f664edee3f433ade9b8c5019ffa")[7ae7a14a90f16f664edee3f433ade9b8c5019ffa] 713 + ] 714 + 715 + ``` 716 + ⚗️ Figure out a POC to get notes from MIDI file into note[ms][stem_name] 717 + 718 + And the conversion from MIDI ticks to milliseconds does not drift at 719 + all, after 6 mins on a real-world track (see research_midi/source.mid), 720 + it's still fucking _spot on_, to the FUCKING CENTISECOND (FL Studio 721 + can't show me more precision anyways). 722 + 723 + So beautiful. 724 + 725 + aight, imma go to sleep now 726 + ```] 727 + 728 + == Fichier de projet 729 + 730 + É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. 731 + 732 + 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. 733 + 734 + 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. 735 + 736 + === FL Studio 737 + 738 + Il existe une bibliothèque Python, pyflp @pyflp, qui permet de parser les fichiers de projets FL Studio, et d'en extraire la quasi totalité. 739 + 740 + #codesnippet( 741 + include-function( 742 + "../research/adapters/flstudio/adapter.py", 743 + "main", 744 + lang: "python", 745 + transform: it => "import pyflp\n\n" + it.replace("\n# end", ""), 746 + ), 747 + ) 748 + 749 + 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. 750 + 751 + Étant donné que je suis utilisatrice de FL Studio, je n'a pas cherché de potentielles solutions pour d'autres logiciels de MAO. 752 + 753 + ==== Performance 754 + 755 + É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. 756 + 757 + == Dépôt de "sondes" dans le logiciel de MAO 758 + 759 + #grid( 760 + columns: (3fr, 1fr), 761 + gutter: 1em, 762 + [ 763 + 764 + Cette dernière solution, dont l'implémentation est encore en cours, consiste à donner la possibilité aux artistes d'exposer directement des signaux depuis leur logiciel, en les exfiltrant à Shapemaker à travers un VST#footnote[Virtual Studio Technology, un standard de plugins audio] @vst dédié. 765 + 766 + 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. 767 + 768 + 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). 769 + 770 + 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. 771 + 772 + ], 773 + imagefigure( 774 + "./flstudioprobe.png", 775 + [Un VST Shapemaker servant de sonde, dans une chaîne d'effets sur FL Studio], 776 + ), 777 + ) 778 + 779 + 780 + #breakout[ 781 + Il est donc possible de recevoir du signal, *autant audio que MIDI*, en entrée d'un VST. 782 + ] 783 + 784 + 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. 785 + 786 + On 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). 787 + 788 + 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 789 + 790 + #codesnippet( 791 + caption: "Implémentation de la fonction permettant à une probe de se signaler auprès du beacon", 792 + [ 793 + #include-function( 794 + "../src/vst/beacon.rs", 795 + "connect_to_beacon", 796 + lang: "rust", 797 + ) 798 + 799 + #include-function( 800 + "../src/vst/beacon.rs", 801 + "register_probe", 802 + lang: "rust", 803 + ) 804 + ], 805 + ) 806 + 807 + Enfin, on utilise la crate _nih-plug_ @nihplug pour exporter la partie VST de notre code en un plugin VST, chargeable dans un logiciel de MAO 808 + 809 + #diagram( 810 + caption: [Exfiltration de données depuis la chaîne de traitement du logiciel de MAO], 811 + size: 80%, 812 + ```dot 813 + digraph G { 814 + rankdir="LR"; 815 + compound=true; 816 + node[shape="record"]; 817 + 818 + subgraph cluster_host { 819 + label = "Logiciel de MAO" 820 + 821 + subgraph cluster_track { 822 + label = "Pour chaque piste" 823 + midi -> instrument -> effects -> probe 824 + midi -> probe 825 + automation -> instrument 826 + automation -> probe 827 + } 828 + } 829 + 830 + subgraph cluster_shapemaker { 831 + label = "Shapemaker" 832 + wip[label="(en développement)", shape="plaintext"] 833 + beacon -> wip 834 + } 835 + 836 + probe -> beacon [label="ws://"] 837 + 838 + } 839 + ```, 840 + ) 841 + 842 + 440 843 == Temps réel: WASM et WebMIDI 441 844 442 845 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. ··· 507 910 508 911 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. 509 912 510 - == Amplitudes de _stems_ 913 + = Performance 511 914 512 - ```rs 513 - let mut reader = hound::WavReader::open(path.clone()) 514 - .map_err(|e| format!("Failed to read stem file: {}", e)) 515 - .unwrap(); 915 + Les premiers prototypes de Shapemaker avait une implémentation sérielle, ou le code Rust ne s'occupait seulement de la partie génération de formes et sérialisation en SVG. Chaque frame SVG étaient sauvegardées dans un fichier, puis converti en PNG en ligne de commande via ImageMagick. Les frames étaient ensuite concaténées en une vidéo via FFmpeg, également en ligne de commande. 516 916 517 - let spec = reader.spec(); 917 + #diagram( 918 + caption: [Pipeline de rendu, premier prototype], 919 + size: 85%, 920 + ```dot 921 + digraph { 922 + rankdir="LR"; 923 + node [shape="record"]; 924 + subgraph cluster_each_frame { 925 + label = "Chaque frame" 926 + subgraph cluster_rust { 927 + label = "Rust" 928 + canvas -> "Frame 0037.svg" 929 + } 930 + "Frame 0037.svg" -> "Frame 0037.png" [label="$ magick convert"] 931 + } 932 + "Frame 0037.png" -> "video.mp4" [label="$ ffmpeg"] 933 + } 934 + ```, 935 + ) 518 936 519 - let sample_index_to_frame = |sample: usize| { 520 - (sample / spec.channels / spec.sample_rate * self.fps) as usize 521 - }; 937 + Un des plus gros gains de performance a été d'éliminer le plus d'I/O#footnote[Input/Output] possible, et notamment aussi d'éviter un encodage/décodage PNG en passant des pixmap (matrices de pixels) directement 522 938 523 - let mut amplitude_db: Vec<f32> = vec![]; 524 - let mut current_amplitude_sum: f32 = 0.0; 525 - let mut current_amplitude_buffer_size: usize = 0; 526 - let mut latest_loaded_frame = 0; 527 939 528 - for (i, sample) in reader.samples::<i16>().enumerate() { 529 - let sample = sample.unwrap(); 530 - if sample_index_to_frame(i) > latest_loaded_frame { 531 - amplitude_db 532 - .push(current_amplitude_sum / current_amplitude_buffer_size as f32); 533 - current_amplitude_sum = 0.0; 534 - current_amplitude_buffer_size = 0; 535 - latest_loaded_frame = sample_index_to_frame(i); 536 - } else { 537 - current_amplitude_sum += sample.abs() as f32; 538 - current_amplitude_buffer_size += 1; 940 + #diagram( 941 + caption: [Pipeline de rendu sans #emph[shell-out]s#footnote[Invoquer un programme en ligne de commande (dans un shell), au lieu de faire tourner du code dans le programme courant]], 942 + size: 85%, 943 + ```dot 944 + digraph { 945 + rankdir="LR"; 946 + node [shape="record"]; 947 + subgraph cluster_rust { 948 + label = "Rust" 949 + subgraph cluster_each_frame { 950 + label = "Chaque frame" 951 + canvas -> "Frame 0037.svg" 952 + "Frame 0037.svg" -> "Frame 0037.png" [label="resvg"] 953 + } 954 + "Frame 0037.png" -> "video.mp4" [label="libx264"] 955 + } 539 956 } 540 - } 541 - 542 - let stem = Stem { 543 - amplitude_max: *amplitude_db.iter().max().unwrap(), 544 - amplitude_db, 545 - duration_ms: (reader.duration() / spec.sample_rate * 1000.0) as usize, 546 - }; 547 - 548 - // Write loaded stem to a CBOR cache file 549 - Stem::save_to_cbor(&stem, &cached_stem_path); 550 - ``` 551 - 552 - == Export MIDI 553 - 554 - #raw( 555 - lang: "rust", 556 - cut-around( 557 - it => it.trim().starts-with("// Add notes"), 558 - it => it == " }", 559 - read("../src/synchronization/midi.rs"), 560 - ), 957 + ```, 561 958 ) 562 959 563 - ``` 564 - Commit 7ae7a14a90f16f664edee3f433ade9b8c5019ffa 960 + 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. 565 961 566 - ⚗️ Figure out a POC to get notes from MIDI file into note[ms][stem_name] 962 + Cela 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): 567 963 568 - And the conversion from MIDI ticks to milliseconds does not drift at 569 - all, after 6 mins on a real-world track (see research_midi/source.mid), 570 - it's still fucking _spot on_, to the FUCKING CENTISECOND (FL Studio 571 - can't show me more precision anyways). 964 + #codesnippet( 965 + caption: "Erreur recontrée pendant la compilation des bindings Rust à libx264", 966 + ``` 967 + Compiling ffmpeg-sys-next v7.1.0 968 + error: failed to run custom build command for `ffmpeg-sys-next v7.1.0` 969 + note: To improve backtraces for build dependencies, set the CARGO_PROFILE_DEV_BUILD_OVERRIDE_DEBUG=true environment variable to enable debug information generation. 572 970 573 - So beautiful. 574 - 575 - aight, imma go to sleep now 576 - ``` 577 - 578 - == Fichier de projet 579 - 580 - #include-function( 581 - "../research/adapters/flstudio/adapter.py", 582 - "main", 583 - lang: "python", 971 + Caused by: 972 + process didn't exit successfully: `C:\Users\…\projects.local\shapemaker\target\debug\build\ffmpeg-sys-next-d2108b58b450b79e\build-script-build` (exit code: 101) 973 + --- stdout 974 + Could not find ffmpeg with vcpkg: Could not look up details of packages in vcpkg tree could not read status file updates dir: The system cannot find the path specified. (os error 3) 975 + ```, 584 976 ) 585 977 586 - == Dépôt de "sondes" dans le logiciel de MAO 978 + Malgré plusieurs guides contradictoires d'installation, utiliser _vcpkg_ @vcpkg pour installer ffmpeg a fini par fonctionner 587 979 588 - #include-function( 589 - "../src/vst/beacon.rs", 590 - "connect_to_beacon", 591 - lang: "rust", 592 - ) 980 + Une fois cette optimisation faite, qui a *divisé par 10* le temps de rendu, on peut se pencher sur le détail de la boucle de rendu pour identifier les potentiels gains de performance 593 981 594 - #include-function( 595 - "../src/vst/beacon.rs", 596 - "register_probe", 597 - lang: "rust", 598 - ) 599 - 600 - = Performance 601 982 602 - #grid( 603 - columns: (auto, auto), 604 - diagram( 605 - caption: [Détail de la boucle de rendu], 606 - scale(90%, reflow: true)[ 607 - ```dot 608 - digraph G { 609 - compound=true; 610 - node[shape="record"]; 983 + #diagram( 984 + caption: [Détail de la boucle de rendu], 985 + scale(90%, reflow: true)[ 986 + ```dot 987 + digraph G { 988 + compound=true; 989 + splines="ortho"; 990 + node[shape="record"]; 611 991 612 - hooks -> canvas [label="Modify"]; 613 - subgraph cluster_tosvg { 614 - label = "SVG string rendering [0.2ms]" 992 + hooks -> canvas; 993 + subgraph cluster_tosvg { 994 + label = "SVG string rendering [0.2ms]" 995 + subgraph g_svg { 996 + rank=same; 615 997 canvas -> render_to_svg [label="0.1ms"] 616 - "render_to_svg" -> stringify_svg [label="0.1ms"] 998 + render_to_svg -> stringify_svg [label="0.1ms"] 617 999 } 618 - subgraph cluster_rasterize { 619 - label = "Encode frame [167ms]" 1000 + } 1001 + subgraph cluster_rasterize { 1002 + label = "Encode frame [167ms]" 1003 + subgraph g_rasterize { 1004 + rank=same; 620 1005 stringify_svg -> "svg string" 621 1006 "svg string" -> "usvg tree" [label="48ms"] 622 1007 "usvg tree" -> pixmap [label="11ms"] 623 1008 pixmap -> "hwc frame" [label="108ms"] 624 1009 } 625 1010 } 626 - ``` 627 - ], 1011 + 1012 + canvas -> "svg string" [weight=10, style=invis] 1013 + } 1014 + ``` 1015 + ], 1016 + ) 1017 + 1018 + #figure( 1019 + caption: "Durées d'exécution par tâche, pour une vidéo de test de 5 secondes", 1020 + table( 1021 + columns: 3, 1022 + inset: 0.75em, 1023 + [*Tâche*], [*Durée [ms]*], [*\#*], 1024 + ..csv("../results.csv").slice(1).flatten() 628 1025 ), 1026 + ) 1027 + 1028 + == Rastérisation parallèle <perf-parallelrasterize> 1029 + 1030 + Si la partie `render_to_svg` n'est pas parallélisable car il faut bien faire exécuter tout les hooks dans l'ordre, la rastérisation des SVG sortants, elle, est bien parallélisable. Malheureusement, le gain de performance n'a pas été significatif. 1031 + 1032 + == Encodage H.264 parallèle? 1033 + 1034 + Si l'on est bien capable de donner à l'encodeur nos frames dans le désordre, tout en lui indiquant le timestamp de chaque frame, l'encodeur ne supporte pas de recevoir les frames dans le désordre: 1035 + 1036 + #align(center)[ 1037 + 1038 + ] 1039 + 1040 + Il est donc impossible de paralléliser l'encodage 1041 + 1042 + == Pixmap et frames HWC: 100ms de standards 1043 + 1044 + L'encodage vidéo étant fait par une bibliothèque totalement séparée de celle s'occupant de la rastérisation SVG, il y a un risque d'incompatibilité entre les formats de pixmap utilisés par les deux bibliothèques, ce qui est le cas ici. 1045 + 1046 + En effet, les SVG rasterisés sont stockées dans un array plat de valeurs RGBA: 1047 + 1048 + #align(center)[ 1049 + ``` 1050 + [R, G, B, A, R, G, B, A, …] 1051 + ``` 1052 + ] 1053 + 1054 + Tandis que la bibliothèque utilisée, _rsvideo_, attend une matrice HWC, ou height-width-channels, de pixels: 1055 + 1056 + #align(center)[ 1057 + ``` 629 1058 [ 630 - // #figure(caption: "Durées d'éxécution par tâche, pour une vidéo de test de 5 secondes", csvtable(read("../results.csv"), columns: (auto, auto, auto), inset: 10pt)) 631 - #figure( 632 - caption: "Durées d'éxécution par tâche, pour une vidéo de test de 5 secondes", 633 - table( 634 - columns: 3, 635 - inset: 0.75em, 636 - [*Tâche*], [*Durée [ms]*], [*\#*], 637 - ..csv("../results.csv").slice(1).flatten() 638 - ), 639 - ) 1059 + [ [R, G, B], [R, G, B], … ], 1060 + [ [R, G, B], [R, G, B], … ], 1061 + 1062 + ] 1063 + ``` 1064 + ] 640 1065 641 - == Pixmap et libx264: le problème des multiples standards 1066 + Il est donc nécéssaire de convertir entre ces deux formats, ce qui est lent car demande de copier les données. 642 1067 643 - == SVG vers string vers SVG <perf-svgstring> 1068 + Une autre solution est de faire proposer une contribution à la bibiothèque de rendu utilisée par _resvg_, _tiny_skia_, pour pouvoir instrumentaliser les lectures et écritures à sa pixmap, et ainsi écrire dans la représentation voulue par libx264 directement. 644 1069 645 - Comme on peut le remarquer, il y a un gain de performance assez conséquent de possible si l'on parvient à utiliser usvg, non seulement pour la rastérisation, mais également pour la construction de l'arbre SVG: sur une boule de rendu de 167 ms, *on passe 29% du temps à parser un arbre SVG sérialisé, alors que l'on vient de construire cette arbre*. 646 - ], 647 - ) 1070 + == SVG vers string vers SVG <perf-svgstring> 1071 + 1072 + Comme on peut le remarquer, il y a un gain de performance assez conséquent de possible si l'on parvient à utiliser usvg, non seulement pour la rastérisation, mais également pour la construction de l'arbre SVG: sur une boule de rendu de 167 ms, *on passe 29% du temps à parser un arbre SVG sérialisé, alors que l'on vient de construire cette arbre*. 648 1073 649 1074 = Conclusion 650 1075 1076 + 1077 + #show: arkheion-appendices 1078 + = Marqueurs dans un logiciel de MAO 1079 + 1080 + #imagefigure( 1081 + "./flstudiomarkers.png", 1082 + [ 1083 + Marqueurs dans FL Studio: 1084 + #smallcaps([intro end, block 1, break 1, buildup 1, …]) 1085 + #linebreak() 1086 + Fichier de projet pour _Onset_ de Postamble @onset 1087 + ], 1088 + ) <flstudiomarkers> 651 1089 652 1090 // Add bibliography and create Bibiliography section 653 1091 // #bibliography("bibliography.yaml", style: "./ieee-with-locations.csl")
+10 -10
paper/shapeshed.svg
··· 1 1 <svg height="170" viewBox="-10 -10 170 170" width="170" xmlns="http://www.w3.org/2000/svg"> 2 2 <rect fill="white" height="170" width="170" x="-10" y="-10"/> 3 3 <g class="layer" data-layer="root"> 4 + <g data-object="--6" style="fill: black;transform-box: fill-box;" transform-origin="150 100"> 5 + <path d="M100,50 L150,50 L150,100 z"/> 6 + </g> 4 7 <g data-object="--7" style="fill: black;transform-box: fill-box;" transform-origin="25 125"> 5 8 <rect height="50" width="50" x="0" y="100"/> 6 9 </g> 7 - <g data-object="--4" style="fill: black;transform-box: fill-box;" transform-origin="25 75"> 8 - <circle cx="0" cy="50" r="5"/> 9 - </g> 10 - <g data-object="--6" style="fill: black;transform-box: fill-box;" transform-origin="150 100"> 11 - <path d="M100,50 L150,50 L150,100 z"/> 10 + <g data-object="--3" style="stroke: black; fill: transparent;transform-box: fill-box;" transform-origin="150 50"> 11 + <path d="M100,50 Q100,0,150,0" stroke-width="5"/> 12 12 </g> 13 13 <g data-object="--8" style="fill: black;transform-box: fill-box;" transform-origin="125 175"> 14 14 <circle cx="100" cy="150" r="2"/> 15 15 </g> 16 + <g data-object="--2" style="stroke: black; fill: transparent;transform-box: fill-box;" transform-origin="100 50"> 17 + <path d="M50,50 Q100,50,100,0" stroke-width="5"/> 18 + </g> 16 19 <g data-object="--5" style="stroke: black; fill: transparent;transform-box: fill-box;" transform-origin="100 100"> 17 20 <line stroke-width="5" x1="50" x2="100" y1="50" y2="100"/> 18 21 </g> 19 - <g data-object="--3" style="stroke: black; fill: transparent;transform-box: fill-box;" transform-origin="150 50"> 20 - <path d="M100,50 Q100,0,150,0" stroke-width="5"/> 21 - </g> 22 22 <g data-object="--1" style="fill: black;transform-box: fill-box;" transform-origin="25 25"> 23 23 <circle cx="25" cy="25" r="25"/> 24 24 </g> 25 - <g data-object="--2" style="stroke: black; fill: transparent;transform-box: fill-box;" transform-origin="100 50"> 26 - <path d="M50,50 Q100,50,100,0" stroke-width="5"/> 25 + <g data-object="--4" style="fill: black;transform-box: fill-box;" transform-origin="25 75"> 26 + <circle cx="0" cy="50" r="5"/> 27 27 </g> 28 28 </g> 29 29 <defs/>
+7 -1
paper/template.typ
··· 22 22 show math.equation: set block(spacing: 0.65em) 23 23 set math.equation(numbering: "(1)") 24 24 set heading(numbering: "1.1 ") 25 + // Écriture inclusive >:3 26 + show "·": sym.dot.op 25 27 // show heading: set text(font: "Martian Mono") 26 28 27 29 // Set run-in subheadings, starting at level 4. ··· 142 144 body 143 145 } 144 146 147 + #let monospace = body => { 148 + text(font: "Martian Mono", size: 0.7em, body) 149 + } 150 + 145 151 #let arkheion-appendices(body) = { 146 152 counter(heading).update(0) 147 153 counter("appendices").update(1) ··· 151 157 let vals = nums.pos() 152 158 let value = "ABCDEFGHIJ".at(vals.at(0) - 1) 153 159 if vals.len() == 1 { 154 - return "APPENDIX " + value 160 + return value 155 161 } else { 156 162 return value + "." + nums.pos().slice(1).map(str).join(".") 157 163 }
+8 -7
paper/utils.typ
··· 52 52 let lines = content.split(regex("\r?\n")) 53 53 let min_indent = lines 54 54 .filter(it => it.trim() != "") 55 - .map(it => it.split().position(c => c.find(regex("[^\s]")) != none)) 56 - .fold(0, (a, b) => calc.min(a, b)) 57 - lines.map(it => it.slice(min_indent)).join("\n") 55 + .map(it => it.clusters().position(c => c != " ")) 56 + .fold(99999, (a, b) => calc.min(a, b)) 57 + 58 + lines.map(it => it.slice(calc.min(it.len(), min_indent))).join("\n") 58 59 } 59 60 60 61 ··· 67 68 ) => { 68 69 let start_pattern = if lang == "rust" { 69 70 if is_method { 70 - regex("^ (pub )?fn " + name) 71 + regex("^ (pub )?fn " + name) 71 72 } else { 72 - regex("^(pub )?fn " + name) 73 + regex("^(pub )?fn " + name) 73 74 } 74 75 } else if lang == "python" { 75 76 regex("^def " + name) ··· 83 84 if is_method { 84 85 regex("^ \}") 85 86 } else { 86 - regex("^\}") 87 + regex("^\}") 87 88 } 88 89 } else if lang == "python" { 89 90 regex("^# end") // TODO pass next line to cut-between ··· 105 106 #raw(lang: lang, read(filepath)) 106 107 ] 107 108 } else { 108 - raw(lang: lang, transform(dedent(contents))) 109 + raw(lang: lang, dedent(transform(contents))) 109 110 } 110 111 }
-1
src/vst/beacon.rs
··· 16 16 pub fn connect_to_beacon<T: FnMut(&ws::Sender) -> ()>(mut action: T) -> Result<()> { 17 17 ws::connect(beacon_url(), |out| { 18 18 action(&out); 19 - 20 19 move |_msg| out.close(ws::CloseCode::Normal) 21 20 })?; 22 21 Ok(())