Sounds very similar to what I was doing.

For the curious, here is the relevant java code
snippet that does my similarity calculation.

Joe H.


public void selectNextSong()
{
try
{
_totalScore = 0;
Song nextSong = null;

// _considering is the list of songs
// that are currently being score.
Enumeration e0 = _considering.elements();

double maxdist = 0.0;
double mindist = 1.0;
double maxtime = 0.0;
double mintime = 0.0;
double now = (double)(new
java.util.Date()).getTime();
double timediff = 0.0;
double maxscore = 0.0;

Enumeration e1 = _considering.elements();

while(e1.hasMoreElements())
{
nextSong = (Song) e1.nextElement();

double dist = computeDistance(nextSong,
_baseSong);
nextSong.setDoubleValue("DistRaw",dist);

// keep track of the most and least
// similar song "distances" for proper
// scaling later.
if(dist > maxdist)
{
maxdist = dist;
}

if(dist < mindist)
{
mindist = dist;
}

Date last =
nextSong.getDateValue("LastPlayed");

if(last != null)
{
timediff = (double) now -
(last.getTime());

// keep track of the most recently
// played time and the least recently
// played time. This gives us a good
range
// over which to scale the rest of
// the songs
if(timediff > maxtime)
{
maxtime = timediff;
}

if(timediff < mintime)
{
mintime = timediff;
}
else if(mintime == 0.0)
{
mintime = timediff;
}
}
}


Enumeration e2 = _considering.elements();

// this is our denominator... we divide the
// end score of all songs by this.
// the _factors are user settable
// Some users might like preference to be most
// important, others time, others similarity.
// So we give them these scalars so they can
// adjust to their own taste.
double denom = Math.sqrt(_pref_factor *
_pref_factor + _time_factor * _time_factor +
_dist_factor * _dist_factor);

while(e2.hasMoreElements())
{
nextSong = (Song) e2.nextElement();

double score = 0.0;

String preference =
nextSong.getStringValue("Preference");
Date lastplayed =
nextSong.getDateValue("LastPlayed");

double pref_score = 0.5;
if(preference != null)
{
if(preference.equals("Excellent"))
pref_score = _pref_weight_Excellent;

if(preference.equals("Very Good"))
pref_score = _pref_weight_VeryGood;

if(preference.equals("Good"))
pref_score = _pref_weight_Good;

if(preference.equals("Fair"))
pref_score = _pref_weight_Fair;

if(preference.equals("Poor"))
pref_score = _pref_weight_Poor;
}

double time_score = 0.5;
if(lastplayed != null)
{
// Scale the time_score to be in a range
from 1 to 10;

time_score = (now - lastplayed.getTime() -
mintime) * 10.0 / (maxtime - mintime);

// Then put that on a asymptotic scale
from 0.0 to 1.0 but never quite reaching 1.0;

time_score = 1.0 - (1.0 / (1.0 +
time_score));
}

double dist_score = 1.0 -
((nextSong.getDoubleValue("DistRaw") - mindist) /
(maxdist - mindist));


nextSong.setDoubleValue("DistScore",dist_score);

nextSong.setDoubleValue("PrefScore",pref_score);

nextSong.setDoubleValue("TimeScore",time_score);

pref_score = (1.0 - pref_score) *
_pref_factor / denom;
time_score = (1.0 - time_score) *
_time_factor / denom;
dist_score = (1.0 - dist_score) *
_dist_factor / denom;

// final score = the square root of
// pref squared plus
// time squared plus
// dist squared
// Effectively a vector summation where each
// of pref, time, and dist are vectors in
// x, y and z axis
score = 1.0 - Math.sqrt(pref_score *
pref_score + time_score * time_score + dist_score *
dist_score);

if(score > maxscore)
{
maxscore = score;
}


double lastScore =
nextSong.getDoubleValue("Score");
if(lastScore > 0)
{
nextSong.setDoubleValue("Change",score -
lastScore);
}
nextSong.setDoubleValue("Score",score);
}

Enumeration e3 = _considering.elements();

while(e3.hasMoreElements())
{
nextSong = (Song) e3.nextElement();

double score =
nextSong.getDoubleValue("Score");

if(maxscore > 0)
{
score = score / maxscore;
score = 1.0 - score;
score =
java.lang.Math.exp(_randomness_factor * score *
score);
nextSong.setDoubleValue("Score",score);
}

_totalScore += score;
}

QuickSort.sort(_considering);

for(int i = 0; i < _considering.size(); i++)
{
Song temp = (Song)_considering.get(i);

long pos = temp.getLongValue("Position");

temp.setLongValue("Old Position",pos);
temp.setLongValue("Change",pos - (i + 1));
temp.setLongValue("Position",i + 1);
}

// now pick a random number from 0 to
_totalScore
double skip =
_rand.nextDouble()*_totalScore*(1.0 -
_randomness_cutoff);


int i = 0;
while(skip > 0 && i < _considering.size())
{
nextSong = (Song) _considering.get(i);
skip = skip -
nextSong.getDoubleValue("Score");
i++;
}

_pending = nextSong;
}
catch(Exception e)
{
LogUtil.log("Trouble getting song from database
"+e,2);
e.printStackTrace();
}
}


private double computeScaledDistance(String
values[], String t1, String t2)
{
if( t1 == null || t2 == null)
return 0.0;

if(t1.equals(t2))
return 0.0;

double d1 = (values.length / 2);
double d2 = (values.length / 2);

for(int i = 0; i < values.length; i++)
{
if(values[i].equals(t1))
d1 = (double)i;

if(values[i].equals(t2))
d2 = (double)i;
}

double dist = d1 - d2;

if(dist < 0.0)
dist = -dist;

if(values.length < 2)
return dist;

return (dist / (values.length - 1.0));
}

/*
computeHalfLife

if value = halflife then the return will be 0.5
if value = halflife *2 then the return will be 0.5
+ 0.25
if value = halflife *3 then the return will be 0.5
+ 0.25 + 0.125
*/
public double computeHalfLife(double value, double
halflife )
{
double retval = 0.0;
double current = value;
double range = 0.5;

while(current > halflife)
{
retval += range;
range = range / 2;
current = current - halflife;
}

if(current > 0)
{
retval += (range * (current / halflife));
}

return retval;
}

public double computeDistance(Song song1, Song
song2)
{
if(song1 == null || song2 == null)
return 0.0;

double retval = 0.0;
double denom = Math.sqrt(_artistWeight *
_artistWeight +
_albumWeight * _albumWeight +
_genreWeight * _genreWeight +
_moodWeight * _moodWeight +
_tempoWeight * _tempoWeight +
_situWeight * _situWeight +
_yearWeight * _yearWeight);
double score = 0;

if( stringCompare(song1.getStringValue("Artist"),
song2.getStringValue("Artist")) )
{
score = _artistWeight / denom;
retval += score * score;
}
if( stringCompare(song1.getStringValue("Album"),
song2.getStringValue("Album")) )
{
score = _albumWeight / denom;
retval += score * score;
}

score =
(lookupGenreValue(song1.getStringValue("Genre"),
song2.getStringValue("Genre")) * _genreWeight) /
denom;
retval += score * score;

if( stringCompare(song1.getStringValue("Mood"),
song2.getStringValue("Mood")) )
{
score = _moodWeight / denom;
retval += score * score;
}
if(
stringCompare(song1.getStringValue("Situation"),
song2.getStringValue("Situation")) )
{
score = _situWeight / denom;
retval += score * score;
}

double year_score = 1.0;

if((song1.getLongValue("Year") > 0) &&
(song2.getLongValue("Year") > 0))
{
long year_diff = song1.getLongValue("Year") -
song2.getLongValue("Year");
if(year_diff < 0)
{
year_diff = year_diff * -1;
}

year_score = computeHalfLife(year_diff,
_yearHalf);
}

score = _yearWeight * (1.0 - year_score) / denom;
retval += score * score;

score = _tempoWeight * (1.0 -
computeScaledDistance(_tempo_scale,
song1.getStringValue("Tempo"),
song2.getStringValue("Tempo"))) / denom;
retval += score * score;

retval = 1.0 - Math.sqrt(retval);

return retval;
}



--- "John A. Tamplin" <jat (AT) jaet (DOT) org> wrote:

> On Tue, 10 Aug 2004, Joseph Hines wrote:
>
> > > The way I did it is the weighted-random play
> > > replaced shuffle-play mode

> >
> > What was your weighting? How did you choose what

> was
> > next?

>
> I manually set the weightings for songs from 0-100.
> 0 meant never play it
> as a random selection, 1-100 were converted using a
> fancy curve I came up
> with via experimentation (roughly S-shaped -- there
> was only a little
> difference in probability between songs with values
> near either end of the
> range, slightly larger difference between songs
> rated in the middle of the
> range). After these values were picked up for all
> the songs in the
> playlist (and these values are generally static), I
> adjusted the values
> based on the last play timestamp (an exponential
> scale with a large
> negative adjustment for recent times, with the
> "half-life" intended to be
> adjustable by a parameter [but I never did that,
> just edited the source
> file]), and weightings based on the album and artist
> ids of
> recently-played songs (which could be positive or
> negative -- one thing I
> played around with was a positive weight for songs
> by the same artist as
> the most recently played song, and negative for any
> other recently played
> songs -- the purpose was the replicate the practice
> of local radio
> stations to periodically play blocks of songs by the
> same artists).
> The particular values were refined to meet my
> tastes, but the basic idea
> should work for anyone -- just tweak the values to
> your liking.
>
> Adding the suggested changes of trying to keep out
> "bad transitions" would
> probably be harder to setup in a user-neutral manner
> and would heavily
> depend on extra data associated with the songs.
>
> > So you've already written this? Am I missing

> something?
>
> Yes, it is a gross hack to really old SlimServer
> code -- it would need to
> be rewritten from scratch for the current code. I
> did it as part of the
> backend rewrite I was working on, but found no
> enthusiasm from anyone
> else. The web pages for the API (incompletely
> documented, and
> incompletely implemented but sufficient for my
> needs) are still available
> at http://www.jaet.org/jat/slimp3/ if you want to
> take a look at it.
>
> I never implemented anything besides the DBI
> backend, which I run from an
> Informix database (the RAM backend was partially
> done but since I wasn't
> going to use it I felt no need to finish it once it
> became clear this
> wouldn't make it into the main code, and the DB
> backend was never even
> started).
>
> Now that DBI support is being integrated into the
> main server, perhaps
> this sort of functionality will be accepted into the
> main server. If I
> were doing it again, I would have the play mode kept
> on a per-playlist
> basis, so you can have playlists that are to be
> played in sequence such as
> an album or playlists that are to be played
> weighted-random etc, and I
> would also have playlists able to contain other
> playlists (so you can have
> a playlist of complete albums to play, or to save
> work by building up
> lists from other lists [dynamically, so if a
> sub-playlist was updated you
> don't have to do anything for that change to be
> visible in the top
> playlist]).
>
> --
> John A. Tamplin jat (AT) jaet (DOT) org
> 770/436-5387 HOME 4116 Manson Ave
> Smyrna, GA 30082-3723
>
>