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 (
{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 (
Draw music. Watch it loop. Then break the loop yourself.
Based on Emanuele Rodolà's Lean 4 proof that music playback is eventually periodic.
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?
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.
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?
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.
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.
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.
"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.
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.
All demos use the Web Audio API. Best with headphones.