#!/usr/bin/perl -w

our $self = $0;
$self =~ s,.*/,,;

our $root = 0;
our $flat2 = 1;
our $second = 2;
our $flat3 = 3;
our $third = 4;
our $fourth = 5;
our $flat5 = 6;
our $fifth = 7;
our $flat6 = $sharp5 = 8;
our $sixth = 9;
our $flat7 = 10;
our $seventh = 11;
our $octave = 12;
our $ninth = $octave + $second;
our $tenth = $octave + $third;
our $eleventh = $octave + $fourth;
our $thirteenth = $octave + $sixth;

our @majorscale = (0, 2, 4, 5, 7, 9, 11);
our @minorscale = (0, 2, 3, 5, 7, 8, 10);
our @major_chord_types = (qw/maj min min maj maj min dim/);

our @intervalnames = (
		qw/r b2 2 b3 3 4 b5 5 b6 6 b7 7/ );

# TODO: lots more chords
# also, try to parse the notation instead of having a fixed table.
# also also, this is the only place in the program where a ninth is
# really a ninth (14 semitones up) instead of being reduced to a 2nd
# (2 semitones up).

# btw, "diminished" means "diminished triad", no 7th (older jazz books
# used "diminished" to mean "diminished triad plus 7th", which we're
# calling "diminished 7th" here)

our %shapes = (
		5 => [ $root, $fifth ],
		no5 => [ $root, $third ],
		maj => [ $root, $third, $fifth ],
		min => [ $root, $flat3, $fifth ],
		'min(no5)' => [ $root, $flat3 ],
		7 => [ $root, $third, $fifth, $flat7 ],
		maj7 => [ $root, $third, $fifth, $seventh ],
		min7 => [ $root, $flat3, $fifth, $flat7 ],
		sus2 => [ $root, $second, $fifth ],
		sus4 => [ $root, $fourth, $fifth ],
		add9 => [ $root, $third, $fifth, $ninth ],
		9 => [ $root, $third, $fifth, $seventh, $ninth ],
		'min/maj7' => [ $root, $flat3, $fifth, $seventh ],
		aug => [ $root, $third, $sharp5 ],
		aug7 => [ $root, $third, $sharp5, $flat7 ],
		dim => [ $root, $flat3, $flat5 ],
		dim7 => [ $root, $flat3, $flat5, $sixth ],
		adim => [ $root, $flat3, $flat5, $flat7 ],
		'dim(no7)' => [ $root, $flat3, $flat5 ],
		'dim(no3)' => [ $root, $flat5 ],
		'dim7(no3)' => [ $root, $flat5, $flat7 ],
		6 => [ $root, $third, $fifth, $sixth ],
		min6 => [ $root, $flat3, $fifth, $sixth ],
		'6/9' => [ $root, $third, $fifth, $sixth, $ninth ],
		'min6/9' => [ $root, $flat3, $fifth, $sixth, $ninth ],
		'maj7(no3)' => [ $root, $fifth, $seventh ],
		'maj7(no5)' => [ $root, $third, $seventh ],
		'7(no3)' => [ $root, $fifth, $flat7 ],
		'7(no5)' => [ $root, $third, $flat7 ],
		'm7(no5)' => [ $root, $flat3, $flat7 ],
		'7#9' => [ $root, $third, $fifth, $flat7, $flat3 ],
		'7#9(no5)' => [ $root, $third, $flat7, $flat3 ],
		'm/add11' => [ $root, $flat3, $fifth, $eleventh ],
		'add11' => [ $root, $third, $fifth, $eleventh ],
		'11' => [ $root, $third, $fifth, $flat7, $eleventh ],
		'min11' => [ $root, $flat3, $fifth, $flat7, $eleventh ],
		'm/add13' => [ $root, $flat3, $fifth, $thirteenth ],
		'add13' => [ $root, $third, $fifth, $thirteenth ],
		'13' => [ $root, $third, $fifth, $flat7, $eleventh, $thirteenth ],
		'min13' => [ $root, $flat3, $fifth, $flat7, $eleventh, $thirteenth ],
);

