Overview
Craptronics Plc brings you the Junk-Phonic 9000 is a single photograph of a boombox that has been talked into working. There's no 3D model, no canvas-drawn cabinet, no sprite sheet — just a real product photo, sliced apart in the places where buttons and dials already exist, and rebuilt as a living interface using nothing but HTML, CSS, and the Web Audio API.
Load an mp3 into it and it plays through a cassette deck with genuine wow & flutter — not a filter that makes things sound "vintage," but a continuous playback-rate wobble that behaves like an actual worn tape transport, pitch drift and all. Tune the internet radio and the VU needles sweep with real programme level. Turn up the bass and the woofers — cut from the same photo, layered back on top of it — visibly vibrate to the actual waveform, not just a generic "louder = bigger" pulse. Press Crap Bass Boost, which despite the name is a boxy telephone-band filter that removes bass, and the woofers audibly and visibly calm down, because that's genuinely what's happening to the signal.
It was built by Craptronics Plc, who also produced the Crap-turon 5000, and the tape currently loaded in the deck is a mix called Hits 1982, which has never existed and never will.
How it's built
The photo is the UI
Every control is an absolutely-positioned HTML element sitting directly on top of the source photograph, with its coordinates measured from the actual image pixels rather than guessed. Concretely: the photo is loaded into Python/PIL, then thresholded and scanned for the distinctive colours or brightness signatures of each control — the yellow REC button, the amber VU meter glow, the dark cassette window, the chrome speaker rims — to pull out exact bounding boxes in pixel space. Those get converted to percentages of the image's width/height, which is what lets every overlay track correctly regardless of how large the photo is rendered (the whole page scales as one fluid unit, from a phone screen up to a 1600px-wide desktop layout).
The two speaker cones use the same trick in reverse: they're crops of the original photo's own woofers, repositioned in a <div> on top of the now-cone-less base image, so they can be independently scaled and brightened without touching the rest of the photo.
Where a control needed to fit into genuinely tiny real estate (the tape transport buttons sit in a strip of chassis roughly 40px wide at native resolution), the UI shrinks to icon-only pill buttons rather than breaking the photo's geometry. Where a piece of text was baked directly into the photo — the brand logo — it was inpainted out (a clean patch of the panel stretched over the old logo) and redrawn with new text, rather than faked with an HTML overlay.
Audio architecture
Two independent AudioContexts run side by side, exactly mirroring the two physical audio sources:
Tape A (Web Audio graph, per file loaded):
<audio> element ──▶ highpass (80Hz) ──▶ lowpass (8kHz) ──▶ compressor ──▶ tone stage ──▶ destination │ ▶ analyser (level meter)
- The highpass/lowpass/compressor stage is the "worn tape" character — a fixed EQ+dynamics preset modelling a well-used cassette's frequency response.
- A
requestAnimationFrameloop continuously nudgesplaybackRatewith layered sine waves (a slow ~0.1Hz "wow" plus a faster ~0.8Hz "flutter," plus a touch of random jitter) — this is what makes the pitch audibly drift rather than just sound EQ'd. - Reverse playback ("Auto Reverse") doesn't use the
<audio>element at all, since HTML5 audio has no reverse-playback mode. Instead the file is decoded once viadecodeAudioData, the channel data is reversed sample-by-sample into a freshAudioBuffer, and that gets played through anAudioBufferSourceNode— with the same wow/flutter and tone-shaping chain applied, so backwards playback sounds identically "tape-y." - A dedicated
preservesPitch = falsesetting on the audio element turned out to matter a lot: with the browser's default real-time pitch-correction active, the constant smallplaybackRatenudges were fighting the correction algorithm 60 times a second and causing audible micro-stutter. Turning it off lets the pitch drift naturally with the rate, which is both cheaper and more authentic.
Radio (Web Audio graph, per stream):
<audio> element ──▶ loudness gain ──▶ tone filters (Crap Bass Boost) ──▶ analyser ──▶ master gain ──▶ destination
- Station list is a static curated set of ~80 known-good streams, extended at runtime with a live "Top 100" pull from the radio-browser.info API (tried across four mirrors in case one's down).
- Every candidate stream gets a real connectivity check before being offered — an
Audio()object per station, raced against an 8-second timeout, batched 20 at a time so a couple hundred simultaneous connection attempts don't just starve each other out. - Streams get sorted onto "FM" or "MW" by bitrate: anything under 96kbps (or with no reported bitrate at all) lands on MW, which is a nod to medium wave's real-world reputation for tinny reception.
- While the initial scan runs, the frequency display sweeps continuously from 88.0 to 108.0MHz — the same sweep animation used for the SEEK button, just looped instead of one-shot.
Bass-driven woofer motion
This went through a couple of iterations. The first pass drove the cones off a slice of frequency-bin energy from the existing spectrum analyser — simple, but at the FFT size in use that "bass" bucket actually covered a broad low-to-mid range, and it was reading loudness, not bass.
The current version taps a dedicated pair of cascaded 150Hz lowpass filters (-24dB/octave combined) sitting off to the side of the audible signal path — connected, but never routed to destination, so they don't affect what you actually hear. Each animation frame reads the time-domain waveform of that filtered band directly (getByteTimeDomainData), rather than an energy average, and uses:
- the frame's peak-to-peak swing to drive the cone's scale (how "loud" this instant of bass is)
- the latest raw sample's sign to drive a few pixels of vertical translation (which direction the cone is currently pushed)
At a 60fps render loop this necessarily aliases against anything much above ~30Hz, but that aliasing is doing useful work here — it's what makes a 60Hz bassline read as a buzz/vibration rather than a smooth swell, which is closer to how a real cone actually behaves at that frequency.
Because Loudness and Crap Bass Boost are wired in before this tap (not after), both toggles genuinely change what the woofers do, not just what you hear: Loudness's flat gain boost scales the visible vibration up, and Crap Bass Boost — which despite the name squeezes the signal down to a 300Hz–3kHz telephone band — chokes off the entire sub-150Hz range the woofer tap is looking at, so the cones visibly settle down along with the bass actually disappearing from the mix.
VU meters
The two analog dials in the photo already have needles painted on them at rest; the overlay needles pivot from a point measured directly off the photo (a few percent left of the dial's visual centre, matching where the real needle hinges) and sweep across a fixed angular range based on a broader-spectrum loudness read — VU meters are supposed to track overall programme level, not just bass, so this deliberately uses a different (wider) frequency slice than the woofer tap. Each channel gets a small independent random jitter so left and right don't move in perfect lockstep, the way real stereo meters don't either.
Stack
Vanilla HTML/CSS/JavaScript, zero build step, zero dependencies. Runs as a single .html file — open it and it works. Audio is entirely Web Audio API; visuals are CSS transforms and <canvas>-free DOM overlays; the only network calls at runtime are to radio-browser.info and to whichever stream URL you tune to.
diddy