← Hplay
The Hplay player: a scroll wheel music player with a blue aluminium body, showing its main menu and a rotating album cover.
Project review · alpha · 14 September 2026

Hplay

A classic scroll-wheel music player, rebuilt from scratch for Android, and handed to its first alpha tester today.

v38.0 “Barney” alpha 60 days old 69 commits Android

It started on 16 July 2026 at 1,703 lines. Sixty days later it is 3,520 lines, has 50 screens, plays video, spins a vinyl record with a tonearm that tracks inward as the song runs down, and keeps a year of listening history it never sends anywhere.

The thing that makes it unusual is the constraint. No framework, no component library, nothing pulled in from a package manager. The scroll wheel physics, the audio graph, the library storage, the marketplace client: all of it written by hand, and all of it shipping as an app you install on a phone.

Lines
3,520
Size
185 KB
Screens
50
Settings
24
Body colours
6
Dependencies
1

The one dependency is jsmediatags, for reading ID3 tags and embedded cover art. Everything else is hand-written: the limiter, the reverb, the wheel acceleration, the vinyl.

1

The Tour

This is what it actually looks like, running. Nothing here is a mockup.

Now Playing showing a tilted album cover with a reflection, track title, artist, album and a progress bar.
Now Playing, cover style. The artwork tilts, catches a shadow and casts a reflection. It sways gently unless you turn that off.
Now Playing showing a black vinyl record with the album art as its label, a tonearm resting on it, and the track details to the right.
Now Playing, vinyl style. The record turns with the music and stops when you pause. The tonearm tracks inward as the track runs down and bobs while it plays.
The same vinyl view on the dark screen, the record lit by a soft halo.
The same record, dark screen. A black disc on a black screen needed its own halo and a sheen that doesn't turn with the record.
The main menu with a small spinning record in the right-hand pane.
The menu keeps turning. As of v38 the split pane shows the record too. It used to show a flat sleeve, which made the setting look half-applied.
The song list, with one row highlighted in blue.
The list. Wheel acceleration kicks in past 25 rows, and a big letter flashes up so you can find your way alphabetically.
The Quick Settings screen with a sleep timer clock and rows for dim screen, speed, tape pitch, effect, bass and sound protect.
Hold play/pause. The one screen you can reach without leaving what you're doing, so the sleep timer shares it with speed, effects and the loudness limiter.
The Statistics screen listing times opened, tracks played, listening time, finish rate, favourite song and artist.
Statistics. Counted for fun, kept on the phone, never sent. Twelve months of history roll forward and the thirteenth is deleted.
The in-app changelog listing version 38.0 Barney and its changes.
Its own changelog. 25 versions, each with a codename, readable on the device. The wheel steps version by version rather than line by line.

Five screens, one player

The menu in Light mode, white screen with dark text.
Light
The menu in Dark mode, near-black screen with pale text.
Dark
The menu in High Contrast mode, pure black and white with ruled rows.
High Contrast
The menu in Night mode, warm amber text on a very dark brown screen.
Night
The menu in Paper mode, warm off-white screen with brown-black text.
Paper
2

How It Got Here

Sixty days, unevenly spent. The shape of the commit history tells the story better than a summary does.

July16th on
38 commits

Everything at once. The player, the scroll wheel, the library in IndexedDB, lyrics, themes, the marketplace, the Android build, decals you could drag around the body. Built fast and broadly. The skeleton of nearly every feature the app has today was laid down in two weeks.

Augustquiet
3 commits

Almost nothing. The project sat untouched for a month and then picked back up without a rewrite. Most side projects die in that month.

Septemberto the 14th
28 commits

The depth pass. Sound Protect, accessibility, usage stats, coloured action rows, settings that get their own screens, video playback, audio toys, the changelog, the entire vinyl mode, the top-bar clock, five screen modes. Also the first proper architecture cleanup, which cut a settings change from seven coordinated edits down to one.

Sixteen codenames

Versions are named off the Valve roster, in order, out of admiration and nothing else. Each one is a real release with its own changelog entry on the device. Hplay is not affiliated with, or endorsed by, Valve Corporation.

