PETRI DISH GAMES

The voice

The generated sound an organism makes, played live from its own shader source and fed back into the image through uniforms.

The voice is the sound an organism makes: an ambient piece generated in the visitor’s browser from the organism’s own shader source, and returned to the image through uniforms the shader reads back. No audio file exists; the engine reads the code into a feature vector, blends that reading with the organism’s id used as a seed, and plays the result. One organism has the voice at a time on a page, and a given organism sounds the same on every visit.

Genome reading

extractGeneFeatures strips comments and preprocessor lines from the shader body, then measures ten features. Every density measure is counted per 1000 collapsed characters and passed through the saturating curve 1 - exp(-x / k), so a value approaches 1 without reaching it.

FeatureRead from the bodyWhat it moves
literalCountfloat literal tokensrhythmic fill: the pulse counts of both euclidean patterns
literalMeanlog-scaled mean magnitude of those literalsdrone root, and drone warmth inversely
literalSpreaddispersion of those magnitudespad cutoff, detune width, drone filter, harmonic wander, melodic range
trigDensitysin cos tan and their inverse and hyperbolic formsmode brightness, pad brightness
noisinesschaos markers: fract floor mod hash noise fbm and kintempo upward, all three trigger probabilities, pad grit, harmonic tension, melodic density, the arc’s energy ceiling
loopWeightsummed constant iteration counts of every for headertempo downward, patience
lengthline count, saturating near 140 linestempo downward, bars per chord, patience
branchinessif switch ? and the selection builtinsrotation of the euclidean patterns, melodic angularity
vectornessvector and matrix constructors, geometric builtinsstereo width, echo send
functionCountuser-defined function definitions, main excludedrestores percussion voices the seed left silent

A for header with no readable constant bound counts as 8 iterations. An empty or non-string body returns every feature at zero.

Blend with the seed

A seed alone already decides a full musical identity: mode, root, tempo, pad envelope and cutoff, the euclidean tick and blip patterns, per-voice levels, echo and reverb sends, and the input scalars handed to the harmony, melody and form planners. The genome does not replace that draw. Each affected scalar becomes 0.6 * genome + 0.4 * seed, and every result is then clamped into a fixed range, so a genome can never push a value past the bounds the engine enforces for itself.

  • The blend draws no random numbers, so an organism’s identity stream sits where the seed left it.
  • It runs before the harmonic plan, motif, form, oscillator spectra and drone are built, so every derived structure sees the blended values.
  • Mode is chosen from six scales ordered darkest to brightest (aeolian, minor pentatonic, dorian, mixolydian, major pentatonic, lydian); the seed’s own mode contributes its position in that order.
  • Roughly one identity in five is missing any given percussive voice. Two user-defined functions restore a silenced blip, three restore a tick, four restore a thump. The genome only raises a silent voice and never silences a sounding one, and an identity that drew all three silent falls back to blips.

Separate generator streams carry the harmonic plan, the motif and the form, each seeded from the organism id mixed with a different constant, so a change in one planner’s branching does not shift another’s draws.

Instrumented alleles

instrumentShader rewrites the body so that selected float literals become alleles driven by a uniform array. Each chosen literal x at index i is replaced by (x * u_gene[i]), and the declaration uniform float u_gene[12]; is prepended. At most 12 alleles are instrumented.

Candidates are picked to spread across the file rather than clustering at the top: the body is cut into 12 positional buckets and the buckets are walked round robin, a random surviving literal drawn from each.

Literals never touched:

  • anything inside a for header, where GLSL ES requires constant bounds
  • anything in a const declaration, through to its semicolon
  • anything inside square brackets, which covers array sizes and indices
  • magnitudes below 1e-4
  • any literal whose offsets do not match the original text exactly

Each allele carries two constants fixed at instrumentation time. Sensitivity is a value between 0.3 and 1.0 setting how far that allele travels. Band selects which measure of the sound drives it, with large magnitudes weighted toward bass and small ones toward high.

Band indexDrives from
0overall level
1bass
2mid
3high

Instrumentation returns the original body unchanged, with a count of zero, when the code is empty, when it already contains the token u_gene, when no literal survives the exclusions, or when anything in the pass throws.

