Journal
5 min read

One grump isn't enough to bury a song

How Orfeo decides which track plays next. One score per song, and behind every rule in the calculation, a very human behaviour that had to be anticipated.

At every party, there's one friend who likes nothing.

Give him an "I don't like this song" button and he'll press it all night. If your system listens to him, he's the one running the playlist. If your system ignores him, what's the button for?

That's exactly the kind of question Orfeo raises. Guests suggest, vote, grumble — and in the end, you have to pick one track. Here's how.

One score, and the best one plays

The principle fits in one line: every candidate track gets a score, you sort, the first one plays.

The score is a sum. Things that push it up, things that pull it down:

score = votes + mood + phase + dedication + era
      − same artist recently − same genre recently − downvotes
      + host preference

Nothing magical. No machine learning, no black box. Each term is a number between 0 and 1, multiplied by a weight. If a track plays and I wonder why, I can read the breakdown.

What's interesting isn't the formula. It's why each piece of the formula exists.

The tenth vote counts less than the first

If you naively add up votes, a hit with fifteen votes crushes everything. Then the next one. Then the next. The party turns into a top 50.

So votes go through a logarithm:

static func votesNorm(_ votes: Int, maxVotes: Int) -> Double {
    guard votes > 0, maxVotes > 0 else { return 0 }
    return log(Double(1 + votes)) / log(Double(1 + maxVotes))
}

In plain terms: going from 0 to 3 votes matters a lot. Going from 12 to 15, barely. A track a few people love keeps its chance against a track everyone adores.

The lone grump

Back to my friend who likes nothing.

The first two downvotes on a track are simply ignored. It takes three before it starts to weigh. One unhappy guest can't bury a track; a real collective rejection can.

I didn't discover that one at a party. I decided it cold, before writing the button, because the scenario was obvious.

Same logic for mood. Guests can say whether they want it calmer or louder. In my first version, a single opinion was enough to shift the target for the whole room. Now a lone opinion weighs little, and several people have to say the same thing for it to move.

The moment of the party

Each track has an energy, from 1 to 5. So does each phase of the party. A 5 during drinks loses points; the same track at midnight gains some.

That's what lets a suggestion wait for its time. It isn't rejected, it's just badly placed for now.

The host has the last word

This is the rule I stand by the most.

I can tell Orfeo "more of this genre" or "less of this genre", on a scale from −2 to +2. And that setting is deliberately stronger than votes: a genre at +2 goes ahead of the most-voted track in the room.

On my birthday, I used it to push electro late in the night. The crowd picks the tracks, but the host holds the direction. A party isn't a perfect democracy, and that's fine.

Don't repeat yourself

Two penalties: one if the same artist just played, one if the same genre just played. Without them, three votes for three tracks by the same band give you a mini-concert.

And a track that has already played is excluded for a while. That one did catch me out. I had set the exclusion at 50 tracks — about three hours of music. Perfect for a normal evening.

Except my birthday lasted a whole day. Over that long, waiting three hours before a hit can come back is too much. I cut it to 25 tracks afterwards. The day's tally: 203 tracks played, and 2 real repeats.

The randomness I ended up switching off

I had planned a grain of randomness in the score, so that ties wouldn't always go to the same track. With a fixed seed, so tests would always give the same result.

In practice, I set it to zero. The calculation runs in a loop during the party, and with randomness the queue kept reordering itself on every pass. Guests watched their song go up and down for no reason. Bad idea.

A deaf brain

Last point, and the one that served me best.

This brain doesn't know what Apple Music is. It knows neither the database nor the interface. It receives a snapshot of the situation — candidates, votes, history, phase — and returns an ordered list of ISRC codes. That's it.

The consequence: I can test it without playing a single note. I give it a situation, I check the order it returns. Every rule above has its tests, including the grump's.

And the day I want to plug in something other than Apple Music, the brain won't even notice.

— Pato