deciphered

Tools / Things

Cadence

15 Oct 2026 · Jul to Aug 2026 · Kotlin, Jetpack Compose, Media3 ExoPlayer

Cadence playing a Kevin MacLeod track: 170 steps a minute set at the top, the track detected at 161 bpm and played at 106% so it lands on 170

Cadence is a music player for runners, with a subtle difference. Usually if you build a playlist or use a running-related music player, tracks are chosen to fit a running speed - you end up with a certain style or genre that falls in line with your footfall while running to the beat. However, that limits you to tracks that happen to be the speed you want to run at. What if you had the playlist you wanted to run to, but it doesn't match your cadence? Or if the track you're currently loving and playing on repeat is just that bit too fast or slow, it's either slightly jarring, or you find yourself speeding up or slowing down to compensate.

Instead of choosing the playlist to match the cadence, I wanted to choose the cadence and match the playlist to it - choose how fast you want to run, and tracks will be adjusted accordingly.

Why not Spotify

There are some limitations, however. Modern consumption of audio content might use any of various streaming services; in exchange for a monthly fee, you can get access to the vast majority of mainstream music from anywhere (assuming internet access), glossing over niche examples. This throws a spanner in the works for Cadence - for DRM reasons the audio streams are encrypted, locked down, and limited in what we can do with them.

As an example, Spotify's developer tools have never had any control over playback speed, so third-party software has no way to change it. In November 2024 Spotify also cut new apps off from a load of useful things, including the one that reported a track's tempo - so we can't even ask Spotify how fast a track is.

Put simply, this means Cadence is not compatible with streaming services. There's no way around that without breaking the DRM, which is illegal in itself. However, there's nothing stopping us from adjusting the playback rate of an unencrypted audio file that we do have the rights to play - for example, tracks bought as a download without DRM (Bandcamp purchases come with MP3 and FLAC downloads, for one), or music released under a licence that allows it, such as Creative Commons. Whatever the method, I leave that to the end user - if you have an audio file of the track you want, Cadence will measure its BPM, and adjust playback speed to match the desired cadence of your run.

Finding the beat

When a track is added to Cadence's library, its BPM is calculated on the device from the audio itself. It listens to up to the first minute of a track for the moments where something hits - a kick, a snare, a chord landing - then checks how regularly those hits repeat. The detail, Fourier transform and all, will get its own post - the first draft of this page went into detail, and it was uhm...quite long, so I've intentionally kept it brief here.

Some tracks are genuinely ambiguous: every beat at 85 BPM is also a beat at 170, so either answer could be correct. Cadence settles it by leaning towards 130, a number that was tested against the 120 that related research suggests. Some tracks are just very difficult to measure, so tapping and holding a track also lets you manually specify a BPM.

Pure Kotlin with no audio library, taking about a second or two per track. The result is cached against the audio rather than the file, so a track that's renamed, moved or retagged doesn't get re-analysed.

No third-party lookup, so it works offline; nothing about your music leaves your device. A possible future improvement is a lookup when adding music - Deezer's public API has a BPM on its track records, free and with no key - though it wouldn't be a catch-all. Of six well-known tracks tried, three came back with a BPM of 0.

Success rate in development was measured against 13 generated tracks with a known tempo, from a bare click to a dense bass-y mix. When it had gained some robustness it was tested against 323 real tracks with a published tempo: 265 read within 4%, and 20 at exactly half or double.

Changing speed without the helium

Once Cadence knows a track's BPM, it can adjust the playback rate to match a desired cadence - whilst eliminating pitch shifting, so it doesn't sound like you've been sucking on helium or sulphur hexafluoride. Playing a recording faster normally raises its pitch; instead it uses a time stretch (audio is chopped into tiny pieces that are imperceptibly overlapped or repeated, so it's a different length of time but at the same pitch). That part I didn't have to write: Android's media player (Media3 ExoPlayer) already does it, so Cadence just tells it how fast to play and to leave the pitch alone. Each track's playback speed is calculated from its own BPM, so the tempo stays the same from one song to the next even though no two were recorded at the same tempo.

How far a track will stretch

by ratio: what Cadence does by arithmetic: 30% each way ÷ 1.3 × 1.3 77% 130% −30% +30% 70% 130% 50%100%200% half speeddouble speed
the default stretch limit, on a scale where halving and doubling are the same distance from 100%
the Stretch limit setting: a slider at its default, reading 77% to 130%
one slider; the slow end is based on the fast end