VersionCodenameHeadline
v27.0ScoutFirst build with a codename. Sleep timer lets the phone sleep.
v28.0SniperSound Protect limiter, and accessibility: size, motion, haptics.
v29.0GordanUsage data you can stop or wipe. Streaks, peak hour, wheel clicks.
v30.0AlexAction rows colour their own highlight: green adds, red deletes.
v31.0LuisSettings with three or more options get a screen of their own.
v32.0HeavyVideo plays in the artwork slot: mp4, m4v, mov, webm.
v33.0DemomanAudio Toys: speed, tape pitch, reverb, radio, echo, bass. And this changelog.
v34.0PyroVinyl cover style, with a tonearm that tracks inward as the song plays.
v35.0SpyA new record rolls in on every track change. Optional top-bar clock.
v36.0MedicVideo washes the screen behind the record instead of fighting the label.
v37.0EngineerSharp video in the slot, blurred behind. Settings described in one place.
v38.0BarneyFive screen modes, vinyl on the menu, quick settings, a rolling year of stats.
v39.0ChellTwelve releases. Four thousand songs scroll instantly, and accessibility gets a screen of its own.
v40.0PyroEvery theme carries its own icon colour. Stations builds mixes out of your own library.
v41.0SpySix releases about how it sounds. The music goes out to the real player while the app is away.
v42.0MedicThe audio focus is left alone while the page holds it, so the music plays.
3

Four Hard Problems

Every project has a few problems that take far longer than they should. Here are four of them.

The tonearm that kept sinking

A transform is enough to trap a z-index

The tonearm has to paint above the text panel that the record slides behind. It had z-index: 3, which should have settled it, and it kept disappearing anyway.

The cause is a CSS rule that catches people out. A transform, a filter or a will-change on an ancestor creates a stacking context, and a child's z-index can never escape it. Removing the wrapper's own z-index wasn't enough, because the base artwork rule re-applied a sway animation, and an animated transform does the same thing. The fix was an explicit animation: none on the vinyl container. It looks like a line that does nothing.

The record that stuttered once a second

Don't re-derive an animation's phase. Carry it.

Now Playing rebuilds itself every second, so the spinning record restarted every second too. The obvious fix was to compute where the record should be from the track position and set the animation delay to match. It stuttered: the disc jumped a degree or two, every second, forever.

CSS animations don't start the instant you insert the element. They start a frame later, and that one frame of drift compounds against a clock taken from somewhere else. The answer was to stop deriving anything and carry each animation's own currentTime across the rebuild via getAnimations(). After that it measured steady to within 17 milliseconds.

The backdrop that never advanced

The engine won't decode a video nobody can see

The plan for video was to sample frames onto a canvas and blur them behind the screen. It produced exactly one frame and then froze. The video element was tried at one pixel, at sixty-four pixels, at zero opacity, at one percent opacity, behind everything in the stacking order. Same result every time.

Chrome stops producing frames for a video that isn't really being rendered, and hiding it more cleverly can't help, because the hiding is the problem. So the whole approach went in the bin. Instead a second, silent copy of the file plays as the backdrop, nudged back into step whenever it drifts more than a third of a second.

The screen that changed size

Absolute positioning measures from the padding box

Playing an MP4 made the screen appear to grow, with a second set of corners inside the bezel. It looked like a rendering glitch. It was arithmetic.

The video stage was inset: 0 inside the bezel. But an absolutely positioned box measures from its ancestor's padding box, not its content box. The bezel has 3vw of padding for the black border, so the picture spread 12.6 pixels past the screen on every side and painted its own corners out in the trim. Both now share the padding as one variable.

4

The State Of It

An honest read, on the day it goes to someone else for the first time. It's an alpha, and the list on the right is why. A real, working player with real holes in it.

Solid

  • Playback, the scroll wheel and navigation are stable and have been for weeks.
  • Settings are defined in one table, so adding one is a single edit. It used to be seven.
  • All 24 settings round-trip through storage, reject rubbish, and survive a reload. Checked automatically, not by hand.
  • Every screen mode is legible selected and unselected, measured against a contrast ratio rather than eyeballed.
  • Usage data stays on the device, can be switched off, and can be wiped.
  • It survived a month of neglect and picked straight back up.

Rough

  • Song IDs are regenerated on every folder rescan, but playlists and the resume point store IDs. A rescan while music is playing can break next and previous. This is the most important thing left to fix.
  • Deleting a song from a scanned folder doesn't stick; it comes back on the next scan.
  • There's no export or backup. Everything lives in one database on one phone.
  • Resume-on-restart doesn't work yet.
  • The build the testers have is five changes behind the current one, including the file filter that lets video through.
  • No test suite in the repo. The checks exist, but they live outside it.