DevLog #003 — Teaching ByteWave Your Taste

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.


Why Spotify’s Approach Doesn’t Scale Down

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:

  • Runs entirely on a Raspberry Pi 4B
  • Learns from your signals only — no one else’s data
  • Gets better the more you use it
  • Has zero dependency on internet or external APIs
  • Uses no deep learning frameworks (no TensorFlow, no PyTorch — pure numpy)

The Signal Layer: Like, Dislike, Skip

ByteWave captures three primary signals:

  • Like — explicit positive signal. Strong weight.
  • Dislike — explicit negative signal. Strong negative weight, track deprioritised significantly.
  • Skip — implicit signal. Skipping in the first 30 seconds is weighted more heavily than skipping at 80% through. Position-weighted skip scoring.

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.


The KNN Recommender

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.


UCB1 Bandit Algorithm — The Exploration Layer

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.


How Scores Compound Over Time

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.


Pure Numpy — No Framework Needed

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

gurteshwar.sandhu
Written by
gurteshwar.sandhu

Leave a Reply

Your email address will not be published. Required fields are marked *