v0.4.0_LIVE

Pimp My Sample

Pimp My Sample records any tab into your sample library, from Chrome's side panel. The primary library is a folder you choose on disk, with WAV files, real subfolders and a library.json manifest.

PERIOD
Aug 5–10, 2026
PLATFORM
Chrome extension (MV3)

Challenge

Chrome extension for music producers: record any browser tab into a real sample library on your disk. Capture runs in an offscreen AudioWorklet, so recording survives closing the panel. Key, BPM and loudness analysis runs in a worker, with a hand-written Goertzel chromagram for key detection. Trim, chop on onsets, export lossless WAV.

chrome.tabCapture.getMediaStreamId works only in the background service worker. It needs a per-tab activeTab grant from a user gesture: an icon click, a context menu or the ⌥⇧R command. No manifest permission bypasses this.

Closing the panel dropped the folder grant, because File System Access session grants end when the extension origin’s last live context closes. Capture APIs cannot be tested headless, so the gate for recording is a manual soak test in Chrome.

Recorder card: a green record button, a timer reading 0:00.0, settings and ⌥⇧R shortcut buttons, a “Saves to” menu set to the “drums” folder, and a green visualiser of wave lines under a striped sun on a horizon.
The recorder card: a green record button, a timer reading 0:00.0, settings and ⌥⇧R shortcut buttons, a “Saves to” menu set to the “drums” folder, and a green visualiser of wave lines under a striped sun on a horizon.

Approach

My proof of concept, dated 5 August 2026, set out to show that a Chrome side panel can record a tab’s audio without muting the tab. It recorded with MediaRecorder, as webm/opus.

My product plan replaced MediaRecorder and webm with WAV through an AudioWorklet, because producers want WAV. A plan principle says recording must not die with the UI. So all capture moved into an offscreen document, and the side panel became a remote control and a view.

