import { useState, useEffect, useRef, useCallback } from "react"; // ── Audio ──────────────────────────────────────────────────────────────── const getCtx = (() => { let c = null; return () => { if (!c) c = new (window.AudioContext || window.webkitAudioContext)(); if (c.state === "suspended") c.resume(); return c; }; })(); const FREQS = [261.63, 293.66, 329.63, 349.23, 392.0, 440.0, 493.88, 523.25]; const NAMES = ["C", "D", "E", "F", "G", "A", "B", "C'"]; const FREQS_LO = [130.81, 146.83, 164.81, 174.61, 196.0, 220.0, 246.94, 261.63]; function beep(freq, dur = 0.1, wave = "square", vol = 0.06) { try { const c = getCtx(), o = c.createOscillator(), g = c.createGain(); o.type = wave; o.frequency.value = freq; g.gain.setValueAtTime(vol, c.currentTime); g.gain.exponentialRampToValueAtTime(0.001, c.currentTime + dur); o.connect(g); g.connect(c.destination); o.start(); o.stop(c.currentTime + dur); } catch (e) {} } // ── Helpers ────────────────────────────────────────────────────────────── const COLS = 16; const ROWS = 8; const emptyGrid = () => Array.from({ length: ROWS }, () => Array(COLS).fill(false)); const cloneGrid = (g) => g.map((r) => [...r]); const gridToState = (g, col) => col + ":" + g.map((r) => (r[col] ? "1" : "0")).join(""); // ── Piano Roll Grid ────────────────────────────────────────────────────── function PianoRoll({ grid, setGrid, step, disabled, highlight, cols = COLS, accent = "#2c6e8a", label }) { const toggle = (r, c) => { if (disabled) return; const ng = cloneGrid(grid); ng[r][c] = !ng[r][c]; setGrid(ng); if (ng[r][c]) beep(FREQS[r], 0.08); }; return (
{label &&
{label}
}
{Array.from({ length: ROWS }).map((_, ri) => { const row = ROWS - 1 - ri; return [
{NAMES[row]}
, ...Array.from({ length: cols }).map((_, col) => { const on = grid[row][col]; const isStep = step === col; const hl = highlight && highlight[row] && highlight[row][col]; return (
toggle(row, col)} style={{ width: "100%", aspectRatio: "1", borderRadius: 3, background: on ? isStep ? "#fff" : hl ? "#b8860b" : accent : isStep ? `${accent}22` : col % 4 === 0 ? "#f0e8da" : "#f7f2ea", border: isStep ? `1.5px solid ${accent}` : "1px solid #e8dcc8", cursor: disabled ? "default" : "pointer", transition: "background 0.08s", boxShadow: on && isStep ? `0 0 8px ${accent}66` : "none", }} /> ); }), ]; })}
); } // ── Cycle Tracker (Section 1) ──────────────────────────────────────────── function CycleTracker({ cycle, looped }) { if (cycle < 1) return null; return (
{looped ? ( <> ⟳ Cycle {cycle} — the playhead is back at step 1, with the same notes ahead of it. Every cycle from here on will be identical. The music is trapped in a loop forever. ) : ( <>Completing cycle {cycle}… )}
); } // ── Rule toggle chip ───────────────────────────────────────────────────── function RuleChip({ label, active, onToggle, color = "#5e548e" }) { return ( ); } // ── Section wrapper ────────────────────────────────────────────────────── function Section({ num, title, children }) { return (
{num}.

{title}

