UNIAUDIO LogoUNIAUDIO
UniAudio

How UniAudio generates music + image

UniAudio is ERC-20 + Uniswap v4 hook + on-chain generative-music NFT. Every swap mutates a rolling random seed in the hook. When a buyer's uAUD balance crosses a whole-unit boundary, the token mints a fresh uAUD NFT carrying a derived 256-bit seed. That seed feeds two non-overlapping decoders — a music engine off-chain and an SVG generator on-chain — to produce a piece that is unique, deterministic, and tied to the swap that minted it.

Sample
GenreSynthwave
KeyD
ModeIonian (Major)
BPM110
Progressioni–VII–VI–V (Andalusian)
DrumLo-fi boom bap
Lead patchSaw bright
Pad patchWarm tri pad
Bass patchSaw bass
Bass style1
Motif rhythm0b1010100010101000
Reverb100%
Delay40%
Swing0%
0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef
Source map — verify each claim against the contract

Every assertion below about on-chain behavior carries a ⟨src⟩ path:lines :: symbol chip pointing into the Solidity. Clone the repo and grep the path — no trust in this page required.

1. Token + hook (ERC-404 lite)

Two contracts: AudioToken (ERC-20 with per-whole-unituAUD NFTs) and AudioHook (V4 hook + on-chain SVG generator). Modeled on the Unipeg / UpegHook reference deployed on Ethereum mainnet — uAUD is our renamed branding of that pattern.⟨src⟩ contracts/src/AudioToken.sol :: contract AudioToken⟨src⟩ contracts/src/AudioHook.sol :: contract AudioHook

swap on AUDIO/X pool ─► PoolManager ─► AudioHook.afterSwap
                                          │
                                          └─ randomSeed = keccak(seedTicks, prev,
                                                                blockhash, prevrandao,
                                                                block.number, timestamp,
                                                                sender, amountSpecified,
                                                                delta0, delta1,
                                                                tick, sqrtPriceX96)

router → buyer ERC-20 transfer
   │
   └─► AudioToken._update
         │   if balanceAfter / 1e18 > balanceBefore / 1e18:
         │       hookSeed = AudioHook.randomSeed()
         │       seed     = keccak(hookSeed, upegId, buyer, block.number)
         │       mint uAUD #N to buyer
         │   symmetric burn on outflow
         ▼
   AudioToken.tokenURI(N) → JSON { image: SVG, attributes: [seed] }
⟨src⟩ contracts/src/AudioHook.sol:108-143 :: _afterSwap⟨src⟩ contracts/src/AudioToken.sol:171-197 :: _update⟨src⟩ contracts/src/AudioToken.sol:201-217 :: _mintUAUD⟨src⟩ contracts/src/AudioToken.sol:124-143 :: tokenURI

2. One seed, two consumers

The 256-bit seed feeds two non-overlapping decoders — they share entropy but never alias.

BitsConsumerDecoder
0..81Music engine (off-chain, Tone.js)parseGenome⟨src⟩ web/src/lib/engine/genome.ts:51-102 :: parseGenome
82..95Reserved (no consumer)
96..247SVG generator (on-chain)AudioMetadataLibrary.decode⟨src⟩ contracts/src/svg/AudioMetadata.sol:35-56 :: decode
248..255Reserved (no consumer)

Do not change either decoder post-launch — existing seeds would render different art / play different music. The on-chain decoder is frozen by deployment; the off-chain decoder is frozen by audit (TS port mirrors Solidity byte-for-byte).⟨src⟩ web/src/lib/engine/audioMetadata.ts :: decodeAudioMetadata (TS port)

3. Constrained random — music engine

Pure note-by-note randomness sounds like noise. The trick is that each decision layer can only pick from a table the music theory has already vouched for. The deeper you go (down to individual notes and drum hits), the tighter the constraints get.

Genre → Key+Mode → Tempo+Form → Progression → Bass → Melody → Drums → Timbre → FX
 (broad)                                                                          (narrow)

Every layer eats a few bits of the 256-bit seed. Genre is decided first, then it gates everything below — e.g. Lo-fi cannot use the Lydian mode and cannot land on 130 BPM.

4. Music engine bitfield (bits 0..81) — off-chain