The implementation plans tell agentic workers to use subagent-driven development, task by task. Wave 1A ran on its own branch, in an isolated git worktree, and landed as the merge commit “merge: wave 1A — typed message router + send wrappers”. The project rule is that decision-making code is a pure function in a non-UI modules/*/ file, with a colocated test written first.

CRT line drawing: a waveform with onset chop markers, a twelve-bin chromagram and a playhead, close up
CRT line drawing: a waveform with onset chop markers, a twelve-bin chromagram and a playhead, close up.

Architecture

The background service worker stays thin: gesture grants, streamId minting, session state and the badge. One offscreen document owns every capture session and writes finished clips to IndexedDB. Side panels read shared state from chrome.storage.session. Several tabs can record at once: sessions are keyed by tabId, and the toolbar badge shows the number of active sessions.

Inside the offscreen document, the engine opens the tab’s stream with getUserMedia, using the stream id minted by the background. It routes the source three ways: into the wav-recorder AudioWorklet for capture, through a gain node to the speakers so the tab stays audible, and into an AnalyserNode with fftSize 512 for the record visual. The worklet posts a copy of each block’s channel data and computes RMS itself, so the level meter reflects the block the encoder receives. PCM chunks accumulate in memory per channel and are encoded to WAV at stop.

On 7 August I made the user’s chosen folder the primary library: WAV files, real subfolders and a library.json manifest. IndexedDB became the recording buffer and UI cache. A pure planner emits a list of filesystem operations, and an impure shell persists the handle, manages permissions and runs the plan.

Studio editor in Chop mode for “Rec14 - dusty soul chords”: readouts of 94 BPM, Fm, -14.2 LUFS and 0:04.000 at 44.1 kHz; the waveform split into 18 coloured slices (R1 to R18) on a 1/4 grid; side panels for slices, transient detection, processing, history and WAV export with “Save 18 samples to library” and “Download all” buttons.
The Studio editor in Chop mode for “Rec14 - dusty soul chords”, with readouts of 94 BPM, Fm, -14.2 LUFS and 0:04.000 at 44.1 kHz. The waveform is split into 18 coloured slices (R1 to R18) on a 1/4 grid. Side panels cover slices, transient detection, processing, history and WAV export.

Decisions

  • A session is “starting” once the offscreen start succeeds, and becomes “recording” only when the first worklet frame arrives: evidence, not intent.
  • The folder is the library. When a tracked file disappears from disk, disk wins: the clip leaves the manifest and the app library. Deleted clips move into a _Trash folder and are never hard-deleted, because the folder holds the user’s data.
  • Chops are saved as separate child WAV clips with a parentId, named <parent>.01/.02, not as pointers into the parent, because library samples must be independently exportable files.
  • Studio is its own WXT entrypoint, limited to one tab, because two Studios on one blob risk corruption.
  • The trim and chop editor became an in-house canvas wave editor whose pointer gestures are a pure reducer, and its pure logic shipped with 179 tests.
  • Transient detection uses spectral flux with a past-only moving-median threshold, because a symmetric window lets the sustain after an attack mask the attack itself. Clips longer than 30 seconds run onset detection in a module Worker, so dragging the sensitivity slider does not block the UI thread.
  • I removed LUFS-matched audition on 10 August, after a slice at about -28 LUFS received the full 4x boost. Clips now play at unity gain in the library and in Studio, and the measured LUFS stays as a badge in the Studio header.
  • An independent review found that integer-bin rounding in the key detector collided seven adjacent semitone pairs from C2 through D3. The fix switched to exact-frequency Goertzel and raised the floor to C3; the range is now C3 (MIDI 48) through B6 (MIDI 95).
modules/analysis/key.ts
// Why C3 and not lower: at FRAME_SIZE=4096/44.1kHz, the DFT bin spacing is sampleRate/FRAME_SIZE
// ≈ 10.77Hz. A naive implementation that rounds each target frequency to the nearest integer bin
// (`k = round(FRAME_SIZE * f / sampleRate)`) collides adjacent semitones onto the *same* bin
// throughout C2–D3 (e.g. MIDI 36/37, 65.41Hz and 69.30Hz, both round to k=6) — seven such
// collisions were verified by direct computation. This implementation instead evaluates Goertzel
// at the *exact* target frequency (no bin rounding — see goertzelMagnitude below), which removes
// the collision bug entirely. But exact-frequency evaluation doesn't remove spectral leakage: with
// only ~10.77Hz of bin resolution, a semitone gap below C3 is still narrow relative to the
// Goertzel filter's main-lobe width, so energy from one pitch class leaks heavily into its
// neighbors' bins. Measured (pure sine, adjacent-semitone magnitude relative to the true bin's
// peak): ~84% leakage into the nearest neighbor at C2 (MIDI 36), dropping to ~40% at C3 (MIDI 48)
// and negligible by C4 (MIDI 60). C3 is the floor where discrimination becomes usable for chroma
// purposes at this frame size; going lower would require a longer frame (finer resolution) at the
// cost of time resolution, which isn't a trade this module needs to make for key detection.
const MIN_MIDI = 48; // C3
Close-up of a green waveform with sharp, decaying peaks on a time ruler from 0.0 to 3.5 seconds; a vertical marker with a square handle sits near 1.4 seconds, and the waveform before it is drawn darker.
Close-up of a green waveform with sharp, decaying peaks on a time ruler from 0.0 to 3.5 seconds. A vertical marker with a square handle sits near 1.4 seconds, and the waveform before it is drawn darker.

Outcome

The launch-post notes record 213 commits over five days, from 6 to 10 August 2026. The v0.2.0 review had counted about 1,550 production lines and 16 tests. At v0.4.0, the notes record 1,102 tests passing in 87 test files and a 1.6 MB built extension.

The landing page went live on 8 August with a demo video. It says the product is free, with no tiers, trials or account. The privacy policy states no accounts, analytics, telemetry or servers, and says the extension makes no network requests.

The v0.2.0 review lists full in-RAM buffering as known, with OPFS streaming deferred to a later tier. The store listing draft recommends up to about 30 minutes per clip in this version. The plan’s launch gate was 30 minutes of real digging with zero lost or corrupted recordings. On 6 August, the v0.4 spec listed the 30-minute soak test as never run on any version.

> OUTPUT: Published on the Chrome Web Store. Built in parallel agent waves in isolated git worktrees, test-first for the logic.

  • Chrome MV3
  • WXT
  • React 19
  • TypeScript
  • AudioWorklet
  • File System Access API
  • Vitest
  • Playwright

ALL_FRAMES

CRT line drawing: a waveform with onset chop markers, a twelve-bin chromagram and a playheadCRT line drawing: a waveform with onset chop markers, a twelve-bin chromagram and a playhead, close upCRT line drawing: a waveform with onset chop markers, a twelve-bin chromagram and a playhead, tuning in from staticCRT line drawing: a waveform with onset chop markers, a twelve-bin chromagram and a playhead, at phone densityRecorder card: a green record button, a timer reading 0:00.0, settings and ⌥⇧R shortcut buttons, a “Saves to” menu set to the “drums” folder, and a green visualiser of wave lines under a striped sun on a horizon.Close-up of a green waveform with sharp, decaying peaks on a time ruler from 0.0 to 3.5 seconds; a vertical marker with a square handle sits near 1.4 seconds, and the waveform before it is drawn darker.Studio editor in Chop mode for “Rec14 - dusty soul chords”: readouts of 94 BPM, Fm, -14.2 LUFS and 0:04.000 at 44.1 kHz; the waveform split into 18 coloured slices (R1 to R18) on a 1/4 grid; side panels for slices, transient detection, processing, history and WAV export with “Save 18 samples to library” and “Download all” buttons.