♩ ♪ ♫
{children}
); } const P = ({ children, style }) => (

{children}

); const Em = ({ children, color = "#2c6e8a" }) => {children}; const Mono = ({ children }) => {children}; function Btn({ playing, onPlay, onStop, label }) { return ( ); } function ResetBtn({ onClick }) { return ( ); } // ── MAIN COMPONENT ─────────────────────────────────────────────────────── export default function Explorable() { // == Section 1: Draw & Play == const initGrid1 = () => { const g = emptyGrid(); [[0,0],[2,1],[4,2],[3,3],[5,4],[4,5],[2,6],[0,7],[1,8],[3,9],[5,10],[7,11],[6,12],[4,13],[2,14],[0,15]].forEach(([r,c]) => { if (r < ROWS && c < COLS) g[r][c] = true; }); return g; }; const [grid1, setGrid1] = useState(initGrid1); const [playing1, setPlaying1] = useState(false); const [step1, setStep1] = useState(-1); const [cycle1, setCycle1] = useState(0); const [looped1, setLooped1] = useState(false); const iv1 = useRef(null); const grid1Ref = useRef(grid1); const running1 = useRef(false); grid1Ref.current = grid1; const stop1 = useCallback(() => { running1.current = false; if (iv1.current) { clearInterval(iv1.current); iv1.current = null; } setPlaying1(false); setStep1(-1); }, []); const reset1 = () => { stop1(); setGrid1(initGrid1()); setCycle1(0); setLooped1(false); }; const play1 = () => { stop1(); setCycle1(0); setLooped1(false); setPlaying1(true); running1.current = true; let pos = 0; iv1.current = setInterval(() => { if (!running1.current) { clearInterval(iv1.current); iv1.current = null; return; } const g = grid1Ref.current; const col = pos % COLS; setStep1(col); for (let r = 0; r < ROWS; r++) if (g[r][col]) beep(FREQS[r], 0.12); if (col === 0 && pos > 0) { const c = Math.floor(pos / COLS) + 1; setCycle1(c); if (c >= 2) setLooped1(true); } pos++; if (pos > COLS * 5) { clearInterval(iv1.current); iv1.current = null; running1.current = false; setPlaying1(false); } }, 180); }; // == Section 2: Live edit == const initGrid2 = () => { const g = emptyGrid(); [[0,0],[2,2],[4,4],[5,6],[3,8],[1,10],[4,12],[6,14]].forEach(([r,c]) => { if (r < ROWS && c < COLS) g[r][c] = true; }); return g; }; const [grid2, setGrid2] = useState(initGrid2); const [playing2, setPlaying2] = useState(false); const [step2, setStep2] = useState(-1); const [edits2, setEdits2] = useState(0); const [uniqueStates2, setUniqueStates2] = useState(0); const [totalSteps2, setTotalSteps2] = useState(0); const [looping2, setLooping2] = useState(false); const [lastEditStep2, setLastEditStep2] = useState(0); const iv2 = useRef(null); const grid2Ref = useRef(grid2); const states2Set = useRef(new Set()); const steps2Ref = useRef(0); const edits2Ref = useRef(0); const running2 = useRef(false); const stepsSinceNew = useRef(0); grid2Ref.current = grid2; const stop2 = useCallback(() => { running2.current = false; if (iv2.current) { clearInterval(iv2.current); iv2.current = null; } setPlaying2(false); setStep2(-1); }, []); const reset2 = () => { stop2(); setGrid2(initGrid2()); setEdits2(0); setUniqueStates2(0); setTotalSteps2(0); setLooping2(false); setLastEditStep2(0); states2Set.current = new Set(); }; const play2 = () => { stop2(); states2Set.current = new Set(); steps2Ref.current = 0; edits2Ref.current = 0; stepsSinceNew.current = 0; setUniqueStates2(0); setTotalSteps2(0); setEdits2(0); setLooping2(false); setLastEditStep2(0); setPlaying2(true); running2.current = true; let pos = 0; iv2.current = setInterval(() => { if (!running2.current) { clearInterval(iv2.current); iv2.current = null; return; } const g = grid2Ref.current; const col = pos % COLS; setStep2(col); for (let r = 0; r < ROWS; r++) if (g[r][col]) beep(FREQS[r], 0.12); const sizeBefore = states2Set.current.size; states2Set.current.add(gridToState(g, col)); if (states2Set.current.size > sizeBefore) { stepsSinceNew.current = 0; } else { stepsSinceNew.current++; } steps2Ref.current++; setUniqueStates2(states2Set.current.size); setTotalSteps2(steps2Ref.current); setLooping2(stepsSinceNew.current >= COLS); pos++; if (pos > 300) { clearInterval(iv2.current); iv2.current = null; running2.current = false; setPlaying2(false); } }, 180); }; const setGrid2Tracked = (g) => { setGrid2(g); if (playing2) { edits2Ref.current++; setEdits2(edits2Ref.current); stepsSinceNew.current = 0; setLooping2(false); setLastEditStep2(steps2Ref.current); } }; // == Section 3: Self-modifying == const initGrid3 = () => { const g = emptyGrid(); [[0,0],[2,1],[4,2],[5,3],[4,4],[2,5],[3,6],[5,7],[7,8],[6,9],[4,10],[3,11],[2,12],[4,13],[6,14],[5,15]].forEach(([r,c]) => { if (r < ROWS && c < COLS) g[r][c] = true; }); return g; }; const [grid3, setGrid3] = useState(initGrid3); const [playing3, setPlaying3] = useState(false); const [step3, setStep3] = useState(-1); const [rules, setRules] = useState({ staleShift: false, mirror: false, gravity: false, chaos: false, echo: false }); const [mutations3, setMutations3] = useState([]); const [uniqueStates3, setUniqueStates3] = useState(0); const [totalSteps3, setTotalSteps3] = useState(0); const iv3 = useRef(null); const grid3Ref = useRef(grid3); const states3Set = useRef(new Set()); const steps3Ref = useRef(0); const rulesRef = useRef(rules); const lastNotes = useRef(Array(COLS).fill(-1)); const repeatCount = useRef(Array(COLS).fill(0)); const running3 = useRef(false); grid3Ref.current = grid3; rulesRef.current = rules; const stop3 = useCallback(() => { running3.current = false; if (iv3.current) { clearInterval(iv3.current); iv3.current = null; } setPlaying3(false); setStep3(-1); }, []); const reset3 = () => { stop3(); setGrid3(initGrid3()); setMutations3([]); setUniqueStates3(0); setTotalSteps3(0); states3Set.current = new Set(); }; const play3 = () => { stop3(); states3Set.current = new Set(); steps3Ref.current = 0; lastNotes.current = Array(COLS).fill(-1); repeatCount.current = Array(COLS).fill(0); setMutations3([]); setUniqueStates3(0); setTotalSteps3(0); setPlaying3(true); running3.current = true; let pos = 0; iv3.current = setInterval(() => { if (!running3.current) { clearInterval(iv3.current); iv3.current = null; return; } const g = grid3Ref.current; const col = pos % COLS; setStep3(col); // Play let highest = -1; for (let r = 0; r < ROWS; r++) if (g[r][col]) { beep(FREQS[r], 0.12); highest = Math.max(highest, r); } // Track state states3Set.current.add(gridToState(g, col)); steps3Ref.current++; setUniqueStates3(states3Set.current.size); setTotalSteps3(steps3Ref.current); // Track repeats if (highest === lastNotes.current[col]) { repeatCount.current[col]++; } else { repeatCount.current[col] = 0; lastNotes.current[col] = highest; } // Apply rules at end of each full cycle if (col === COLS - 1 && pos >= COLS) { const ng = cloneGrid(g); const R = rulesRef.current; const muts = []; if (R.staleShift) { for (let c = 0; c < COLS; c++) { if (repeatCount.current[c] >= 2) { for (let r = ROWS - 1; r >= 0; r--) { if (ng[r][c]) { ng[r][c] = false; const nr = Math.min(ROWS - 1, r + 1); ng[nr][c] = true; muts.push(`col ${c}: ${NAMES[r]}→${NAMES[nr]} (stale)`); repeatCount.current[c] = 0; break; } } } } } if (R.mirror) { for (let c = 0; c < Math.floor(COLS / 2); c++) { const mc = COLS - 1 - c; for (let r = 0; r < ROWS; r++) { if (ng[r][c] && !ng[r][mc]) { ng[r][mc] = true; muts.push(`col ${mc}: +${NAMES[r]} (mirror)`); } } } // Then shift the source side for (let c = 0; c < Math.floor(COLS / 4); c++) { for (let r = 0; r < ROWS; r++) { if (ng[r][c]) { ng[r][c] = false; const nr = (r + 1) % ROWS; ng[nr][c] = true; muts.push(`col ${c}: ${NAMES[r]}→${NAMES[nr]} (evolve)`); break; } } } } if (R.gravity) { for (let c = 0; c < COLS; c++) { for (let r = 1; r < ROWS; r++) { if (ng[r][c] && !ng[r - 1][c]) { ng[r][c] = false; ng[r - 1][c] = true; muts.push(`col ${c}: ${NAMES[r]}→${NAMES[r - 1]} (gravity)`); break; } } } } if (R.echo) { for (let c = COLS - 1; c >= 1; c--) { for (let r = 0; r < ROWS; r++) { if (g[r][c - 1] && !ng[r][c] && Math.random() < 0.3) { ng[r][c] = true; muts.push(`col ${c}: +${NAMES[r]} (echo)`); } } } // Remove some old notes to prevent saturation for (let c = 0; c < COLS; c++) { let count = 0; for (let r = 0; r < ROWS; r++) if (ng[r][c]) count++; if (count > 3) { for (let r = 0; r < ROWS; r++) { if (ng[r][c] && Math.random() < 0.4) { ng[r][c] = false; muts.push(`col ${c}: -${NAMES[r]} (thin)`); break; } } } } } if (R.chaos) { const c1 = Math.floor(Math.random() * COLS); const c2 = Math.floor(Math.random() * COLS); if (c1 !== c2) { for (let r = 0; r < ROWS; r++) { const tmp = ng[r][c1]; ng[r][c1] = ng[r][c2]; ng[r][c2] = tmp; } muts.push(`swap cols ${c1}↔${c2} (chaos)`); } } if (muts.length > 0) { grid3Ref.current = ng; setGrid3(ng); setMutations3((p) => [...p.slice(-(12 - muts.length)), ...muts]); } } pos++; if (pos > 500) { clearInterval(iv3.current); iv3.current = null; running3.current = false; setPlaying3(false); } }, 160); }; // == Section 4: Two voices == const initGridA = () => { const g = emptyGrid(); [[4,0],[5,2],[7,4],[5,6],[4,8],[5,10],[7,12],[5,14]].forEach(([r,c]) => { if (r < ROWS && c < COLS) g[r][c] = true; }); return g; }; const initGridB = () => { const g = emptyGrid(); [[0,0],[0,4],[1,8],[0,12]].forEach(([r,c]) => { if (r < ROWS && c < COLS) g[r][c] = true; }); return g; }; const [gridA, setGridA] = useState(initGridA); const [gridB, setGridB] = useState(initGridB); const [playing4, setPlaying4] = useState(false); const [step4, setStep4] = useState(-1); const [voiceRules, setVoiceRules] = useState({ avoid: false, call: false, trade: false }); const [msgs4, setMsgs4] = useState([]); const iv4 = useRef(null); const gARef = useRef(gridA); const gBRef = useRef(gridB); const vrRef = useRef(voiceRules); const running4 = useRef(false); gARef.current = gridA; gBRef.current = gridB; vrRef.current = voiceRules; const stop4 = useCallback(() => { running4.current = false; if (iv4.current) { clearInterval(iv4.current); iv4.current = null; } setPlaying4(false); setStep4(-1); }, []); const reset4 = () => { stop4(); setGridA(initGridA()); setGridB(initGridB()); setMsgs4([]); setVoiceRules({ avoid: false, call: false, trade: false }); }; const play4 = () => { stop4(); setMsgs4([]); setPlaying4(true); running4.current = true; let pos = 0; iv4.current = setInterval(() => { if (!running4.current) { clearInterval(iv4.current); iv4.current = null; return; } const a = gARef.current, b = gBRef.current; const col = pos % COLS; setStep4(col); for (let r = 0; r < ROWS; r++) { if (a[r][col]) beep(FREQS[r], 0.1, "square", 0.05); if (b[r][col]) beep(FREQS_LO[r], 0.14, "triangle", 0.06); } if (col === COLS - 1 && pos >= COLS) { const na = cloneGrid(a), nb = cloneGrid(b); const R = vrRef.current; const ms = []; if (R.avoid) { for (let c = 0; c < COLS; c++) { for (let r = 0; r < ROWS; r++) { if (na[r][c] && nb[r][c]) { if (r < ROWS - 1) { na[r][c] = false; na[r + 1][c] = true; ms.push(`Voice A moves up at col ${c} to avoid clash`); } break; } } } } if (R.call) { for (let c = 0; c < COLS - 2; c++) { for (let r = 0; r < ROWS; r++) { if (na[r][c] && !nb[r][c + 2] && Math.random() < 0.25) { nb[r][c + 2] = true; ms.push(`Voice B echoes A's ${NAMES[r]} at col ${c + 2}`); } } } // Thin B if too dense for (let c = 0; c < COLS; c++) { let cnt = 0; for (let r = 0; r < ROWS; r++) if (nb[r][c]) cnt++; if (cnt > 2) for (let r = ROWS - 1; r >= 0; r--) { if (nb[r][c]) { nb[r][c] = false; break; } } } } if (R.trade) { const c1 = Math.floor(Math.random() * COLS); for (let r = 0; r < ROWS; r++) { const t = na[r][c1]; na[r][c1] = nb[r][c1]; nb[r][c1] = t; } ms.push(`Voices swap material at col ${c1}`); } if (ms.length > 0) { gARef.current = na; gBRef.current = nb; setGridA(na); setGridB(nb); setMsgs4((p) => [...p.slice(-6), ...ms]); } } pos++; if (pos > 400) { clearInterval(iv4.current); iv4.current = null; running4.current = false; setPlaying4(false); } }, 170); }; useEffect(() => () => { running1.current = false; running2.current = false; running3.current = false; running4.current = false; [iv1, iv2, iv3, iv4].forEach((r) => { if (r.current) { clearInterval(r.current); r.current = null; } }); }, []); return (
{/* Header */}
{Array.from({ length: 10 }).map((_, i) =>
)}
An Explorable Explanation