There are limits of course: too much time adjustment can make a track sound broken regardless, so by default the app won't play a track faster than 130% or slower than 77%. Those two numbers may look lopsided, but they're actually the same amount of stretch: tempo works in ratios. Half speed is a whole octave of tempo down and sounds destroyed, while 150% is just quick, so "50% either way" isn't symmetrical at all. Speeding up by 1.3 times is undone by slowing down by 1.3 times, and 100 divided by 1.3 is 77. The limit is tweakable in Settings - you set the high end anywhere from 110% to 160%, and the slow end is calculated from that: 91% at 110, down to 63% at 160.

In practice, take the 54 tracks in the playlist below and a cadence of 170 steps a minute. With a step on every beat and the default limit, 29 of them fit. A 161 BPM track plays at 106%, close enough that you'd barely notice. A 123 BPM track would need 138%, which is past the limit, so it's left out.

More of a library can be brought in by running two steps to each beat instead of one. An 85 BPM track needs no stretching at all that way, and 10 more of the 54 fit. The catch is what it does to a run - you get used to running to the music at a certain pace, then a slower song comes on and you're expected to know it's now two steps a beat. Working that out mid-run is arbitrary mental arithmetic, and it throws you off. So, those tracks are skipped by default - the option is there if you want it, but I'm not a fan. Cadence never chooses two steps a beat just to save stretching, either: if a track can be stretched to a step on every beat within the limit, that's how it plays.

Some tracks don't fit either way. At 170, anything between about 111 and 131 BPM is too slow to stretch up to a step on every beat, and too fast to stretch down to two steps a beat. The other 15 tracks in the playlist are left out as overstretched.

Tracks that don't fit

the playlist at 170 spm: 29 of 54 tracks will play, 15 skipped as overstretched and 10 for needing two steps per beat, each row showing its bpm, why it is in or out, and its stretch
every track says why it's in or out

Anything still outside that range is left out of the playlist, with the reason shown beside it: "overstretched" or "2 steps per beat". There are various options for what to do with them:

  • Tap an overstretched track to force it anyway. A song overly sped up might just be funny; the limit stops the app choosing something unlistenable on its own, not you choosing it on purpose.
  • Tracks that need two steps a beat have their own setting, Slow tracks. They're skipped by default, but you can choose to include them either played at half time, or doubled so every step still lands on a beat, up to 260% at the default limit. That's rushed, and the setting itself says so:

may utterly butcher a favourite. Your call.

Cadence's Slow tracks setting, on playing them fast
the Slow tracks setting with three choices: play at half time, skip them (selected), or play them fast
three answers rather than a switch

Even with those options, only part of a library will fit any one cadence: across a general library of 824 tracks, about a third (287) fit at 170 with a step on every beat. And some music won't work at any cadence. Detection needs a steady tempo and something vaguely beat-like, and time-stretching floopificates (technical term, not made up at all) sparse acoustic material the most.

Music for testers

Because it's fun (for me), I built a 'secret' music downloader into Cadence. Tapping the version number seven times reveals a "Get music" option that's otherwise invisible. It's basically a login form for my depot service, which I'm using here just as a way to put music on a device for testing. Getting in needs the depot's address and a password, and from there a tester can browse some test bundles of Creative Commons licensed music.

Slight catch: the bundles are encrypted, with 256-bit AES in GCM (Galois/Counter Mode), which both encrypts and proves nothing was changed. The depot only stores the encrypted files, never the key to decrypt them. That way if depot was ever compromised or a config change accidentally dropped the password check, nothing is exposed or readable. I will refrain from going into detail on AES GCM for now, tempting as it may be. A password-protected zip would have been simpler, but classic zip encryption can be cracked by anyone who can guess about a dozen bytes of a file from the zip, and the start of an MP3 is very guessable. The newer AES kind of zip is fine, but Android would need an extra library to open it and no one likes dependencies.

Entering a 256-bit key by hand would be unwise, same for the password, which is 32 random characters. So, both are handed out in a QR code - scan it with your device, and Cadence opens with the depot address, password and key already filled in.

QR code phone camera Cadence's link Cadence opens everything filled in https instead password sprayed all over the internet
the QR holds Cadence's own kind of link, so it can only ever open Cadence. the dashed route is what an ordinary web link risked

Android lets an app claim a type of link that starts with a name of its own instead of https, which nothing else on the device knows how to open; the QR contains one of those. If it was an ordinary web link, a phone that didn't know what Cadence was would open it in a browser, potentially dropping the password off in some logs somewhere. With Cadence's own kind of link there's nowhere else for it to go: it either opens Cadence or does nothing at all.

There's a scanner in the app too, for someone already looking at the super secret spy screen. It runs inside Google Play services rather than Cadence, so Cadence itself never needs permission to use the camera.

There wasn't really anything to hide or secure with this, it was a very low-stakes thing. But, I like tech stuff, and it's satisfying when a camera, a QR code and a link only one app understands all synergise so a tester doesn't have to think about login forms or encryption; it just works. Android only - no apple.

back to Tools / Things