#!/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 <<EOF;
$0: Find key of music in audio (or maybe video) files.

Usage: $0 [-m|-M] file.wav [file.wav ...]

-q: Quiet, only output the most likely key.
-m: Force major keys to relative minor.
-M: Force minor keys to relative major.
EOF
	exit 0;
}

for(@ARGV) {
	if(/^-M/) {
		$fold_major = 1;
		$fold_minor = 0;
		next;
	} elsif(/^-m/) {
		$fold_major = 0;
		$fold_minor = 1;
		next;
	} elsif(/^-q/) {
		$quiet++;
		next;
	}

	my $sec;
	{
		my $sf;
		eval {
			$sf = Audio::SndFile->open("<", $_);
		};
		if($@) {
			my $rq = $quiet ? "-really-quiet" : "";
			warn "Extracting to tmp.wav\n" unless $quiet;
			system("mplayer $rq -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 unless $quiet;

	my $oldstamp = -1;
	my $oldkey = -1;

	my $got = `vamp-simple-host qm-vamp-plugins:qm-keydetector "$_" 2 2>/dev/null`;
	die "Analysis failed\n" unless defined $got;

	print "\n" unless $quiet;

	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);
		}
		$relkey = get_relative_key($key);
		if($oldstamp != -1) {
			$times{$oldkey} += ($stamp - $oldstamp);
		}
		$oldstamp = $stamp;
		$oldkey = $key;
		print "$stamp: $key ($relkey)\n" unless $quiet;
	}

	$times{$oldkey} += ($sec - $oldstamp);

	print "\n" unless $quiet;
	my @sorted = sort { $times{$b} <=> $times{$a} } keys %times;
	if($quiet) {
		print $sorted[0] . " (" . get_relative_key($sorted[0]) . ")\n";
	} else {
		for(@sorted) {
			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;
}

sub get_relative_key {
	my $key = shift || die "missing key";
	if($key =~ /m$/) {
		$relkey = get_relative_major($key);
	} else {
		$relkey = get_relative_minor($key);
	}
	return $relkey;
}

__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