Why Every Score Loops
(And How to Break It)

Draw music. Watch it loop. Then break the loop yourself.
Based on Emanuele Rodolà's Lean 4 proof that music playback is eventually periodic.

{/* ── SECTION 1: Draw & hear the loop ── */}

Click the grid to place notes — each column is a beat, each row a pitch. Then press play and let it run. Watch what happens when the playhead reaches the end and starts over.

Of course it repeats — the notes haven't changed. The playhead returns to beat 1, sees the exact same score, and does the exact same thing. But this isn't just about short loops. A MIDI file with ten thousand events across sixteen channels still has a finite number of internal states. The loop might take longer to emerge, but it's mathematically guaranteed. After enough steps, some state must recur, and from that point on: eternal repetition.

This is the core of Rodolà's Lean 4 proof: any fixed score played by any standard engine is eventually periodic. A finite score plus a finite player equals a finite number of possible states. By the pigeonhole principle, repetition is inevitable. The interesting question is: what would it take to escape?

{/* ── SECTION 2: Live editing ── */}

Same grid, but this time: click notes while the music is running. Add notes. Remove them. Reshape the melody mid-flight. Every edit you make creates a score the player has never seen before — which means it can't fall back into its old loop.

{(totalSteps2 > 0) && (
0 ? "#3a5a4010" : "#f5ebe0", borderLeft: `3px solid ${looping2 ? "#a63d40" : edits2 > 0 ? "#3a5a40" : "#d5c4a1"}` }}>
{looping2 ? ( edits2 > 0 ? <>You stopped editing, and the score is fixed again. It's looping. Make another edit to break free. : <>Looping — without edits, the fixed score repeats just like in Section 1. Click a note to change it! ) : edits2 === 0 ? ( <>Playing… the score hasn't been edited yet. It will loop after one full cycle. ) : ( <>You've made {edits2} edit{edits2 !== 1 ? "s" : ""}, creating {uniqueStates2} unique states the player has never seen. Still evolving. )}
)}

