#!/usr/bin/perl -w # Key detection. Uses libsndfile + its perl bindings, vamp-plugin-sdk, # qm-vamp-plugins, and mplayer (for decoding mp3 files, since sndfile # doesn't) # Output is the output from the qm-keydetector plugin (with its key numbers # converted to key names), followed by a list of all keys detected, sorted # descending by the amount of the song that was in that key. # The user is expected to understand what relative keys are: if 70% of the song # is in G and 30% is in E minor (same actual key, relative minor), it's up to # the user to listen to the song and decide whether it sounds more major or minor. # 20140107 bkw: hack to add support for anything SndFile can't read, by # using mplayer to convert to tmp.wav use Audio::SndFile; our @keynames = split /\s/, 'none C Db D Eb E F F# G Ab A Bb B Cm C#m Dm Ebm Em Fm F#m Gm G#m Am Bbm Bm none'; our %relative_majors = ( 'Cm' => 'Eb', 'C#m' => 'E', 'Dm' => 'F', 'Ebm' => 'F#', 'Em' => 'G', 'Fm' => 'Ab', 'F#m' => 'A', 'Gm' => 'Bb', 'G#m' => 'B', 'Am' => 'C', 'Bbm' => 'Db', 'Bm' => 'D', ); our %relative_minors = ( 'Eb' => 'Cm', 'E' => 'C#m', 'F' => 'Dm', 'F#' => 'Ebm', 'G' => 'Em', 'Ab' => 'Fm', 'A' => 'F#m', 'Bb' => 'Gm', 'B' => 'G#m', 'C' => 'Am', 'Db' => 'Bbm', 'D' => 'Bm', ); if(!@ARGV || $ARGV[0] =~ /^--?h(elp)?/) { print <open("<", $_); }; if($@) { warn "Extracting to tmp.wav\n"; system("mplayer -vo null -ao pcm:fast:file=tmp.wav \"$_\""); $sf = Audio::SndFile->open("<", "tmp.wav"); $_ = "tmp.wav"; } $sec = ($sf->frames) * (1 / $sf->samplerate); } printf "file is %03.2f sec\n", $sec; my $oldstamp = -1; my $oldkey = -1; my $got = `vamp-simple-host qm-vamp-plugins:qm-keydetector "$_" 2`; die "Analysis failed\n" unless defined $got; print "\n"; my @got = split "\n", $got; ## for my $line (@got) { ## my ($pre, $post) = split(/:/, $line); ## my $gotkey = $keynames[$post]; ## my $gotrel; ## if($gotkey =~ /m$/) { ## $gotrel = get_relative_major($gotkey); ## } else { ## $gotrel = get_relative_minor($gotkey); ## } ## $line =~ s/:\s(\d+)\s*/": $gotkey ($gotrel)"/e; ## print $line . "\n"; ## } for(@got) { my ($key, $relkey); s/^\s*//; s/\s*$//; my ($stamp, $keynum) = split /: /, $_; $keynum =~ s/\s.*//; $key = $keynames[$keynum]; if($fold_major) { $key = get_relative_major($key); } elsif($fold_minor) { $key = get_relative_minor($key); } if($key =~ /m$/) { $relkey = get_relative_major($key); } else { $relkey = get_relative_minor($key); } if($oldstamp != -1) { $times{$oldkey} += ($stamp - $oldstamp); } $oldstamp = $stamp; $oldkey = $key; print "$stamp: $key ($relkey)\n"; } $times{$oldkey} += ($sec - $oldstamp); print "\n"; for(sort { $times{$b} <=> $times{$a} } keys %times) { printf "%3s: %4.2f sec, %4.2f%%\n", $_, $times{$_}, $times{$_} / $sec * 100; } } sub get_relative_major { my $key = shift || die "missing key"; return $relative_majors{$key} || $key; } sub get_relative_minor { my $key = shift || die "missing key"; return $relative_minors{$key} || $key; } __END__ Output of plugin: - Estimated key (from C major = 1 to B major = 12 and C minor = 13 to B minor = 24) Looks like: 0.000000000: 23 54.984852607: 16 59.443083900: 4 71.331700680: 16 78.019047619: 18 78.762086167: 23 95.108934240: 18 97.338049886: 4 110.712743764: 9 112.198820861: 23 210.279909297: 18 215.481179138: 4 216.967256235: 18 222.168526077: 11 231.084988662: 4 234.057142857: 2