Spotify’s algorithm frustrates me. Not because it doesn’t work — it works fine for most people. It frustrates me because it doesn’t know me. It knows what millions of people who vaguely resemble me listen to. That’s not the same thing.
I wanted ByteWave to actually learn my taste. Not by phoning home to a server. Not by building a profile that gets sold to advertisers. Locally. Privately. On the device itself.
No cloud. No tracking. Just yours.
Spotify uses collaborative filtering at massive scale — your behaviour compared against hundreds of millions of users. That requires enormous infrastructure, constant internet access, and surrendering your listening data. None of those are acceptable constraints for ByteWave.
I needed something that:
ByteWave captures three primary signals:
There’s also a listen-through signal — finishing a track without skipping or liking is a mild positive. Replaying a track is a stronger positive. These signals feed into a per-track score that compounds over time.
K-Nearest Neighbours is the backbone of the recommendation engine. Here’s the simplified version:
Every track gets represented as a feature vector — audio features extracted locally (tempo, energy estimated from waveform analysis, spectral centroid, etc.) plus metadata (genre tags, artist). When you like a track, ByteWave finds the K tracks in your library most similar to it (nearest neighbours in feature space) and boosts their scores. When you dislike one, it finds similar tracks and slightly reduces theirs.
The beauty of KNN for this use case: it’s interpretable, fast to compute on a small library (even 10,000 tracks runs in milliseconds on a Pi), and requires zero training in the traditional ML sense. It just needs your signal history and the feature vectors.
Pure KNN has a problem: it converges. If you only play tracks similar to what you’ve liked before, you never discover anything new in your own library. It becomes an echo chamber of your own taste.
UCB1 (Upper Confidence Bound) is borrowed from the multi-armed bandit problem in reinforcement learning. The idea: balance exploitation (play what you know you like) with exploration (play things you haven’t heard much, or haven’t heard recently).
UCB1 scores each track as:
score = avg_reward + C * sqrt(ln(total_plays) / track_plays)
The second term — the “confidence bound” — automatically increases for tracks you haven’t played in a while or haven’t played enough times to have a confident estimate of how much you like them. This means ByteWave will periodically surface obscure tracks from your library that you’ve forgotten about, giving them a fair chance.
The constant C controls how adventurous ByteWave is. Low C = conservative, plays proven favourites. High C = adventurous, explores more. I made it user-adjustable in the settings.
Every interaction updates scores immediately. But scores also decay slowly over time — a track you loved six months ago but haven’t played since will gradually return to neutral, giving it a fresh chance on the next cycle. This mirrors how real musical taste evolves: what you loved last year might not hit the same today.
Decay is exponential with a long half-life (roughly 90 days) so your preferences feel stable but aren’t permanently frozen.
The entire recommender system runs on numpy. No TensorFlow. No PyTorch. No scikit-learn even — just numpy arrays, dot products, and sorting. The full recommendation pass over a 5,000-track library takes under 50ms on a Pi 4B. Plenty fast enough.
This was a deliberate constraint I set early. If I can’t implement it with numpy, I shouldn’t be implementing it on this device. It kept the system lean and forced me to understand exactly what the algorithms are doing at every step.
Next DevLog: cutting the cord — going fully portable for the first time, what broke, and why I had to rethink the entire power architecture.
— Gurteshwar Sandhu, Founder, IronLabs Tech
Leave a Reply