Notice what happens: each edit breaks the loop, but only while you keep editing. The moment you stop, the score is fixed again, and it falls back into a cycle within one pass. Your intervention keeps the music alive — but it requires constant effort. What if the music could modify itself, without waiting for you?

{/* ── SECTION 3: Self-modifying rules ── */}

Draw a starting pattern, then toggle on one or more mutation rules below. These are simple instructions that the sequencer applies to its own score after each full cycle. Watch the grid change as the rules reshape the music in real time.

setRules((r) => ({ ...r, staleShift: !r.staleShift }))} color="#2c6e8a" /> setRules((r) => ({ ...r, gravity: !r.gravity }))} color="#3a5a40" /> setRules((r) => ({ ...r, echo: !r.echo }))} color="#b8860b" /> setRules((r) => ({ ...r, mirror: !r.mirror }))} color="#5e548e" /> setRules((r) => ({ ...r, chaos: !r.chaos }))} color="#a63d40" />
{(totalSteps3 > 0) && (
COLS * 2 ? "#5e548e10" : "#f5ebe0", borderLeft: `3px solid ${uniqueStates3 > COLS * 2 ? "#5e548e" : "#d5c4a1"}` }}>
After {totalSteps3} steps, the rules have generated {uniqueStates3} unique configurations{uniqueStates3 > COLS * 2 ? " — far beyond what a fixed score could produce." : "."}
)}
{mutations3.length > 0 && (
{mutations3.map((m, i) =>
{m}
)}
)}

