← Index

Probablyaminor

2026web audio · workerLiveRepo

The Institute for A-Minor Studies is a fake musicology institute built around one line in Kendrick Lamar's "Not Like Us." Its centerpiece is a harmonic analysis bench: play any chord on the on-screen keyboard, run the analysis, and the finding is always A minor.

The verdict is hardcoded. It has to be — that's the joke:

function finish() {
  // The finding.
  [57, 60, 64].forEach(function (m, i) { play(m, 0.06 + i * 0.055, 2.6, 1.05); });
  document.getElementById("chord").textContent = "A minor";
  document.getElementById("conf").textContent = (99.1 + Math.random() * 0.8).toFixed(1) + "%";

Those MIDI numbers are A3, C4, E4. The confidence figure is noise with a decimal place, which is the only part of academic presentation that really matters.

If you actually play A minor, the bench says: "You built it yourself. That one's on you."

The joke is the front door

Behind it is /studio/ — 2,557 lines of Web Audio that owes the bit nothing. A 16-step, four-pattern sequencer with song mode, twenty-odd synthesized drum voices, a two-oscillator subtractive synth, sample loading by drag-and-drop or microphone, and an offline bounce to WAV.

Timing is the classic lookahead scheduler — a 25ms timer filling a 120ms window, because setTimeout cannot be trusted with anything you intend to hear. Two things sit on top of it. Swing is applied only to odd steps, so it displaces the offbeat without dragging the pulse. And the beat-repeat freezes the transport: while stutter is engaged curStep stops advancing and the scheduler re-emits a frozen step at a subdivided duration, so releasing it resumes exactly where the pattern would have been rather than wherever the stutter wandered to.

The part worth stealing

The offline bounce re-renders through the same code path as live playback.

Every voice takes the audio context as its first argument — buildChain(c), createSynth(c, dest), fire(c, dest, i, t, vel) — rather than closing over a module-level context. That single constraint is what lets OfflineAudioContext run the identical fire() and laneStep() functions the speakers just used. One code path, two contexts, byte-identical output.

The alternative — a separate render path that reimplements the voices — is the version where the export sounds subtly wrong and nobody can say why. The WAV header is then assembled by hand into a DataView, which is less work than it sounds and the only honest way to avoid a dependency.

Zero runtime dependencies. Two HTML files, 3,292 lines. Deploys with wrangler deploy.