Cap'n Steve has asked for the wisdom of the Perl Monks concerning the following question:

What I want this script to do is go through the specified directory with File::Find and build a list of all sound files. It then uses File::Stat to give each one a score; newer files get a point, files that haven't been accessed in a while get a point, etc. I want it to play a random music file, but the files with higher scores should get played more often.

Problem 1 comes from the fact that one of the modules apparently touches all the files, since last access times are the same throughout the directory. Problem 2 is adding the bias. Not only is it massively ugly (please don't kick me off the site for using all those eval()s), but it also doesn't seem to work.

#!/usr/bin/perl use warnings; use diagnostics; use File::Find; use File::stat; my (%files, %score, $rand, $array, @file0, @file1, @file2, @file3, @fi +le4, @file5, @file6, @file7, @file8); my $epoch = time(); find(\&file_check, qw(C:/music)); while (my($filename, $stats) = each %files) { # accessed in the last 2 days #if ($stats->atime + 60 * 60 * 24 * 2 >= $epoch) { # delete $score{$filename}; # next; #} # not accessed in the last month if ($stats->atime + 60 * 60 * 24 * 30 < $epoch) { $score{$filename} += 4; } # created in the last week if ($stats->mtime + 60 * 60 * 24 * 7 >= $epoch) { $score{$filename} += 2; } } while (my($file, $score) = each %score) { $file =~ s/'/\\'/g; eval("push \@file$score, '$file';"); } &pick_another(); sub file_check() { if (/\.(mp3|wav|wma)$/i) { $files{$File::Find::name} = stat($File::Find::name); $score{$File::Find::name} = 0; } } sub continue_loop() { print "\n\nPlay another (y/n)?"; my $response = <STDIN>; chomp($response); if (lc($response) eq 'n') { exit 0; } else { pick_another(); } } my $length; sub add_bias() { $array = int(rand() * 9); if (int(rand() * 10) % 2) { $array -= 2; } elsif (int(rand() * 10) % 5) { $array--; } eval("\$length = \@file$array;"); $rand = int(rand($length - 1)); my $value = '$file' . $array . '[' . $rand . ']'; if ($rand > $length || $rand < 0 || $array > 8 || $length == 0 + || eval($value) eq '') { return 0; } else { return 1; } } sub pick_another() { while (!add_bias()) { next; } my $variable = '$file' . $array . '[' . $rand . ']'; my $command = "\\\"c:\Program Files\Windows Media Player\wmpla +yer.exe\\\" \\\"$variable\\\""; print "Playing file with score $array\n"; eval("`$command`;"); eval("delete $variable;"); continue_loop(); }

Replies are listed 'Best First'.
Re: Opening random files (with bias) based on File::Stat information.
by blokhead (Monsignor) on Mar 19, 2006 at 02:46 UTC
    I don't know about access times in Windows -- you may have to keep track separately of when you've played the audio files if other things are interfering with the access times.

    As for the general structure of your code, you are right -- all those evals are atrocious. Instantiating global variables via eval as a way to implement argument passing? @file0 through @file8?? Ew.

    And for prior art in the subject, this node is a good starting place for picking a weighted random number..

    sub score { ## however you decide to assign a score to a filename } sub choose_weighted { ## from that other node } my @audio_files; find( sub { /\.(mp3|wav|wma)$/ && push @audio_files, $File::Find::name } +, 'C:\music' ); ## this could go inside the while loop if you needed to recalculate ## relative weights after every song is played: my @weights = map { score($_) } @audio_files; while (1) { my $index = choose_weighted(\@weights); system 'c:\Program Files\Windows Media Player\wmplayer.exe', $audio_files[$index]; print "Another? "; last unless <STDIN> =~ /^y/i }
    Take a look at that system line. It is about a million times safer (and easier on the eyes) than eval'ing a backtick command. With multi-arg system, you don't have to worry about quoting issues either.

    Update: loaded @audio_files from File::Find

    blokhead

Re: Opening random files (with bias) based on File::Stat information.
by xdg (Monsignor) on Mar 19, 2006 at 14:05 UTC

    For the bias, something you might consider is this:

    • Sort files by newest to oldest
    • Generate a random number between 0 and 1
    • Invert that number against a bounded cumulative probability distribution function
    • Scale the inverse to the length of your list
    • Pick a file using the scaled inverse as the index

    If you pick a distribution that is weighted towards 0, you'll wind up picking newer files. Note: this isn't technically weighting by time -- it's biasing towards certain array slots, irrespective of whether those slots are close in access time or far apart. However, that may be sufficient for your particular application.

    A good distribution for this may be the Kumaraswamy, which is bounded between 0 and 1 and has a closed form that is easy to invert. By changing the two input parameters, you'll get different shapes, including ones that bias towards 0. (You'll have to try graphing some PDF's and see what you like.)

    Here's an example of how it could be used to bias in the way I described:

    use strict; use warnings; my $param_a = 1.5; my $param_b = 6; my @array = ( 1 .. 100 ); sub invK { my ($F, $Ka, $Kb) = @_; return ( 1 - ( 1 - $F )**( 1 / $Kb ) )**( 1 / $Ka ); } for ( 1 .. 20 ) { my $pick = int( invK( rand(), $param_a, $param_b ) * @array ); print "$pick\n"; }

    A test run gave this: 7 11 12 26 18 27 10 30 6 3 28 2 35 7 29 40 26 15 3 44

    -xdg

    Code written by xdg and posted on PerlMonks is public domain. It is provided as is with no warranties, express or implied, of any kind. Posted code may not have been tested. Use of posted code is at your own risk.

Re: Opening random files (with bias) based on File::Stat information.
by Cap'n Steve (Friar) on Mar 20, 2006 at 03:41 UTC
    Thanks for the help guys. blokhead, your code seems to work fine although it generates a ton of warnings about uninitialized variables in matching and substitution, but they say "at eval(1) line 1000+, <STDIN>". Could that be from the modules?

    xdg, your code is very interesting, but math isn't my strong suit so it's a little over my head.

    I still haven't been able to find anything about File::stat behaving differently on Windows. Has anyone heard anything about that?