our @sharpnames = ( 'c', 'c#', 'd', 'd#', 'e', 'f', 'f#', 'g', 'g#', 'a', 'a#', 'b' );
our @flatnames =  ( 'c', 'db', 'd', 'eb', 'e', 'f', 'gb', 'g', 'ab', 'a', 'bb', 'b' );
our $notenames = \@sharpnames;
our @sharpkeys = (0, 7, 2, 9, 4, 11);
our @keysigs = (
		'nat', '5b', '2#', '3b', '4#', '1b', '6b', '1#', '4b', '3#', '2b', '5#');

our $transpose = 0;

our @openstrings;
our $fretpos = 0;
our $fretspan = 3;
our $allowpartial = 0; # set with -p
our $allowholes = 1; # set with -l

our %notevals = (
		'c' => 0,
		'c#' => 1,
		'db' => 1,
		'd' => 2,
		'd#' => 3,
		'eb' => 3,
		'e' => 4,
		'f' => 5,
		'f#' => 6,
		'gb' => 6,
		'g' => 7,
		'g#' => 8,
		'ab' => 8,
		'a' => 9,
		'a#' => 10,
		'bb' => 10,
		'b' => 11 );

our @chordints = (qw/I bII II bIII III IV bV V bVI VI bVII VIII/);

our @keynotes = ( 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 );
our @chords = ();
our $key; # undef = guess
our $forceroot = 0; # set with -r

sub fix_note {
	my $note = shift;
	$note %= 12;
	return $note;
}

sub get_note_name {
	return ucfirst($$notenames[fix_note($_[0])]);
}

sub name_to_note {
	my $note = lc($_[0]);
	return $notevals{$note};
}

sub add_interval {
	return fix_note($_[0] + $_[1]);
}

sub get_interval {
	return fix_note($_[0] - $_[1]);
}

sub determine_chord {
	my $root = shift;
	my @notes = @_;

	for my $name (sort keys %shapes) {
		my $shape = $shapes{$name};
		my $rootname = $$notenames[$root];
		my @trynotes;
		for(@$shape) {
			push @trynotes, add_interval($root, $_);
		}

		@trynotes = sort { $a <=> $b } @trynotes;

		if(@trynotes != @notes) {
#warn "$rootname$name: extra notes\n";
		} elsif(@trynotes < @notes) {
#			warn "$rootname$name: missing notes\n";
		} else {
#			warn "$rootname$name: same # of notes\n";
			my $match = 1;
			for(my $i=0; $i<@notes; $i++) {
#warn $notes[$i] . ", " . $trynotes[$i];
				$match = 0 if $notes[$i] != $trynotes[$i];
			}
			return $rootname . $name if $match;
		}
	}

	return undef;
}

sub discover_chord {
	my @frets = split /[^x\d]/i, $_[0];

	my %notes;
	my $root; # undef = try them all
	for(my $string = 0; $string < @openstrings; $string++) {
		next if (not defined $frets[$string]) || ($frets[$string] =~ /x/i);
		my $note = ($openstrings[$string] + $frets[$string]) % 12;
		if($forceroot && not defined $root) {
			$root = $note;
		}
		$notes{$note}++;
	}

	my @notes = sort { $a <=> $b } keys %notes;
	print "discover_chord(): notes are ";
	print "$$notenames[$_] " for @notes;
	print "\n";

#warn "@notes";

	if(defined $root) {
		return determine_chord($root, @notes);
	} else {
		for(@notes) {
			$ret = determine_chord($_, @notes);
			return $ret if $ret;
		}
	}

	return undef;
}

