← back to work
software Node.js · Socket.IO · Python · Raspberry Pi built

Deckling Air

Two apps sharing one editable grid: a network remote for a Windows mixer, and a music dashboard that builds its own recommendation engine out of your listening, because the platform stopped providing one.


project snapshot

Type Self-hosted web application
Server Node.js · Express · Socket.IO
Bridge Python · ctypes → VoiceMeeter Remote DLL
Frontend Vanilla JS · CSS, no framework
Data Flat files on the Pi
External Spotify Web API · ReccoBeats · Last.fm

about this build

Deckling Air started as the software half of Deckling, pulled off the hardware. The physical controller is good at my desk and useless everywhere else, so the control surface moved to a Raspberry Pi and became something any device on the network can open: an iPad on the couch, a phone, a second machine.

It has since grown a second half that has very little to do with mixing. What began as a media-key shortcut card turned into a full Spotify dashboard with a recommendation engine behind it, and that half is now where most of the interesting work lives.

The two share one surface: an editable grid of cards you drag, resize, and organise into pages. The layout persists on the Pi, so every device opens the same panel.

the problem

Two unrelated problems ended up in the same app, which is the honest reason it looks like two apps.

01

The controller only works where it is

A hardware deck is fixed to one desk. The parts of it that are purely software (faders, mutes, routing, macros, shortcuts) have no reason to be, but they were locked to the device anyway.

02

Spotify removed the parts that make playback smart

Over time the Web API stopped serving ordinary apps the things a music tool needs: audio features, the recommendations endpoint, related artists, even artist genres. Start a single track through the API and Up Next stays empty. There is no autoplay to lean on.

03

Generic taste is not taste

Even while recommendations worked, they described listeners in general. Nothing knew that my "sad" sits somewhere unusual, or which specific song-to-song transitions actually survive in my listening.

what I built

The grid

An editable card surface: drag, drop, resize, configure, and group cards into pages like Gaming or Music. The layout lives on the Pi rather than in a browser, so it follows you between devices instead of resetting.

VoiceMeeter control

Faders, full channel strips, output buses, parameter toggles, multi-parameter macros with an optional momentary (push-to-talk) mode, and live VU meters. State streams both ways through the bridge, so the panel reflects the real mixer rather than its last known guess.

Desktop shortcuts

Cards that fire actions on the PC: launch an app, open a URL, media keys, volume and system actions, screenshots, or an arbitrary key combination.

Player, search, playlists, queue

The expected surface (transport, scrubber, device picker, search, playlists), plus an Up Next that annotates each track with why it is there.

The Smart Queue engine

With no autoplay to inherit, the engine sources its own continuation: it builds a window of discovery anchors with taste-matched picks woven into the roughest transitions, enqueues it into Spotify's native queue so playback looks entirely normal, and extends it as the queue drains. Skip or play something new and it rebuilds from wherever you actually are.

Mixes: a genre and mood map

A 2D pad where one axis is mood and the other is energy, with your own listening drawn behind it as a heat cloud, so you can see where your music actually sits before picking a point in it. Genre chips layer on top. Mood, genre, or both run through one builder with different parameters.

Taste learning

Explicit artist ratings from Love to Never that override the engine's guesses, learned scores that move on engaged listens and early skips, and transition memory that reinforces the song-to-song pairs that survive in practice. Selection stays randomised, so preference raises the odds without flattening into the same session every night.

Feelings, tuning, and diagnostics

Occasional check-ins record how you felt against what was actually playing. Six dials (fresh versus familiar, variety, fade smoothness, mood lock, skip sensitivity, lookahead) shape every feature at once. A read-only Data tab exposes everything the engine believes about you, including where your learned feelings have drifted from their generic definitions.

engineering decisions

Rebuilding what the platform removed

Spotify's recommendation endpoint silently ignores its seed and falls back to your top tracks, so leaning on it produces a queue that looks personalised and isn't. Instead the engine builds a target from the played song's own energy, valence and genre, and routes the whole window through the same builder that powers Mixes. Play a specific song and Up Next matches that song, not merely its artist.

Routing around blocked endpoints rather than degrading

Two data sources were rebuilt on free alternatives: audio features come from ReccoBeats, and genres from Last.fm tags rolled up into about ten macro buckets and cached permanently. A throttled background job resolves every artist in the library until coverage is complete, then re-scans for new ones. The alternative, shipping a music tool with no concept of genre or energy, wasn't really one.

Local-first, API-frugal

Everything learned lives in flat files on the Pi and is computed in memory; live API calls are a last resort, rate-limit-gated and spaced out. This began as politeness toward the APIs and became the feature: the engine keeps working when they are slow, throttled, or blocked, because it is mostly reading its own data.

A feeling is a label, not a command

The check-in asks how you feel and then deliberately does nothing to what is playing. It records the association instead. That restraint is the entire point. After enough check-ins the app can tell that your "sad" sits nowhere near the textbook definition and play what you actually reach for, which only works because the question never steered the answer.

Keeping favourites from eating the queue

Favourites are a high-trust signal, so the obvious move is to play them more, which produces a queue that loops the same handful all night. Their nudge is deliberately set below the repeat-tolerance penalty, so a favourite that has already played this session backs off like anything else. More of what you love, without it becoming all you hear.

Sequencing for flow, not just fit

Choosing the right songs is only half of it; the order decides whether it sounds like a mix or a shuffle. The window is sequenced on Camelot key compatibility, BPM and energy smoothness, with a soft genre-similarity term so metalcore into hardcore reads as close while metal into bedroom-pop reads as far, which is what avoids the jarring same-key, opposite-genre jump.

reflection

This project changed shape underneath me. It was meant to be a remote for a mixer: a couple of weekends of plumbing between a browser and a Windows DLL. The Spotify half started as a single media-key card and ended up the larger and more interesting program.

The constraint is what made it interesting. Every capability the platform withdrew (features, recommendations, genres, autoplay) turned into a question about what could be rebuilt from data I already had. The answer was usually more than I expected, and the local version was usually more personal than the one it replaced, because it was learning from one listener instead of describing all of them.

It is also the project that taught me how much design lives in restraint: not playing favourites too often, not letting a check-in steer the music, not hard-filtering on a genre guess. Most of the good decisions here were about doing less with a signal than I could have.

say hello

Copy what you need, or open your mail app.

compose email →