Try combining rules. "Gravity" pulls notes downward each cycle. "Stale → shift up" detects when a column repeats the same note and nudges it higher. Together they create a tug-of-war that keeps the music moving. "Echo" spreads notes rightward, filling the grid until it thins itself out. Turn them all on and watch the pattern dissolve and reform.

The grid is no longer a fixed score — it's a process. Each cycle produces a configuration the system has never been in before. And processes don't have to be periodic.

{/* ── SECTION 4: Two voices ── */}

Rules get more interesting when there's more than one voice. Below are two grids — Voice A (square wave, higher) and Voice B (triangle wave, lower). Draw patterns for each, then turn on interaction rules that let the voices respond to each other.

setVoiceRules((r) => ({ ...r, avoid: !r.avoid }))} color="#a63d40" /> setVoiceRules((r) => ({ ...r, call: !r.call }))} color="#2c6e8a" /> setVoiceRules((r) => ({ ...r, trade: !r.trade }))} color="#5e548e" />
{msgs4.length > 0 && (
{msgs4.map((m, i) =>
{m}
)}
)}

"Avoid clashes" makes Voice A move up when both voices hit the same pitch. "Call & response" has Voice B echo fragments of A, two steps later. "Trade material" randomly swaps columns between voices. These voices aren't just playing — they're listening to each other and adapting.

And this is just two voices with three simple rules. Imagine dozens of voices, each with rich rules, modifying each other continuously. The score becomes a living thing — never repeating, always evolving, structurally incapable of looping. The proof's constraint dissolves the moment the score becomes mutable.

{/* ── Coda ── */}
♩ ♪ ♫ ♬

Rodolà's proof is mathematically trivial and conceptually profound. Fixed scores loop. That's not a bug in music — it's a bug in how we build music tools. The escape isn't more notes or bigger files. It's architecture: systems where the score is a living document, modified by processes that listen, reason, and react. The question isn't whether this is possible. It's what it would sound like.

Further Reading
{[ ["The proof", "Emanuele Rodolà's Lean 4 proof that music is eventually periodic", "https://x.com/EmanueleRodola/status/2042509481221767633"], ["The researcher", "Emanuele Rodolà — GLADIA Lab, Sapienza University of Rome", "https://gladia.di.uniroma1.it/authors/rodola/"], ].map(([label, desc, url], i) => ( {label} {desc} ))}

All demos use the Web Audio API. Best with headphones.

); }