None of these fields exist in Solidity. Verify by grepping the contracts — grep -rn 'genre\|bpm\|reverb\|delay\|melody\|drumPattern' contracts/src returns zero hits. The contract stores the seed as an opaque uint256 and the FE music engine (parseGenome + Tone.js, in the browser) is the only consumer of bits 0..81.⟨src⟩ contracts/src/AudioToken.sol:211 :: seedOfUAUD[id] = seed⟨src⟩ web/src/lib/engine/genome.ts:51-102 :: parseGenome
That is why marketplaces like OpenSea show no Genre / BPM / Reverb attributes — only the SVG image and the raw seed. The bit map below is purely a frontend convention; the on-chain decoder lives in section 7.

parseGenome(hex) slices the low 82 bits of the seed into named fields:

BitsFieldNotes
0–3genreIdx0..3 → 4 genres
4–7rootNote0..11 → C..B
8–10modeIdxfiltered through genre.modeFilter
11–13bpmIdxindex into genre.bpms
20–27progressionIdxfiltered through genre.progressionFilter
28–30bassStyle0..3 → pedal/octave/fifth/arp
31–38motifIdxindex into MOTIF_RHYTHMS
39–62melodyVarseed for mulberry32 PRNG
55–58drumPatternIdx-1 = no drums (Ambient)
59–63leadPatchIdxfiltered through genre.leadPatches
64–68padPatchIdxfiltered through genre.padPatches
69–73bassPatchIdxfiltered through genre.bassPatches
74–77reverbAmount0..15 → 0..1
78–81delayAmount0..15 → 0..1

5. The 8 inviolable laws

This rule set is what keeps the output from collapsing into noise. Each law has a concrete music-theory reason behind it.

  1. Genre gates every layer belowgenre is decided first; mode, BPM, drums, patches, and progression all filter through it
  2. Every note lives in the scalechromatic passing tones are allowed only on the weak 16th-note slots
  3. Downbeat = chord toneslot 0 (beat 1) and slot 8 (beat 3) MUST be the root/3rd/5th of the chord — anti-atonal anchor
  4. Progressions come from a hardcoded paletteI-V-vi-IV, ii-V-I, Andalusian… chords are never picked at random
  5. Voice leading ≤ 2 semitonespad voicing in close root position; common tones held between chords
  6. Cadence rulelast note of bar 8 → chord root; last note of bar 16 → tonic. Phrases must close
  7. Motif loop + variationone rhythmic pattern persists through the section; melody is never built from scratch every bar
  8. Patches & drum patterns are tuned presets32 lead/pad/bass patches, 5 drum patterns — synth params are never randomized

6. Per-layer details

Genre (4 bit)

Four genres ship today: Lo-fi Hip Hop · House · Ambient · Synthwave. Each genre is a schema that gates everything below it — BPM range, allowed modes, drum kit, synth patches, progression palette.

Mode (3 bit)

Eight 7-note modes: Ionian, Dorian, Phrygian, Lydian, Mixolydian, Aeolian, Harmonic Minor, Melodic Minor. Pentatonic / blues are excluded because stacking thirds for chords behaves inconsistently on a 5-note scale.

Progression (8 bit)

15 progression palette in three groups. Chords are never random:

Melody — the heart of the engine

Each 4/4 bar = 16 sixteenth-note slots. Each slot has its own pitch rule:

SlotRolePitch pool
0Downbeat (beat 1)chord tone (root/3rd/5th)
8Beat 3 (medium-strong)chord tone
4, 12Beats 2 / 465% chord tone, 35% scale tone
2, 6, 10, 14Off-8scale tone, step from prev
odd16thpassing/neighbor (step only)

Cadence rule: the last note of bar 8 → root of the chord currently sounding; the last note of bar 16 → tonic of the scale. Without cadence the music feels "unresolved" — tiring to listen to on loop.

7. On-chain SVG (bits 96..247)

The cover image is generated fully on-chain inside the hook. Three deterministic steps: bit-slice the high half of the seed into 19 fields, pick a layer variant + color for each, render in a fixed z-order.⟨src⟩ contracts/src/svg/AudioMetadata.sol:35-56 :: decode⟨src⟩ contracts/src/svg/SvgGenerator.sol:144-192 :: generateSvg