sub parse_chord {
	my $chord = lc(shift);
	my $fingering;
	my @frets;

	parse_tuning($tunings{std}) unless @openstrings;

	# tablature input instead of chord symbol?
	if($chord =~ /^[x\d]/) {
		@frets = split /[^x\d]/i, $chord;
		my $gotchord = discover_chord($chord);
		if(!$gotchord) {
			warn "$self: can't figure out chord name for fingering $chord\n";
			return 0;
		}
		$chord = $gotchord;
	}

	# alternate notation
	$chord =~ s/\+/aug/;
	$chord =~ s/\-/dim/;

	# rudimentary support for english (e.g. "A minor 7")
	$chord =~ s/flat/b/gi;
	$chord =~ s/sharp/#/gi;
	$chord =~ s/minor/m/gi;
	$chord =~ s/major/maj/gi;
	$chord =~ s/\s//g;

	my ($note, $type) = ($chord =~ /^([a-g][#b]?)(.*)?/);

	unless($note) {
		warn "$self: Invalid chord $chord\n";
		return 0;
	}

	$type = "maj" unless $type;

	$type =~ s/^m(#|b|\d|$)/min$1/;

	my $shape = $shapes{$type};

	unless($shape) {
		warn "$self: Unknown chord type $type\n";
		return 0;
	}

	my $val = $notevals{$note};

	unless(defined $val) {
		warn "$self: Unknown note $note\n";
		return 0;
	}

	$val = ($val + $transpose) % 12;
	$note = $$notenames[$val];

	my @notes;
	for(@$shape) {
		my $n = ($val + $_) % 12;
		$keynotes[$n]++;
	}

	my $parsed = [ $val, $note, $type ];
	if(@frets) {
		for(@frets) {
			$_ = undef if $_ =~ /x/i;
		}
		$fingering = render_fingering($val, \@frets);
	} else {
		for(my $startfret = $fretpos; $startfret < $fretpos + 12; $startfret++) {
			$fingering = find_guitar_chord($startfret, $parsed);
			last if $fingering;
		}
	}
	push @$parsed, $fingering if $fingering;
	push @chords, $parsed;

	return 1;
}

sub dump_chord_shapes {
	for(sort keys %shapes) {
		my $shape = $shapes{$_};
		print "$_: ";
		for(@$shape) {
			print $intervalnames[$_ % 12] . " ";
		}
		print "\n";
	}
}

sub print_chord {
	my $c = shift;
	my ($val, $note, $type, $fingering) = @$c;
	my $shape = $shapes{$type};

	$type =~ s/maj$//; # in output, say A, not Amaj

	my @notes;
	for(@$shape) {
		$_ = fix_note($_); # TODO: this turns e.g. 9ths into 2nds
		my $n = add_interval($val, $_);
		push @notes, get_note_name($n) . "=" . $intervalnames[$_];
	}

#my $chordname = ucfirst($note) . $type;
	my $chordname = get_note_name($val) . $type;
	if(defined($key)) {
		my $keyint = $val - $key;
		$keyint += 12 if $keyint < 0;
		my $chordint = $chordints[$keyint];
		if($type =~ /(min|dim)/) { # TODO: this heuristic is lame
			$chordint = lc($chordint);
		}
		$chordname .= " (" . $chordint . ")";
	}

	print $chordname . ": " . join(" ", @notes) . "\n";

	if($fingering) {
		print $fingering . "\n";
	} else {
		print "(couldn't find fingering at given fret pos)\n";
	}
	print "\n";
}

sub parse_tuning {
	my $tuning = shift;
	my $offset = shift || 0;

	$tuning =~ s/\s//g; # allow & ignore spaces
	$tuning = lc $tuning; # allow uppercase

	die "$self: Invalid tuning '$tuning'\n" unless $tuning =~ /^[a-g#]+$/;

	my @notes = ($tuning =~ /([a-g][#]?)/g);
	die "$self: Invalid tuning '$tuning'\n" unless @notes;

	@openstrings = ();
	my @names;
	for(@notes) {
		my $note = fix_note(add_interval($notevals{$_}, $offset));
		push @openstrings, $note;
		push @names, get_note_name($note);
	}

	print "Tuning: " .  join(" ", @names) . "\n";
}

# Tuning list isn't really meant to be exhaustive, but should include
# plenty of variants...
our @tuning_list = (
		[ 'std', 'eadgbe', 'Standard Guitar [default]' ],
		[ 'eb', 'd#g#c#f#a#d#', 'Guitar 1/2 step flat (Eb)' ],
		[ 'd', 'dgcfad', 'Guitar 1 step flat (D)' ],
		[ 'c#', 'c#f#beg#c#', 'Guitar 1 1/2 steps flat (C#)' ],
		[ 'c', 'cfa#d#gc', 'Guitar 2 steps flat (C)' ],
		[ 'od', 'dadf#ad', 'Open D' ],
		[ 'og', 'dgdgbd', 'Open G' ],
		[ 'oe', 'ebeg#be', 'Open E' ],
		[ 'oeb', 'd#a#d#ga#d#', 'Open Eb' ],
		[ 'oa', 'eac#eae', 'Open A' ],
		[ 'dd', 'dadgbe', 'Drop D' ],
		[ 'dc#', 'c#g#c#f#a#d#', 'Drop C#' ],
		[ 'dc', 'cgcfad', 'Drop C' ],
		[ 'nst', 'cgdaeg', 'Fripp\'s New Standard Tuning' ],
		[ 'b', 'eadg', 'Bass (4-string)' ],
		[ '5', 'beadg', 'Bass (5-string)' ],
		[ '6', 'beadgc', 'Bass (6-string)' ],
		[ '6b', 'beadgb', 'Bass (6-string alt)' ],
		[ 'r', 'beadf#b', 'Baritone Guitar or alt 6-string bass' ],
		[ '7', 'beadgbe', '7-string Guitar (low B)' ],
		[ '7h', 'eadgbea', '7-string Guitar (high A)' ],
		[ '7d', 'aeadgbe', '7-string Guitar (drop A)' ],
		[ 't', 'gcfa#de', 'Terz Guitar' ],
		[ 'm', 'gdae', 'Mandolin' ],
		[ 'l', 'eadf#be', 'Lute' ],
		[ 'h', 'adgbe', 'Mexican Vihuela' ],
		[ 'cv', 'cgda', 'Cello or Viola' ],
		[ 'v', 'gdae', 'Violin' ],
		[ 'vs', 'adae', 'Violin (scordatura)' ],
		[ 'u', 'gcea', 'Ukulele' ],
		[ 'ub', 'dgbe', 'Baritone Ukulele' ],
);

our %tunings;

sub tuning_help {
	for(@tuning_list) {
		my ($tag, $notes, $name) = @$_;
		printf("-t%-4s %s (same as -t%s)\n", $tag, $name, $notes);
	}
	exit 0;
}

sub usage {
	warn <<EOF;
Usage: $self [globalopts] [chordopts] <chord> [[chordopts] <chord>] ...

Chords can be either standard notation (e.g. A, Bm, C#maj7), or comma-
separated fret positions, low to high, (e.g. 3,2,0,0,3,3 is a G in
standard guitar tuning). Use x to indicate strings not played (e.g.
x,x,0,2,3,2 is a commonly-used D chord).

Global Options:
   -t<notes> Set tuning, low string to high. Default is eadgbe. Instrument
             is assumed to have as many strings as there are notes. Do not
             specify notes as flats (because the letter b is used for both
             the b note and the flat symbol). Use e.g. d# instead of eb.
   -t<type>  Set one of the built-in tunings. Use -th or -thelp for list.
   -x<x>     Transpose x semitones (x is positive or negative integer)
   -D        Dump all known chord types
   -k<key>   Set the key for all chords (default is to guess key)

Per-Chord Options: Once set, these stay in effect for subsequent chords.

   -b        Print flat note names (e.g. Db instead of C#)
   -s        Print sharp note names (e.g. C# instead of Db) [default]
   -f<x>     Place chord at fret position x (x=0 default, means open strings).
             (Actually, this is the minimum position: if no useful notes
             are found at x, we try x+1, x+2, ..., x+12)
   -s<x>     Maximum fret span. Default is 3.
   -v        Find variant chord voicing. Doesn't always do anything useful.
             (for example, this gives 3,2,0,0,3,3 instead of 3,2,0,0,0,3
             for an open G chord in standard tuning)
   -n        Find normal voicing (default), turns off previous -v option.
   -r        Force lowest bass note to be root of chord.
   -i        Allow inversions (turns off -r). This is the default.
   -p        Allow partial voicings (e.g. only C and E for a C major chord).
             Use with caution, especially with 7, 9, etc. chords (the
             matcher will consider a C major triad to match C7 or C9).
EOF
	exit 0;

# future options:
#   -l        Loose matching. Accepts chord fingerings with "holes" in
#             the middle (e.g. 65X766 for A#).
#   -o        Include open strings even in non-open -f positions.
#   -a        Chart all chord voicings found for each chord (default is to
#             stop after first one is found). May be combined with -f, e.g.
#             -f5 -a means "all chord voicings from 5th fret up".
#   -c<x>     Capo on fret x.
#   -T        Tablature notation instead of chord charts, for -t option.
#   -m[file]  Generate MIDI file of chords. With [file], output to file.
#             Without [file], play using timidity.
#   -O<type>  Output type: text (default), ps, pdf, html, maybe xml someday.
#   -w<x>     Set output width for text output. Default: $COLUMNS from
#             environment, or 80 if not set.
}

sub has_hole {
	my @voicing = @_;
	shift @voicing while not defined $voicing[0];
	pop @voicing while not defined $voicing[-1];
	return 1 if @voicing == 0; # whoopsie!
	for(@voicing) {
		return 1 if not defined $_;
	}
	return 0;
}

# for each string, go thru the note values for frets $fretpos to
# $fretpos+$fretspan, looking at each note in @vals.
# TODO: de-shittify this. It was thrown together between 2 and 5AM,
# and it looks like it.
sub find_guitar_chord {
	my $startfret = shift;
	my $c = shift;
	my ($val, $note, $type) = @$c;
	my $shape = $shapes{$type};
	my @voicing;
	my $string;
	my $fret;

	$note = uc $note;
	$type =~ s/maj$//; # in output, say A, not Amaj

	my @vals;
	for(@$shape) {
		my $n = ($val + $_) % 12;
		push @vals, $n;
	}
	@vals = sort @vals; # for e.g. 9 and add9 chords

	my $chordname = ucfirst($note) . $type;

	push @voicing, undef for @openstrings;

	my %foundvals;
	my $rootfound = 0;
	for($string=0; $string<@openstrings; $string++) {
		my ($start, $end, $step);
		if($findbackwards) {
			$start = $startfret + $fretspan;
			$end = $startfret - 1;
			$step = -1;
		} else {
			$start = $startfret;
			$end = $startfret + $fretspan + 1;
			$step = 1;
		}
FRET:
		for($fret = $start; $fret != $end; $fret+=$step) {
			my $fretval = ($openstrings[$string] + $fret) % 12;
VAL:
			for(@vals) {
#warn "\$string==$string \$fret==$fret \$fretval==$fretval \$_==$_\n";
				if($_ == $fretval) {
					if($forceroot && !$rootfound && $_ != $val) {
						next VAL;
					}
					$rootfound = 1 if $_ == $val;
					$voicing[$string] = $fret;
					$foundvals{$_}++;
					last FRET;
				}
			}
		}
	}

	if(scalar keys %foundvals < @vals) {
		if($allowpartial) {
#warn "$note$type: didn't find all note values, return partial chord\n";
		} else {
#warn "$note$type: didn't find all note values, return nothing\n";
			return undef;
		}
	}

	if(not $allowholes) {
		if(has_hole(@voicing)) {
#warn "$note$type: has a hole\n";
			return undef;
		}
	}

	my $r = render_fingering($val, \@voicing);
	return $r;
}

sub render_fingering {
	my $val = $_[0];
	my @voicing = @{$_[1]};
	my ($lowfret, $highfret) = (999, -1);
	for(@voicing) {
		next unless defined $_;
		$lowfret = $_ if $_ < $lowfret;
		$highfret = $_ if $_ > $highfret;
	}

	my @grid;
	my ($intervals, $names);
	for($string=0; $string<@openstrings; $string++) {
		my $fret = $voicing[$string];
		if(not defined($fret)) {
			$intervals .= "   ";
			$names .= "   ";
		} else {
			my $noteval = ($openstrings[$string] + $fret) % 12;
			my $interval = $noteval - $val;
			$interval += 12 if $interval < 0;
			$interval %= 12;
			$intervals .= sprintf("%3s", $intervalnames[$interval]);
			$names .= sprintf("%3s", ucfirst $$notenames[$noteval]);
		}

		for($lowfret..$highfret) {
			my $empty = "|";
			if($_ == $lowfret && $lowfret == 0) {
				$empty = "+";
			}

			$grid[$_ - $lowfret][$string] = $empty;
		}

		if(defined($fret)) {
			$grid[$fret-$lowfret][$string] = $fret;
		} else {
			$grid[0][$string] = 'X';
		}
	}

	my @rendered;
	for(@grid) {
		$_->[0] =~ s/^/ / if length($_->[0]) == 1;
		$_->[0] =~ s/^/ /;
		for($string=1; $string<@openstrings; $string++) {
			$_->[$string] =~ s/^/-/ if length($_->[$string]) == 1;
			$_->[$string] =~ s/^/-/;
		}
		push @rendered, join("", @$_);
	}
	push @rendered, $names;
	push @rendered, $intervals;
	return join("\n", @rendered);
}

# TODO: make this smarter. Currently it completely ignores accidentals
# and takes the first match it finds. Also it gives up if it can't get
# an exact match with a major scale (e.g. if given C and G chords only,
# it gives up, whereas it really should guess either C or G as the key)
# Probably need some kind of weighting system.
sub guess_key {
	my ($k, $n, $ret, @possibles);
	for $k (0..11) {
		my $found = 1;
		for $n (0, 2, 4, 5, 7, 9, 11) {
			$found = 0 if not $keynotes[($k + $n) % 12];
		}
		if($found) {
			push @possibles, uc($$notenames[$k]);
			$ret = $k unless defined $ret;
		}
	}

	if(@possibles == 0) {
		warn "$self: Can't guess key, use -k to set\n";
	} elsif(@possibles > 1) {
		warn "$self: Possible keys: " . join(" ", @possibles) .
			", guessing " . $possibles[0] . ", use -k to set\n";
	}

	if($ret) {
		if(grep { $_ == $ret } @sharpkeys) {
			$notenames = \@sharpnames;
		} else {
			$notenames = \@flatnames;
		}
	}

	return $ret;
}

sub parse_opt {
	my $opt = shift;
	$opt =~ s/^-+//;

	if($opt eq 'h') {
		usage();
	} elsif($opt eq 'b') {
		$notenames = \@flatnames;
	} elsif($opt eq 's') {
		$notenames = \@sharpnames;
	} elsif($opt =~ /^x([+-]?\d+)/) {
		$transpose = $1 + 0;
	} elsif($opt =~ /^k([a-g][#b]?)(m?)/) {
		my $keyopt = $1;
		my $minor = $2;
		$key = $notevals{lc $keyopt};
		if(defined($key) and $minor) {
			$key += 3;
			$key %= 12;
		}
		if(defined($key)) {
			if(grep { $_ == $key } @sharpkeys) {
				$notenames = \@sharpnames;
			} else {
				$notenames = \@flatnames;
			}
		} else {
			warn "$self: Invalid key '$1', ignoring\n";
		}
	} elsif($opt =~ /^f(\d+)/) {
		$fretpos = $1 + 0;
	} elsif($opt =~ /^s(\d+)/) {
		$fretspan = $1 + 0;
	} elsif($opt =~ /^t(.*)/) {
		my $offset = 0;
		my $tuning = $1;
		if($tuning =~ s/([-+]\d+)$//) {
			$offset = $1;
		}

		if($tuning =~ /^h(?:elp)?/i) {
			tuning_help();
		} elsif($tunings{$tuning}) {
			parse_tuning($tunings{$tuning}, $offset);
		} else {
			parse_tuning($tuning, $offset);
		}
	} elsif($opt eq 'r') {
		$forceroot = 1;
	} elsif($opt eq 'i') {
		$forceroot = 0;
	} elsif($opt eq 'n') {
		$findbackwards = 0;
	} elsif($opt eq 'v') {
		$findbackwards = 1;
	} elsif($opt eq 'p') {
		$allowpartial = 1;
#} elsif($opt eq 'l') {
#$allowholes = 0;
	} elsif($opt eq 'd') {
		dump_chord_shapes();
	} else {
		warn "$self: Invalid option '$_', use -h for help\n";
	}
}

# main()

for(@tuning_list) {
	$tunings{$_->[0]} = $_->[1];
}


for(@ARGV) {
	if(/^-/) {
		parse_opt($_);
	} else {
		parse_chord($_);
	}
}

if((defined $key) && !@chords) {
# got a -k but no chords, show all triad chords in this key.
	for(my $i = 0; $i < @majorscale; $i++) {
		my $note = add_interval($majorscale[$i], $key);
		my $name = $$notenames[$note];
		my $type = $major_chord_types[$i];
		parse_chord($name . $type);
	}
}

unless(@chords) {
	warn "$self: Found no valid chords\n";
	usage();
	exit 1;
}

unless(defined($key)) {
	$key = guess_key();
}

for(@chords) {
	print_chord($_);
}

if(defined $key) {
	my $keyname = ucfirst($$notenames[$key]);
	my $minkey = fix_note($key - 3);
	my $minkeyname = ucfirst($$notenames[$minkey]);
	print "Key: " . $keyname . " (" . $minkeyname . "m), " . $keysigs[$key] . "\n";
	print $keyname . " major scale: ";
	for(@majorscale) {
		my $note = ($key + $_) % 12;
		print ucfirst($$notenames[$note]) . " ";
	}
	print "\n";
	print $minkeyname . " minor scale: ";
	for(@minorscale) {
		my $note = ($minkey + $_) % 12;
		print ucfirst($$notenames[$note]) . " ";
	}
	print "\n";
}

# examples:

# I know how to finger some chords, and need to know what they're called,
# what notes are in them, and what key (if any) those chords imply:
# chordspeller.pl 3,2,0,0,3,3 x,0,2,2,2,0 x,x,0,2,3,2

# I know the chords in a song, and need to know the key of the song:
# chordspeller.pl G A D

# I know the chords in a song, and need to transpose it from G to A
# (up 2 semitones):
# chordspeller.pl -x2 G C Am D

# Any of the above can be done in any tuning other than standard guitar
# tuning by way of a -t<tuning> argument (see the help for the list).

__END__
---=cut-here=-------
Gmin (ii)
10-10--|--|--|-10
 |  |  |  |  |  |
 |--|--|--|-11--|
 |  |  |  |  |  |
 |--|-12-12--+--|
 |  |  |  |  |  |
 |--|--|--|--+--|
 |  |  |  |  |  |
 D  G  D  G Bb  D
 5  r  5  r b3  5

---=cut-here=-------

Each chord is 20x12 with fretspan 3 and 6 strings (actually they're 19x11
with 3 columns and one row of padding). This allows a printer page that's
80x66 chars to have a 4x5 grid of them (20 chords), with some room left
at the top/bottom for things like the key, scales, etc. Also a 80x24
xterm can display a 4x2 grid (8 chords).

Formula:
width = strings*3+1 + 3(padding)
height = (fretspan+1) * 2 + 1(padding) + 1(title) + 1(notes) + 1(intervals)

Compressed form:
---=cut-here=-------
Gmin (ii)
10-10--|--|--|-10
 |--|--|--|-11--|
 |--|-12-12--|--|
 |--|--|--|--|--|
 D  G  D  G Bb  D
 5  r  5  r b3  5

---=cut-here=-------
height = (fretspan+1) + 1(padding) + 1(title) + 1(notes) + 1(intervals)

6string, span3, you get 20x8, or 4x8 on 80x66 page, or 4x3 on 80x25 xterm.


Very compressed form:
---=cut-here=
Gmin (ii)
1010-|-|-|10
 |-|-|-|11-|
 |-|1212-|-|
 |-|-|-|-|-|
 D G D GBb D
 5 r 5 rb3 5

---=cut-here=
width = strings*2 + 1(padding)
height = (fretspan+1) + 1(padding) + 1(title) + 1(notes) + 1(intervals)

13x8. Gives 6x8 on paper, 6x3 on xterm, but harder to read... not so bad
if all frets are single-digit though:

---=cut-here=
G7 (I)
 +-+-0-0-0-+
 |-|-|-|-|-1
 |-2-|-|-|-|
 3-|-|-|-|-|
 G B D G B G
 1 3 5 1 3b7

---=cut-here=
(notice the + instead of |, in the 0 position)