Both pages compile the instrumented body behind the prelude first. If that fails and the count was above zero, the pristine code gets exactly one further attempt, so an organism broken only by the extra uniform still draws. When neither program builds, the arena reports the failure to the lab and skips the pair, which is a canary strike rather than a cull; the gallery replaces the canvas with a legible note naming the failure as a shader error, a link error, or a missing WebGL2 context.

On the pristine retry the u_gene lookup is null, and the per-frame upload is skipped entirely.

Per-frame modulation

Each frame the page reads four smoothed levels from the analyser and uploads them.

u_gene entry i is set to 1 + 0.12 * sensitivity[i] * band level. Every entry beyond the instrumented count has a sensitivity of zero. With sound off the engine reports all four levels as zero, so every entry of the array is exactly 1.0 and the organism draws its pristine self.

u_audio is the reverse direction: a vec4 of level, bass, mid and high, uploaded only when the compiled program exposes that uniform. Organisms bred before the audio pass do not declare it, so the lookup is null and the whole reactive path stays idle.

Shader time is accumulated rather than read off a clock. Each frame it advances by the frame delta, capped at 0.1 seconds, multiplied by 1 + 1.6 * level, and wraps at 20 seconds. Loudness can push an organism forward faster but never steps it backwards.

The four levels come from one analyser on the master output, at an FFT size of 2048 and a smoothing constant of 0.6. Bins below 250 Hz form bass, 250 to 2000 Hz mid, 2000 to 12000 Hz high, and everything below 8000 Hz the overall level. The four are not a partition: the high band runs past the ceiling the overall level is measured to, so a bright frame can raise high without raising level. Each band is averaged over its own bin count and trimmed by its own gain. Each follows its target quickly upward and slowly downward, so the picture responds to an attack and relaxes after it.

Structural limits of the graph

The graph is closed by construction. It has no microphone and no other input node, so acoustic feedback cannot occur.

  • The echo feedback gain is 0.5, under a stated ceiling of 0.6, and the loop passes through a lowpass at 1800 Hz, so every repeat strictly decays.
  • The convolution reverb sits outside the feedback loop. The echo output bleeds into the reverb, not the reverse.
  • The impulse response is generated from a fixed seed, so the room is a constant of the site and identities differ only in how much of it they use.
  • The master chain runs a 25 Hz highpass, a compressor and a limiter at -3 dB with a ratio of 20 before the analyser and the output.
  • The master gain starts at zero and ramps in, so nothing is ever heard before the graph has settled.
  • Concurrent transient sources are capped at 26; the scheduler skips a voice rather than exceeding it. Sustained sources, the drone oscillators and the filter LFO, are exempt from that budget and bounded instead by the retirement rule below.

Changing the voiced organism crossfades identities over 1.5 seconds: the outgoing bus and its send ramp to zero while the incoming pair ramp up. Setting the seed already voiced does nothing. At most two identities may retire in parallel; a faster sweep cuts the oldest immediately, which bounds the number of live sustained sources however fast the pointer moves.

The voiced organism

One engine serves a page, and it touches no browser API before it starts. Audio may only begin on a real gesture: the shared preference key rorschach_sound holds on or off, and a remembered on arms a single pointer or key listener rather than starting anything at load.

The organism id is the seed. The gallery folds a non-numeric id to a stable number, so the same organism always sounds alike.

PageDefault holderPointerHold
The arena, where pairs are judgedThe two sides trade the voice every 8 secondsEntering a stage takes the voiceHovering, a hidden tab, or silence suspends the trade
The galleryOn-screen cards take it in turn every 14 seconds, in document orderEntering a card takes the voiceThe detail overlay holds it while open, and hands it back to the previous card on close

A ring marks the element the engine is voiced by. It is drawn only while sound is running, so a silent page never marks one side as sounding.

Bound to

The source this page's claims were read against, named by symbol and pinned by the digest of the file it lives in.

SymbolDigest
audio.ts d355ff71c8c7
form.ts a2695e1d7ba2
gallery.astro 005d7b298795
genome.ts 09b333a8984b
melody.ts 0f0631615330
rorschach.astro b439b3c1f84b
theory.ts cf4f69960a45
timbre.ts 748837c5e9e4

Near this subject

These are the subjects this one is joined to in the field. Step out of this card and they are the wells around the one you are standing in.