BitsFieldNotes
96–103backGroundColor% 6 → background palette
104–111frame% frameCount → variant id (also feeds spectrum)
112–119frameColor% 36 → primary palette (also feeds spectrum)
120–127wave% waveCount
128–135waveColor% 36
136–143motif% motifCount
144–151motifColor% 36
152–159lead% leadCount
160–167leadColor% 36
168–175pad% padCount
176–183padColor% 36
184–191bass% bassCount
192–199bassColor% 36
200–207drum% drumCount
208–215drumColor% 36
216–223fx% fxCount
224–231fxColor% 36
232–239grid% gridCount
240–247gridColor% 36
seed (256 bit)
   │
   │ (1) AudioMetadataLibrary.decode(seed)         — slice bits 96..247
   ▼
{ backGroundColor, frame, frameColor, wave, waveColor,
  motif, motifColor, lead, leadColor, pad, padColor,
  bass, bassColor, drum, drumColor, fx, fxColor,
  grid, gridColor }                                — 19 × uint8

   │ (2) per layer:
   │       id    = (field      % count) + 1
   │       color = colors[colorField % 36]
   ▼
   Rect[] storage  →  toSvg(color)                  — concat <rect> elements

   │ (3) z-order:
   │       bg → grid → spectrum → frame → pad → bass → drum
   │          → motif → lead → wave → fx
   ▼
   <svg viewBox='0 0 24 24'> ... </svg>
⟨src⟩ contracts/src/svg/SvgGenerator.sol:144-192 :: generateSvg⟨src⟩ contracts/src/svg/SvgGenerator.sol:160-189 :: layer z-order⟨src⟩ contracts/src/svg/SvgGenerator.sol:196-198 :: _color (idx % 36)⟨src⟩ contracts/src/svg/SvgRects.sol:14-43 :: toSvg

Palette + canvas

Combinatorics

With 4 variants per layer and the full palette, the artwork space is6 × 4¹⁰ × 36⁹ ≈ 6.4 × 10²⁰ distinct pieces (10 layers × 4 variants, 9 layer colors × 36 palette, 6 backgrounds; spectrum reuses the frame field and color so it isn't counted separately). Real-world uniqueness is bounded by mint count (10,000), not combination space — no two minted uAUD will visually collide in practice.

8. Determinism contract

Same seed + same on-chain storage → same SVG, byte-for-byte. To preserve this contract:

  1. Don't change the decoder — bit layout must freeze post-launch.⟨src⟩ contracts/src/svg/AudioMetadata.sol:35-56 :: decode⟨src⟩ web/src/lib/engine/audioMetadata.ts :: decodeAudioMetadata (TS mirror)
  2. Don't change the palette — color indices would map to different hex strings.⟨src⟩ contracts/src/svg/SvgGenerator.sol:63-73 :: _colors / _backgroundColors⟨src⟩ web/src/lib/engine/audioPalette.ts :: COLORS / BACKGROUND_COLORS (TS mirror)
  3. Don't change the z-order — render order is part of the contract.⟨src⟩ contracts/src/svg/SvgGenerator.sol:144-192 :: generateSvg⟨src⟩ web/src/lib/engine/audioSvg.ts :: renderAudioSvgFromMetadata (TS mirror)
  4. Lock variant counts before first mint. Adding a variant changes (field % count) retroactively for old uAUD.⟨src⟩ contracts/src/svg/SvgGenerator.sol:160-189 :: (m.field % count) + 1
  5. Bits 248..255 are reserved. The decoder stops at bit 247 — leave room for future fields.⟨src⟩ contracts/src/svg/AudioMetadata.sol:36-56 :: seed >> 96; 19 × uint8

Off-chain music engine determinism is independent: the PRNG is mulberry32(melodyVar) and the only side-channel is AudioContext sample-rate (timing). There is no Solidity counterpart to cross-check against — bits 0..81 are FE-only. Visual cross-engine parity (TS port ↔ Solidity) can be eyeballed by running forge test --match-path test/Render.t.sol and comparing svg-out/sample-*.svg against the same seeds rendered by renderAudioSvg.⟨src⟩ contracts/test/Render.t.sol :: RenderTest.test_render_sample_svgs