Petri-netten als muzieksequencer
Achter dit systeem zit geen piano roll, geen tijdlijn-grid en geen DAW. Alles draait op Petri-netten: cirkels die door pijlen met elkaar verbonden zijn, waarbij tokens (symbolen) erdoorheen bewegen. Elke noot is letterlijk een token die één stap door het diagram verplaatst.
Een drumcomputer gemaakt van tokens
Stel je een ring voor van 8 cirkels die door pijlen zijn verbonden. Eén stip — een token — bevindt zich bij de eerste cirkel. Bij elke tik springt deze naar de volgende. Sommige cirkels triggeren een kickdrum wanneer de token landt; de rest is stil. De token gaat in een oneindige loop rondjes. Zo ontstaat er een beat.
Het patroon van de slagen komt voort uit het Bjorklund-algoritme, dat K slagen zo gelijkmatig mogelijk over N stappen verspreidt. Met 3 slagen over 8 stappen krijg je de tresillo, een ritme dat overal voorkomt, van Afro-Cubaanse son tot Rihanna.
Die ring van cirkels wordt een Petri-net genoemd. De cirkels zijn plaatsen (places), de pijlen lopen via transities (transitions) en de stip is een token. Dat is het volledige vocabulaire. Verander de getallen — bijvoorbeeld 4 slagen over 16 stappen — en je hebt four-on-the-floor techno. Dezelfde structuur, ander ritme.
Elk instrument heeft zijn eigen ring: kick, snare, hihat, clap — allemaal parallel draaiend. Door de ringen verschillende lengtes te geven, lopen ze uit sync, wat gratis polyritmiek oplevert. Een 6-staps hihat over een 16-staps kick klinkt als de cross-ritmes in Afro-Cubaanse en West-Afrikaanse muziek, omdat het exact dezelfde wiskunde is.
Alles is een ring
Melodieën werken op dezelfde manier. In plaats van drumslagen bevat elke stap een noot: toonhoogte, velocity en duur. De muziektheorie selecteert noten die passen bij het akkoord (akkoordtonen op sterke tel, doorgangstonen op zwakke tel, rusten om het stuk te laten ademen) en plaatst deze in een ring.
Baslijnen? Een ring. Arpeggio's? Een ring. Harmonie-pads? Je raadt het al: een ring.
Kies een genre — techno, jazz, ambient, drum & bass — en een preset vult de details in: toonladders, akkoorden, drumpatronen, hoeveelheid swing en humanize. De generator stelt een reeks ringen samen en de sequencer voert ze simpelweg uit. Geen AI, geen samples — alleen muziektheorie en willekeurige getallen op basis van een seed. Dezelfde seed betekent elke keer hetzelfde nummer.
Van loops naar nummers
Loops zijn eenvoudig: tokens gaan rond en patronen herhalen zich. Maar een echt nummer heeft intro's, drops en breakdowns nodig. De hihat moet bijvoorbeeld voor 8 maten verdwijnen om daarna terug te keren, of de bas moet tijdens het tweede couplet subtiel instromen.
Daarom zijn er controle-netten toegevoegd: ringen die geen geluid maken, maar commando's afvuren zoals "mute de hihat" of "voeg de bas toe". Het is een kleine regisseur gemaakt van cirkels en pijlen.
De eerste versie was hilarisch inefficiënt: een keten van 1.536 individuele stappen (één per tik), met elke paar honderd stappen een commando en niets daartussenin. Het werkte, maar het projectbestand was 22 MB voor een track van 3 minuten.
De oplossing: countdown-timers. In plaats van 384 lege stappen vóór een sectiewisseling, plaatsen we 384 tokens in één enkele cirkel en laten we er per tik één weglopen. Een speciale verbinding (een inhibitor arc) houdt het controle-event tegen totdat de countdown nul bereikt. Het resultaat is hetzelfde, maar het diagram is veel kleiner:
| Kenmerk | Voor | Na |
|---|---|---|
| Plaatsen | 37.148 | 800 |
| Transities | 37.120 | 800 |
| Bestandsgrootte | 22 MB | 242 KB |
Eén cirkel met veel tokens doet het werk van veel cirkels met elk één token. Petri-netten zijn zo flexibel.
Transities zijn simpelweg meer netwerk
De Auto-DJ genereert op de grens van een track een nieuw nummer. We wilden dat een gecureerde filter sweep, white-noise wash of riser precies op de eerste tel landde, in plaats van te racen met de projectwissel vanuit de hoofdthread (main thread).
De eerste poging was om timers in de hoofdthread te coördineren met de regeneratie-status aan de kant van de worker. Dit werkte grotendeels, behalve wanneer een gebruikersactie overlapte met de grens en de macro stilletjes werd gedropt. We vochten tegen de runtime.
De oplossing was om transities niet langer als een zijkanaal te behandelen. Elke Auto-DJ-transitie wordt nu geïnjecteerd als een controle-net met één transitie en een fire-macro binding. t0 vuurt op tik 1 van het nieuwe nummer; de worker zendt control-fired uit; de hoofdthread voert de macro uit via de normale wachtrij. Omdat de Petri-net executor al de scheduler is, is dit per definitie tik-gesynchroniseerd.
Twee timing-bugs — de project-swap race en de macro-queue drop — zijn zo samengevoegd tot één structureel onderdeel. De transitie is gewoon weer een token die door de graaf beweegt. Details hiervan staan in de release notes van v1.1.0; de gecureerde transitie-pool zelf werd geïntroduceerd in v1.0.8.
Wat deze structuur opleverde
De token-status is de enige status, waardoor dezelfde seed op elke machine hetzelfde nummer produceert — geen verborgen tellers, geen drift. Hetzelfde diagram dat de muziek aanstuurt, kan op het scherm worden getekend en bekeken: als er iets niet goed klinkt, kunnen we zien of een token vastloopt op de verkeerde plek of dat een ring uit fase loopt.
Omdat elk instrument zijn eigen net is en de songstructuur een aparte laag vormt, kan er één worden verwijderd of een nieuwe worden toegevoegd zonder dat andere zaken kapot gaan. Dit vereiste geen extra werk; het is simpelweg het resultaat van het gebruik van één enkel primitief voor concurrency, status en scheduling.
Probeer het uit
Bezoek de demo op beats.bitwrap.io — 19 genres, volledig client-side, niets te installeren.
Een grote dank aan Tone.js, die al het zware werk aan de audiozijde doet. De Petri-netten bepalen wat er speelt en wanneer; Tone.js zorgt dat het ook echt goed klinkt. Polyfone synths, reverb, compressie en scheduling met sub-milliseconde precisie kregen we hiermee essentieel gratis. Het is een van die libraries waarbij je verwacht tegen een muur aan te lopen, maar dat nooit gebeurt.
De Petri-net executor bestaat uit ongeveer 400 regels vanilla JS, met nog eens circa 700 regels in de sequencer worker. De rest — muziektheorie, generators, UI — is groter, maar geen van alles is framework-code. Geen npm, geen bundler. Tone.js via CDN, een Go-server die statische bestanden serveert, en een 60 jaar oud idee over cirkels en pijlen dat een verrassend goede sequencer bleek te zijn.
Bron: github.com/stackdump/beats-bitwrap-io
Voor de formele theorie achter waarom token-flow werkt als computationeel model, zie Declarative Differential Models. Om je eigen netten te bouwen en simuleren, kun je pflow.xyz proberen.
Groetjes,